背景:
在做UI自动化时无论是PC端还是移动端,其中一个核心步骤就是定位控件,只要控件可以获取到,剩下的步骤就很好操作 ,获取控件的方式有很多,基本的方法有文本,控件Id,父控件加子控件索引,xpath以及轴定位等等,今天再给大家分享一个通过图片进行定位的方法:
语言:python
库:opencv
我们以Android UI自动化为例:
import subprocess
import threading
import time
import numpy
from PIL.Image import Image
from cv2 import cv2
from skimage.metrics import structural_similarity
import imutils
class ElementsApiMobile:
def __init__(self, driver):
self.BASE_DIR = Path(__file__).resolve().parent
self.screen_shot_dir = os.path.join(self.BASE_DIR, 'save_screen_shot')
self.make_dir(self.screen_shot_dir)
self.my_driver = driver
self.mobile_snap_shot_save_path = "/sdcard/Pictures/Screenshots/"
def check_if_in_picture(self, my_small_picture, my_mobile_pic_path=None, my_picture_dir=None, expect_result=True):
'''
检查指定区域图片是否在当前设备界面中
:param my_small_picture: 指定区域的小图
:param my_mobile_pic_path: 图片截图保存到移动端的路径,可以不传会报错到一个默认路径,此路径在设备上不一定存在,会导致截图失败
:param my_picture_dir: 本地图片路径
:param expect_result: 默认存在,如果传False ,期待结果为不存在
:return:
'''
if expect_result:
if self.find_if_in_picture(my_small_picture, my_mobile_pic_path, my_picture_dir):
assert True
else:
print(f"远程路径:{my_mobile_pic_path} ,图片 {my_picture_dir}中没找到 {my_small_picture}")
assert False
else:
if self.find_if_in_picture(my_small_picture, my_mobile_pic_path, my_picture_dir):
print(f"远程路径:{my_mobile_pic_path} ,图片 {my_picture_dir}中有找到 {my_small_picture}")
assert False
else:
assert True
# my_pic_name 需要带后缀的图片名
def click_by_picture(self, my_pic_name, my_mobile_pic_path=None, my_pic_dir=None):
try:
self.take_screen_shot("my_big_pic.png", my_mobile_pic_path, my_pic_dir)
if my_pic_dir is not None:
my_x, my_y = self.find_picture(os.path.join(my_pic_dir, "my_big_pic.png"),
os.path.join(my_pic_dir, my_pic_name), 0.9)
else:
my_x, my_y = self.find_picture(os.path.join(self.screen_shot_dir, "my_big_pic.png"),
os.path.join(self.screen_shot_dir, my_pic_name), 0.9)
if my_x != -1:
self.click_by_location(my_x, my_y)
print("点击位置:" + str(my_x) + " " + str(my_y))
except:
print(f"[click_by_picture] 通过图片点击出错:图片名称:{my_pic_name}")
raise
time.sleep(1)
def find_picture(self, src_path, find_path, threshold):
scale = 1
img = cv2.imread(src_path) # 要找的大图
img = cv2.resize(img, (0, 0), fx=scale, fy=scale)
template = cv2.imread(find_path) # 图中的小图
template = cv2.resize(template, (0, 0), fx=scale, fy=scale)
template_size = template.shape[:2]
img, x_, y_ = self.search_returnPoint(img, template, template_size, threshold)
if img is None:
print("没找到图片")
return -1, -1
else:
print("找到图片 位置:" + str(x_) + " " + str(y_))
return x_, y_
def search_returnPoint(self, img, template, template_size, threshold):
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
template_ = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
result = cv2.matchTemplate(img_gray, template_, cv2.TM_CCOEFF_NORMED)
# threshold = 0.9
# res大于70%
loc = numpy.where(result >= threshold)
# 使用灰度图像中的坐标对原始RGB图像进行标记
point = ()
for pt in zip(*loc[::-1]):
cv2.rectangle(img, pt, (pt[0] + template_size[1], pt[1] + + template_size[0]), (7, 249, 151), 2)
point = pt
if point == ():
return None, None, None
return img, point[0] + template_size[1] / 2, point[1] + template_size[0] / 2
def take_screen_shot(self, my_pic_name, my_mobile_pic_path=None, my_pic_dir=None):
try:
if my_pic_dir is not None:
if '.png' in my_pic_name:
self.take_picture_from_mobile(my_pic_name, my_mobile_pic_path, my_pic_dir)
else:
self.take_picture_from_mobile(my_pic_name + '.png', my_mobile_pic_path, my_pic_dir)
else:
if '.png' in my_pic_name:
self.take_picture_from_mobile(my_pic_name, my_mobile_pic_path, self.screen_shot_dir)
else:
self.take_picture_from_mobile(my_pic_name + '.png', my_mobile_pic_path, self.screen_shot_dir)
except:
print("[take_screen_shot] 截图失败!!!")
raise
time.sleep(1)
# 此方法不需要初始化appium引擎连上手机可以直接使用
def take_picture_from_mobile(self, pc_pic_name, my_mobile_pic_path=None, my_pic_dir=None):
if my_mobile_pic_path is None:
remote_file_path = os.path.join(self.mobile_snap_shot_save_path, "1.png")
else:
remote_file_path = os.path.join(my_mobile_pic_path, "1.png")
my_current_path = self.get_current_path()
if '.png' in pc_pic_name:
pc_file_path = os.path.join(my_pic_dir, pc_pic_name)
else:
pc_file_path = os.path.join(my_pic_dir, pc_pic_name+'.png')
my_start_time = time.time()
self.run_cmd_in_thread(self.take_snapshot, 10, remote_file_path)
self.run_cmd_in_thread(self.pull_file_from_android, 10, remote_file_path, pc_file_path)
my_end_time = time.time()
print("截屏拉取文件耗时:" + str(my_end_time - my_start_time))
print("图片保存路径:" + pc_file_path)
def take_snapshot(self, remote_full_path):
# print("截图")
self.invoke(f"adb shell screencap -p {remote_full_path}")
def run_cmd_in_thread(self, fun, wait_time=10, *args):
my_thread = threading.Thread(target=fun, args=args)
my_thread.setDaemon(True)
my_thread.start()
my_thread.join(wait_time)
def invoke(self, cmd):
output, errors = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
my_str = output.decode("utf-8")
return my_str
def pull_file_from_android(remote_full_path, pc_path):
self.invoke(f"adb pull {remote_full_path} {pc_path}")
def get_current_path(self):
return os.path.dirname(__file__)
def click_by_location(self, my_x, my_y sleep_time=1):
try:
self.click_x_y_by_adb_command(my_x, my_y)
except:
print(f"[click_by_location] 通过坐标点击出错:my_x:{my_x} my_y:{my_y}")
raise
time.sleep(sleep_time)
def click_x_y_by_adb_command(self, my_x, my_y):
self.invoke(f"adb shell input tap {my_x} {my_y}")
def make_dir(self, my_dir_path):
if not os.path.exists(my_dir_path):
os.makedirs(my_dir_path)
优点:使用简单方便 ,脚本相对于xpath定位要稳定很多,节省大量脚本编写时间,特别对于难以 定位和xpath不稳定的控件效果明显,可以明显提升自动化覆盖率。
缺点:对于不同型号设备做兼容性测试较为麻烦,分辨率变化以后图片需要重新截取且脚本需要适配不同路径。
实现说明:
1. 前置步骤:用户需要预先截取部分小图作为操作图片,保持到某个PC目录,参数中my_small_picture就对应这个小图。
2. 路径:除了小图路径,还涉及两个路径一个移动端截图后图片保存在移动端的路径,还有一个是将图片从移动端拉取到PC端 保存的路径,分别对应参数my_mobile_pic_path,my_picture_dir
3. 图片方法:代码中提供了两个图片方法一个为检查图片是否存在于当前移动端页面(check_if_in_picture), 另外一个根据图片执行点击动作(click_by_picture)
示例列举:
在微信中有个功能:在给好友先发一个粑粑表情,再发送一个炸弹表情,粑粑就会被炸开,对于这个效果UI自动化如何来检查呢,对于常规的方法这个场景就很难实现自动化覆盖。但是通过图片对比就可以很轻松实现:
对于这个效果,我们其实只要检查到有粑粑留着眼泪飞崩这个特有表情出现即可,对于图片变化的过程中其实我们依然不好对比,那我们就进一步找一个即使图片在变化的过程中还能保持基本不变的特征进行对比,比如那个眼泪,我们只要将表情中眼泪这个特殊点截取下来进行对比即可,如下图:
经过试验这个方案是可行的。