做图色识别或者屏幕自动化时很多初学者第一反应是找大漠插件这类现成工具结果卡在注册、版本兼容、绑定窗口这些环节反而没有把 Python 本身的逻辑理清楚。等过几个月再回头看真正值得反复练习的其实是条件判断、循环控制、多线程协作这些基础能力。本文将围绕“Python 图色识别”这个应用场景写透条件运算符的用法并演示不依赖大漠插件、用纯 Python 完成一个简单的多线程图色监视器。整个系列定位在零基础到实战所以代码会尽量完整重点步骤都会解释“为什么这样写”。图色识别是屏幕自动化中的一个常见思路很多桌面工具都有这个需求。本文中的《永恒之塔》等字样只作为示例场景的参考不涉及游戏内数据修改、封包或破解逻辑。如果要在真实游戏中使用自动化脚本请务必先阅读并遵守游戏服务条款仅用于个人学习、软件自动化测试或合法授权的场景。1. 图色实战到底在做什么1.1 什么是“图色识别”图色识别并不是图像识别里的深度学习也不是 OCR 文字识别而是更原始的一类操作通过程序读取屏幕某个坐标或某个区域内的像素颜色再根据这些颜色值判断当前画面的状态。例如某个软件出现了红色图标说明任务失败。某个界面上出现了特定色块说明页面加载完成。游戏小地图中出现了某片海域的蓝色说明角色可能移动到了海边。自动化脚本需要等待某个按钮从灰色变成亮色再执行点击。这些场景都可以用“读取颜色”的方式完成。相比目标检测模型图色识别不需要大量样本也不依赖 GPU逻辑简单直接非常适合用来练习 Python 基础语法。1.2 为什么本文不用大漠插件大漠插件在 Windows 自动化领域确实很流行很多自动练级、自动打怪的工具都基于它开发。但插件的问题也比较明显第一插件注册复杂。很多版本需要收费注册账号、调用接口、处理字体保护对于新手来说门槛较高。第二绑定窗口后的运行效果依赖系统环境。不同 Windows 版本、不同权限模式、不同游戏引擎都可能出现抓屏黑屏、鼠标失效、绑定失败等问题。第三不利于理解原理。大漠插件帮你封装好了后台截图、找色、找图、模拟点击一旦离开它底层的 Python 并发、像素读取、窗口坐标换算能力仍然没有掌握。本文用纯 Python 完成图色读取不依赖大漠插件也不涉及复杂的插件注册流程。你只需要安装几个常用库比如 Pillow就能从屏幕上截取指定区域并读取像素颜色。理解这些基础原理后无论将来采用哪种自动化工具你的底层判断逻辑都能复用。1.3 条件运算符和多线程是什么关系条件运算符本质上就是“根据条件的真假选择不同结果”。多线程则是让多个任务交替或并行执行。把两者结合在一起的场景非常常见后台线程 A 负责监视屏幕左上角区域是否出现颜色 C1。主线程或后台线程 B 负责监视屏幕右下角区域是否出现颜色 C2。两个线程各自得出一个布尔结果再通过条件表达式决定是否执行后续操作。如果不用条件运算符代码里会出现大量if else逻辑冗长。如果不用多线程程序只能一个区域一个区域轮流截图响应速度会慢很多。2. 环境准备与基础概念2.1 Python 环境搭建本文示例以 Windows 系统为主。Python 建议使用 3.8 及以上版本这里使用的是常见稳定版本读者可以按自己电脑实际情况安装。官方下载地址是www.python.org/downloads/windows/下载后安装时务必勾选 “Add Python to PATH”。安装完成后打开命令行输入python --version如果能输出类似Python 3.10.11说明安装成功。如果提示python不是内部或外部命令说明环境变量没有配置好。可以将 Python 安装目录和Scripts目录分别添加到 PATH 环境变量中。配置完成后重新打开命令行窗口再运行python --version。2.2 安装第三方库本示例主要用到 Pillow 来完成屏幕截图和像素读取。Pillow 是 Python 生态中一个非常成熟的图像处理库语法简单适合入门。在命令行中执行pip install pillow如果下载速度偏慢可以临时使用国内镜像源pip install pillow -i https://pypi.tuna.tsinghua.edu.cn/simple后续如果涉及窗口标题定位可以再安装pywin32pip install pywin32安装完成后可以用一行命令确认 Pillow 是否可用from PIL import ImageGrab print(Pillow 已就绪)2.3 本文的项目结构建议新建一个目录专门保存本次实战代码color_project/ ├── monitor.py # 多线程图色监视器 └── get_pixel.py # 单点取色与区域取色练习这种拆分方式很符合工程习惯基础取色功能写一个文件多线程应用写在另一个文件中。这样即使之后改动线程调度逻辑也不会影响取色函数的稳定性。3. 条件运算符核心语法3.1 三元条件表达式Python 中的条件运算符通常指三元条件表达式语法如下结果 值1 if 条件 else 值2当条件成立时整个表达式等于值1否则等于值2。先看一个简单的例子lv 50 title 高级账号 if lv 30 else 普通账号 print(title)程序会根据lv的大小决定输出高级账号还是普通账号。在这里条件运算符的优点是让你把“判断”和“取值”写在同一行尤其是当一个变量的值由简单条件决定时代码会非常清爽。传统写法lv 50 if lv 30: title 高级账号 else: title 普通账号两种写法都能工作但三元表达式适合简单分支。复杂分支不要强行使用三元表达式否则代码可读性会下降。3.2 与比较运算符和逻辑运算符配合图色判断中的条件往往不止一个颜色值相等那么简单。比如判断某个像素是不是“偏蓝色的海水”不能只要求红色通道等于多少、蓝色通道等于多少因为屏幕压缩、抗锯齿和硬件颜色偏差会导致颜色不一致。通常我们允许一个误差范围。常用比较运算符包括两个值是否相等!两个值是否不相等、、、比较大小常用逻辑运算符包括and多个条件同时成立or多个条件至少一个成立not对条件取反以下代码演示如何判断一个像素是否接近指定目标颜色r, g, b 120, 80, 30 target_r, target_g, target_b 110, 90, 25 tolerance 20 match ( abs(r - target_r) tolerance and abs(g - target_g) tolerance and abs(b - target_b) tolerance ) result 颜色匹配 if match else 颜色不匹配 print(result)这组代码中的tolerance是容差可以理解成允许的误差范围。如果真实颜色和理想颜色相差不超过 20就认为匹配成功。3.3 条件运算符在函数返回值中的用法在封装图色判断函数时条件运算符常常直接出现在return语句中。例如def check_target_color(rgb, target, tolerance30): r, g, b rgb[:3] tr, tg, tb target[:3] return True if ( abs(r - tr) tolerance and abs(g - tg) tolerance and abs(b - tb) tolerance ) else False这里写return True if 条件 else False有时候会让新手觉得多此一举因为条件本身就是布尔值。实际上直接在条件后面写return也可以但在一些更复杂的业务逻辑中条件运算符可以直接返回状态文本、颜色编号或其他业务对象这比先if再赋值更紧凑。下面这种写法更常见def get_status_by_count(count): return 目标出现 if count 50 else 目标未出现值得注意的是三元表达式会先计算条件再选择对应结果分支所以在条件分支里可以调用不同的函数。不过为了可读性最好不要把复杂调用塞进单行表达式。4. 从屏幕读取像素颜色4.1 单点取色在 Windows 下Pillow 的ImageGrab可以很方便地抓取屏幕指定区域。抓取一个 1x1 的区域就相当于读取这个点的颜色。新建文件get_pixel.py输入以下代码from PIL import ImageGrab import time def get_pixel_color(x, y): 获取屏幕上指定坐标的颜色。 返回值为 (R, G, B)。 img ImageGrab.grab(bbox(x, y, x 1, y 1)) return img.getpixel((0, 0)) def show_color(x, y): rgb get_pixel_color(x, y) desc 目标色 if is_target(rgb) else 普通色 print(f坐标 ({x}, {y}) 的颜色是 {rgb}判断结果{desc}) def is_target(rgb, target(255, 0, 0), tolerance20): r, g, b rgb[:3] tr, tg, tb target return ( abs(r - tr) tolerance and abs(g - tg) tolerance and abs(b - tb) tolerance ) if __name__ __main__: show_color(100, 100)这段代码先抓取屏幕上(100, 100)这个坐标附近 1x1 的区域再通过getpixel方法取出颜色。这里需要特别留意屏幕分区可能是逻辑坐标也可能是物理坐标。Windows 如果开启了显示缩放例如 125% 或 150%部分程序的坐标会和实际像素坐标不一致。为了调试时减少干扰可以先临时把显示缩放调整为 100%等逻辑稳定后再考虑兼容多分辨率。4.2 读取指定区域的颜色单点取色虽然简单但稳定性较差。屏幕上很多区域会出现渐变、阴影、抗锯齿边缘只靠一个点很容易误判。更稳妥的方案是截取一个小区域统计区域内符合目标色像素的数量。下面这段代码以(200, 200)为左上角截取100x100的区域并统计其中接近目标色的像素个数。from PIL import ImageGrab def count_color_in_region(box, target(0, 0, 255), tolerance30): box: (left, top, right, bottom) target: 目标颜色 (R, G, B) img ImageGrab.grab(bboxbox) tr, tg, tb target count 0 total 0 for pixel in img.getdata(): total 1 r, g, b pixel[:3] if ( abs(r - tr) tolerance and abs(g - tg) tolerance and abs(b - tb) tolerance ): count 1 ratio count / max(total, 1) return count, ratio if __name__ __main__: box (200, 200, 300, 300) count, ratio count_color_in_region(box) status 高置信度 if ratio 0.5 else 低置信度 print(f目标像素数量{count}占比{ratio:.2%}状态{status})程序的逻辑很简单遍历整个区域的像素每发现一个接近目标色的像素就累加一次。最后用命中数量除以区域总像素数量得到占比。当占比超过一定阈值时再判断“这块区域存在目标颜色”。这样做比单点判断可靠得多因为即使画面抖动或目标物体没有完全覆盖中心点只要区域内大部分颜色符合特征依然能正确识别。4.3 注意坐标系问题用ImageGrab截图默认抓取整个屏幕坐标原点在屏幕左上角。如果你的程序需要操作某个固定窗口例如控制一个单独的桌面工具窗口不要直接把窗口内部控件坐标当成屏幕坐标。在 Windows 中可以先用用户手动输入的“窗口左上角坐标”和“窗口宽高”来确认截图范围left int(input(请输入窗口左侧坐标: )) top int(input(请输入窗口顶部坐标: )) right int(input(请输入窗口右侧坐标: )) bottom int(input(请输入窗口底部坐标: )) box (left, top, right, bottom)实际开发中更推荐用win32gui.FindWindow等方法根据窗口标题拿到窗口句柄再获取窗口矩形。但对于零基础阶段先用固定坐标把图色判断逻辑跑通更重要。5. 用颜色区域判断“是否到达目标地图”5.1 从界面区域提取特征以《永恒之塔》这类角色扮演游戏为例不同地图的海水、草地、地面颜色往往有明显差异。比如某一张海岛地图的海水偏蓝绿色而城镇地表偏灰黄色。要判断角色是否在某张地图内不需要对整张游戏画面做复杂分析只需要取屏幕中一块固定位置上的“地表或天空区域”统计该区域的主色占比并设定条件性别名。假设目标区域的海水蓝色目标值为(60, 190, 220)容差为35颜色占比超过40%时判断当前在海岛区域。如果使用空口描述读者很难形成代码记忆。下面给出完整可运行示例。from PIL import ImageGrab def region_has_target_color(box, target, tolerance35, need_ratio0.4): img ImageGrab.grab(bboxbox) tr, tg, tb target match_count 0 total 0 for pixel in img.getdata(): total 1 r, g, b pixel[:3] if ( abs(r - tr) tolerance and abs(g - tg) tolerance and abs(b - tb) tolerance ): match_count 1 ratio match_count / max(total, 1) return ratio need_ratio, ratio left, top 300, 240 right, bottom 500, 420 box (left, top, right, bottom) target (60, 190, 220) ok, ratio region_has_target_color(box, target) map_name 海岛 if ok else 未识别 print(f区域命中占比{ratio:.2%}当前地图判断{map_name})注意这里只是演示图像颜色判断思路并不是一篇“游戏外挂教程”。游戏软件的用户协议通常会对自动化脚本做出限制只有合法授权的自动化测试、无障碍工具或学习研究环境才适合使用这些代码。5.2 多区域联合判断一些场景下单一区域的颜色不够明显比如地图边缘可能出现大面积水面也可能因为镜头旋转导致水面位置变化。这时就可以把多个区域的结果通过条件运算符组合起来。假设现在有两个区域区域一个在左上角一个在右下角。只有当“左上角偏蓝”和“右下角偏绿”同时成立时才认为地图特征匹配。def both_regions_ok(box1, target1, box2, target2): ok1, _ region_has_target_color(box1, target1) ok2, _ region_has_target_color(box2, target2) result 地图匹配 if (ok1 and ok2) else 条件不充分 return result这里体现的是一种组合思想单点不稳定就用多点单区域不稳定就用多区域一个颜色特征不够就用多个颜色特征。这些条件组合与 Python 中的and、or、not天然契合。5.3 为什么不能只用一个颜色值屏幕像素颜色会受到以下因素影响显示器色温不同。系统夜间模式或滤镜。游戏画面动态光影。抗锯齿导致的边缘渐变。录制压缩带来的色差。如果代码里直接写if color (255, 0, 0)只要有一点颜色偏差判断就会失败。因此图色识别开发中最常见的提法不是“完全相等”而是“在一定容差范围内相似”。这也是条件运算符经常配合abs()函数计算差值的原因。6. 多线程与图色监视器6.1 单线程图色循环的缺陷如果只检测一个固定区域单线程循环也可以实现。例如import time from PIL import ImageGrab while True: img ImageGrab.grab(bbox(200, 200, 250, 250)) pixel img.getpixel((0, 0)) if pixel[0] 200: print(检测到红色) time.sleep(0.2)但这种写法最大的问题是程序只能按顺序检查区域。如果你需要在同一时间监控 3 个区域单线程就必须依次截图每增加一个区域轮询周期就会变长响应灵敏度明显下降。6.2 使用 threading 创建后台任务Python 多线程的用法很直观。导入threading后创建Thread对象把一个函数传给target即可。import threading import time def worker(name, duration): for i in range(duration): print(f线程 {name} 正在工作第 {i 1} 秒) time.sleep(1) threads [] for t_name in [A, B]: t threading.Thread(targetworker, args(t_name, 3)) threads.append(t) t.start() for t in threads: t.join() print(所有线程执行完毕)start()表示启动线程join()表示等待线程结束。设置daemonTrue时只要主程序退出线程也会强制退出适合长时间巡检类任务但最终工程中还是建议使用事件对象优雅退出。6.3 用 Event 控制线程停止屏幕监控线程通常是一个死循环。如果直接在循环里写while True主程序将很难停止子线程。建议使用threading.Event控制退出。import threading import time stop_event threading.Event() def monitor(): while not stop_event.is_set(): print(监控中...) time.sleep(0.2) print(收到停止信号) t threading.Thread(targetmonitor, daemonTrue) t.start() time.sleep(1) stop_event.set()Event的内部状态是False调用set()后变成True。监控线程在每个循环里先检查stop_event.is_set()一旦为True就退出循环。这种写法比直接终止线程安全因为线程可以在退出前清理资源、打印日志或保存状态。6.4 多线程代码中的等待技巧监控循环中如果使用time.sleep在主线程执行stop_event.set()后子线程最多需要等待一次time.sleep的时长才能退出。如果希望退出更及时可以使用Event.wait()。例如def monitor(): while True: result do_check() if result: print(检测到目标) if stop_event.wait(0.2): breakEvent.wait(0.2)的作用是如果事件被设置立即返回True如果没被设置则最多阻塞 0.2 秒。这样既能控制轮询频率又能在停止信号出现时快速退出。7. 综合实战多线程条件识别7.1 需求说明现在做一个简单的多线程图色监视器功能如下用户可以设置多个监视区域。每个监视区域由一个独立线程负责。每个线程每隔一段时间截取指定区域。区域内目标色占比超过阈值时条件运算符输出“触发状态”。主线程运行若干秒后发起停止信号。示例场景中我们假设要同时监控两个区域左上角检测蓝色右下角检测红色。这种模式和前面提到的“地图状态判断”基本思路一致但换成多线程后每个区域互不干扰。7.2 完整代码新建monitor.py输入以下代码import threading import time from PIL import ImageGrab class ColorRegionMonitor: def __init__(self, name, box, target_color, tolerance30, priority0.3): name: 线程名称 box: (left, top, right, bottom) target_color: 目标颜色 (R, G, B) tolerance: 容差 priority: 目标色占比达到多少算触发 self.name name self.box box self.target_color target_color self.tolerance tolerance self.priority priority self._stop_event threading.Event() self._thread None def start(self): if self._thread and self._thread.is_alive(): print(f[{self.name}] 已经在运行) return self._stop_event.clear() self._thread threading.Thread( targetself._run, nameself.name, daemonTrue ) self._thread.start() print(f[{self.name}] 开始监控) def stop(self): self._stop_event.set() print(f[{self.name}] 已发送停止信号) def _run(self): while True: if self._stop_event.is_set(): print(f[{self.name}] 已退出) break try: img ImageGrab.grab(bboxself.box) total 0 match_count 0 for pixel in img.getdata(): total 1 if self._is_match(pixel): match_count 1 ratio match_count / max(total, 1) status 触发 if ratio self.priority else 未触发 print( f[{self.name}] 命中占比 {ratio:.1%}状态{status}, flushTrue ) except Exception as exc: print(f[{self.name}] 截图或判断异常{exc}, flushTrue) if self._stop_event.wait(0.2): print(f[{self.name}] 已退出) break def _is_match(self, pixel): r, g, b pixel[:3] tr, tg, tb self.target_color return ( abs(r - tr) self.tolerance and abs(g - tg) self.tolerance and abs(b - tb) self.tolerance ) def main(): monitor_a ColorRegionMonitor( name区域A-蓝, box(200, 200, 260, 240), target_color(60, 190, 220), ) monitor_b ColorRegionMonitor( name区域B-红, box(400, 300, 460, 340), target_color(220, 60, 60), ) monitor_a.start() monitor_b.start() time.sleep(3) monitor_a.stop() monitor_b.stop() print(主线程结束) if __name__ __main__: main()运行方式python monitor.py如果屏幕两个区域没有对应颜色输出大概率是[区域A-蓝] 开始监控 [区域B-红] 开始监控 [区域A-蓝] 命中占比 0.0%状态未触发 [区域B-红] 命中占比 0.0%状态未触发 ... [区域A-蓝] 已发送停止信号 [区域A-蓝] 已退出 [区域B-红] 已发送停止信号 [区域B-红] 已退出 主线程结束7.3 条件运算符如何在这个例子中生效_run函数里有这样一行status 触发 if ratio self.priority else 未触发条件运算符直接根据判断结果生成了状态文本。如果不使用三元表达式代码会变成至少三行的if else赋值。在监控线程里这种简写能减少代码层级让核心逻辑更突出。同时_is_match中使用了多个比较条件的and组合虽然这些不是严格意义上的三元表达式但它们的本质也是一种条件判断。图色识别正是由这些条件表达式组成的最小判断单元。7.4 多线程截图是否真的会更稳定要注意Python 多线程并不能绕过多核 CPU 的负载限制也不能让大量计算突然快起来。截图操作本身耗时较长如果两个线程同时调用ImageGrab.grabWindows 图形环境是否允许两个线程并行抓取取决于操作系统和图像接口实现。刚才这段代码的价值不在“极速”而在于让读者掌握一种扩展模型不同区域对应不同线程线程之间通过Event协作通过条件表达式输出状态。如果你希望处理更复杂的图像识别任务后续还可以改为进程池加队列的模式。8. 常见问题与排查方法下面是图色实战中最容易遇到的一些问题。问题现象常见原因解决思路ModuleNotFoundError: No module named PIL没有安装 Pillow 库执行pip install pillowImageGrab.grab截图失败或返回黑色当前环境不支持屏幕抓取或区域超出屏幕使用 Windows 桌面环境确保区域坐标在屏幕范围内运行win32gui相关函数时报错没有安装 pywin32执行pip install pywin32颜色识别偏差大显示器颜色不一致、抗锯齿、动态光影调宽容差采用区域占比统计画面有蓝绿色滤镜时识别不准系统夜间模式、HDR、显示滤镜关闭系统滤镜或把目标色校准到真实屏幕值多线程停止不下来子线程使用了time.sleep无法及时感知事件把time.sleep(0.2)改成stop_event.wait(0.2)多线程输出内容混乱多个线程同时打印日志使用logging模块或把结果放入queue.Queue屏幕坐标和鼠标坐标不一致Windows 显示缩放不是 100%在启动脚本里设置SetProcessDpiAwareness或临时调整缩放为 100%如果出现“读取黑色”的情况需要确认当前窗口是否处于后台或被其他窗口遮挡。很多截图接口并不会后台截取特定窗口画面中如果最顶层窗口挡住了截图区域读取到的内容也会受到遮挡层影响。9. 工程实践建议9.1 不要把可复现代码和真实业务混在一起教程代码为了教学方便会把截图、颜色判断、线程启动都放在同一个类中。实际项目中如果要长期运行建议把截图函数、颜色匹配函数、逻辑判断函数和线程调度函数分开维护。推荐目录结构src/ ├── capture.py # 屏幕截图 ├── color_match.py # 颜色匹配 ├── monitor.py # 多线程监控框架 ├── scene_rule.py # 业务场景状态 └── main.py # 主函数截图函数只负责截图颜色匹配函数只负责判断某个像素或区域是否命中。以后如果从 Pillow 换成 mss或者从颜色识别换成成更复杂的图像匹配只需要修改对应模块。9.2 使用日志代替 print在控制台里使用print做演示很方便但多线程场景下多个线程同时输出日志格式很难看。而且在发布时为定位凌晨运行出现的问题你通常需要日志文件。建议尽早使用 Python 自带logging模块import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(threadName)s] %(message)s ) logger logging.getLogger(color_monitor) logger.info(监控线程启动)9.3 设置合理的容差与轮询频率图色监控不是越快越好。CPU 占用率过高、截图次数过于频繁会让电脑卡顿也可能影响其他程序正常运行。建议先摸清业务需要多快的响应速度。如果 0.3 秒轮询一次已经足够就不必盲目的优化到每秒 30 次。每次截图后解析整个区域的像素必然有 CPU 消耗。 如果命中率稳定但响应慢可以优先考虑减少截图区域。9.4 多线程任务中谨慎调用共享资源如果多个线程需要把结果写入同一个列表或字典可能存在数据覆盖问题。解决办法是给共享资源加锁。import threading lock threading.Lock() result {} def write_result(key, value): with lock: result[key] value锁可以保护数据但不能加得太粗否则多个线程又会变成排队执行。本例中线程只是打印结果没有涉及共享资源所以不需要加锁。一旦后面扩展为“线程把颜色状态写到全局变量”就需要考虑锁或队列。9.5 遵守合规边界图色识别技术本身是通用的可用于软件自动化测试、数据采集辅助、无障碍辅助工具等。如果在网络游戏场景中使用它可能会被游戏服务条款禁止甚至带来账号封禁风险。写代码前一定要明确自己的使用场景是否合法合规不要为了演示某个功能而将脚本用于违背产品规则或他人利益的场景。10. 下一步提升方向完成这里的多线程图色识别后你已经掌握了一条比较清晰的路线用 Python 读取屏幕某一点的 RGB 值。用区域颜色占比判断画面状态。用条件运算符简化状态输出。用多线程同时监控多个区域。用Event优雅停止线程。这些能力在爬虫自动化、桌面软件测试、界面按钮状态检测、机器人流程自动化等方向都会反复出现。如果需要继续深入可以从下面几个方向选择学习 Windows 窗口管理根据窗口标题定位窗口坐标。学习 mss 截图库获得更快的截屏速度。学习 OpenCV 色彩空间掌握 HSV 颜色匹配和多模板匹配。学习queue.Queue线程安全队列把线程产生的监测结果交给主线程统一处理。学习逻辑回归等简单的机器学习模型用更智能的方式解决画面分类问题。其实图色识别最核心的难点从来不是某个库的 API而是对坐标、颜色、并发与业务场景的理解。把条件运算符写熟练把多线程的生命周期管理清楚你后续再看任何自动化框架都会轻松很多。如果这篇文章对你有帮助可以先收藏备用然后打开编辑器把monitor.py亲手敲一遍你会比只看不写记得更牢。
