之前做一个截图需求的时候,根据资料实现了利用GDI+或者DirectX获得任一窗口的截屏。在制作完成之后,给自己提了两个新的需求:滚动截屏和视频截屏。这篇文章主要讨论滚动截屏的实现。

滚动截屏主要由两个模块构成:

  • 首先要让窗口“滚”起来,获取到每次滚动的画面
  • 然后把几张截图合成一个长图

首先讨论“滚”的实现,我尝试了三种方法:

  • 使用SendInput,向指定窗体发送鼠标中键的输入,只能根据mouseData字段确定滚动多少行;
  • 发送WM_VSCROLL消息,可以传入SB_LINEDOWN或者SB_PAGEDOWN参数;
  • 发送WM_MOUSEWHEEL消息,其中wParam的低四位可以确定中键按下,高四位设置成滚动行数(乘120,正数向前滚,负数向后滚,说到这里,如果用户设置了鼠标滚动方向为反向,那这个参数一定要改),lParam的低高四位分别是鼠标指针的x、y坐标。同方法(1),也能控制滚动多少行。

三种方法的共同点是都只能按行数进行滚动,一次滚动的行数是在这里定义的,大家应该都手动设置过:

那么一行的高度是多少像素呢?答案是不确定,跟每个应用程序设置的该行的字体大小有关。又考虑到Windows在高DPI情况下对文本进行的缩放操作,行高变得更加的不确定。这也为后面合成长图造成了一定困难。嗯这个坑后面再填,继续讨论“滚”的问题。

后两种直接发送Windows消息的方法,经过验证需要发送到正确的Window才能起作用。有人会问,我直接获取到一个窗口的句柄就好了呀。事实上一个窗口内部可能包含多个可滚动的Window,如果不指定是对哪个Child Window进行滚动的话,直接对主窗口发消息是没有任何效果的。使用Spy++也可以看到,对于IE浏览器,要追溯到直接显示Content的Child Window需要解析多少层:

所以最终使用第一种SendInput的方法,对指定窗口发送一个输入,让系统判断该让谁滚动起来。对于浏览器而言,通常只有网页内容是可滚动的,这样就不需要再手动对地址栏、导航栏做类型判别了;而对于有多个可滚动Window的窗体,需要把鼠标指针放在要滚动的地方,这也不难做到。

小结一下,我们现在已经可以做到让任一窗口滚动起来了,并且可以控制滚动的行数,先小小高兴一下,因为大麻烦在后头XD

==================我是华丽丽哒分割线====================

经过上面的步骤,我们获取到了三张示例截图,接下来就要用这三张图生成一张长图:

程序中我们不对这些截图进行输出,而是使用Bitmap对象在内存中操作。接下来需要思考长图要怎样生成了。

事实上滚动截屏的过程中,对每张截图是有要求的:每次不能滚动太多,要恰好留出一部分内容来,让这部分内容也出现在下次滚动的画面中。利用这部分区域,就能生成连续的画面。

在每张截图中,发生变化的只有显示网页内容的区域,而标题栏、导航栏、网页的Head Bar和窗口底部的状态栏都是没有产生变化。好,那这部分内容就是首先需要剔除掉的。然后要做的,就是获取每两张截图的公共区域,得到公共区域分别出现在两张截图中的位置,在合成时将第二张图的坐标减去公共区域的高度,这样就能完美的合成出互不重复的长图了。

经过以上思考,给自己列出的开发安排如下:

  • MileStone 1、实现向某个窗口发送滚动消息,然后看着它滚动起来(需要完成Get窗口句柄、向其发送消息的工作)
  • MileStone 2、实现将每次滚动后的截图保存下来(需要完成按窗口句柄截屏并保存截图的工作)
  • MileStone 3、实现检测每张截图中发生变化的区域(需要按像素检查,涉及细节比较多)(方法:逐行扫描,找出相同的部分,有上、下两部分,那么取中间不同的部分就是发生变化的区域了)
  • MileStone 4、实现检测该区域中,每次滚动内容的起点和终点,即试图查找尾部的重合区域的起始行号(方法:在变化区域中逐行扫描,找出相同的部分,那么这部分的line_number_start和line_number_end就是区域边缘了)
  • MileStone 5、实现拼接截取出来的连续内容的方法

MileStone 1、2的代码太菜,就直接贴上不解释了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
VOID ScrollLines(const UINT lines = 5)
{
INPUT in;
in.type = INPUT_MOUSE;
in.mi.dx = 0;
in.mi.dy = 0;
in.mi.dwFlags = MOUSEEVENTF_WHEEL;
in.mi.time = 0;
in.mi.dwExtraInfo = 0;
in.mi.mouseData = 0-120*lines;
SendInput(1,&in,sizeof(in));
}

VOID MakeScrollingShotsToFile(const UINT count = 5, const UINT eachScrollLines = 5, const TCHAR* saveDirPath=NULL, const UINT delayMiliSeconds=1000)
{
UINT counter = count;
while (counter-->0)
{
ShotForeWindowToFile(saveDirPath);
ScrollLines(eachScrollLines);
Sleep(delayMiliSeconds);
}
}

BOOL ShotForeWindowToFile(const TCHAR* saveDirPath=NULL)
{
HWND hTargetWindow = GetForegroundWindow();
WINDOWPLACEMENT windowPlacement;
windowPlacement.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(hTargetWindow, &windowPlacement);
int left, right, top, bottom;
switch(windowPlacement.flags)
{
case 0:
left = windowPlacement.rcNormalPosition.left;
right = windowPlacement.rcNormalPosition.right;
top = windowPlacement.rcNormalPosition.top;
bottom = windowPlacement.rcNormalPosition.bottom;
break;
case 2:
left = 0;
right = 0;
top = 0;
bottom = 0;
break;
case 1:
std::wcout<<"Window minimized..."<<std::endl;
return FALSE;
default:
std::wcout<<"Something happened..."<<std::endl;
return FALSE;
}
ScreenShotToFile(saveDirPath, left, top, right, bottom);
return TRUE;
}

//ScreenShotToFile和ScreenShotGDIToFile函数的实现在上一篇文章中提到过,就不再重复了,前者是对后者GDI方式和DX方式实现的包装

MileStone 3、4、5:

在准备直接对Bitmap对象进行操作时,发现微软只提供了GetPixel方法用来按照(x,y)位置坐标获取每个点的像素信息,想想看遍历一副1024x768的图像要调用七十多万次这个函数,后果简直……

所以放弃使用Bitmap类,考虑将字节数组导出,直接对其进行处理。

其实答主本科学的专业就是做图像处理(咦,答主?额,知乎上多了= =),很快想到了用Matlab对上面提出的拼接方法进行快速验证,验证成功后再用C++实现一遍,这样最后写C++的时候也踏实一点。

考虑到Matlab处理矩阵的便利程度,还有对后期移植到C++的需要,要尽量少的使用Matlab的高级函数,不然在Matlab里写起来方便,用C++重写时会蛋疼无比。好吧,如果你问哪些算是Matlab的高级函数……

imabsdiff:求图像差异,比较两幅图像的绝对差异,差值小于0时,赋绝对值,可直接用来检测变化区域(和不变的区域XD)。

1
2
>> Image1=imload('S1.bmp'); Image2=imload('S2.bmp');
>> figure;imshow(imabsdiff(Image1,Image2));

输出结果是这样的:

其中被减掉的黑色区域就是两张截图中的公共区域:IE标题栏、导航栏、网页的Header Bar、状态栏。比起在Bitmap类里一个pixel一个pixel地手动操作像素、或是一层一层for循环里先把unsigned short转换成signed int16然后依次处理RGB三个颜色通道接着#¥%…(_…&*说不下去了有没有,感动到哭有没有

ismember:判断一个变量是否属于第二个变量,可用来检测第一张图像中的某一行是否出现在第二张图像中。

1
2
>> [existence, linenumber]=ismember(Image1(:,:,1),Image2(:,:,1),'rows'); %只处理红色
>> plot(existence);

你会得到一张图表:

纵轴只有俩值,0和1。那么这个小表子代表什么意思呢?代表第一张图里的每一行(行号是横坐标)在第二张图里是否存在内容完全一样的行。existence是一个1乘以图像高度的布尔矩阵,linenumber是同样尺寸的矩阵,里面的值是如果对应行存在的话,对应第二张图的行号。直接定位出滚动过程中两张图衔接的区域有没有!!!

matchFeatures:寻找匹配的特征点。

1
>> [match_bool,match_int]=matchFeatures(Image1(:,:,1),Image2(:,:,1));

这次代码更短是不是?只有一行,让我们瞧瞧match_bool这个矩阵里写的是什么:

第一张图里的第924行,对应第二张图里的第273行,还有在上面没显示出来的,第762行对应第二张图里的第111行。那么我们看看这几行之间对应哪些信息呢?

还用再说什么吗?开头写完这一行,就可以直接写程序结尾了有没有!!!

答主眼睁睁地看着自己玩high了,继续敲出下面的代码:

1
2
3
4
5
6
7
8
9
10
Gray1=rgb2gray(Image1);Gray2=rgb2gray(Image2); % 转灰度图
% indexPairs=matchFeatures(Gray1,Gray2);
points1 = detectHarrisFeatures(Gray1); % 使用Harris检测算法求角点
points2 = detectHarrisFeatures(Gray2);
[features1, valid_points1] = extractFeatures(Gray1, points1); % 提取相邻特征
[features2, valid_points2] = extractFeatures(Gray2, points2);
indexPairs = matchFeatures(features1, features2); % 匹配两张图的特征
matchedPoints1 = valid_points1(indexPairs(:, 1), :); % 第一张图像的原始点
matchedPoints2 = valid_points2(indexPairs(:, 2), :); % 第二张图像的对应点
figure; showMatchedFeatures(Gray1, Gray2, matchedPoints1, matchedPoints2);

大概标了下注释,其中提到了“角点”,那么角点是什么呢?

原谅我直接把专业课的PPT拉了过来,怎么样,没看懂吧?

课上直接放这个我也没懂,哈哈,最讨厌这么有意思的东西全被学术用语给毁了,那么角点是什么呢?类似抛物线中由升转降的顶点,角点就是图像中黑块和白块之间的连接点,如果你玩过“别踩白块”的话,一眼就能看懂(角点在交界处,用绿色标出):

在一般的图像中,这些点就是角点:

那么得到角点意味着什么呢?比如返校前我在我的工位拍了张图留作纪念,在毕业后回到工位又拍了一张,位置相同,但亮度、色调、曝光值总会有所出入,将角点作为图像的特征点,就能得到如下结果:

直接识别出临走前物品摆放的位置,还能跟现在的位置对应起来有没有!!!

注意上面求角点所用的Harris算法,如果写成“detectHarrisFeatures(gpuI,……)”还可以用GPU来加速,硬件加速啊有没有有没有!!!

都解释完了,那么把两张截图放上去是什么效果呢?

可以看出地址栏的文字被识别到了、选项卡上网页的标题被识别到了、甚至“MSDN中文网”几个大字也被识别到了(好吧并没有”D”),什么?你问那片shi黄色的东西是什么鬼?瞅见黄色区域上边儿的绿色标记没有?再看看底下的密集的红色标记,这就是刚才提到的“两张图之间衔接的区域啊”……就算不用来做滚动截屏,把截图中的“人物特征”提取出来,直接扔到黄色图片数据库的分类器里,直接就是黄图检测有没有,妈妈再也不用担心我的学习!!!

导师问我为什么跪在Matlab面前敲代码。。。

==================我是华丽丽哒分割线====================

好了,规则如下:不准使用Matlab的高级函数,载入和绘图函数除外,按照最开始提出的思路依次检测:网页内容区域、两张图衔接区域的行号,然后把两张图连起来。前两步直接逐行扫描,至于拼接图片嘛……你猜在Matlab里要怎么写?

1
2
3
4
5
%拼两张图
Image12 = [Image1(起始行号:结束行号,:,:); Image2(起始行号:结束行号,:,:)];

%拼三张图
Image123 = [Image1(起始行号:结束行号,:,:); Image2(起始行号:结束行号,:,:); Image3(起始行号:结束行号,:,:)];

那么我们现在可以直接看代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
Image1=imread('S1.bmp'); Image2=imread('S2.bmp'); Image3=imread('S3.bmp');
% turn uint8 into double used in matrix
Image1=im2double(Image1); Image2=im2double(Image2); Image3=im2double(Image3);
% retrieve rgb matrix from above two image
R1=Image1(:,:,1); G1=Image1(:,:,2); B1=Image1(:,:,3);
R2=Image2(:,:,1); G2=Image2(:,:,2); B2=Image2(:,:,3);
[height,width,color]=size(Image1);
% difference value line by line
line_diff=zeros(height,1);
for i=1:height % lia=ismember(R1,R2,'rows');
%line_diff[i,1]
for j=1:width
line_diff(i,1)=line_diff(i,1)+abs(R1(i,j)-R2(i,j));
line_diff(i,1)=line_diff(i,1)+abs(G1(i,j)-G2(i,j));
line_diff(i,1)=line_diff(i,1)+abs(B1(i,j)-B2(i,j));
end
end
% draw line_diff
figure;
plot(line_diff);
title('Each Line Difference');
% get indexs of area of content start and end
content_linenumber_start=1; content_linenumber_end=height;
for i=1:height
if line_diff(i)<=0 % threshold
content_linenumber_start=content_linenumber_start+1;
else
break;
end
end
for i=height:-1:1
if line_diff(i)<=0 % threshold
content_linenumber_end=content_linenumber_end-1;
else
break;
end
end
figure;
subplot(1,3,1),
imshow(Image1(content_linenumber_start:content_linenumber_end,:,:));
title('Content Area 1');
subplot(1,3,2),
imshow(Image2(content_linenumber_start:content_linenumber_end,:,:));
title('Content Area 2');
subplot(1,3,3),
imshow(Image3(content_linenumber_start:content_linenumber_end,:,:));
title('Content Area 3');
% get indexs of repeated content start and end
image_1_end=0;image_2_start=0; % sencond value useless
% [bool_line_exist_r,int_line_number_r]=ismember(Image1(content_linenumber_start:content_linenumber_end,:,1),Image2(content_linenumber_start:content_linenumber_end,:,1),'rows');
% [bool_line_exist_g,int_line_number_g]=ismember(Image1(content_linenumber_start:content_linenumber_end,:,2),Image2(content_linenumber_start:content_linenumber_end,:,2),'rows');
% [bool_line_exist_b,int_line_number_b]=ismember(Image1(content_linenumber_start:content_linenumber_end,:,3),Image2(content_linenumber_start:content_linenumber_end,:,3),'rows');
bool_line_match_r=false(height,1); bool_line_match_g=false(height,1); bool_line_match_b=false(height,1);
int_line_match_r=zeros(height,1); int_line_match_g=zeros(height,1); int_line_match_b=zeros(height,1);
bool_line_match=false(height,1);
int_line_match=zeros(height,1);
for i=1:1:height % for each line in Iamge1
for j=1:1:height % for each line in Image2
if sum(abs(R1(i,:)-R2(j,:)))<30 % threshold
bool_line_match(i,1)=true;
int_line_match=j;
end
end
end
figure;
plot(bool_line_match);
title('Line match result');
for i=content_linenumber_end:-1:content_linenumber_start
if bool_line_match(i)==0
image_1_end=i;
break;
end
end
% Junction
Image12=[Image1(content_linenumber_start:image_1_end,:,:);Image2(content_linenumber_start:content_linenumber_end,:,:)];
figure;
imshow(Image12);
title('Image 1&2 Junction Result');
imwrite(Image12,'S12.bmp','bmp');
Image123=[Image1(content_linenumber_start:image_1_end,:,:);Image2(content_linenumber_start:image_1_end,:,:);Image3(content_linenumber_start:content_linenumber_end,:,:)];
figure;
imshow(Image123);
title('Image 1&2&3 Junction Result');
imwrite(Image123,'S123.bmp','bmp');

首先读取图像、分别提取RGB通道到R1 R2 G1 G2 B1 B2矩阵,对每行求差,结果存储在line_diff中,line_diff被图像化是这样的:

注意到开头和结尾处、纵坐标为0的那两段了吗?这就是之前所说的,IE标题栏、导航栏、网页Header bar、底部状态栏,把这两段的行号保存下来,我们就可以提取到截图中的主要内容了。

然后逐行扫描,得到第一张图里的每一行,在第二张图中是否存在内容一致的行,以及对应的行号。其中表示是否存在的布尔值,绘制成图表是这样子的:

注意到尾部连续的纵坐标为1的那段儿了吗?其中后半部分是刚才检测到的底部状态栏——他们在两张图中也是完全一致的,前半部分就是两张图衔接区域了。将其对应的行号保存下来,直接完成图像拼接。

大功告成,接下来做性能分析:

对于一般网页,不需要分别处理RGB三个色彩通道,在进行处理之前,首先将其转换成灰度图像是比较明智的做法。在使用C++实现时可以采用这一点。另外,由于是逐行扫描,需要花费O(n)的时间,如果对两张图做交叉扫描,就需要O(n^2)的时间了,显然不合适。考虑到交叉扫描后最终提取到的衔接区域很小,可以调整循环的步长,在循环结束后在对衔接点附近的行进行精确扫描,可以大大降低在逐行扫描中浪费的时间。

鉴于这次程序设计只是使用Matlab做出原型,对源图片有一定的要求:

  • 图像不允许进行有损压缩,不然难以判别图像矩阵是否相等
  • 要求内容区域以外的地方不可产生变化,比如标题栏如果在截图过程中不断变色,就会误检成内容区域

对于后者,可以判别两行矩阵是否一致时,允许在阈值范围内波动,均算作一致。阈值不建议设置过大,否则会干扰识别过程。

最终成功验证了文章开头提出的长图拼接方案,后续使用C++重写就能做出一个实用的滚动截图工具了。使用图像检测的方法,其优势在于图片的衔接处会比较完美,难以看出差别。如果采用事先算出滚动的像素数、然后进行拼接的方法,如有干扰,或者在根据字号计算行高时四舍五入了半个像素,乘以行数,就会产生几个像素的偏移,而这种偏移在滚动截图的结果中,会直接表现为一行文本的重合或残缺,这显然是不可接受的。

最后提一下全景图片的拼接,这个对算法的鲁棒性要求更高,应该会直接在频域而不是时域上进行处理了,对专业素养要求也更高一些。

好像没什么可说的了,那就结束吧:)