
玩OpenMV的人十有八九是从识别颜色入门的。这块板子自带摄像头和MicroPython环境几行代码就能搞定色块识别做循迹小车、分拣机械臂、智能门禁这些项目基本都绕不开颜色识别这个基础功能。但很多人跑完官方例程把红色阈值改成自己想要的颜色发现怎么调都不对劲——要么识别不出来要么把整个屏幕都框进去了。这篇文章我不打算重复官方文档就把自己调通OpenMV颜色识别的完整思路、源码细节、以及踩过的坑一次性说清楚。先说结论OpenMV识别颜色的核心是LAB颜色空间下的阈值分割整个过程可以分为三步——采集目标颜色的LAB阈值、用find_blobs函数在画面里搜索色块、对上一步的结果做过滤和输出。代码量不大但这里的坑非常多尤其是阈值设置和灯光环境的影响直接决定你的项目能不能在真实场景里稳定跑起来。1. 项目核心设计思路拆解1.1 先搞清一件事OpenMV到底怎么“看”颜色OpenMV的摄像头传感器采集到的是RGB565格式的图像数据每个像素用16位来表示其中红色占5位、绿色占6位、蓝色占5位。为什么不用RGB直接做颜色判断因为RGB三个分量对光照非常敏感——同一张红色纸在白色LED灯下和自然光下RGB数值可能相差特别大用无数个if去判断这些取值组合写出来的代码既要处理大量条件判断而且换个环境就得重新调基本不可维护。OpenMV真正用的是LAB颜色空间来做处理。L代表亮度A代表从绿色到红色的分量B代表从蓝色到黄色的分量。这个空间的好处在于它将颜色信息和亮度信息分离开来识别时主要依赖A、B两个分量亮度对结果的影响就小很多。你在OpenMV IDE里打开阈值编辑器看到的那六个数LMin、LMax、AMin、AMax、BMin、BMax就是某个颜色在LAB空间下的一个立方体范围只要目标颜色的LAB值落在这个范围内这个像素就会被判定为“命中”。1.2 为什么我用LAB而不是RGB或HSV很多入门玩家会问为什么网上还有用HSV的方案确实HSV也能用但OpenMV内置的图像处理算法包括find_blobs官方SDK默认就用LAB空间做阈值分割IDE的阈值编辑器也是直接输出LAB的上下限。如果你强行把图像转成HSV再写一套逻辑不仅丢掉官方工具链的支持而且OpenMV的MicroPython环境本身资源有限每帧多做一次色彩空间转换性能就白白损失了。我自己在项目里对比过RGB阈值和LAB阈值在相同光照下的表现LAB对阴影和反光的容忍度明显更高。举个实际例子识别一个红色的乐高积木光照充足时RGB的三个分量归一化后大约是220, 50, 50但积木侧边有一小块反光区域RGB瞬时变成了235, 70, 65如果用RGB做范围判断这个范围就得放宽一放宽就容易误识别。而LAB空间下反光主要影响的是L值A和B的变化幅度很小你只要把L的范围稍微拉大一点就能在保证A、B严格限制的同时覆盖反光区域的干扰。1.3 这套方案的适用场景和技术边界颜色识别方案适合的场景很明确目标颜色单一、颜色对比度高、环境光照可控或半可控。典型应用包括红色小球追踪配合云台做动态跟随红绿蓝三色物块分拣用机械臂或传送带实现自动分类黑线巡线利用深色与浅色地面的LAB差异来提取路径特定颜色目标定位比如救援机器人寻找指定颜色的标识物不太适合的场景是目标颜色在环境中大量存在比如在绿色操场上识别绿色树叶、颜色之间互相遮挡、光照剧烈变化且无法补光。这时候单靠颜色阈值就不够用了需要引入形状识别、模板匹配、或者训练分类器。我在做了几个实际项目之后发现颜色识别永远只是第一步真正的工程价值在于你如何处理“识别出来之后的事情”比如坐标换算、偏移量输出、通信协议。2. 核心源码解析find_blobs才是真正的主角2.1 阈值参数到底怎么填很多人拿到手就开始敲代码照着网上的示例把thresholds写成[(30, 90, 30, 90, 30, 90)]但这样的话阈值范围太宽画面里的浅红、橙红、甚至部分皮肤颜色都会被打上标记。正确流程是先利用OpenMV IDE的“工具→机器视觉→阈值编辑器”功能打开摄像头实时画面勾选你要的颜色通道然后拖动六个滑条观察画面里目标区域变成白色的效果——白色区域就是算法认为是目标颜色的区域。我来解释一下阈值编辑器输出的本质。每个通道的上下限合起来定义了一个六维的立方体find_blobs会逐像素判断当前像素的L、A、B值是否同时落在这个立方体内部。如果目标颜色分布比较集中阈值范围会很窄比如红色大约是(20, 60, 20, 70, 30, 80)。如果目标颜色有深浅变化就需要把L的范围拉宽比如从10拉到80让高光和阴影部分也能被识别到。2.2 核心函数与关键参数逐一拆解这是OpenMV颜色识别最核心的一行代码blobs img.find_blobs(thresholds, pixels_threshold200, area_threshold200, mergeTrue, margin10)我先说说参数含义thresholds它是颜色阈值列表每个元素是一个包含六个数LMin, LMax, AMin, AMax, BMin, BMax的元组。列表里可以放多个元组表示要同时识别多种颜色。pixels_threshold像素个数阈值。如果一个色块的像素总数小于这个值就被当作噪点过滤掉。这个参数非常重要不然画面上零点几秒的反光闪点就会让你误判。area_threshold面积阈值。它和pixels_threshold有点类似但area指的是色块的边界框面积。如果色块的边界框面积太小同样会被过滤。merge是否合并相邻的色块。识别同一个物体时如果颜色有断裂mergeTrue可以把它们合并成一个整体。但合并过度也会出问题后面我会讲到。margin合并时允许的间距。只有两个色块的距离小于这个值才会被合并。调用find_blobs后返回的是一个blobs列表里面的每个元素是一个色块对象常用属性有blob.centroid()返回色块中心点的(x, y)坐标这是追踪和目标定位最常用的数据blob.area()返回色块的面积像素值可以用来粗略判断目标的大小和远近blob.rotation()返回色块的旋转角度适合做条形物体的朝向判断blob.pixels()返回色块的实际像素数比area更准blob.x(),blob.y(),blob.w(),blob.h()返回色块的边界框坐标和宽高这些属性按需取用最常见的用途就是把中心点坐标提取出来用来控制舵机云台瞄准目标或者通过串口发给单片机做决策。2.3 ROI区域设置不是整张图都要看在实际项目中整幅图像里有很多干扰区域全部扫描既费时间又容易误判。find_blobs支持通过roi参数指定一个矩形区域只用在这个区域内查找色块。语法是img.find_blobs(thresholds, roi(x, y, w, h))。举个例子你的摄像头俯视传送带传送带的位置大概固定占画面下方的2/3部分那就可以把roi设置为(0, 60, 160, 60)。这样画面顶部那些与项目无关的背景就被排除了一方面可以减少运算时间另一方面能显著降低误识别率。ROI的四个值的单位是像素不要用百分比。ROI的坐标原点在左上角x向右增加y向下增加。调试时可以在IDE里先画块白色区域确定ROI位置但我更推荐的做法是先在整幅图像上跑通算法打印色块出现位置然后再根据打印结果反推合适的ROI这样比你瞎猜坐标要准得多。3. 完整实操从采集阈值到跑通识别3.1 第一步用阈值编辑器采集目标颜色的LAB范围打开OpenMV IDE连接摄像头点击工具菜单里的机器视觉选择阈值编辑器。界面上会直接显示摄像头的实时画面右侧有一个LAB三通道的滑块面板。你把目标颜色放在画面中央开始拖动滑块你会看到画面分成了两层效果原始画面和黑白掩膜。调阈值的原则是目标区域要尽量全白。这是最基本的要求。背景区域尽量全黑。如果背景里也有大片白色区域说明阈值范围太宽要收缩。如果目标颜色有高光点或阴影区域先让主体区域变白再缓慢扩大L的范围把明暗变化也覆盖进去。不要一上来就把所有通道拉到最宽。一个容易忽视的细节是目标颜色在画面不同位置、不同角度会有色差。采集阈值时要把目标物体在画面里挪几个位置甚至转动角度确认调整后的阈值对所有位置都有效而不是只在画面中心有效。调好后点击“文件→导出”IDE会把thresholds以NumPy数组或者普通元组列表形式的Python代码生成好你直接复制到自己的程序里就行。3.2 第二步写一个最简颜色识别程序下面这段代码是我在实际项目中用的基础版本比官方示例多加了几个保护逻辑防止因为色块列表为空导致报错import sensor, image, time, lcd # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240速度和精度折中 sensor.skip_frames(time2000) sensor.set_auto_whitebal(False) # 关闭自动白平衡重要 sensor.set_auto_exposure(False) # 关掉自动曝光后面细讲 # 这一组阈值是实际项目中调出来的红色阈值 thresholds [(25, 60, 25, 80, 30, 80)] lcd.init() clock time.clock() while True: clock.tick() img sensor.snapshot() blobs img.find_blobs( thresholds, pixels_threshold100, # 过滤小噪点 area_threshold100, mergeTrue, margin10 ) if blobs: # 取面积最大的色块作为目标避免多个小色块干扰 max_blob max(blobs, keylambda b: b.area()) img.draw_rectangle(max_blob.rect()) img.draw_cross(max_blob.cx(), max_blob.cy()) print(目标位置: (%d, %d) 面积: %d % (max_blob.cx(), max_blob.cy(), max_blob.area())) lcd.display(img)这段代码里有两个容易被新手跳过的关键点第一是set_auto_whitebal(False)。自动白平衡会在不同色温的光源下自动调整图像的色调平衡听起来很智能但它会让同一张红色纸在暖光和冷光下显示成不同的颜色颜色识别最忌讳的就是颜色不稳定。关闭自动白平衡之后至少你可以保证同样环境下每次采集到的颜色数据是一致的。第二是set_auto_exposure(False)。自动曝光算法会根据画面亮度自动调整曝光时间当你把镜头对着亮部区域时画面会突然变暗对着暗部时画面会变亮这样LAB值会剧烈跳动。玩颜色识别必须先把曝光固定住我建议在固定场景下先让自动曝光跑一两秒读出当前环境光的曝光值然后手动固定这个值或者直接关闭自动曝光用默认值。3.3 第三步多颜色同时识别实际工程中很少只识别一种颜色。一次识别红、绿、蓝三种颜色也很常见。做法就是expand把多个阈值放进同一个列表red_threshold (25, 60, 25, 80, 30, 80) green_threshold (30, 70, -60, -20, 10, 40) blue_threshold (20, 60, 0, 30, -50, -10) thresholds [red_threshold, green_threshold, blue_threshold] blobs img.find_blobs(thresholds, pixels_threshold200, area_threshold200, mergeTrue) for blob in blobs: if blob.code() 1: color RED elif blob.code() 2: color GREEN elif blob.code() 4: color BLUE print(颜色: %s, 坐标: (%d, %d) % (color, blob.cx(), blob.cy()))这里用到了blob.code()这个方法它返回一个整数表示该色块匹配的是阈值列表里的第几个元组。第一个元组匹配结果是1第二个是2第三个是4以此类推2的幂次。这个特性可以同时识别多种颜色而且能区分色块具体是哪一种。我在分拣项目里就是靠这个code值区分颜色种类然后给机械臂发送不同的抓取指令的。3.4 第四步把坐标数据通过串口发出去OpenMV单独跑颜色识别应用价值有限大多数项目都有个配套的单片机或者上位机在等着接收识别结果。通过串口发送数据是最常见的通信方式代码也很简单import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) sensor.set_auto_whitebal(False) uart UART(3, 115200, timeout_char1000) thresholds [(25, 60, 25, 80, 30, 80)] while True: img sensor.snapshot() blobs img.find_blobs(thresholds, pixels_threshold100, area_threshold100) if blobs: max_blob max(blobs, keylambda b: b.area()) # 自定义协议帧头0xAA 坐标 帧尾0x55 data bytearray([0xAA, max_blob.cx(), max_blob.cy(), 0x55]) uart.write(data) time.sleep_ms(30)自定义协议里我用0xAA作为帧头、0x55作为帧尾接收端单片机就可以通过这两个特殊字符来确定一帧数据的边界。一个坐标是8位最大值为255而OpenMV的QVGA分辨率的x坐标最大是319所以单独用一个字节发x坐标会溢出。我在这里简化了示例实际项目中需要把坐标拆成两个字节发送或者把分辨率改成更小的QQVGA160x120让坐标塞进一个字节。接线的方案是OpenMV的UART3对应P4TX和P5RX引脚P4接单片机的RXP5接单片机的TX注意电平匹配如果单片机是5V供电务必加电平转换模块。4. 常见问题与排查技巧实录4.1 为什么识别结果时好时坏这是频率最高的问题。我排查的思路一般是从外到内先看硬件固定是否稳定摄像头有没有松动再看光源有没有变化灯是不是坏了、位置有没有挪动然后看代码里有没有关闭自动白平衡和自动曝光最后看阈值是否在变化的光照下需要重新采集。我在一个分拣项目里遇到过特别诡异的现象上午运行完全正常到下午三四点就开始漏检。排查半天发现下午阳光从窗户斜射进厂房正好打在地面上环境色温从白光变为偏黄的暖光目标物块的LAB值整体偏移了。解决方法是给设备加了一个遮光罩并额外补了一盏固定色温的LED灯此后问题彻底消失。所以颜色识别系统在厂房现场能不能用的关键不完全是算法很多时候是物理环境的可控性。4.2 阈值编辑器里调得好好的跑起来就不行这个问题通常是曝光引起的。阈值编辑器界面在你调整滑块时摄像头可能还在自动曝光模式下画面亮度一直在变你调的阈值是某一帧画面下的结果。写进代码后一旦场景亮度变化曝光值也跟着变阈值自然就不匹配了。正确做法是在阈值编辑器里先把画面调整到正常工作时的亮度和曝光再开始调阈值或者在程序里固定曝光之后再接入阈值编辑器调参。另外OpenMV IDE调试时默认是RGB显示但find_blobs内部用的是LAB空间即使你在IDE看到颜色正常也不能直接证明LAB值在预期范围内阈值编辑器才是唯一可信的调参入口。4.3 画面里有相似颜色误识别怎么处理误识别的根本原因是两个不同颜色在LAB空间下的范围有重叠。这时候可以做的处理有把阈值范围收紧到只覆盖目标颜色的核心区域宁可漏掉边缘像素也不要混入干扰颜色利用形状信息过滤。比如目标是圆形积木干扰物是长条形可以通过blob的宽高比或者椭圆拟合来判断利用位置过滤。比如目标只会出现在画面下方区域直接用ROI把上方区域排除综合利用色块的面积分布。如果目标物体面积稳定而干扰物面积忽大忽小可以直接设置area的上限我在识别红球和橙球的测试中两个颜色在LAB空间的A和B分量相当接近单靠阈值怎么调都会误判。最后我在代码里加了颜色饱和度判断通过计算色块的平均饱和度来区分红色和橙色效果立竿见影。4.4 OpenMV与STM32通信时的小坑OpenMV与STM32的通信本身不复杂串口一开就能收数据。但很多人在联调时发现数据收不到或者收到的是乱码主要集中在几个原因电平不匹配。OpenMV是3.3V逻辑电平STM32有3.3V和5V两种供电模式如果接5V单片机的串口需要确认引脚能否容忍3.3V输入。波特率不一致。两边设置的都是115200但晶振误差或者初始化顺序问题可能导致实际波特率有偏差。发送频率太快。OpenMV每帧都发数据如果单片机处理不过来串口缓冲区就会溢出表现为丢帧或卡死。我一般会在发送之间加一个time.sleep_ms(30)让频率控制在30帧左右而不是满帧率。如果一定要满帧率发送建议把数据格式设计得紧凑一点不要每帧发一长串字符串而是用固定长度的字节流这样接收端解析也容易。5. 进阶方向和性能优化5.1 从颜色识别到巡线巡线是颜色识别最适合的延伸应用之一。思路是把黑线或者白线作为一个阈值在画面底部取一条横向的扫描带在扫描带内找色块中心点根据中心点偏离画面中心的程度来控制舵机或电机转向。我写巡线代码时一个很重要的优化是只对ROI区域做扫描而不是全图处理。假设画面是QVGA分辨率全图扫描每个像素都要做LAB阈值判断循环开销很大把ROI设置在底部60像素高度的条带内计算量直接降到原来的1/4帧率能提升不少。import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) sensor.set_auto_whitebal(False) # 黑色线阈值 black_threshold [(0, 40, -20, 20, -20, 20)] # 底部扫描带ROI roi (0, 180, 320, 60) while True: img sensor.snapshot() blobs img.find_blobs(black_threshold, roiroi, pixels_threshold50, area_threshold50) if blobs: max_blob max(blobs, keylambda b: b.area()) error max_blob.cx() - img.width() // 2 print(偏移量: %d % error)这里的error值就是控制转向的输入量。误差为负说明线在左边误差为正说明线在右边。利用这个误差配合PID控制算法就能实现简单的循迹。5.2 色块追踪与云台跟随颜色识别最直观的玩法就是控制二轴云台追踪色块。核心逻辑是色块中心点与画面中心点的差值作为误差信号把误差映射成舵机的目标角度增量。一个简单的P控制器的核心部分如下x_error blob.cx() - img.width() // 2 y_error blob.cy() - img.height() // 2 pan_angle x_error * 0.1 # 比例系数需要实测调整 tilt_angle - y_error * 0.1 pan_angle max(0, min(180, pan_angle)) tilt_angle max(0, min(180, tilt_angle)) pan_servo.angle(pan_angle) tilt_servo.angle(tilt_angle)系数0.1是我调试中比较常用的起始值但具体数值要看云台的响应速度。系数太大云台容易震荡系数太小追踪会显得迟钝。另外对误差做一个死区判断也很有必要当误差小于3个像素时不要更新舵机角度避免云台一直微抖。5.3 性能优化如何稳住30帧OpenMV的处理器性能有限颜色识别虽然不像深度学习那样吃算力但处理的每一帧都需要逐像素进行阈值判断。我实测QVGA分辨率下find_blobs大约需要20~30ms加上图像采集时间整体就接近30帧了。如果再加上draw_rectangle、draw_cross等绘制操作帧率会进一步下降。优化的方向有这几个降低分辨率。QVGAA降到QQVGA160x120像素数量只有原来的1/4find_blobs的速度提升非常明显。如果你的应用只需要坐标类数据不需要人眼看得多清楚QQVGA完全够用。减少绘制操作。调试阶段用draw_rectangle可行发布阶段就去掉这些绘制代码能省几毫秒。合理设置area_threshold和pixels_threshold。过小的阈值会导致大量的边缘噪点进入算法流程增加计算时间调大一点既滤噪又提速。尽量少在循环里做print。print本身就是阻塞操作一帧数据可能print就要花几毫秒调试时可以保留但正式运行时建议删掉或者改为有条件的打印。我在实际项目里从QVGA全图扫描降到QQVGA局部ROI扫描之后帧率从18帧直接涨到45帧功耗还下降了这就说明了优化空间有多大。5.4 颜色识别和AprilTag、模板匹配怎么选如果你的目标是识别二维码、基准标记AprilTag的鲁棒性远超过颜色识别它不依赖颜色信息直接检测几何特征对光照变化不敏感。如果目标是识别特定形状的物体比如六边形零件但颜色不固定模板匹配或者形状检测是更好的选择。颜色识别最适合的场景永远是“目标颜色足够特殊且环境可控”的情况。我自己的经验是颜色识别可以作为视觉方案的“前锋”先通过颜色快速锁定目标区域再把ROI交给更复杂的算法去处理这样整体性能和准确率都能兼顾。在一个曲轴装配项目中我先用蓝色阈值把工件的定位孔区域框出来然后在ROI内做边缘检测来找中心整个流程比直接全图做边缘检测快了3倍以上。写在后面的几点实操体会如果你打算把这套东西抄到自己的项目里我最后再啰嗦几句第一OpenMV的阈值调节没有银弹必须针对实际场景去采集。网上找来的阈值只能作为起点绝对不要偷懒直接照搬。第二颜色识别系统在实验室里跑通只是第一步到了现场很可能会一团糟。提前把光源固定好、把景深范围控制住比事后调代码管用得多。第三多颜色识别时优先保证目标颜色之间在LAB空间里尽量分散。如果两种颜色在阈值编辑器里怎么调都有重叠区域说明它们在颜色空间里本来就很近靠颜色阈值区分不现实这时候该换方案就换方案。我最初玩OpenMV的时候被一个“识别红色”的demo卡了一整天后来发现只是忘关了自动白平衡。这种问题不会写在官方文档里它只会出现在你的调试日志里。上面这些内容基本覆盖了从入门到实战的关键环节希望能让你在这条路上少走几步弯路。