screenshots.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. import logging
  2. import subprocess
  3. from PIL import Image
  4. import os
  5. import time
  6. import cv2
  7. import numpy as np
  8. import uiautomator2 as u2
  9. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
  10. class ScreenshotStitcher:
  11. def __init__(self, device_serial, adb_path):
  12. self.device_serial = device_serial
  13. self.adb = adb_path
  14. self.d = u2.connect(device_serial)
  15. self.screen_width = 0
  16. self.screen_height = 0
  17. def check_adb_connection(self):
  18. try:
  19. output = subprocess.check_output([self.adb, '-s', self.device_serial, 'devices']).decode('utf-8')
  20. if self.device_serial in output and 'device' in output:
  21. logging.info(f"ADB 设备 {self.device_serial} 连接正常")
  22. return True
  23. logging.error(f"ADB 设备 {self.device_serial} 连接异常")
  24. return False
  25. except FileNotFoundError:
  26. logging.error("ADB 未找到,请确保已安装并配置 ADB")
  27. return False
  28. def adb_shell(self, command):
  29. try:
  30. output = subprocess.check_output(
  31. [self.adb, '-s', self.device_serial, 'shell', command],
  32. stderr=subprocess.STDOUT
  33. ).decode('utf-8')
  34. return output
  35. except subprocess.CalledProcessError as e:
  36. logging.error(f"ADB 命令执行失败:{e.output.decode('utf-8')}")
  37. raise
  38. def get_screen_size(self):
  39. output = self.adb_shell('wm size')
  40. if "Physical size: " in output:
  41. size = output.split('Physical size: ')[1].strip().split('x')
  42. self.screen_width, self.screen_height = int(size[0]), int(size[1])
  43. elif "Size: " in output:
  44. size = output.split('Size: ')[1].strip().split('x')
  45. self.screen_width, self.screen_height = int(size[0]), int(size[1])
  46. else:
  47. raise ValueError("无法获取屏幕尺寸")
  48. def get_screenshot(self, filename):
  49. self.adb_shell(f'screencap -p /sdcard/{filename}')
  50. subprocess.call([self.adb, '-s', self.device_serial, 'pull', f'/sdcard/{filename}', filename])
  51. self.adb_shell(f'rm /sdcard/{filename}')
  52. def merge_screenshots_vertical(self, screenshots):
  53. images = [np.array(img) for img in screenshots]
  54. max_width = max(img.shape[1] for img in images)
  55. aligned_images = []
  56. for img in images:
  57. if img.shape[1] != max_width:
  58. img = cv2.resize(img, (max_width, int(img.shape[0] * max_width / img.shape[1])))
  59. aligned_images.append(img)
  60. return cv2.vconcat(aligned_images)
  61. def is_bottom_reached(self, last_screenshot, current_screenshot):
  62. """检查是否到达底部"""
  63. # 将图片转换为numpy数组
  64. last_img = np.array(last_screenshot)
  65. current_img = np.array(current_screenshot)
  66. # 计算最后一部分的相似度
  67. last_portion = last_img[-100:, :]
  68. current_portion = current_img[-100:, :]
  69. # 使用结构相似性指数(SSIM)比较
  70. similarity = cv2.matchTemplate(last_portion, current_portion, cv2.TM_CCOEFF_NORMED)
  71. return similarity.max() > 0.95
  72. def scroll_to_top(self):
  73. """滚动到页面顶部"""
  74. try:
  75. # 多次向上滑动以确保到达顶部
  76. for _ in range(3):
  77. # 从屏幕底部往上滑
  78. self.adb_shell(
  79. f'input swipe {self.screen_width // 2} {self.screen_height - 200} '
  80. f'{self.screen_width // 2} {200} 800'
  81. )
  82. time.sleep(1)
  83. logging.info("已滚动到页面顶部")
  84. return True
  85. except subprocess.CalledProcessError as e:
  86. logging.error(f"滚动到顶部失败:{e}")
  87. return False
  88. def images_are_different(self, img1, img2, threshold=0.95):
  89. """检查两张图片是否不同"""
  90. # 转换为OpenCV格式
  91. img1_cv = cv2.cvtColor(np.array(img1), cv2.COLOR_RGB2BGR)
  92. img2_cv = cv2.cvtColor(np.array(img2), cv2.COLOR_RGB2BGR)
  93. # 计算相似度
  94. result = cv2.matchTemplate(img1_cv, img2_cv, cv2.TM_CCOEFF_NORMED)
  95. similarity = np.max(result)
  96. return similarity < threshold
  97. def capture_long_screenshot(self, output_filename='full_screenshot.png', max_screenshots=10):
  98. """改进的长截图函数"""
  99. if not self.check_adb_connection():
  100. return False
  101. self.get_screen_size()
  102. screenshots = []
  103. # 第一次截图
  104. self.get_screenshot('temp_screenshot_0.png')
  105. last_screenshot = Image.open('temp_screenshot_0.png')
  106. screenshots.append(last_screenshot)
  107. # 循环滚动和截图
  108. for i in range(1, max_screenshots):
  109. try:
  110. # 从屏幕底部向上滑动
  111. self.adb_shell(
  112. f'input swipe {self.screen_width // 2} {self.screen_height - 200} '
  113. f'{self.screen_width // 2} {200} 800'
  114. )
  115. time.sleep(1.5) # 等待滚动完成
  116. # 截取当前页面
  117. self.get_screenshot(f'temp_screenshot_{i}.png')
  118. current_screenshot = Image.open(f'temp_screenshot_{i}.png')
  119. # 只有当图片不同时才添加
  120. if self.images_are_different(last_screenshot, current_screenshot):
  121. screenshots.append(current_screenshot)
  122. last_screenshot = current_screenshot
  123. else:
  124. logging.info("检测到重复页面,停止截图")
  125. break
  126. except subprocess.CalledProcessError:
  127. break
  128. try:
  129. if screenshots:
  130. result = self.merge_screenshots_vertical(screenshots)
  131. cv2.imwrite(output_filename, result)
  132. logging.info(f"长截图已保存至 {output_filename}")
  133. except Exception as e:
  134. logging.error(f"截图拼接失败:{e}")
  135. return False
  136. finally:
  137. # 清理临时文件
  138. for i in range(max_screenshots):
  139. temp_file = f'temp_screenshot_{i}.png'
  140. if os.path.exists(temp_file):
  141. os.remove(temp_file)
  142. return True
  143. def main():
  144. # 配置参数
  145. adb_path = '/home/fzxs/Android/Sdk/platform-tools/adb' # 替换为实际的ADB路径
  146. device_serial = '127.0.0.1:6555' # 替换为实际的设备序列号
  147. # 创建截图工具实例
  148. stitcher = ScreenshotStitcher(device_serial, adb_path)
  149. # 执行长截图
  150. stitcher.capture_long_screenshot('long_screenshot.png')
  151. if __name__ == '__main__':
  152. main()