尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Python屏幕捕获实战:mss选型、DPI坐标与差分检测
我把它当成一篇学习日志而不是一份教程。标题里那个“scre_python”其实是我自己起的代号scre 就是 screen凡是跟屏幕像素打交道的操作我都归到这个标签下截一张图、在图上定位一个区域、算一块地方有没有变化、跑一个最简陋的帧循环。前面九篇基本都在数据侧文件读写、字典与列表的取舍、网络请求拉数据、正则清理、用 pandas 做点小统计。到第10篇我想把方向转到“看得见”的一侧。原因很实在屏幕上的画面是很多自动化脚本的最后一步。数据你处理得再干净最终要落到动作上比如判断某个界面到底加载完没有、某个角落有没有冒提示、某个进度条走到哪儿了最直接的办法就是看一眼屏幕。人眼看这件事换成 python 来做本质就是取像素。scre_python 这一篇记录的就是取像素这一层的踩坑和代码。这篇适合谁会一点 Python、能读懂函数、会 pip 装包但没系统碰过屏幕捕获的人也适合用 pyautogui 截过图、结果坐标全乱或者截出来一片黑的人。代码都能直接复制参数我给了实测值和计算过程别的不敢保证至少能让你跑起来。1. 第10篇日志为什么拐到屏幕这条路上1.1 需求是怎么冒出来的起因很小。我在写一个每天要重复十几次的小工具等一个本地渲染的图表生成完然后把图表区域裁下来存成图片按日期归档。前九篇学到的东西足够我把“存”和“归档”写好卡住的只有一步怎么知道图表已经画完了。试过几种办法。监听进程退出渲染是个后台线程没有明确的结束信号。轮询某个中间文件那文件是先写后改的存在写了一半被读到的窗口期。设个固定 sleep跑十次有两次失败纯赌。最后一个笨办法反而最稳每两秒截一张图只取图表那一小块区域比较前后两次的像素差连续两次差值小于阈值就认定画完了。这件事让我意识到屏幕捕获不是一个“花活”而是很多脚本的兜底手段。它不优雅但它不依赖对方程序配合不需要接口不需要权限你只要能看到就能测。这也决定了它的定位宁可慢一点、土一点也要稳。所以这一篇我按“能跑、能查错、能长期跑”的标准来写不是按“跑分最快”的标准。1.2 scre 这个前缀我给自己划的边界既然是自己起的标签边界得说清楚不然写着写着就跑偏了。我的定义是凡是把屏幕或窗口的可视内容取成像素数据以及对这些像素数据做轻量处理的操作都算。具体包括四类单张截图、按区域截图、连续帧采集、对帧做简单运算差分、均值、裁剪、缩放、灰度。不归进来的界面控件的程序内操作、读取目标程序的内存、模拟输入。这些东西属于另一个层面的技术跟“看”是两回事混在一起写容易让代码结构拧巴。我自己的习惯是分两层一层负责“看”本篇内容一层负责“动”键盘鼠标模拟。看和动之间的桥梁是一个坐标系这也是本篇会重点讲坐标的原因因为绝大部分“截图对不上、点歪了”的坑根源都在坐标系没统一。另外提一句命名。日志文件夹里我建了scre_python/里面按天分子目录第10篇是d10_capture/。这个习惯看着无所谓实际很有用屏幕截图脚本通常要写一堆实验性的小文件不隔离很快就变成一坨。每篇日志一个目录跑废了整目录删掉不影响别的。1.3 这一篇我明确不做的三件事第一不做高性能录屏。真正的录屏要处理编码、丢帧、音画同步那是另一个工程量的东西本篇只做到“连续取帧 存关键帧”这一层够用于检测类任务。第二不封装成通用库。学习日志的最大价值是过程透明一旦封装成capture_utils.py里几个看不懂的函数下次出问题还得回来拆。所以我全部写成平铺的脚本函数不超过五个一眼能看到底。第三不追求跨所有桌面环境一致。屏幕捕获这块不同系统、不同桌面环境的行为差异是客观存在的硬要抹平会引入一堆判断分支。我的做法是代码里留一个PLATFORM_NOTE常量把当前环境下确认的行为写清楚换环境时先改这个常量再跑不假装它是通用的。2. 截屏方案选型四条路子怎么挑2.1 候选清单和各自的脾气python 里能截屏的库比想象中多我实际跑过的有四类按上手难度从低到高排第一类是PIL.ImageGrab。装了 Pillow 就有零额外依赖grab()一行出图支持bbox指定区域支持all_screensTrue抓多屏。缺点是内部每次调用都要走一遍系统的位图拷贝单帧开销在二十几毫秒这个量级而且多屏和缩放的处理在不同版本上有过差异。第二类是mss。纯 ctypes 封装的轻量库接口就三行建实例、读monitors、grab()。返回的是一个带.rgb/.size的对象可以直接numpy转数组。它的monitors[0]是整块虚拟屏幕monitors[1:]是逐块屏负坐标也能正确处理这一点在副屏摆在左边的时候特别关键。第三类是pyautogui.screenshot()。很多人第一个接触的就是它因为它顺便还提供鼠标键盘操作。但它内部其实也是转调PIL.ImageGrab多了一层封装还会顺手做 PNG 编解码单帧开销明显更高纯粹为了截图用它不划算。第四类是走系统桌面复制接口的方案单帧能压到几毫秒但依赖具体系统版本代码复杂度高一个档。检测类任务用不上我记一笔需要高频采集时再回头找。方案首帧开销单帧耗时1920×1080多屏支持负坐标额外依赖PIL.ImageGrab约 40 ms23 ~ 35 ms需参数开启表现不稳定无Pillow 自带mss约 15 ms8 ~ 12 ms原生支持支持仅 msspyautogui约 60 ms60 ~ 90 ms部分支持不支持pyautogui Pillow桌面复制接口约 30 ms2 ~ 4 ms需自行处理支持平台相关库表里的数字是同一台四核轻薄本、同一次运行里取二十次的中位数不是绝对基准但相对关系很稳定mss 大约是 ImageGrab 的三分之一耗时pyautogui 是 ImageGrab 的两倍多。这个差异在单张图上无感在每秒两帧的循环里就是 CPU 占用 3% 和 15% 的区别。2.2 一笔必须算的存储开销选型不能只看耗时还得算拷贝量。一张 1920×1080 的 RGBA 位图是 1920 × 1080 × 4 8,294,400 字节约 7.91 MiB。这个数字本身不大但它乘以帧率就很吓人每秒 2 帧约 15.8 MiB/s每秒 10 帧约 79 MiB/s每秒 30 帧约 237 MiB/s这意味着如果你把一个循环里的帧都塞进 list 里我第一版就这么干的想着“先攒着再处理”三十秒就能吃掉七个 G。所以第一条铁律帧对象用完立刻丢不要在循环外持有引用。需要保留的只有处理结果比如一个布尔值、一个均值、一个小尺寸的缩略图。第二笔开销是受区域大小影响的。捕获耗时基本和像素数线性相关把 1920×1080 缩到 480×270像素数降到十六分之一实测 mss 单帧从 9 ms 左右掉到 3 ms 上下。所以第二个习惯能用区域就别截全屏。绝大多数检测任务只看一块固定区域全屏截取纯属浪费。这个优化不需要任何技巧改一下bbox参数就行收益却是最大的。第三笔是保存。PNG 无损但慢一张 1920×1080 的内容复杂截图save()能吃掉 80 到 300 ms比截图本身还贵。JPEG 快但质量参数低于 85 就开始出块状伪影做像素比对会引入噪声。我的结论是检测链路里不落盘落盘只在“认定结果”那一刻做一次存档格式按需选需要精确比对用 PNG只需要人眼看用 JPEG 质量 90。2.3 我的选型结论和理由检测类任务我固定用 mss。理由三条一是它把多屏和负坐标的处理放在库内部我不用每次写代码都提心吊胆二是返回的原始字节可以直接进 numpy不经过中间图像对象少一层转换三是它没有额外依赖装一个包就够部署到别的机器上省事。兜底方案留 ImageGrab。因为 Pillow 几乎是所有 Python 环境的标配万一 mss 在某个环境下装不上或者行为异常改一行就能退回去不至于整个脚本瘫痪。我的代码里一般这么写try: import mss def grab(region): with mss.mss() as sct: return np.asarray(sct.grab(region))[:, :, :3] except ImportError: from PIL import ImageGrab def grab(region): box (region[left], region[top], region[left] region[width], region[top] region[height]) return np.asarray(ImageGrab.grab(bboxbox))[:, :, :3]注意[:, :, :3]这个切片。mss 返回的是 BGRA 四通道切掉 alpha 得到三通道但通道顺序仍然是 BGR不是 RGB。这个细节下一章单独说因为它是最容易埋雷的地方——你用 opencv 或者 PIL 去显示颜色会偏而偏色在检测任务里可能表现为“阈值怎么调都不对”。3. 坐标、缩放、DPI三个最容易翻车的地方3.1 逻辑分辨率和物理分辨率不是一回事这是我第一次踩坑踩得最惨的地方。系统设置里显示“缩放 150%”屏幕物理分辨率是 1920×1080但你在代码里拿到的屏幕尺寸可能是 1280×720。两个数都对只是说的不是同一件事前者是物理像素后者是逻辑像素也叫设备无关像素。关系很简单$$物理像素 逻辑像素 \times 缩放比$$1280 × 1.5 1920对上了。麻烦在于不同的接口用不同的坐标系鼠标位置上报的是逻辑坐标而grab(bbox...)吃的是物理像素。于是你会看到非常诡异的画面——截出来的图总是偏而且偏得随缩放比例变化。我试过两种解法。第一种是换算先查当前缩放比把所有坐标乘一遍再传进去。能跑但缩放比要去读系统设置换台机器又要重查不牢靠。第二种是声明进程的 DPI 感知让程序的坐标系直接对齐物理像素。在 Windows 上就是在导入截图库之前先调一句import ctypes try: ctypes.windll.shcore.SetProcessDpiAwareness(2) # 2 每显示器感知 except Exception: try: ctypes.windll.user32.SetProcessDPIAware() except Exception: pass # 非 Windows 或老版本跳过这段必须放在最前面在import mss之前否则库初始化的时候已经把缩放信息缓存下来了后面再改就不生效。我养成习惯把它扔进一个dpi_fix.py所有相关脚本第一行import dpi_fix。注意如果你在多屏环境里每个屏的缩放比不同用值 2每显示器感知用值 1系统感知只按主屏比例算副屏坐标会错。这个参数选错症状是“主屏截得准副屏一直偏”。3.2 多屏拼接和负坐标有了mss之后我把每块屏的几何信息打出来看了一遍很开眼import mss with mss.mss() as sct: for i, m in enumerate(sct.monitors): print(i, m)典型输出副屏摆在主屏左边0 {left: -1920, top: 0, width: 3840, height: 1080} 1 {left: 0, top: 0, width: 1920, height: 1080} 2 {left: -1920, top: 0, width: 1920, height: 1080}monitors[0]是整块虚拟屏的包围盒左边起点是 -1920。monitors[1]是主屏monitors[2]是左侧副屏。这意味着如果你直接写left0, top0, width1920, height1080抓到的一直是主屏副屏上的东西压根没进画面——而且不会报错只是抓错了地方这种静默失败最难查。我的处理方式是不手写坐标改用“相对某块屏的偏移”def region_of(screen_index, left, top, width, height): with mss.mss() as sct: base sct.monitors[screen_index] return {left: base[left] left, top: base[top] top, width: width, height: height}调region_of(2, 100, 100, 400, 300)就是“左侧副屏上距左上角 100,100 那块 400×300 的区域”。坐标写成相对的换屏、加屏都不用改。这个抽象很小但省了我好几次重排版面。还有一个跟多屏有关的配置得记一下缩放比例改过之后monitors里的数值会跟着变所以不要把它缓存到配置文件里写死每次启动重新读一遍。我吃过这个亏——有一次调完缩放脚本不认账查了四十分钟才发现是配置文件里的旧坐标。3.3 通道顺序和灰度转换前面提过 mss 返回 BGR(A)。这里补充完整np.asarray(sct.grab(region))出来是(h, w, 4)通道顺序 BGRA。切掉最后一维得到 BGR。而 PIL 的Image.fromarray默认按 RGB 理解直接喂进去红蓝互换。同一份逻辑三种结果来源形状通道顺序直接喂 PIL 的后果mss(h, w, 4)BGRA不合法需先切mss 切三通道(h, w, 3)BGR红蓝互换ImageGrab(h, w, 3)RGB正常灰度.mean(axis2)(h, w)单通道正常所以我的写法是统一在入口做一次转换之后全程只认一种顺序。习惯上我选 RGB因为 PIL 用得多frame np.asarray(sct.grab(region))[:, :, :3][:, :, ::-1][:, :, ::-1]是倒序切片把 BGR 翻成 RGB是视图操作不复制内存几乎零开销。要注意的是这个结果和原数组共享内存如果后面要原地修改先.copy()不然会把原帧也改了排查起来很懵。灰度转换这块也有个细节。frame.mean(axis2)是最快的但它和标准灰度公式不等权标准公式是 0.299R 0.587G 0.114B。做差分检测的时候这个差异完全可以忽略因为两边都一样偏但如果你的阈值是按标准灰度调的换成等权均值后整体亮度会偏阈值要重调。我的做法是在代码里写死一种然后注释里标注“此处为等权均值非标准灰度”免得半年后自己都忘了。4. 从截一张图到跑一个循环完整实操4.1 最小可运行版本先确认能看到东西任何屏幕相关的脚本我都建议第一步只做一件事截一张全屏存盘用系统看图器打开。这一步花三十秒能排除掉后面百分之八十的困惑。import mss import numpy as np from pathlib import Path from datetime import datetime OUT Path(shots) OUT.mkdir(exist_okTrue) with mss.mss() as sct: monitor sct.monitors[1] raw sct.grab(monitor) arr np.asarray(raw) print(shape:, arr.shape, dtype:, arr.dtype) from PIL import Image img Image.fromarray(arr[:, :, :3][:, :, ::-1]) name OUT / f{datetime.now():%Y%m%d_%H%M%S_%f}.png img.save(name) print(saved:, name)几个点值得说明。OUT.mkdir(exist_okTrue)比if not exists: mkdir简洁而且并发时不会抛异常。文件名带%f微秒是因为我早期用秒级时间戳两帧存下来直接覆盖丢了数据还查不出来。arr.shape打印出来如果第三维是 4说明拿到了 alpha 通道正常如果只有二维说明抓到了灰度或者出错了得回头看。dtype一般是uint8如果变成别的类型后面做减法就要注意溢出方向。图打开之后认真看一眼颜色对不对偏色说明通道顺序错了、边缘有没有裁掉坐标错了、是不是全黑见第 5 章。这三眼下去基本就没盲区了。4.2 连续帧时间基准和限速单张没问题就开始跑循环。这里的第一个问题不是技术问题是节奏问题你到底需要多快我见过很多人上来写while True:疯狂截图然后说“CPU 打满”。其实他的任务十秒检查一次完全够。帧率应该由业务需求决定而不是由库的极限决定。我的经验值界面状态检测某个东西出现没每 1 到 2 秒一帧足够进度条跟踪每 0.2 到 0.5 秒一帧动画/滚动检测每 0.05 到 0.1 秒一帧需要超过每秒十帧的先想清楚能不能改用别的手段触发顺手说个坑定时不要用time.time()它受系统时间调整影响校时跳一下你的间隔就乱了。用time.perf_counter()单调递增专为测时间间隔设计。import time INTERVAL 0.5 next_at time.perf_counter() prev None while True: now time.perf_counter() if now next_at: time.sleep(next_at - now) # 睡到点不空转 continue frame grab(region) # ... 处理 ... next_at INTERVAL if next_at time.perf_counter(): # 处理超时了重新对齐别追帧 next_at time.perf_counter() INTERVALnext_at INTERVAL这种按固定节拍累加的方式比每次sleep(INTERVAL)更准因为处理耗时被自动吸收了。但处理偶尔会超时这时候如果还坚持累加next_at 会落后于当前时间然后连续触发好几次补帧——这就是“追帧”会让 CPU 短时间飙升。所以补一句判断落后就重新对齐宁可跳一拍也不追。这个细节很小但长期跑稳定性的差别很明显。4.3 加一层处理降采样差分检测回到最初的需求判断画面有没有变化。最朴素的写法是直接两张图做差diff np.abs(frame_a.astype(np.int16) - frame_b.astype(np.int16)) changed diff.mean() 3.0能跑但浪费。1920×1080 的三通道图做减法、绝对值、求均值一次几毫秒跑起来不心疼但如果只是想判断“变没变”你根本不需要这么多像素。我改成先降采样到很小的尺寸def thumb(arr, size(160, 90)): h, w arr.shape[0], arr.shape[1] ys np.linspace(0, h - 1, size[1]).astype(np.int32) xs np.linspace(0, w - 1, size[0]).astype(np.int32) small arr[ys][:, xs] # 最近邻抽点不插值 return small.astype(np.int16) def score(a, b): return float(np.abs(thumb(a) - thumb(b)).mean())为什么用最近邻抽点而不是resize因为resize要做插值插值会把小块的变化摊平抽点虽然会有锯齿但保留了原始像素的真实强度对“有没有变”这个问题更敏感。而且抽点只是切片操作比任何插值都快。阈值定多少要试。我的一般起点是 3.0在 0 到 255 的尺度上然后拿实际场景跑二十次正常的、五次异常的看分布。如果正常波动的最大值是 2.1异常的最小值是 12那阈值取 5 就很安全中间留了很大余量。反之如果两者重叠说明这个判据不成立得换思路比如改成比较特定区域的平均亮度或者干脆用面积判断——统计“变化像素占比”而不是平均差值后者对小面积强变化更敏感前者对大面积弱变化更敏感。def changed_ratio(a, b, thresh20): ta, tb thumb(a), thumb(b) d np.abs(ta - tb).max(axis2) # 每个像素取通道最大差 return float((d thresh).mean()) # 变化像素占比max(axis2)的作用是把三通道压成一通道取最大值意味着任何一个通道变化超过阈值都算数比取均值更不容易漏掉小红点、小绿勾这类单色元素。4.4 存盘和命名别在这上面偷懒检测链路里我基本不落盘但“认定结果”那一刻要存一张用来回溯。存盘这块的坑集中在两点格式和路径。格式上PNG 无损适合做后续比对和存档JPEG 快、体积小适合人眼看。实测一张 1920×1080 的界面截图PNG 约 250 到 900 KB保存耗时 100 到 300 msJPEG 质量 90 约 120 到 250 KB耗时 20 到 50 ms。差一个数量级。所以“存图给人看”和“存图给程序读”应该用不同格式别混为一谈。路径上中文目录、空格目录在 Python 3 里基本不是问题但如果你把路径拼成字符串再传给某些外部工具就会翻车。统一用pathlib.Path做拼接Path(shots) / f{ts}.png跨平台也省心。命名我固定的格式是YYYYmmdd_HHMMSS_微秒_标签.png标签写场景名比如chart、dialog。有了标签文件管理器里按名字排序就能一眼看出是哪一类比打开一张张看强太多。另外一个小习惯截图目录加个自动清理。我的脚本启动时先扫一遍shots/超过七天的文件挪到归档子目录。屏幕截图这类数据增长很快不清会堆到几十个 G而且你根本不会回头去看。5. 常见问题与排查实录5.1 截出来全黑或者全白这是被问得最多的。我按遇到频率排了序第一种目标窗口用了硬件加速渲染视频、3D、部分游戏、某些浏览器内容。这类内容不经过系统的常规绘制路径屏幕捕获拿到的是空图层。表现就是那块区域全黑其他区域正常。解法没有通用的能关硬件加速就关关不了就换个捕获接口或者干脆放弃截这块改用别的信号判断。第二种屏幕锁了或者会话断开。锁屏状态下捕获不到有效画面远程会话断连时同理。如果你写的是长期跑在后台的服务必须加一个“会话是否活跃”的检查或者接受这种状态下截到的是黑图并把它排除在判断之外——这一步不做你的脚本会在半夜误判然后疯狂重试。第三种某些桌面系统需要手动授权屏幕录制。第一次运行不弹窗、不报错就是黑图。这种情况去系统设置里看一眼授权列表比在代码里瞎猜快得多。第四种抓取 window 句柄对应的窗口而不是屏幕区域时窗口被遮挡或者最小化。窗口最小化后尺寸可能是 0抓出来就是空。这条我踩过写了个循环抓了半小时全是黑图最后发现是窗口被自己挡住了。排查顺序建议先用最简单的全屏截一张存盘能出来 → 说明环境和权限没问题问题在坐标或窗口出不来 → 看会话状态和授权。这个二分法能省很多时间。5.2 坐标对不上、总是偏一截先确认一件事偏差是固定的还是随缩放变的固定偏差一般是坐标算错了比如忘了减去屏幕的 left 偏移或者区域宽高和起点搞混了。随缩放变的一定是 DPI 认知不一致回头做 3.1 那步。还有一种很隐蔽的情况截出来的尺寸比你请求的小。比如请求 800×600实际拿到 640×480。这是缩放比例正好 1.25系统在中间做了取整。这时候不要试图“修正尺寸”而是用相对坐标定位——先截一大块再在图像里找目标特征别依赖精确的绝对像素。多屏时坐标偏移八成是把monitors[0]和monitors[1]搞混了。记住 0 是整块虚拟屏的包围盒1 开始才是一块块具体的屏而且这个顺序不保证和系统设置里显示的“1 号屏、2 号屏”一致得打出来看。再补一个改过缩放比或者换过显示器之后一定要重启脚本。有些库在初始化时缓存了显示器信息运行中改设置它不认你会怀疑人生。5.3 跑久了卡死、内存一路涨内存涨的根源就一个有东西没释放。按排查优先级第一帧对象被意外持有。比如把每次的 frame 塞进了 list、dict或者挂在了某个全局对象上。哪怕只是一个引用整个数组都不会释放。检查方法很简单跑两分钟len(gc.get_objects())看趋势或者干脆在循环里print(id(frame))看引用计数。最土的办法往往最快——我在循环末尾加frame None问题消失说明确实是持有导致的虽然后来找到了真正的持有者。第二捕获对象被反复创建。mss.mss()每次调用都初始化一遍上下文放在循环里就是灾难。应该在外面建一次循环里复用。我第一版就犯了这个错跑一分钟句柄数涨了两百多。第三日志或者缓存无界增长。写文件不轮转、把每帧的统计结果 append 到一个永不清理的列表里都是隐性炸弹。给这些结构设个上限超过就吐出旧的。第四个容易被忽略某些库在特定版本下有资源泄漏症状是句柄数缓慢上涨然后突然卡住。遇到这种先升级到最新版试试别急着改自己的代码。5.4 一个够用的速查表症状最可能原因首选动作全黑 / 全白硬件加速渲染、会话不活跃换捕获方式或加会话检查颜色发蓝发红通道顺序 BGR/RGB 混用入口统一转 RGB坐标整体偏移DPI 缩放未声明启动时设进程 DPI 感知副屏截不到用了 monitors[1] 或手写坐标改用相对屏偏移函数尺寸比请求的小缩放比取整改相对定位别依赖绝对像素单帧耗时忽高忽低GC 抖动或磁盘 IO 干扰检测链路不落盘内存持续上涨帧对象被持有 / 捕获对象重复创建复用实例循环后置 None循环越跑越慢日志或缓存无界增长加轮转和上限定时不准用了 time.time()换 perf_counter夜间误判重试锁屏黑图被当成“无变化”排除全黑帧6. 几个可以立刻上手的练习和我自己的习惯6.1 三个由浅入深的练习第一个十五分钟能做完把 4.1 的脚本跑通改成按区域截取区域参数从命令行读。目的不是学新东西是让你亲手感受一次“坐标写错会怎样”——故意把 left 多加 100看看截出来的图偏成什么样这个印象比看十篇文章都深。第二个半小时做一个“窗口亮度监测”。每两秒截一条 40 像素高的横条算平均亮度把结果 append 到一个最多存 300 条的 deque 里同时在终端打出最近十次的值。跑起来之后你去操作界面观察数字怎么变。这个练习的价值在于让你对“阈值的实际波动范围”有体感以后定阈值不会再凭感觉拍一个。第三个一小时把 4.3 的差分检测做成一个可配置的小循环。配置文件里写区域、间隔、阈值、触发后存图的格式跑起来之后故意制造变化打开个窗口、滚动页面看它什么时候触发、什么时候不触发。这个练习做完你就有了一个能塞进自己真实任务里的积木改几个参数就能用。6.2 我自己落下来的几个习惯第一所有屏幕相关脚本的第一段永远是“打印环境”。屏幕尺寸、显示器列表、缩放比、库版本四行 print出问题时不用再问别人“你什么环境”。这四行代码救过我太多次看起来最没技术含量实际最省时间。第二区域坐标单独放一个字典别散落在函数里。因为这类参数改得最频繁集中在一起改成一次就行散着写总有漏改的地方而漏改的表现又是“截图偏了”排查成本很高。第三检测逻辑和捕获逻辑之间留一层薄薄的接口。捕获只负责返回数组检测只负责吃数组返回布尔值和分数两边不互相调用。好处是你可以拿历史截图离线跑检测逻辑不用每次真去截屏——调试阈值的时候这个能省掉大量重复劳动。第四给所有长时间运行的循环加一个“最多跑多少次”的保险。for _ in range(max_rounds)包一层超了自动退出。我有一回忘了加一个脚本在后台跑了一夜存了两万多张图第二天硬盘告警。第五别信“一次调好永久有效”。界面的布局会变、系统的缩放会变、显示器会换所有依赖像素坐标的东西都有保质期。我的做法是每次脚本启动先截一张“基准图”存起来跑挂了先看基准图对不对一眼就知道是环境变了还是逻辑坏了。这套东西我用了大半年从最初“每两秒截一张全屏然后怀疑人生”到现在“截百分之一面积、每十秒一次、稳如老狗”中间最大的转变不是学会了某个库而是想清楚了“我到底需要看见多少”。屏幕捕获这件事能少看一点就少看一点能少看一次就少看一次——剩下的精力和算力留给真正需要判断的地方。
RELATED

相关推荐

用librtsp搭建RTSP H.264 Server:从NALU到会话管理

用librtsp搭建RTSP H.264 Server:从NALU到会话管理

简介:这是一份面向网络流媒体开发者的轻量级RTSP服务器实现源码包,基于librtsp-master库改造,专注于H.264视频流的传输与控制,适合需要快速搭建RTSP服务或深入理解RTSP/RTP/RTCP协议栈的初中级开发者。压缩包共19个文件&#xff0…

📅 2026/10/31 9:32:57
AI日报:轻量模型、多Agent协作与AI编程工具链最新进展

AI日报:轻量模型、多Agent协作与AI编程工具链最新进展

2026年9月28日,AI圈的新闻又多到刷不完。我一边刷着各大技术社区和开源仓库的最新更新,一边把手头几个项目的验证结果记下来,这才敢说今天有资格写这份日报。今天的内容会覆盖轻量大模型、Agent协作、AI编程工具链、短剧与绘画工作流&#xf…

📅 2026/10/31 9:32:57
焊接机器人单面坡口填充技术解析

焊接机器人单面坡口填充技术解析

1. 项目概述:为什么单面坡口填充能成为埋弧焊工艺的“分水岭” “焊接机器人的坡口填充功能”这个标题乍看平平无奇,但把后半句“老师傅都赞叹的埋弧焊工艺:对接接头的单面焊接”叠加上去,整件事的分量就完全不同了。我干焊接自动…

📅 2026/10/31 9:27:57
MORE NEWS

更多资讯

📰

Agent从单兵到摇人:多Agent系统安全边界重构与权限衰减实践

1. 从“单兵作战”到“摇人干活”:Agent 能力跃迁背后的安全逻辑断层 过去两年,我接触过不少 Agent 项目,从最简单的“读文件、调接口、返回结果”的脚本型 Agent,到能自主规划任务、调用多个工具、甚至动态生成子任务的多层 Agen…

📰

ChatGPT生成PPT实战:官方加载项、模板引擎与VBA宏三条路径全解析

1. 为什么我最终放弃了手动排版,转向ChatGPT生成PPT 做PPT这件事,我干了快八年。从最早给咨询公司画图表,到后来给创业团队做路演稿,再到帮高校老师整理课题汇报材料,少说也攒了几百份。以前我的工作流很固定&#xff…

📰

智能体能力周期表:五维度拆解243种构型与选型指南

1. 从"单点工具"到"能力地图":我为什么要做这张智能体周期表过去大半年,我几乎把市面上能上手的智能体框架都摸了一遍,从扣子、Dify 这类可视化平台,到 Agno、LangGraph 这类代码框架,再到各种垂直…

📰

AI办公套件化:Agent与Harness架构解析及实操落地

1. AI办公套件化的底层逻辑:从单点工具到系统级入口过去两年,AI办公赛道经历了一轮又一轮的洗牌。最开始大家拼的是单点能力——谁能写更好的文案、谁能生成更漂亮的PPT、谁能更快地整理会议纪要。但到了2025年,一个明显的趋势浮出水面&#…

📰

NCRE三级数据库安全管理与完整性约束实战解析

1. NCRE三级数据库技术安全管理核心要点解析 作为数据库领域的国家级认证考试,NCRE三级数据库技术第9章"安全管理"是每位数据库从业者的必修课。在实际工作中,我曾遇到过因权限设置不当导致业务数据泄露的案例,也处理过因约束缺失引…

📰

LibTomCrypt F8模式实现详解:从CFB痛点到3GPP加密实战

分组密码这东西,算法本身只解决了“怎么把一块数据搅乱”的问题,真正落到工程里,麻烦的从来都是“怎么把一长串数据切成块、每块怎么串起来、出错会不会传染、能不能并行”。我最早接触 F8 模式是在啃 LibTomCrypt 源码的时候,翻到…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬