如果你有一台苹果手机,想要截取超过一个屏幕高度的长截图,需要多次截屏之后,再使用另外一个APP,才能将多张图像拼接成一整张长截图:
如果你用的是安卓手机,可以使用系统自带的长截图功能。
但是有的APP不支持长截屏怎么办?有的画面比长截图能截取的范围还要长怎么办?长截图滚动的速度太慢了,还有更快的办法吗?
我就遇到了这样的场景,我希望获取微信步数排行榜单里的数据,但是没有找到抓包的接口,所以只能先截屏、然后再做图像识别。
但是微信步数排行的榜单实在太长了,截取完成一次要截好几屏,并且长截屏的速度真的太慢了。
于是我想到一个方法,先录制屏幕滚动的视频,然后再拼图长截屏,会不会更快一点呢?
需要注意以最高的画质和帧率屏幕录像,录屏画质越高,之后拼图的画质自然就越高。
并且录屏滚动的速度可以慢一点,避免两帧之间的画面移动的距离太远,为后面的拼图带来困难。
计算每两帧图像之间的偏移量,是能够合成长截图的基础。而能够找到两张图像的偏移量,首先要找到一种方法评估两张图像的重合度。
所以3.4
节虽然是程序运行的第4个步骤,但却我首先尝试解决的问题,我首先介绍这个环节:
将视频中的每一帧图像读取为矩阵,将两帧图像的矩阵偏移后相减,如果得到的值全部为零,那么这个偏移量就是这两帧图像偏移的距离。
但是事与愿违,视频中的画质是有损失的。图像重叠区域的像素并不精确相等,只是近似一致。所以需要找到一种评估两个区域像素相似程度的指标。
将重叠区域的像素数值相减,然后再取绝对值的平均值,作为评估相似程度的依据,数值越小则说明相似程度越高:
def overlay(d1, d2, d):
'''计算重叠部分差异绝对值的平均值'''
L1 = d1.shape[0]
L2 = d2.shape[0]
dd1 = d1[max(0, d):min(L1, L2+d)]
dd2 = d2[max(-d, 0):min(L1-d, L2)]
return np.abs(- 1 * dd2 + dd1).mean() # safe plus
将两张图像从上到下滑动匹配一遍,找到重合度最高、也就是差值绝对值的平均值最小的那一次,认为是两帧图像的相对偏移量:
def offset(img1, img2):
'''计算两张图片的平移偏移量'''
height = img1.shape[0]
avg = np.inf
for h in range(- 100, height // 2): # 搜寻范围
avg1 = overlay(img1, img2, h)
if avg > avg1:
avg = avg1
dh = h
return dh, avg
这个方法得到了不错的运行结果,但是我很快就发现了问题——运行效率太低。
计算两帧图像的数据需要好几秒钟,视频一秒就可以有60帧,十几秒就可能有上千帧。总时间太长了,还有别的更好的办法吗?
最近在学习图像识别算法,了解到有一个叫做模板匹配的算法2。Python的图形处理cv2
库内建这种算法,参数method
有3种可选的匹配模式,本例中设置为了cv2.TM_SQDIFF_NORMED
,其他模式经测试运行速度和效果也基本一致。
计算运行720×1306分辨率的图像,速度大约可以达到每秒计算15帧:
def match(image, templ, method):
'''模板匹配图像位置'''
th, tw = templ.shape[:2]
result = cv2.matchTemplate(image, templ, method)
result2 = (result - result.min()) / (result.max() - result.min())
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
return min_loc if method == cv2.TM_SQDIFF_NORMED else max_loc
使用迭代器逐帧读取视频中的图像数据:
def imiter(file, st=None, ed=None):
'''封装的视频读取迭代器'''
cap = cv2.VideoCapture(file)
while cap.isOpened():
ret, img = cap.read()
if not ret:
cap.release()
return
yield img[st:ed]
屏幕录像中,顶部状态栏和底部导航栏有时会占用一部分高度,但这不是我们需要拼接的内容。我在函数中设置了st
和ed
参数,可以手动设定需要裁切的高度。
也可以自动测量,逐一读取每行的像素数据,计算每行的方差,找到数值突变的那个部分,就是需要裁切的起始和结束位置。
def repeat(file):
'''计算视频中存在变化的区域起止范围'''
data = []
for img in imiter(file):
h, w, n = img.shape
data.append(img.reshape((h, -1)).mean(1))
data = np.array(data)
var = data.var(0)
where = np.where(var > var.mean())[0]
return where.min(), where.max() + 1
综合前面的方法,就可以计算出录屏视频中各帧图像的偏移数值序列,将图像拼接成完整的长截图。
图像拼接有两种策略:
第一种方法,将新一帧的追加内容直接拼接在图像的底部:
def join_bottom(file, gaps, st=None, ed=None):
'''滚动拼接到图片底部'''
imgs = imiter(file, st, ed)
data = next(imgs)
for n, (img, gap) in enumerate(zip(imgs, gaps)):
data = np.concatenate([data, img[img.shape[0] - gap:]])
return data
第二种方法,将重叠区域的图像平均后再保存。对于屏幕录屏来说,每一帧的画质是有损失的,进行平均处理后,画质会有明显的提升:
def join_avg(file, gaps, st=None, ed=None):
'''加权拼贴图像'''
imgs = imiter(file, st, ed)
data = next(imgs).astype(np.int16)
data = np.concatenate([data, np.zeros((sum(gaps), *data.shape[1:]), np.int16)]) # 扩展填0
weight = np.zeros_like(data)
height = data.shape[0]
st = 0
for n, (img, gap) in enumerate(zip(imgs, gaps)):
st += gap
ed = min(height, st + img.shape[0])
data [st:ed] += img[:ed-st]
weight[st:ed] += np.ones_like(img[:ed-st])
return data // weight
不过这种方法,一旦某两帧的偏移值计算出现错误,拼接就会错位,导致保存图像出现重影,并且不容易被看出来。
把之前的内容串起来,就能得到录屏视频生成长截图的程序,可以留给读者作为课后练习。
我也把完整的程序、和测试用例上传到了网盘,有需要的同学可以自行下载运行:
https://download.csdn.net/download/weixin_39804265/84993982
测试了一段12秒的录屏视频,总共908帧,合成了720×50557分辨率的长截图,相当于39个屏幕高度,总共用时62.98秒,平均每秒处理14.4帧图像。
而之前用的滑动对齐法,同样的908帧的视频,总共用时,呃。。7853秒,也就是2小时11分钟。。耗时为模板匹配方法的124.7倍,平均每帧用时8.66秒。
使用手机录制示例:
程序拼接效果示例:
Picsew——iPhone上的截图工具: https://baijiahao.baidu.com/s?id=1660058225659119872 ↩︎
python实现模板匹配: https://www.cnblogs.com/april0315/p/13741888.html ↩︎