桌面状态识别里,最容易把新手卡住的地方不是算法,而是整条链路没有理顺。所谓“图色识别”,本质上就是持续截取屏幕上的某个区域,把像素颜色读出来,然后通过阈值、条件判断和状态机决定当前画面处于什么状态。再往后走,为了避免一个区域识别过慢拖垮整体,又需要引入多线程并发处理。这篇文章围绕“条件运算符做状态判断”这个技术主线,从单区域识别一步步改写成多区域并行监控,全程不使用商业图色插件,只依赖 Python 生态里的 OpenCV、NumPy 和 mss。
很多视频教程里喜欢用“大漠插件”封装好的命令做找图找色,但这类方案存在环境绑定、收费授权和调试困难的问题。更关键的是,插件把底层细节都封装掉了,学习者很难真正理解图像是怎么变成布尔条件的。因此这里的项目会把核心逻辑自己实现出来,先读取屏幕区域,再计算目标颜色占比,最后用 Python 条件表达式输出一个稳定状态。这样既能学到图像处理基础,也能理解多线程下共享状态该怎样保护。
本篇文章希望达成的结果是:读者看完后,能自己写出一个针对多个屏幕区域进行颜色识别、状态分类、并发打印的小框架。它适合刚开始学 Python、想了解屏幕图像识别、又不想被商业插件裹挟的开发者在本地练习。
1. 先把“条件运算符 + 图色识别”这条主线理清楚
1.1 图色识别不是神秘黑盒,而是把画面变成数字然后做比较
屏幕画面在计算机里本质上是一个二维数组。以 OpenCV 为例,一帧图片读入后会变成height x width x channel的 NumPy 数组,三个通道在 OpenCV 里默认顺序是 BGR,不是 RGB。所谓“找色”,就是遍历图像的某个区域,检查某个位置的b、g、r数值是否接近目标颜色。
常见需求通常长这样:
- 屏幕左上角出现红色图标,代表某个状态开启。
- 地图上某个位置变成蓝色,代表区域切换完成。
- 血条颜色从绿色变成黄色,需要改变后续处理分支。
这些需求落到代码上,最后都会变成一个布尔值:目标颜色是否达到足够占比。布尔值再交给 if、条件表达式或独立状态函数去决定业务状态。
这时“条件运算符”才真正发挥作用。它把一个“真或假”的像素判断结果,转换成便于阅读和流传的状态字符串,比如:
state = "RUNNING" if is_target_visible else "STOPPED"建议初学者先在心里建立一个公式:
图像采集 -> 颜色检测 -> 布尔结果 -> 条件判断 -> 状态输出后续所有的多线程、防抖和队列改造,都是围绕这条链路展开的。
1.2 Python 条件表达式的形态和易错点
Python 里的条件表达式通常写成:
值1 if 条件 else 值2它和if...else...最大的区别是:它本身是一个表达式,可以赋值给变量,也可以作为函数参数传入。举例说明:
ratio = 0.35 threshold = 0.3 result1 = "OPEN" if ratio > threshold else "CLOSE"如果使用普通写法,就是:
if ratio > threshold: result1 = "OPEN" else: result1 = "CLOSE"看起来只是少了几行,但在需要连续判断状态时,条件表达式能明显压缩代码:
def classify_by_ratio(ratio): if ratio >= 0.5: return "HIGH" elif ratio >= 0.2: return "MIDDLE" else: return "LOW"等价写法是:
def classify_by_ratio(ratio): return "HIGH" if ratio >= 0.5 else "MIDDLE" if ratio >= 0.2 else "LOW"第二种写法的可读性其实并不好。工程中更推荐第一种,因为当判断层级超过两层时,elif的可读性和可调试性更稳定。条件表达式更多用于“单层二元选择”,比如“启动 / 停止、正常 / 异常、存在 / 不存在”。
容易陷入的误区是:条件表达式不是万能速写。如果某个分支后面还需要执行多行代码,一定要回到if...else...正常写法,不要为了压缩而把所有逻辑塞在一个表达式里。
1.3 所谓“非大漠插件”,指的是自己掌控识别链路
大漠插件等商业图色工具,在按键脚本、窗口绑定、后台找色等场景里确实很成熟。但它的主要问题有三个:
- 需要额外注册或授权,环境迁移成本高。
- 很多封装命令进入黑盒,报错时难以判断是图色差异还是坐标问题。
- 一旦脱离对应运行环境,代码无法在其他项目中复用。
本文使用 mss 做屏幕区域抓取,使用 OpenCV 做颜色计算,使用 threading 并发监控,用不到任何第三方商业插件。这样做的好处是代码是透明的,处理流程可读、可改、可查。
当然,这里要强调:本文只实现“识别和状态监控”,不包含向目标软件发送点击、按键或其他控制指令。凡是将图色识别用于规避安全机制、破解验证码或自动操作他人服务的行为,都属于不当使用。学习这些技术应停留在本地画面监控、软件 UI 自动化测试和正当桌面工具开发领域。
2. 从零准备一套可运行的最小环境
2.1 依赖清单与安装命令
先创建一个干净的虚拟环境,然后安装当前项目需要的库:
python -m venv venv venv\Scripts\activate在 Windows 环境下,激活命令如上。macOS 或 Linux 环境下激活脚本是source venv/bin/activate。
安装依赖:
pip install opencv-python numpy mss各库作用如下表所示:
| 库 | 主要用途 | 安装后的 import |
|---|---|---|
| opencv-python | 读取图像、处理 BGR 像素、颜色计算 | import cv2 |
| numpy | 处理图像数组、计算颜色匹配矩阵 | import numpy as np |
| mss | 快速截取屏幕指定区域,返回像素帧 | import mss |
如果后续要查找某个顶层窗口并获取窗口坐标,还可以使用pywin32:
pip install pywin32这个库不是必选项,但 Windows 下做窗口坐标定位会更方便。
2.2 先准备一张模拟屏幕区域的测试图
真实截屏会受到桌面环境干扰,入门阶段最好先用代码生成一张固定测试图,确认颜色计算逻辑没有问题。
生成测试图的脚本如下:
import cv2 import numpy as np image = np.full((360, 480, 3), 20, dtype=np.uint8) red_bgr = (0, 0, 255) cv2.rectangle(image, (60, 80), (420, 220), red_bgr, -1) cv2.imwrite("sample_roi.png", image) print("sample image created, shape:", image.shape)这段代码做了以下几件事:
- 创建一张 480 像素宽、360 像素高的纯深色图。
- 背景 BGR 值是
(20, 20, 20),是一个接近黑色的灰度背景。 - 在
(60, 80)到(420, 220)之间画一个红色矩形。 - 红色区域在 BGR 空间中写作
(0, 0, 255)。
运行后,当前目录会出现sample_roi.png文件。这个文件用来验证后续的颜色占比计算是否正确。
2.3 验证环境:把图片正确读出来
环境安装完,先确认 OpenCV 能正常读取图片:
import cv2 img = cv2.imread("sample_roi.png") print("img shape:", img.shape) print("img dtype:", img.dtype)正常输出是:
img shape: (360, 480, 3) img dtype: uint8shape表示高度、宽度和通道数,这一点需要留意:OpenCV 返回的形状顺序不是宽度 x 高度,而是高度 x 宽度。后面裁剪区域时一旦把顺序弄反,会出现“坐标看着没问题,实际检测区域却跑偏”的情况。
3. 单个区域颜色识别:让条件运算符进入实战
3.1 不要只判断单个像素,要计算目标颜色占比
实际桌面画面并不稳定,同一个红色图标可能因为阴影、高光、抗锯齿而产生颜色偏移。只判断某一个像素很容易误判,更稳的方法是统计整个区域里有多少像素接近目标颜色。
颜色匹配的基本思路是:计算每个像素和目标的通道差值,再判断差值是否小于容差。
示例代码如下:
import cv2 import numpy as np def calc_color_ratio(frame, target_bgr, tolerance=60): if frame is None or frame.size == 0: return -1.0 frame_i = frame.astype(np.int16) target_i = np.array(target_bgr, dtype=np.int16) diff = np.abs(frame_i - target_i) mask = np.all(diff <= tolerance, axis=2) return float(mask.mean())这段代码的原理可以拆成三步。
第一步,把原始图像转换成int16。因为uint8无符号类型在做0 - 255时会发生截断,导致差值计算完全错误。比如红色通道从 0 变成 255,无符号减法很可能得到 1,这会让人误判“两个颜色几乎一样”。
第二步,计算每个通道上的绝对差值,然后判断是否同时小于容差。
第三步,用mask.mean()求出整张图片中匹配像素的占比。如果图片有 10000 个像素,其中 1200 个像素符合目标颜色,占比就是 0.12。
return -1.0是一个异常标记。当某个区域没有画面或者内容为空时,用负数占位,后续条件判断可以避开它。
3.2 用条件表达式把占比变成状态
拿到ratio之后,状态判断就变得非常简单。先定义一个阈值,比如“红色像素占比不低于 30% 就算开启”。
def get_status(ratio): if ratio < 0: return "NO_SCREEN" return "OPEN" if ratio >= 0.3 else "CLOSE"上面这段使用了两层判断。第一层处理异常状态,第二层是标准的条件表达式。更完整地看:
TEST_IMAGE = "sample_roi.png" TARGET_BGR = (0, 0, 255) THRESHOLD = 0.1 frame = cv2.imread(TEST_IMAGE) ratio = calc_color_ratio(frame, TARGET_BGR) status = get_status(ratio) print("ratio:", round(ratio, 4)) print("status:", status)在示例图中,红色矩形实际占图像比例约为 29%,所以阈值设置成 0.5 会误判为CLOSE,设置成 0.1 就会输出OPEN。
用这种“区域占比 + 阈值”的方式,比只比较中心点像素更能抗噪声,也更容易理解。真正到屏幕实时监控时,这个frame会被替换成 mss 抓到的实时画面帧,其余逻辑完全可以复用。
3.3 单独识别时很容易踩的三个坑
第一个坑是 BGR 和 RGB 通道顺序混淆。用 OpenCV 读取图片时,红色是(0, 0, 255);用 mss 抓屏后得到的是 BGRA 数据,需要先把通道转成 BGR,再交给calc_color_ratio处理。
常见的错误是直接使用 PIL 的 RGB 概念去写目标颜色,红色写成了(255, 0, 0),结果检测区域里出现蓝色像素时才被匹配上。
第二个坑是容差设置过严或过松。容差过小会导致真实画面稍微偏色就匹配失败;容差过大会导致背景颜色也进入候选区。
建议先输出一张实际截图的中间结果:
diff = np.abs(frame_i - target_i) mask = np.all(diff <= tolerance, axis=2) cv2.imwrite("mask_check.png", mask.astype(np.uint8) * 255)把匹配结果保存成图片,用肉眼看一眼到底是哪些区域被选中,比反复调阈值效率更高。
第三个坑是直接用单帧判断状态。屏幕上如果偶尔闪烁一帧,或者画面有锯齿抖动,单帧识别就会在OPEN和CLOSE之间快速跳动。解决这个问题需要引入“连续多帧稳定”,也就是下一节要讨论的历史帧策略。
4. 多线程并行监控多个区域时,线程之间要怎样协作
4.1 一个采集线程加一个识别线程,比每个区域开一个线程更合理
如果只是监控一个区域,单循环已经够用。但监控两个或更多区域时,如果每个区域单独开线程去截屏,会出现多个 mss 实例同时在屏幕抓图,资源消耗变大,反而更容易抖动。
工程上更推荐的拆分方式是:
- 一个采集线程负责把多个区域截屏,并把图像帧放进队列。
- 一个识别线程不停从队列取帧,做颜色匹配和状态判断。
- 一个打印线程负责定时输出状态,避免主线程被循环阻塞。
如果将来区域数量很多,也可以把识别线程改成多个,每个识别线程消费同一个队列中的图像帧。这样可以更充分地利用 CPU 多核,但要考虑顺序和重复消费问题。
下面的示例实现“一个采集线程、一个识别线程、一个打印线程”的结构。
4.2 完整代码:多区域图色状态监控框架
这里写成一个独立文件region_monitor.py。
import queue import threading import time from collections import defaultdict import cv2 import numpy as np import mss REGIONS = { "left_area": { "left": 80, "top": 120, "width": 240, "height": 90, "target_bgr": (0, 0, 255), "threshold": 0.15, "interval": 0.2, }, "right_area": { "left": 400, "top": 120, "width": 240, "height": 90, "target_bgr": (255, 0, 0), "threshold": 0.15, "interval": 0.2, }, } HISTORY_SIZE = 3 STOP_EVENT = threading.Event() FRAME_QUEUE = queue.Queue(maxsize=20) STATE_LOCK = threading.Lock() STATE_MAP = {name: "UNKNOWN" for name in REGIONS} def calc_color_ratio(frame, target_bgr, tolerance=60): if frame is None or frame.size == 0: return -1.0 frame_i = frame.astype(np.int16) target_i = np.array(target_bgr, dtype=np.int16) diff = np.abs(frame_i - target_i) mask = np.all(diff <= tolerance, axis=2) return float(mask.mean()) def classify_history(history): if len(history) < HISTORY_SIZE: return "PENDING" recent = history[-HISTORY_SIZE:] if sum(recent) == HISTORY_SIZE: return "DETECTED" if sum(recent) == 0: return "LOST" return "CHANGING" def update_state(name, current_match, histories): history = histories[name] history.append(current_match) if len(history) > HISTORY_SIZE: history.pop(0) state = classify_history(history) with STATE_LOCK: STATE_MAP[name] = state def capture_loop(regions): with mss.mss() as sct: while not STOP_EVENT.is_set(): for name, cfg in regions.items(): area = ( cfg["left"], cfg["top"], cfg["left"] + cfg["width"], cfg["top"] + cfg["height"], ) frame = np.asarray(sct.grab(area)) frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR) try: FRAME_QUEUE.put((name, frame, time.time()), timeout=0.2) except queue.Full: try: FRAME_QUEUE.get_nowait() except queue.Empty: pass FRAME_QUEUE.put((name, frame, time.time())) time.sleep(0.05) def recognize_loop(regions): histories = defaultdict(list) while not STOP_EVENT.is_set(): try: name, frame, _ = FRAME_QUEUE.get(timeout=1) except queue.Empty: continue cfg = regions[name] ratio = calc_color_ratio(frame, cfg["target_bgr"]) if ratio < 0: with STATE_LOCK: STATE_MAP[name] = "NO_SCREEN" continue current_match = ratio >= cfg["threshold"] update_state(name, current_match, histories) def print_loop(): last_print = 0.0 while not STOP_EVENT.is_set(): now = time.monotonic() if now - last_print >= 1.0: with STATE_LOCK: snapshot = dict(STATE_MAP) print(time.strftime("%H:%M:%S"), snapshot) last_print = now time.sleep(0.05) def main(): threads = [ threading.Thread(target=capture_loop, args=(REGIONS,), daemon=True), threading.Thread(target=recognize_loop, args=(REGIONS,), daemon=True), threading.Thread(target=print_loop, daemon=True), ] for t in threads: t.start() try: while True: time.sleep(1) except KeyboardInterrupt: STOP_EVENT.set() print("\nmonitor stopped") if __name__ == "__main__": main()这个示例里没有发送任何鼠标或键盘指令,它只是周期性截取两个矩形区域、计算颜色占比,并把状态打印到控制台。
4.3 关键设计点:锁、队列和防抖
这个框架里有几个容易被忽视的设计。
STATE_LOCK保护的是STATE_MAP字典。识别线程和打印线程都会读写这个字典,Python 虽然在某些场景下对字典的单次读写有原子性,但“读取 + 复制”或“读取 + 赋值”并不是严格保证安全的操作。加上锁可以让多个线程对共享状态的访问顺序变得明确,减少状态串扰。
FRAME_QUEUE是采集线程和识别线程之间的缓冲。采集线程不能直接把帧交给识别线程,因为屏幕抓取速度可能比颜色计算速度快。如果没有队列,实时性虽然会高些,但内存峰值和线程耦合会比较强。队列的maxsize相当于一个背压机制,防止采集太快后内存无限增长。
classify_history是防抖逻辑。每帧识别结果存进历史列表,最近 3 帧全部匹配才输出DETECTED,全部不匹配才输出LOST,中间状态统一输出为CHANGING。
条件表达式在这里的典型应用就是状态归并:
state = ( "DETECTED" if sum(recent) == HISTORY_SIZE else "LOST" if sum(recent) == 0 else "CHANGING" )逻辑等价,但写成多层条件表达式后,肉眼扫过去的成本明显提高。所以在实际代码里,我更喜欢把它拆成if分支,让每个状态的分界语义更清楚。
4.4 为什么 Python 多线程能用于这类图像任务
有 GIL 存在,很多人说 Python 多线程不适合 CPU 密集任务。但图色识别不是纯粹的 CPU 计算场景,里面有很大一部分时间在等待屏幕数据返回。mss 的抓屏底层通过系统接口获取画面,等待期间线程不会一直占着 GIL。OpenCV 和 NumPy 涉及大规模数组操作时,底层核心库也会释放 GIL。因此这种场景使用多线程是可行的。
如果每个区域已经是从队列取帧的独立消费者,那么瓶颈通常不是 GIL,而是屏幕抓取频率和颜色匹配算法本身。需要把每个区域的截图间隔控制在合理范围,比如 0.1 秒到 0.5 秒之间。不要写一个无 sleep 的循环,那样会让 CPU 占用率飙升,也会让系统截图服务来不及响应。
5. 运行验证与结果分析
5.1 先跑“本地文件模式”验证颜色计算
完整的多线程代码依赖真实屏幕内容,验证起来不方便。建议先单独验证核心颜色函数:
import cv2 from region_monitor import calc_color_ratio frame = cv2.imread("sample_roi.png") red_ratio = calc_color_ratio(frame, (0, 0, 255)) blue_ratio = calc_color_ratio(frame, (255, 0, 0)) print("red ratio:", red_ratio) print("blue ratio:", blue_ratio)输出应该是红色占比接近 0.29,蓝色占比接近 0.0。如果红色占比是 1.0 或 0.0,优先检查目标颜色是不是写成了 RGB 顺序。
5.2 再跑真实屏幕模式
运行多线程脚本:
python region_monitor.py如果屏幕坐标设置正确,且对应区域确实存在目标颜色,则控制台输出类似:
14:03:12 {'left_area': 'PENDING', 'right_area': 'PENDING'} 14:03:13 {'left_area': 'DETECTED', 'right_area': 'CHANGING'} 14:03:14 {'left_area': 'DETECTED', 'right_area': 'DETECTED'}出现PENDING是正常的,它表示历史帧数量还没有达到 3 帧,系统还在积累数据。之后 3 帧结果一致时,状态才会稳定下来。
5.3 验证时不能只看进程是否启动
比较常见的错误是:程序跑起来了,控制台也在打印,但检查一下发现打印结果永远是UNKNOWN或PENDING。这说明采集线程可能没有抓到预期画面,或者区域里根本没有匹配颜色。
验证流程建议按下面顺序执行:
- 确认屏幕区域坐标真实存在。
- 在坐标区域放一个纯色色块,观察状态是否变为
DETECTED。 - 截图保存当前帧,检查图像尺寸和颜色通道。
- 调低
threshold,确认判断是否变得容易触发。 - 观察 CPU 占用和队列长度,判断是否存在抓帧过快的问题。
6. 常见问题排查:从报错到判断不准逐层看
6.1 常见现象、原因和处理方式
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 打印状态一直是 PENDING | 采样帧数不足,或识别线程没有拿到帧 | 检查队列、控制台异常,确认采集线程是否在跑 | 增加历史帧窗口或调低识别耗时 |
| 颜色总是匹配不上 | 目标颜色写成了 RGB | 打印一帧图像的[0,0]像素值 | 在 OpenCV 环境使用 BGR 顺序 |
| 识别区域错位 | 截屏坐标与窗口坐标不一致 | 打印 mss 抓取到的图像尺寸 | 用窗口句柄获取坐标,并处理系统缩放 |
| CPU 占用接近 100% | 循环 sleep 过短或图像区域过大 | 查看任务管理器,尝试加大 sleep | 设置为 0.1 秒以上,或降低采集频率 |
| 程序退出卡住 | 线程阻塞在队列读写或截屏等待中 | 打断后看线程栈 | 设置STOP_EVENT并限制队列超时 |
| 颜色一点点变化就抖动 | 阈值过低或没有做防抖 | 打印最近几帧的 ratio 值 | 提高阈值,增加“连续 N 帧”判断 |
6.2 坐标系与 Windows 缩放的坑
Windows 在 125% 或 150% 缩放时,很多程序拿到的坐标并不一致。mss 默认使用物理像素还是逻辑像素,会随环境和分辨率设计有所不同。如果你发现识别区域总是在屏幕左上角偏移,或者区域大小明显不对,优先怀疑系统缩放。
常见解决方法是让进程感知 DPI:
import ctypes try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except AttributeError: ctypes.windll.user32.SetProcessDPIAware()这段代码要在创建窗口或抓屏前调用。它属于屏幕兼容处理,不是绕过系统限制。
6.3 多线程状态下状态泄漏
多个线程同时往列表或字典里写值时,可能偶尔出现状态值非常奇怪的情况。例如一个线程刚写入DETECTED,另一个线程打印出DETECTEDE,这大概率是打印与赋值之间缺少锁保护,或者状态值本身有脏字符。
更常见的是只加锁赋值而不加锁读取。实际操作中,打印线程复制STATE_MAP之前也需要加锁:
with STATE_LOCK: snapshot = dict(STATE_MAP)否则可能在复制过程中看到半修改状态的字典。虽然 Python 字典复制在 CPython 下通常能完成,但养成“共享状态统一加锁”的习惯能避免后续引入复杂 bug。
7. 把这套代码做成可维护的桌面状态监控框架
7.1 核心识别函数要保持纯函数化
建议把calc_color_ratio、classify_history这类函数设计成不依赖全局变量,参数和返回值都明确独立。这样将来单元测试可以直接传入数组,不需要真的去截屏。
可测试示例:
import numpy as np def test_calc_color_ratio(): frame = np.zeros((100, 100, 3), dtype=np.uint8) frame[:, :] = (0, 0, 255) ratio = calc_color_ratio(frame, (0, 0, 255)) assert ratio > 0.99这种纯函数测试跑起来很快,是防止后续改动破坏颜色逻辑的有力手段。
7.2 实时监控和生产环境要补的东西
如果只是学习,上面代码已经足够。如果要放到桌面工具或自动化测试项目里,至少要补上:
| 关注点 | 本地学习阶段 | 可维护工具阶段 |
|---|---|---|
| 配置 | 硬编码在字典里 | 使用 JSON 或 YAML 外置 |
| 日志 | print 输出 | 增加时间戳、写入日志文件 |
| 告警 | 手动盯控制台 | 状态变化时触发回调或通知 |
| 窗口定位 | 手动填坐标 | 根据窗口标题动态获取坐标 |
| 异常恢复 | 直接退出 | 记录错误并自动重试 |
| 帧率控制 | 固定 sleep | 根据耗时动态调整下一帧等待 |
比如把区域配置放到 JSON:
{ "left_area": { "left": 80, "top": 120, "width": 240, "height": 90, "target": [0, 0, 255], "threshold": 0.15 } }读取后转换为字典,代码主体不需要变化。
7.3 从“颜色比例”向“颜色直方图”扩展
calc_color_ratio适合目标色占比明确的场景。如果画面里有多个相近颜色,或者需要识别“这张图整体偏向某个色系”,可以用颜色直方图。
简单做法是把 BGR 图像转成 HSV,再用cv2.calcHist统计特定色相的占比。HSV 对光照变化比 BGR 稳定,缺点是增加了参数,调起来也更复杂。
入门阶段不建议一上来就写复杂算法,先把 BGR 占比和防抖状态跑通,再根据实际画面扩展。
7.4 可复用清单:桌面颜色识别上线前检查
在把这类工具放到真实桌面场景前,建议逐项检查:
- 屏幕分辨率、缩放比例是否固定。
- 监控区域是否受窗口位置影响。
- 目标颜色会不会因为主题切换而变化。
- 采集线程异常时是否有兜底状态。
- 识别结果是否经过连续多帧确认。
- 共享状态是否都加了锁。
- 退出时是否所有线程都能正常结束。
- 是否把截图频率控制在合理范围。
- 是否只在合规的本地软件监控或测试场景中使用。
- 是否预留了开关,避免误把识别结果用于不当自动化操作。
8. 学习路径和扩展方向
通过这个项目,比较合理的下一步是继续做三件事。
第一,尝试自己写一个“区域颜色发生变化时打印一次变化日志”的观察者,而不是让打印线程每秒钟轮询一次状态。这能帮你理解事件驱动和回调函数。
第二,给STATE_MAP增加一个最近一次变更时间字段。每次状态从CHANGING变成DETECTED或LOST时记录当前时间,这样你就能统计某个颜色持续出现多久,这在 UI 测试中很有用。
第三,把纯文件测试跑起来,写 5 个以上单元测试,分别覆盖背景灰色、红色占位、蓝色占位、空帧、颜色偏差较大等场景。这种测试对后续换成 HSV 或直方图识别时非常有帮助。
如果你刚开始尝试 Python 图色识别,不要急着去研究商业插件的高级命令。优先把“截图、颜色数组、条件判断、多线程共享状态”这条链路用最朴素的库走通。走通后再根据真实画面去调整容差、阈值和线程数量。这样积累下来的能力,将来迁移到任何桌面自动化、软件测试或图像处理项目里