特征匹配
特征匹配是 mower 中最方便的识别方法。在截图中匹配目标图像时,首先提取截图与目标图像的特征点,然后找出匹配的特征点对,利用匹配结果在截图中定位目标图像。
特征与特征点
arknights_mower.utils.matcher
中提供 keypoints()
与 keypoints_scale_invariant()
函数,可用于提取特征点。接受的参数为需要提取特征点的灰度图像。返回值为特征点元组与描述子矩阵构成的元组。
keypoints_scale_invariant()
提取的特征点与描述子具有尺度不变性,可用于匹配尺度不确定或发生变化的图像,但速度相较 keypoints()
更慢。
拐角、复杂的图案与纹理(包括文字)处可以提取到较多的特征点;在空白处很难提取到特征点。
使用示例
import cv2
from matplotlib import pyplot as plt
from arknights_mower.utils.image import loadimg
from arknights_mower.utils.matcher import keypoints
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/201/20240713031749.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
kp, des = keypoints(sc)
print(len(kp))
img = cv2.drawKeypoints(sc, kp, None, (0, 255, 0), flags=0)
plt.imshow(img)
plt.show()
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/103/20240713152407.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
kp, des = keypoints(sc)
print(len(kp))
img = cv2.drawKeypoints(sc, kp, None, (0, 255, 0), flags=0)
plt.imshow(img)
plt.show()
特征点的匹配
arknights_mower.utils.matcher
中的 flann
可用于匹配特征点。
对于目标图像中的每个特征点 $A$,使用 flann.knnMatch(k=2)
找出截图中与之距离最近的和第二近的两个特征点 $B_1$、$B_2$. 如果 $A$ 与 $B_1$ 的距离 $\text{d}(A, B_1)$ 和 $A$ 与 $B_2$ 的距离 $\text{d}(A, B_2)$ 的比值 $\frac{\text{d}(A, B_1)}{\text{d}(A, B_2)} <$ GOOD_DISTANCE_LIMIT
,就认为 $A$ 与 $B_1$ 是一对"好"的匹配。
下面的例子展示了如何利用特征匹配在终端页面定位活动入口。其中 res
图像来自明日方舟网站,尺寸与游戏内截图未必一致,因此对于 res
使用 keypoints_scale_invariant()
提取特征点。对于截图,使用 keypoints()
提取特征点,仍然可以得到很好的匹配结果。
使用示例
import cv2
from matplotlib import pyplot as plt
from arknights_mower.utils.image import loadimg
from arknights_mower.utils.matcher import (
GOOD_DISTANCE_LIMIT,
flann,
keypoints,
keypoints_scale_invariant,
)
res = loadimg("/home/zhao/Documents/hot_update/hortus/terminal.jpg", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
kp1, des1 = keypoints_scale_invariant(res)
img = cv2.drawKeypoints(res, kp1, None, (0, 255, 0), flags=0)
plt.imshow(img)
plt.show()
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/501/20240705031952.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
kp2, des2 = keypoints(sc)
img = cv2.drawKeypoints(sc, kp2, None, (0, 255, 0), flags=0)
plt.imshow(img)
plt.show()
matches = flann.knnMatch(des1, des2, k=2)
good = []
for pair in matches:
if (len_pair := len(pair)) == 2:
x, y = pair
if x.distance < GOOD_DISTANCE_LIMIT * y.distance:
good.append(x)
elif len_pair == 1:
good.append(pair[0])
good = sorted(good, key=lambda x: x.distance)
debug_img = cv2.drawMatches(
res,
kp1,
sc,
kp2,
good[:50],
None,
(0, 255, 0),
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
plt.imshow(debug_img)
plt.show()
debug_img = cv2.cvtColor(sc, cv2.COLOR_GRAY2RGB)
center = list(map(int, kp2[good[0].trainIdx].pt))
print(center)
cv2.circle(debug_img, center, 20, (0, 255, 0), 5)
plt.imshow(debug_img)
plt.show()
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/501/20240705071219.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
kp2, des2 = keypoints(sc)
img = cv2.drawKeypoints(sc, kp2, None, (0, 255, 0), flags=0)
plt.imshow(img)
plt.show()
matches = flann.knnMatch(des1, des2, k=2)
good = []
for pair in matches:
if (len_pair := len(pair)) == 2:
x, y = pair
if x.distance < GOOD_DISTANCE_LIMIT * y.distance:
good.append(x)
elif len_pair == 1:
good.append(pair[0])
good = sorted(good, key=lambda x: x.distance)
debug_img = cv2.drawMatches(
res,
kp1,
sc,
kp2,
good[:50],
None,
(0, 255, 0),
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
plt.imshow(debug_img)
plt.show()
debug_img = cv2.cvtColor(sc, cv2.COLOR_GRAY2RGB)
center = list(map(int, kp2[good[0].trainIdx].pt))
print(center)
cv2.circle(debug_img, center, 20, (0, 255, 0), 5)
plt.imshow(debug_img)
plt.show()
利用特征匹配定位目标图像
arknights_mower.utils.matcher
中的 Matcher
类可用于定位目标图片。
实例化 Matcher
类时,需传入灰度图像 origin
。实例化过程中计算 origin
图像的特征点。
Matcher
类的实例方法 match()
接受 6 个参数,其中 query
必选:
query
:灰度目标图像;draw
:控制是否绘制并显示匹配过程;scope
:origin
在此区域内的特征点参与匹配;dpi_aware
:匹配尺寸不确定,或尺寸有变化的目标图像时,将此选项设置为True
。默认为False
。prescore
:SSIM 分数阈值。如果此参数为正值,则根据 SSIM 分数直接决定是否接受匹配结果。judge
:在prescore
为 0 时生效。如果为True
,使用支持向量机判断是否接受匹配结果,否则直接接受匹配结果。
如果匹配成功,match()
返回目标图片在截图中匹配到的区域;否则返回 None
。
使用示例
from matplotlib import pyplot as plt
from arknights_mower.utils.image import cropimg, loadimg, loadres
from arknights_mower.utils.matcher import Matcher
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/202/20240714174003.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
res = loadres("infra_collect_factory", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
matcher = Matcher(sc)
matcher.match(res, draw=True)
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/201/20240714214339.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
res = loadres("control_central", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
matcher = Matcher(sc)
matcher.match(res, draw=True, dpi_aware=True)
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/1105/20240712065207.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
res = loadimg("/home/zhao/Documents/mower-profile/screenshot/1106/20240712065212.png", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
res = cropimg(res, ((1020, 230), (1210, 280)))
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
matcher = Matcher(sc)
matcher.match(res, draw=True, dpi_aware=True)
sc = loadimg("/home/zhao/Documents/Desktop/crc/108/20240222230009.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
res = loadres("login_captcha", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
matcher = Matcher(sc)
matcher.match(res, draw=True, dpi_aware=True)
利用特征匹配求解单应性矩阵
游戏首页的界面随重力改变角度,为识别界面上的数字,可使用特征匹配求解单应性矩阵,将界面内容变换至平面,再进行进一步的识别。
使用示例
import cv2
import numpy as np
from matplotlib import pyplot as plt
from arknights_mower.utils.image import cropimg, loadimg, loadres
from arknights_mower.utils.matcher import (
GOOD_DISTANCE_LIMIT,
flann,
keypoints_scale_invariant,
)
res = loadres("sanity", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
sc = loadimg("/home/zhao/Downloads/Screenshot_20240716-180721.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
img = cropimg(sc, ((1400, 000), (2000, 800)))
plt.imshow(img, cmap="gray", vmin=0, vmax=255)
plt.show()
kp1, des1 = keypoints_scale_invariant(res)
kp2, des2 = keypoints_scale_invariant(img)
bf = cv2.BFMatcher(cv2.NORM_HAMMING)
matches = flann.knnMatch(des1, des2, k=2)
good = []
for pair in matches:
if (len_pair := len(pair)) == 2:
m, n = pair
if m.distance < GOOD_DISTANCE_LIMIT * n.distance:
good.append(m)
elif len_pair == 1:
good.append(pair[0])
print(len(good))
src_pts = np.float32([kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
dst_pts = np.float32([kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)
M, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC, 5.0)
matchesMask = mask.ravel().tolist()
h, w = res.shape
pts = np.float32([[0, 0], [0, h - 1], [w - 1, h - 1], [w - 1, 0]]).reshape(-1, 1, 2)
dst = cv2.perspectiveTransform(pts, M)
disp = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
disp = cv2.polylines(disp, [np.int32(dst)], True, (255, 0, 0), 2, cv2.LINE_AA)
good = sorted(good, key=lambda x: x.distance)
disp = cv2.drawMatches(
res,
kp1,
disp,
kp2,
good[:50],
None,
(0, 255, 0),
flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS,
)
plt.imshow(disp)
plt.show()
M
offset = (80, 250)
A = np.array([[1, 0, -offset[0]], [0, 1, -offset[1]], [0, 0, 1]])
disp = cv2.warpPerspective(img, M.dot(A), (800, 1100), None, cv2.WARP_INVERSE_MAP)
plt.subplot(1, 2, 1)
plt.imshow(img, cmap="gray", vmin=0, vmax=255)
plt.subplot(1, 2, 2)
plt.imshow(disp, cmap="gray", vmin=0, vmax=255)
plt.show()
注意事项
性能
从截图提取特征点一般会花费数十毫秒,一次 FLANN 匹配也可能花费几毫秒到几十毫秒。大量使用特征匹配会导致脚本很慢。但是,对于不限制区域的匹配,特征匹配往往比在整个截图上进行模板匹配更快。
示例:生息演算地图匹配地点
from matplotlib import pyplot as plt
from arknights_mower.utils.matcher import Matcher
from arknights_mower.utils.image import loadimg, loadres
sc = loadimg("/home/zhao/Documents/Desktop/ra_map.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
res = loadres("ra/map/资源区_射程以内", True)
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
matcher = Matcher(sc)
matcher.match(res, draw=True)
import cv2
from arknights_mower.utils.vector import va
result = cv2.matchTemplate(sc, res, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
print(max_val)
h, w = res.shape
scope = max_loc, va(max_loc, (w, h))
print(scope)
img = cv2.cvtColor(sc, cv2.COLOR_GRAY2RGB)
cv2.rectangle(img, scope[0], scope[1], (0, 255, 0), 3)
plt.imshow(img)
plt.show()
from arknights_mower.utils.log import logger
logger.setLevel("INFO")
%timeit matcher = Matcher(sc)
%timeit matcher.match(res)
%%timeit
result = cv2.matchTemplate(sc, res, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
目标图像截取
目标图像应尽量满足特征点数量多、尺寸小、与其它目标图像有较大区别。例如对于按钮,只截取按钮中独特的图案,或在文字中截取 3-5 字的关键词,相比截取整个按钮,往往能得到更好的效果。
随机性
FLANN 和 RANSAC 算法具有一定的随机性。若结果变化较大,可考虑重新截取目标图像,或换用其它匹配方式。
多目标
当目标图像在截图中多次出现时,目标图像的特征点与截图对应特征点的若干对匹配距离相近。在应用比例测试筛选特征点时,这些匹配都会被过滤掉,导致目标图像多次出现时无法匹配到结果。
如果要处理目标多次出现的情况,如果能够预知出现的范围,可以指定 scope
参数进行限制。
查看例子
from matplotlib import pyplot as plt
from arknights_mower.utils.image import loadimg, cropimg
from arknights_mower.utils.matcher import Matcher
sc = loadimg("/home/zhao/Documents/mower-profile/screenshot/1106/20240712065212.png", True)
plt.imshow(sc, cmap="gray", vmin=0, vmax=255)
plt.show()
res = cropimg(sc, ((322, 530), (363, 572)))
plt.imshow(res, cmap="gray", vmin=0, vmax=255)
plt.show()
matcher = Matcher(sc)
matcher.match(res, draw=True)
matcher.match(res, draw=True, scope=((1360, 466), (1830, 940)))
matcher.match(res, draw=True, scope=((300, 465), (700, 940)))