Delphi下实现全屏快速找图找色 一、数据提取
前言
最近有好几个朋友都在问我找图找色的问题,奇怪?于是乎写了一个专门用于找图找色的单元文件“BitmapData.pas”。在这个单元文件中我实现了从文件中导入位图、屏幕截图、鼠标指针截图、在图片上查找子图、在图片上查找颜色等功能。在查找过程中可以设定颜色变化范围、可以从左到右从上到下查找、也可以从指定点向四周查找。关于这个文件的下载和使用,可以参考本文的第四节。下面详细说说这些功能的实现。
一、数据提取
位图其实可以看成是一个由象素组成的矩阵,找图找色可以看成是象素值的比对。很多新手在设计这类的程序时喜欢使用TBitmap.Canvas.Pixels属性,这个属性其实是对API函数GetPixel的封装,这个函数执行速度是很慢的,主要用来对位图象素进行偶尔的访问。而比对过程中需要对象素进行频繁的访问,造成程序运行缓慢。另外一种方法是使用TBitmap.ScanLine属性,利用它可以直接访问位图的数据。但是这些数据和当前位图的格式有关,主要是色深方面的问题,不同的色深会有不同格式的数据。另外比对过程中也需要对该属性进行频繁的调用。由于比对过程完全是数据的比较,不需要进行绘制操作。所以可以一次性将位图的数据提取出来放置到一个缓冲区中再进行比对,这样程序的性能会更高,也便于查找算法的实现。这时可以调用API函数GetDIBits获得设备无关位图的RGB数据,其实ScanLine属性也是调用这个函数实现的。GetDIBits函数格式声明如下:
function GetDIBits(
DC: HDC; //设备上下文句柄;
Bitmap: HBitmap; //位图句柄,注意不是TBitmap对象;
StartScan, //开始检索的第一条扫描线;
NumScans: UINT; //共检索的扫描线数;
Bits: Pointer; //数据缓冲区指针;
var BitInfo: TBitmapInfo; //位图信息结构,此结构确定了设备无关位图的数据格式;
Usage: UINT //指定TBitmapInfo结构的bmiColors成员的格式。
): Integer; stdcall;
其中TBitmapInfo结构的格式如下:
tagBITMAPINFO = packed record
bmiHeader: TBitmapInfoHeader; //位图信息头,该结构用于说明位图的格式;
bmiColors: array[0..0] of TRGBQuad; //颜色表,给出调色板数据。
end;
在上述结构中主要使用bmiHeader成员,TBitmapInfoHeader结构的格式如下:
tagBITMAPINFOHEADER = packed record
biSize: DWORD; //当前结构的大小;
biWidth: Longint; //以像素为单位,给出该结构所描述位图的宽度;
biHeight: Longint; //以像素为单位,给出该结构所描述位图的高度;
biPlanes: Word; //目标设备的平面数,必须为1;
biBitCount: Word; //每个像素所需要的位数,当图像为真彩色时,该成员的取值为24;
biCompression: DWORD; //位图的压缩类型,若该成员的取值为BI_RGB,则图像数据没有经过压缩处理;
biSizeImage: DWORD; //以字节为单位,给出图像数据的大小,若图像为BI_RGB位图,则该成员的值必须设为0;
biXPelsPerMeter: Longint; //以每米像素数为单位,给出位图水平方向的分辨率;
biYPelsPerMeter: Longint; //以每米像素数为单位,给出位图垂直方向的分辨率;
biClrUsed: DWORD; //位图实际使用的颜色表中的颜色变址数;
biClrImportant: DWORD; //位图显示过程中重要颜色的变址数。
end;
在上面两个结构中,bmiColours成员指向一个颜色表,它包含多少个表项是由bmiHeader.biBitCount成员定义。当该成员的取值为24时,则颜色表中的表项为空。当biBitCount取值24同时biCompression取值BI_RGB时表示当前位图为24位真彩色无压缩位图。这时可以将位图数据缓冲区看成是一个一维的字节数组。其中每3个字节代表1个像素。这3个字节以蓝(B)、绿(G)、红(R)为顺序,直接定义了像素颜色。这里要注意一个字节顺序,一般我们使用的TColor颜色格式是以红(R)、绿(G)、蓝(B)为顺序的RGB颜色,而缓冲区中使用的是顺序相反的BGR颜色。另外利用GetDIBits提取的位图数据是自下而上从左到右保存到缓冲区中的,即先保存位图最后一行从左到右的象素数据,再保存倒数第二行的数据,以此类推第一行最后保存。除了数据反相保存外,每行数据都以4字节(32位)对齐,一行数据的长度不能被4整除时就在每行的末尾填充值为0的字节使之能被4整除。例如:对于宽5象素的位图每行数据占16个字节,前15个字节每3个字节保存1个象素颜色,最后填充1个字节。对于宽10象素的位图每行数据占32个字节,前30个字节每3个字节保存1个象素颜色,最后填充2个字节。
知道了缓冲区数据的格式,就可以对缓冲区中的数据进行访问。现在给出相关访问的示范代码:首先位图数据缓冲区是一个一维的字节数组,那么这个数组Bits可以按以下代码进行定义:
type
TByteAry = array [0..0] of Byte;
PByteAry = ^TByteAry;
var
Bits : PByteAry;
接着假设有一个位图,高Height象素,宽Width象素。那么对齐后每行数据长度LineWidth字节可以用以下的代码计算出来:
LineWidth:=(((Width*24)+31) and ($7FFFFFFF-31)) shr 3;
于是前面数组Bits的大小Size就为:LineWidth*Height。对于任意一个象素在位图上的位置Left,Top(二维)可以用以下代码换算出该象素数据在数组Bits中的位置Off(一维):
Off:=((Height-Top-1)*LineWidth)+(Left*3);
假设一个BGR格式的颜色值Color,以下代码可以从数组Bits的Off位置读取一个象素颜色值:
Color:=((PInteger(@(Bits[Off])))^ and $FFFFFF);
使用GetDIBits函数后就可以不再使用TBitmap对象。以下的示范代码实现对当前屏幕的全屏截图,并将截图后的位图数据提取到缓冲区中返回:
procedure CopyScreen(var Bits : PByteAry; var Size : Integer);
var
Width,Height,LineWidth : Integer;
Wnd : HWND;
DC,MemDC : HDC;
Bitmap,OldBitmap : HBITMAP;
BitInfo : TBitmapInfo;
begin
//数据初始化
Width:=GetSystemMetrics(SM_CXSCREEN);
Height:=GetSystemMetrics(SM_CYSCREEN);
LineWidth:=(((Width*24)+31) and ($7FFFFFFF-31)) shr 3;
Size:=LineWidth*Height;
GetMem(Bits,Size);
//截图
Wnd:=GetDesktopWindow();
DC:=GetWindowDC(Wnd);
MemDC:=CreateCompatibleDC(DC);
Bitmap:=CreateCompatibleBitmap(DC,Width,Height);
OldBitmap:=SelectObject(MemDC,Bitmap);
BitBlt(MemDC,0,0,Width,Height,DC,0,0,SRCCOPY);
Bitmap:=SelectObject(MemDC,OldBitmap);
//位图信息初始化
with BitInfo.bmiHeader do
begin
biSize:=SizeOf(TBitmapInfoHeader);
biWidth:=Width;
biHeight:=Height;
biPlanes:=1;
biBitCount:=24;
biCompression:=BI_RGB;
biSizeImage:=0;
biXPelsPerMeter:=0;
biYPelsPerMeter:=0;
biClrUsed:=0;
biClrImportant:=0;
end;
//提取数据
GetDIBits(DC,Bitmap,0,Height,Pointer(Bits),BitInfo,DIB_RGB_COLORS);
DeleteDC(MemDC);
DeleteObject(Bitmap);
DeleteObject(OldBitmap);
ReleaseDC(Wnd,DC);
end;
对于标准的24位BMP位图文件,其中的位图数据也是以上述格式保存的。有的24位BMP文件并不标准,所以文件最好使用Windows自带的画图程序保存。以下的示范代码实现从标准的24位BMP格式文件中导入位图数据到缓冲区中返回:
procedure LoadFile(const FileName : string; var Bits : PByteAry; var Size : Integer);
var
Stream : TFileStream;
FileHeader : TBitmapFileHeader;
InfoHeader : TBitmapInfoHeader;
LineWidth : Integer;
begin
Stream:=TFileStream.Create(FileName,fmOpenRead);
//读取文件头
Stream.Read(FileHeader,SizeOf(TBitmapFileHeader));
Stream.Read(InfoHeader,SizeOf(TBitmapInfoHeader));
with FileHeader,InfoHeader do
begin
//确定图片格式
if (bfType<>$4D42) or (biSize<>SizeOf(TBitmapInfoHeader)) or
(biBitCount<>24) or (biCompression<>BI_RGB) then
begin
Bits:=nil;
Size:=0;
exit;
end;
//数据初始化
LineWidth:=(((biWidth*24)+31) and ($7FFFFFFF-31)) shr 3;
Size:=LineWidth*biHeight;
GetMem(Bits,Size);
end;
//读入数据
Stream.Read(Bits^,Size);
Stream.Free;
end;
综上所述,当位图数据提取到一个缓冲区中,找图找色就是对这个缓冲区中的数据进行访问的过程。而这个缓冲区可以作为一个矩阵来进行访问。只要对矩阵进行遍历就可以实现找图找色的算法。