资讯动态

Delphi屏幕找图实战:模板匹配、DPI缩放与坐标偏移优化

发布时间:2026/10/25 2:14:34 来源:尧图企业网站定制
简介面向Delphi开发者以及从事自动化测试、游戏辅助、屏幕抓取的程序员这份下载包围绕“屏幕找图与快速图像匹配”展开定位在帮助读者理解从屏幕捕获、目标图定位到匹配结果判定的实现路径。压缩包约6.63MB文件总数与类型明细暂未提供分别显示为0和暂无数据具体目录与文件构成需以下载后的实际内容为准。已有541人学习下载说明该主题在Delphi实践社区具有一定关注度。内容涉及BitBlt等屏幕抓取方式模板匹配中的像素差分比较平均颜色哈希、差分哈希与感知哈希的差异以及SIFT、SURF、ORB等特征匹配思路同时讨论滑动窗口、多线程与GPU加速对效率的影响并提醒用阈值控制误报和漏报。描述中的TBitmap与ScanLine示例可作为理解像素级比较的切入点便于读者按截图尺寸、匹配精度和性能要求选择方案。1. 屏幕匹配图片为什么总在坐标上翻车Delphi 找图的四个变量桌面自动化里最怕的不是功能写不出来而是昨天还能点中的按钮今天换了个分辨率就偏了 30 像素。Delphi 找图这件事说到底就是“截屏 模板匹配 坐标回写”但真正决定它能不能稳定跑的是四个变量截图区域是否包含目标、像素格式和容差是否匹配、扫描步长是否合理、坐标是否经过 DPI 换算。这份资源适合正在用 Delphi 做 RPA、批量录入、测试辅助的开发者不适合要求毫秒级响应、目标长期被遮挡或画面走硬件加速的场景。下面按“先取图、再比对、后封装、最后排坑”的顺序拆开讲代码可以直接改成自己的单元。2. 屏幕取图与像素比对BitBlt、ScanLine 和容差阈值怎么定2.1 取图链路从 GetDC 到 TBitmap 的内存布局屏幕找图的第一步不是比对而是拿到一张颜色值可信的位图。Delphi 里常见做法是用GetDC(0)拿桌面 DC再用BitBlt把指定矩形拷到TBitmap.Canvas。这里有一个容易被忽略的点TBitmap默认可能是pfDevice或pf24bit而后面用ScanLine按 32 位像素读的时候字节序和行宽会对不上表现出来就是颜色错位、匹配率极低。我一般会强制PixelFormat : pf32bit让每个像素固定 4 字节顺序按小端是 B、G、R、A比对时只读前三个字节Alpha 单独处理。取图函数还要处理异常释放。GetDC(0)拿到的 DC 必须ReleaseDCTBitmap创建失败或BitBlt抛异常时也要保证对象被释放否则跑久了就是句柄和内存一起涨。下面这个版本把截图区域、像素格式和释放顺序都固定下来适合作为找图链路的入口。uses Windows, Graphics, SysUtils; // 截取屏幕指定矩形统一转为 32 位像素便于 ScanLine 按 BGR(A) 读取 function CaptureScreen(const ARect: TRect): TBitmap; var ScreenDC: HDC; begin Result : TBitmap.Create; try Result.PixelFormat : pf32bit; Result.SetSize(ARect.Right - ARect.Left, ARect.Bottom - ARect.Top); ScreenDC : GetDC(0); try // SRCCOPY 直接拷贝颜色不做混合适合大多数桌面窗口 BitBlt(Result.Canvas.Handle, 0, 0, Result.Width, Result.Height, ScreenDC, ARect.Left, ARect.Top, SRCCOPY); finally ReleaseDC(0, ScreenDC); end; except Result.Free; raise; end; end;逻辑上ARect是屏幕物理坐标下的矩形左上角为(ARect.Left, ARect.Top)Result.Width/Height决定位图大小后面模板比对时屏幕位图必须比模板大。参数上PixelFormat : pf32bit是整条链路的关键别在比对函数里再猜格式SRCCOPY表示只拷颜色不做 Alpha 混合桌面截图一般够用。如果截出来全黑先别怀疑匹配算法拿一个空白桌面区域截一张保存成 BMP 看颜色是否正常这是排查黑匣子的第一步。2.2 像素比对核心循环为什么 ScanLine 比 Pixels 快一个量级模板匹配的本质是滑动窗口把模板图放到屏幕图的每一个候选左上角逐像素比较颜色差异。Delphi 里最容易写出来的是Canvas.Pixels[x, y]代码短但每次访问都要走 GDI 的GetPixel/SetPixel在 1920×1080 的屏幕上跑一遍就是灾难。更常见的做法是用TBitmap.ScanLine[y]拿到行首指针再按字节偏移读像素。32 位像素下第x个像素的偏移是x * 4B、G、R、A 依次排列。这样写出来的循环是纯内存访问速度差一个量级并不夸张。比对时还要决定“怎么算匹配”。绝对相等太脆弱窗口边框、字体抗锯齿、颜色管理都会让同一个按钮差几个色阶容差太大又会把相邻图标认成目标。我一般用“最大通道差”做判定B、G、R 三个通道分别求绝对差只要有一个通道超过阈值就立刻退出当前候选点全部通道都在阈值内才算命中。这样比算平均差更严格也能提前剪枝。模板带透明通道时Alpha 为 0 的像素跳过不参与比较但要注意如果模板整张图 Alpha 都是 0就会变成“任何位置都匹配”所以模板预处理时要么保留不透明区域要么给 Alpha 填 255。// 在屏幕位图的 (ALeft, ATop) 处比对模板AScore 返回粗略相似度 function MatchAt(const ScreenBmp, TemplBmp: TBitmap; ALeft, ATop, ATolerance: Integer; out AScore: Double): Boolean; var tx, ty, diff, maxDiff: Integer; ScreenLine, TemplLine: PByte; begin Result : False; AScore : 0; maxDiff : 0; for ty : 0 to TemplBmp.Height - 1 do begin ScreenLine : ScreenBmp.ScanLine[ATop ty]; TemplLine : TemplBmp.ScanLine[ty]; for tx : 0 to TemplBmp.Width - 1 do begin // 32 位像素B,G,R,AAlpha0 的模板像素视为透明不参与比对 if TemplLine[tx * 4 3] 0 then Continue; diff : Abs(ScreenLine[(ALeft tx) * 4 0] - TemplLine[tx * 4 0]); if diff maxDiff then maxDiff : diff; if maxDiff ATolerance then Exit; diff : Abs(ScreenLine[(ALeft tx) * 4 1] - TemplLine[tx * 4 1]); if diff maxDiff then maxDiff : diff; if maxDiff ATolerance then Exit; diff : Abs(ScreenLine[(ALeft tx) * 4 2] - TemplLine[tx * 4 2]); if diff maxDiff then maxDiff : diff; if maxDiff ATolerance then Exit; end; end; Result : True; AScore : 1 - maxDiff / 255; // 仅作粗筛参考最终判定仍以容差为准 end;这段代码的逻辑是先按行遍历模板再按列遍历遇到透明像素跳过遇到任一通道超差立即退出。参数ATolerance取 0 到 255实际项目里常见 8 到 20文字边缘和抗锯齿明显的模板可以放到 20 到 30纯色图标可以压到 5 到 10。AScore只是用最大差异反推的粗相似度不适合当唯一判定条件真正决定命中与否的是ATolerance。另外调用前必须保证ATop TemplBmp.Height ScreenBmp.Height、ALeft TemplBmp.Width ScreenBmp.Width边界判断放在外层循环别让ScanLine越界。2.3 容差与步长把误匹配和耗时压到可接受范围容差不是越小越好也不是越大越稳。容差太小模板图和屏幕图只要有一个像素受字体平滑影响就找不到容差太大同一排按钮里相邻的图标会被认成同一个。我的血泪经验是先把模板图从屏幕上按 1:1 裁下来不缩放、不压缩、不带鼠标指针然后用 0 容差跑一次确认能命中再逐步加到 8、12、16看候选点数量。如果 8 就能稳定命中就不要为了“保险”加到 30否则多目标场景会冒出一堆重复结果。步长是另一个容易被忽视的参数。全量扫描的复杂度大约是(W-w1) × (H-h1) × w × h屏幕越大、模板越大耗时越夸张。常见做法是先按Step2或Step3跳着扫命中后再在命中点附近做一次全像素复核如果目标可能落在奇数坐标上粗扫阶段可以只跳行不跳列或者用Step2加邻域补扫。步长不是越大越好Step3时一个 30×30 的模板可能直接跳过目标尤其是小图标。我一般把步长和模板尺寸绑定模板宽高都大于 40 像素时用 2小于 40 像素时用 1先用小步长保证召回再谈性能。注意容差和步长要一起调。只调容差会把误匹配放大只调步长会把漏匹配放大每次改完参数至少用三种分辨率各跑一遍。3. 从模板图到屏幕坐标Delphi 找图函数的完整落地3.1 模板图预处理裁剪、格式统一与预乘 Alpha 的坑模板图的质量直接决定找图的上限。最常见的问题是模板图从网页或设计稿里拿尺寸和屏幕上的实际渲染尺寸不一致比如设计稿是 2 倍图屏幕上是 1 倍图像素比对必然失败。我一般要求模板图从目标屏幕上直接截取裁剪时只保留目标本身不要把窗口阴影、边框渐变、相邻文字带进去带得越多容差越难调。裁剪完统一存成 PNG 或 BMP加载后强制转成pf32bit和屏幕位图的像素格式保持一致。如果模板是 PNG 带透明背景加载后要确认 Alpha 通道还在。Delphi 自带TPngImage可以读 PNG但TBitmap.Assign之后透明像素的 RGB 可能是 0Alpha 可能是 0如果后面比对时跳过 Alpha0 的像素这没问题但如果误把整张图 Alpha 都抹成 255透明区域就会变成黑色参与比较匹配率立刻掉下去。下面这个加载函数按扩展名分流PNG 走TPngImage其他格式走LoadFromFile最后统一pf32bit。参数上AFile是模板路径ABmp是调用方创建好的位图函数内部不负责释放ABmp。uses Graphics, SysUtils, Vcl.Imaging.pngimage; // 加载模板图统一转 32 位像素PNG 额外保留 Alpha 通道 procedure LoadTemplate(const AFile: string; ABmp: TBitmap); var png: TPngImage; begin if LowerCase(ExtractFileExt(AFile)) .png then begin png : TPngImage.Create; try png.LoadFromFile(AFile); ABmp.Assign(png); ABmp.PixelFormat : pf32bit; finally png.Free; end; end else begin ABmp.LoadFromFile(AFile); ABmp.PixelFormat : pf32bit; end; end;逻辑说明PNG 分支先用TPngImage解码再赋给TBitmap这样透明通道不会在LoadFromFile阶段被简单丢掉非 PNG 分支直接加载再强制 32 位。参数上ABmp建议由调用方TBitmap.Create后传入函数不创建也不释放避免所有权混乱。如果发现模板透明区域变成黑块先检查ABmp.AlphaFormat和PixelFormat再检查比对函数里是否正确跳过了 Alpha0 的像素这一步不确认后面调容差都是玄学。3.2 单目标与多目标返回坐标、相似度和命中数量单目标找图只需要返回第一个命中点多目标则要扫完整个屏幕并收集所有候选点。多目标场景最容易出现“同一个按钮返回十几个坐标”因为相邻几个像素的窗口都能通过容差。常见做法是加一个最小距离去重新命中的坐标和已收集坐标的距离小于模板宽高的一半就认为是同一个目标跳过。这样既能保留不同位置的相同图标又不会让结果列表爆炸。返回结构里除了X/Y最好带上Score和命中数量方便上层决定是点击第一个、还是按顺序点完全部。下面这个FindAllMatches用双重循环遍历候选左上角内部调用MatchAt命中后做距离去重最多收集AMaxCount个结果。参数AStep控制粗扫步长ATolerance控制颜色容差AMaxCount防止异常画面里候选点无限增长。注意循环上界是ScreenBmp.Width - TemplBmp.Width不是ScreenBmp.Width否则最后一列会越界。uses Types, Math; // 在屏幕位图中查找所有匹配位置按最小距离去重 function FindAllMatches(const ScreenBmp, TemplBmp: TBitmap; ATolerance, AStep, AMaxCount: Integer): TArrayTPoint; var sx, sy, i: Integer; score: Double; pt: TPoint; duplicated: Boolean; begin SetLength(Result, 0); if (ScreenBmp.Width TemplBmp.Width) or (ScreenBmp.Height TemplBmp.Height) then Exit; for sy : 0 to ScreenBmp.Height - TemplBmp.Height do begin if (AStep 1) and (sy mod AStep 0) then Continue; for sx : 0 to ScreenBmp.Width - TemplBmp.Width do begin if (AStep 1) and (sx mod AStep 0) then Continue; if not MatchAt(ScreenBmp, TemplBmp, sx, sy, ATolerance, score) then Continue; pt : Point(sx, sy); duplicated : False; for i : 0 to High(Result) do begin // 距离小于模板宽高一半视为同一个目标 if (Abs(Result[i].X - pt.X) TemplBmp.Width div 2) and (Abs(Result[i].Y - pt.Y) TemplBmp.Height div 2) then begin duplicated : True; Break; end; end; if not duplicated then begin SetLength(Result, Length(Result) 1); Result[High(Result)] : pt; if Length(Result) AMaxCount then Exit; end; end; end; end;逻辑上外层先行后列AStep用来跳过部分候选点MatchAt负责单点比对命中后与已有结果做距离去重。参数AMaxCount建议设成 20 到 50超过这个数量通常说明模板太泛或容差太大先回去检查模板不要继续加去重逻辑。TemplBmp.Width div 2是去重半径模板越小半径越小如果两个目标本来就挨得很近这个半径要适当调小否则会把第二个目标误删。3.3 把找图封装成可复用单元接口与调用示例真正落地时不要把截图、加载模板、比对、坐标换算全塞在一个按钮事件里。常见做法是拆成三层截图层只负责CaptureScreen匹配层只负责MatchAt/FindAllMatches业务层负责加载模板、调匹配、把屏幕坐标换算成鼠标坐标。这样换分辨率时只改截图区域换模板时只改路径排查问题时也能单独跑匹配层。下面这个调用示例把屏幕截图、模板加载、多目标查找和结果输出串起来Screen.Width/Height是当前屏幕尺寸实际项目里可以换成目标窗口矩形。var screenBmp, templBmp: TBitmap; matches: TArrayTPoint; i: Integer; begin screenBmp : CaptureScreen(Rect(0, 0, Screen.Width, Screen.Height)); templBmp : TBitmap.Create; try LoadTemplate(button.png, templBmp); matches : FindAllMatches(screenBmp, templBmp, 12, 2, 20); for i : 0 to High(matches) do WriteLn(Format(match %d: (%d,%d), [i, matches[i].X, matches[i].Y])); finally templBmp.Free; screenBmp.Free; end; end;逻辑说明先截全屏再加载模板容差 12、步长 2、最多 20 个结果结果坐标是屏幕位图内的左上角坐标如果截图区域不是从(0,0)开始还要加上ARect.Left/Top才是真实屏幕坐标。参数上Screen.Width/Height在 DPI 缩放下可能不是物理像素后面第 4 章会专门讲换算模板路径建议用绝对路径或相对于程序目录的路径别依赖当前工作目录。这个例子跑通之后再把WriteLn换成鼠标移动或点击不要一上来就在业务代码里调坐标。4. 性能与稳定性步长、提前退出、多线程和 DPI 缩放4.1 步长与提前退出把 O(W×H×w×h) 拉回可用区间全量匹配的耗时和屏幕面积、模板面积都成正比1920×1080 屏幕上找一个 60×60 的模板候选点大约两百万个每个候选点最多比较三千六百个像素最坏情况是几十亿次字节比较。实际项目里不可能每次都跑满因为MatchAt在第一个超差像素就会退出大多数候选点只比几个像素就被淘汰。真正拖慢速度的是“几乎相似但又不完全相似”的区域比如大片纯色背景、渐变面板、重复列表。这时候步长和提前退出就是主要手段。我一般把找图分成两段粗扫阶段用Step2或Step3只跑MatchAt的快速失败路径粗扫命中后再在命中点周围±Step范围内做一次全像素复核确认左上角精确位置。这样既不会因为跳步漏掉目标也不会让全量比对拖垮 UI。如果目标一定在某个窗口内先把截图区域裁到窗口矩形比全屏扫描快得多如果目标在屏幕下半部分先截下半屏别一上来就GetDC(0)全屏。下面这个FindFirstFast演示粗扫加复核的结构返回值是第一个可信命中点。// 先按 AStep 粗扫命中后在邻域内全像素复核返回精确左上角 function FindFirstFast(const ScreenBmp, TemplBmp: TBitmap; ATolerance, AStep: Integer; out AScore: Double): TPoint; var sx, sy, dx, dy: Integer; score: Double; begin Result : Point(-1, -1); AScore : 0; for sy : 0 to ScreenBmp.Height - TemplBmp.Height do begin if (AStep 1) and (sy mod AStep 0) then Continue; for sx : 0 to ScreenBmp.Width - TemplBmp.Width do begin if (AStep 1) and (sx mod AStep 0) then Continue; if not MatchAt(ScreenBmp, TemplBmp, sx, sy, ATolerance, score) then Continue; // 粗扫命中后在周围 ±AStep 内找更精确的左上角 for dy : -AStep to AStep do for dx : -AStep to AStep do begin if (sx dx 0) or (sy dy 0) then Continue; if (sx dx ScreenBmp.Width - TemplBmp.Width) or (sy dy ScreenBmp.Height - TemplBmp.Height) then Continue; if MatchAt(ScreenBmp, TemplBmp, sx dx, sy dy, ATolerance, score) then begin Result : Point(sx dx, sy dy); AScore : score; Exit; end; end; end; end; end;逻辑上粗扫阶段用AStep减少候选点命中后不直接返回而是在±AStep邻域内重新比对拿到更精确的左上角。参数AStep建议 2 或 3模板小于 30×30 时不要超过 2ATolerance和前面保持一致不要在复核阶段突然放宽否则会把粗扫的误命中固化下来。这个函数的返回值是屏幕位图坐标调用方还要加截图偏移。如果粗扫一直不命中先把AStep改成 1 验证模板本身能不能匹配再回头调步长。4.2 多显示器与 DPI 缩放坐标偏了不是玄学坐标偏移是找图落地时最常见的翻车点。屏幕上明明找到了鼠标点下去却偏了二三十像素十有八九是 DPI 缩放。Windows 在 125%、150% 缩放下应用程序看到的逻辑坐标和屏幕物理像素不是 1:1BitBlt截的是物理像素SetCursorPos或鼠标事件用的可能是逻辑坐标中间少了一次换算。多显示器场景更麻烦不同显示器可能不同缩放主屏 100%、副屏 150%同一个找图函数在副屏上就会偏。常见做法是先把进程声明为 Per-Monitor DPI Aware让 API 返回真实像素如果改不了进程清单就在坐标回写前用GetDpiForWindow或GetDeviceCaps(LOGPIXELSX)算缩放比。下面这个GetScreenScale用桌面 DC 的LOGPIXELSX做兜底返回 1.0 表示 100%1.25 表示 125%1.5 表示 150%。参数上GetDC(0)拿的是桌面 DCReleaseDC必须成对这个函数适合老系统或没开 Per-Monitor 的进程新系统更稳的是按窗口取 DPI。uses Windows; // 取桌面 DPI 缩放比96 DPI 为 1.0120 为 1.25144 为 1.5 function GetScreenScale: Double; var dc: HDC; dpi: Integer; begin dc : GetDC(0); try dpi : GetDeviceCaps(dc, LOGPIXELSX); if dpi 0 then dpi : 96; Result : dpi / 96.0; finally ReleaseDC(0, dc); end; end;逻辑说明LOGPIXELSX返回当前桌面水平 DPI除以 96 得到缩放比dpi 0时按 96 兜底避免除零。参数上这个比例用于把“逻辑坐标”换成“物理坐标”或者反过来如果截图用的是物理像素鼠标 API 用的是逻辑像素就要除以缩放比。多显示器下不要只取一次全局比例最好按目标窗口所在显示器取 DPI否则副屏 150% 还是会偏。坐标换算不要拍脑袋加固定偏移先打印截图矩形、匹配坐标、鼠标坐标三组值确认偏的是比例还是偏移。4.3 多线程找图线程里碰 TBitmap 的边界屏幕大、模板多的时候单线程找图会卡 UI。常见做法是把屏幕分成若干块每个线程截一块、匹配一块结果回主线程合并。这里有几个边界TBitmap是 GDI 对象不是 VCL 控件线程里创建和使用一般没问题但不要多个线程共享同一个TBitmap实例GetDC(0)和BitBlt可以在工作线程调用但截图区域要提前算好不要在线程里读 VCL 控件属性匹配结果通过线程安全队列或Synchronize回主线程别在工作线程里直接改 UI。下面这个线程类只做一件事截取指定矩形、加载模板、调用FindFirstFast结果放在FResult里。调用方在Terminate后读取结果避免边跑边读。参数FScreenRect是线程负责的屏幕块FTemplFile是模板路径每个线程自己加载模板不共享位图。type TFindThread class(TThread) private FScreenRect: TRect; FTemplFile: string; FTolerance: Integer; FResult: TPoint; FScore: Double; protected procedure Execute; override; public property FindResult: TPoint read FResult; property Score: Double read FScore; end; procedure TFindThread.Execute; var screenBmp, templBmp: TBitmap; begin screenBmp : CaptureScreen(FScreenRect); templBmp : TBitmap.Create; try LoadTemplate(FTemplFile, templBmp); // 注意结果坐标是当前分块内坐标合并时要加 FScreenRect.Left/Top FResult : FindFirstFast(screenBmp, templBmp, FTolerance, 2, FScore); finally templBmp.Free; screenBmp.Free; end; end;逻辑说明每个线程独立截图、独立加载模板、独立匹配互不共享TBitmapFResult是分块内坐标主线程合并时加上FScreenRect.Left/Top才是全屏坐标。参数上FTolerance要和单线程版本保持一致FScreenRect的宽高不要小于模板宽高否则FindFirstFast直接返回(-1,-1)。线程数量不要超过 CPU 核心数屏幕分块之间留 1 到 2 像素重叠避免目标正好卡在分块边界被切掉。提示多线程找图先保证单线程能稳定命中再拆块。拆块后如果结果数量变少先检查分块边界和坐标合并不要先怀疑算法。5. 避坑与排查颜色格式、遮挡、句柄泄漏和误匹配5.1 现象截图全黑或花屏匹配率直接归零现象很直观CaptureScreen返回的位图打开一看全黑或者颜色像花屏后面不管怎么调容差都找不到。原因通常有三类一是截图区域在锁屏、远程桌面或硬件加速窗口上BitBlt拿不到真实像素二是TBitmap.PixelFormat没有固定成pf32bitScanLine按 4 字节读实际行宽是 3 字节颜色错位三是截图后没有等窗口重绘截到了半帧。解决顺序是先把截图区域换到桌面空白处保存 BMP 看是否正常再检查PixelFormat和ScanLine的字节偏移最后确认目标窗口没有走独占全屏或硬件覆盖。如果是硬件加速窗口BitBlt可能只拿到黑块常见做法是改用窗口自绘截图或系统提供的捕获接口但这类方案兼容性和权限要求更高。我的建议是找图目标尽量选普通 GDI 窗口、浏览器页面、办公软件界面对视频播放器、3D 画面、独占全屏程序不要硬用像素比对先确认截图链路能不能拿到颜色。排查时加一句保存调试图比盯着匹配函数改参数有效得多。5.2 现象明明图在屏幕上却找不到容差加到 50 也没用这种问题最容易被误判成“算法不行”。实际原因往往是模板和屏幕不是同一尺度模板从 2 倍设计稿截的屏幕是 1 倍渲染模板被 JPEG 压缩过边缘有色差模板带透明背景但加载后 Alpha 被抹掉透明区域变成黑色参与比较或者截图区域本身偏了根本没包含目标。先做三件事用屏幕截图工具按 1:1 裁一张新模板把容差从 0 开始每 4 个色阶加一档看最大差异卡在哪个通道打印模板的不透明像素比例如果 Alpha0 的像素占 90% 以上匹配函数等于只比了很少的像素结果不可信。还一个常见坑是颜色管理。同一张图在不同显示器、不同色彩配置文件下RGB 值可能差几个色阶如果模板是从另一台机器上截的容差 8 可能不够。我一般会在目标机器上重新裁模板不跨机器复用如果必须复用先把模板和屏幕图都转成同一色彩空间或者把容差调到 16 到 24再用多目标去重控制误匹配。容差加到 50 还找不到基本不是容差问题回去检查尺度和截图区域。5.3 现象找图越跑越慢、内存上涨最后卡死跑几分钟后速度明显下降任务管理器里内存曲线一路向上多半是对象或句柄泄漏。常见泄漏点每次找图都TBitmap.Create但没有FreeGetDC(0)后异常路径漏了ReleaseDC多线程里每个线程都加载模板但线程结束时没释放结果数组只增不减。Delphi 的TBitmap是 GDI 对象泄漏多了不仅吃内存还会拖慢 GDI 调用表现出来就是“越跑越慢”。解决方式是所有创建都配try...finally截图函数内部保证异常时释放线程结果只在主线程合并。下面这个对象池写法适合频繁找图的场景复用屏幕位图和模板位图避免每轮都创建销毁。参数上FPool是池里缓存的位图列表Acquire取一个可用对象Release放回池大小设成线程数加 1 就够不要无限缓存。type TBitmapPool class private FItems: TListTBitmap; public constructor Create; destructor Destroy; override; function Acquire(AWidth, AHeight: Integer): TBitmap; procedure Release(ABmp: TBitmap); end; function TBitmapPool.Acquire(AWidth, AHeight: Integer): TBitmap; begin if FItems.Count 0 then begin Result : FItems.Last; FItems.Delete(FItems.Count - 1); end else Result : TBitmap.Create; Result.PixelFormat : pf32bit; Result.SetSize(AWidth, AHeight); end;逻辑说明Acquire优先从池里取取不到再创建取出来后统一设pf32bit和尺寸保证像素布局一致。Release只把对象放回池不释放池的Destroy统一释放。参数上池大小不要超过并发线程数的两倍太大反而占内存如果某张位图尺寸经常变化SetSize会重新分配池的收益会下降这时候按尺寸分池更合适。排查泄漏时先看TBitmap数量是否随轮次增长再看 GDI 对象数别只看内存。5.4 现象坐标偏了 20 到 30 像素换台机器偏得不一样坐标偏移通常不是匹配错了而是坐标系没对齐。截图用的是物理像素鼠标 API 可能用逻辑像素进程没有声明 DPI 感知时系统还会做一次虚拟化缩放。125% 缩放下100 物理像素对应 80 逻辑像素偏差 20 像素150% 下偏差更大。多显示器不同缩放时同一个全局比例也不适用。解决方式是先确认进程 DPI 感知级别再用目标窗口所在显示器的 DPI 算比例如果截图区域有偏移匹配结果还要加ARect.Left/Top。调试时不要猜打印三组值截图矩形的Left/Top/Width/Height、匹配返回的X/Y、最终传给鼠标的X/Y。如果匹配坐标在截图内是对的鼠标坐标偏了问题在换算如果匹配坐标本身就偏问题在截图区域或模板左上角。坐标换算只做一次不要在多个函数里反复乘除否则误差会累积。5.5 现象相似度很高但点下去没反应或者点到了旁边匹配返回Score0.98鼠标也移过去了但按钮没反应。原因可能是匹配到的是半透明遮罩、相似图标或动画中间帧点击坐标用的是模板左上角不是目标中心目标窗口没有激活点击被拦截或者用了PostMessage发鼠标消息目标程序不认。解决方式是返回模板中心点而不是左上角点击前把目标窗口置前必要时用SendInput模拟真实鼠标事件多目标场景先按距离和相似度排序选最可信的一个不要闭眼点第一个。下面这个MatchCenter把匹配左上角换算成中心点参数AMatch是FindAllMatches返回的左上角ATemplWidth/Height是模板尺寸。中心点偏移量取模板宽高的一半如果模板带了阴影或边框裁剪时就要把阴影去掉否则中心点会偏。// 把匹配左上角换算成模板中心点点击一般用中心点更稳 function MatchCenter(const AMatch: TPoint; ATemplWidth, ATemplHeight: Integer): TPoint; begin Result : Point( AMatch.X ATemplWidth div 2, AMatch.Y ATemplHeight div 2 ); end;逻辑说明中心点等于左上角加模板宽高的一半适用于按钮、图标这类对称目标如果目标可点击区域偏左或偏上可以在中心点基础上再加固定偏移。参数上ATemplWidth/Height必须和实际加载的模板尺寸一致不要用裁剪前的原始尺寸。点下去没反应时先手工把鼠标移到中心点看能不能点中再排查消息发送方式别在匹配算法里绕圈。6. 进阶技巧灰度投影加候选点复核把找图耗时压到三分之一模板匹配的瓶颈在候选点太多而候选点里绝大多数在第一行就被淘汰。可以把模板先做灰度投影按列统计灰度梯度或方差找出边缘强的列屏幕图同样按列投影用滑动窗口比较投影向量只有投影相似的列才进入全像素比对。这样做的原理是模板里有文字或图标的列灰度变化明显纯色背景列投影很平先被筛掉。投影比较只做一维数组计算量远小于二维像素比对适合大屏幕、多模板轮询的场景。我一般先用投影筛出候选列再在候选列附近按原步长扫行最后用MatchAt复核 RGB 容差。下面这个ColumnProjection把 32 位位图转成灰度并按列求平均梯度返回长度等于位图宽度的数组。参数ABmp是输入位图AGray是输出数组灰度用(B G*2 R) div 4近似够用且快。投影只做粗筛最终命中仍以MatchAt为准不要用投影分数直接当匹配结果。// 按列计算灰度平均梯度用于候选列粗筛 procedure ColumnProjection(ABmp: TBitmap; var AGray: TArrayDouble); var x, y: Integer; line: PByte; g, prevG: Integer; sum: Double; begin SetLength(AGray, ABmp.Width); for x : 0 to ABmp.Width - 1 do begin sum : 0; prevG : -1; for y : 0 to ABmp.Height - 1 do begin line : ABmp.ScanLine[y]; // 32 位像素B,G,R,A灰度近似 g : (line[x * 4 0] line[x * 4 1] * 2 line[x * 4 2]) div 4; if prevG 0 then sum : sum Abs(g - prevG); prevG : g; end; AGray[x] : sum / ABmp.Height; end; end;用法上先对模板和屏幕图各算一次列投影模板投影长度是w屏幕投影长度是W在屏幕投影上滑动窗口计算与模板投影的绝对差之和只保留差值最小的前 10% 列作为候选列。候选列确定后再在这些列附近按Step1扫行调用MatchAt复核。参数上投影阈值不要设得太死先看候选列数量控制在总列数的 10% 到 20% 之间太少会漏太多就失去意义。这个技巧对文字按钮、图标列表效果本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑