
2025年电赛E题里那句“视觉靶点识别”一出来我反而松了口气。相比一堆人还在纠结“用电脑跑OpenCV还是用树莓派”我在备赛第一天就决定把识别算法全部压到亚博K230开发板上完成。这篇文章就把我从读题、选型、环境搭建、算法实现到现场调优的实战过程完整说一遍重点拆解亚博K230上的视觉靶点识别算法怎么做才稳、怎么调才准给后面准备类似赛题的同学一条可以直接抄的路线。1. 拿到E题第一件事把“识别靶心”拆成四步1.1 先读题再选型别急着碰代码E题这类视觉靶点识别题赛题里通常会给一张靶面示意图常见的是环形靶或者带靶心的圆形标志可能还分不同环区要求系统自动检测出靶心的位置并引导云台、机械臂之类的执行机构对准。很多人拿到题就急着去下载OpenCV、跑YOLO这其实是弯路。我的习惯是把赛题要求翻译成功能清单不纠结“识别”两个字而是拆成四个子问题检测当前画面里到底有没有靶面有没有靶心。定位靶心在图像里的像素坐标是多少是单目标还是多目标。解算把像素坐标换算成云台该转的角度或者机械臂该移动的偏移量。输出通过什么接口、什么协议、以多快的频率把结果送给主控。这四个子问题对应的工作量完全不同。检测和定位是视觉算法的事解算是几何标定的事输出是通信协议的事。如果你上来就抱着“我要训练一个高精度神经网络”的思路大概率会在赛前把时间耗在标注数据和调参上反而忽略了后面更影响拿分的标定和联动。这里插一句我们按环形靶场景来拆解靶心是红色圆形区域周围有白色环区再往外有黑色数字环。不管题目细节怎么变核心逻辑都在这四步里你只要把这一步吃透换靶面形式也能套。1.2 为什么是亚博K230而不是电脑OpenCV赛场上最常见的方案有三种我直接说结论电脑USB摄像头OpenCV开发最顺手但最不适合比赛树莓派看似先进但启动慢、功耗高、环境容易出问题亚博K230单板搞定视觉全流程是我这次的选择。先说电脑方案的问题。比赛现场不是实验室桌面空间有限一台笔记本加一个USB摄像头再加一根串口线设备一多供电、走线、搬动全是隐患。而且评委看的是整体装置不可能允许你满地拖线。另一个致命问题是延迟USB摄像头把画面传到电脑电脑处理完再通过串口发给单片机这一圈下来延迟轻松超过100ms云台转动时画面一直在晃动识别很不稳定。树莓派方案看起来高级但Linux系统启动要十几秒还要额外装OpenCV、numpy这些依赖赛前一旦SD卡出问题或者系统崩溃恢复环境非常痛苦。功耗也偏高赛题如果用电池供电电压波动就可能触发降频画面处理就会卡顿。亚博K230的优势恰恰在“板级视觉”。它是一颗RISC-V双核C908处理器主频1.6GHz内部还集成了2TOPS算力的KPU跑传统视觉算法绰绰有余。开发环境是CanMV K230API风格接近OpenMV写起Python脚本非常快摄像头直接接在板载MIPI接口上图像采集和算法处理都在同一块板子里完成最后通过UART把坐标发给主控MCU整个链路干净利落。它也有短板。K230的中文资料不算特别丰富很多细节要靠自己翻官方文档和踩坑CanMV固件里的一些API和OpenMV并不完全一致网上能搜到的例子经常是新旧版本混着来。这也是我写这篇文章的动机之一把我踩过的坑直接摊开来讲。1.3 整体系统长什么样我们的系统链路是这样的亚博K230开发板接GC2093摄像头板上运行视觉靶点识别算法通过UART串口把靶心像素坐标、云台目标角度、置信度发送给STM32主控主控解析后驱动两轴舵机云台转动让镜头始终对准靶心。这套结构的好处是视觉部分完全独立。K230只负责“看”和“算”不参与云台闭环控制主控只管“动”。两边通过协议解耦调试的时候可以分开测哪边出问题直接定位不用整个系统一起查。还有一个容易被忽略的架构决策图像画面通过K230的LCD接口接到一块小屏幕上现场调试时直接看屏幕上的十字线和检测框比对着电脑屏幕猜强太多。这块屏在比赛现场是我最重要的调试工具后面会细说。2. 环境搭建与图像采集先把画面弄稳定2.1 CanMV固件和亚博K230的启动流程亚博K230的烧录流程不算复杂但第一次做容易在驱动和固件版本上卡住。我当时的步骤是下载CanMV K230固件以及对应的烧录工具。把固件烧写到SD卡里注意SD卡要用FAT32格式容量别太大16GB以内最稳。烧好之后把SD卡插回K230接上USB线电脑会识别出一个串口设备。安装驱动打开CanMV IDE连接对应的COM口IDE左下角出现连接成功的提示。这里有几个坑需要提醒。第一很多同学烧录时报错是因为烧录工具和固件版本不匹配务必用同一份教程里配套的软件。第二第一次插上开发板电脑如果没识别到新串口别急着怀疑板子坏了先看看是没装驱动还是没有共地。第三亚博K230板载的WiFi模块我建议直接用USB线调试无线连接虽然方便但IDE偶尔会断连比赛现场最怕这种不确定因素。固件版本我会特意记下来写到配置注释里因为CanMV K230的API一直在更新不同版本对find_blobs、sensor这些模块的支持细节有差异。如果你的代码在我下面例子的基础上跑不通优先检查固件版本。2.2 分辨率、帧率和曝光怎么配K230接GC2093摄像头感光性能不错2.9微米大像素暗光下比普通摄像头强很多。但在电赛场景里画面“清晰”不等于“稳定”我更看重的是曝光稳定。我最初用VGA分辨率640x480画面细节确实好但一帧加算法处理就要70ms左右帧率只有14fps云台一动画面就拖影。后来降到了QVGA320x240靶面本身占画面比例不小这个分辨率完全够用处理速度能提到35ms一帧接近28fps视觉跟随的流畅度立刻上来了。分辨率设置的关键代码大概是这个样子import sensor import image sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240速度优先 sensor.set_vflip(True) # 根据安装方向确定是否需要 sensor.set_hmirror(True) # 根据安装方向确定是否需要 sensor.skip_frames(60) # 跳过前60帧等自动曝光收敛比分辨率更重要的是曝光和白平衡的处理。很多队伍发现识别突然失灵就是自动曝光在捣乱。摄像头和云台固定在一起云台转动时画面里亮暗区域不断变化自动曝光会一直调整亮度导致红色靶心的颜色也跟着变颜色阈值分割自然就不稳定了。解决办法是锁定曝光参数。比赛现场光线相对固定完全可以手动设置曝光时间和增益sensor.set_auto_exposure(False, exposure_us12000) # 锁定12ms曝光 sensor.set_auto_whitebal(False, rgb_gain_db(65, 63, 65)) # 锁定白平衡 sensor.skip_frames(30) # 重新让参数生效曝光时间选多大要根据现场亮度试。室内LED灯下8000到15000微秒比较常见。有个快速判断方法锁定曝光后让云台转一圈屏幕上靶心的颜色如果没怎么变说明这个参数能用。2.3 现场调试的三个技巧第一个技巧是LCD屏一定要装。亚博K230支持板载LCD显示直接把sensor的画面实时显示出来现场调阈值、看检测框都靠它。没有屏你就得一直连电脑比赛场地可不一定给你摆电脑的位置。第二个技巧是合理使用ROI。靶面在画面里的位置基本固定给find_blobs划定一个感兴趣区域比如画面中央的某个矩形既能排除周围环境干扰又能减少计算量。ROI太大容易误检到背景里的红色物体ROI太小云台转到边缘时靶心会跑出区域。我习惯把ROI设成画面的一半大小居中放置。第三个技巧是调试时先不开LCD显示和IDE预览。LCD、IDE窗口这些都会占CPU资源影响帧率。我通常是在调阈值时开IDE预览调好后关掉换成板载LCD现场观察最后再用真实帧率验证一遍。不要嫌麻烦镜头稳定、颜色稳定的画面是后面所有算法的地基。3. 靶点检测算法从颜色分割到靶心定位3.1 靶面特征分析不要把问题想复杂环形靶的靶面特征其实非常明确白色底红色圆形靶心黑色数字环。比赛场景是室内背景大都是墙面或桌面颜色比较单纯。所以这个题目完全不需要上深度学习传统颜色分割加轮廓筛选就够了。有人可能会问YOLO不是更智能吗但电赛场景有几个现实约束一是标定数据有限场地灯光一变模型就可能崩二是你不一定能快速定位问题出在哪神经网络就是个黑盒现场没法改三是K230虽然能跑轻量模型但部署流程比传统视觉复杂得多。传统颜色阈值方案的最大优点就是可控、可解释、改起来快这是电赛最需要的属性。3.2 LAB颜色空间分割与动态阈值颜色分割最忌讳直接用RGB。RGB三个通道都和亮度强相关光线稍微变暗红色分量、绿色分量一起变阈值很容易失效。我用的是LAB颜色空间L是亮度A是红绿分量B是蓝黄分量。靶心的红色主要由A通道的正值体现对亮度变化不那么敏感这是它在比赛场景下稳定的关键原因。K230 CanMV的image库里find_blobs支持传LAB阈值范围是(L_min, L_max, A_min, A_max, B_min, B_max)。我是这么获得阈值的赛前多采集几组不同光照下的靶面图片在CanMV IDE的阈值编辑器里查看红心区域的像素分布把L、A、B的范围记录下来。以我们当时的靶心为例阈值大概是RED_LAB (20, 90, 20, 90, 0, 60)这个阈值不是凭空来的一定要用自己的靶面标定。有个小技巧把阈值范围调得稍微保守一些宁可把靶心漏掉一部分也不要让背景乱入。因为后面还有面积和圆度筛选漏掉的边缘可以通过形态学操作补回来。我还加了一层动态保护。比赛现场换来换去色温很难完全一致。我给系统设计了多套阈值方案通过按键或者串口指令切换赛前测试时把“灯光A下的阈值”和“灯光B下的阈值”都存进去。到了比赛现场先按一下看哪套识别稳定就用哪套。这个思路帮我避免了重新调参的尴尬。3.3 形态学滤波、圆度筛选与同心靶环判定找到红色色块之后要做三件事去噪、筛形、验同心。去噪用形态学操作。分割出来的二值图像常有离散的小点可以用erode去掉孤立噪点再用dilate把靶心的真实面积补回来。K230的image库里有erode和dilate函数注意顺序先腐蚀后膨胀。筛形用两个指标。第一是面积pixels_threshold设个下限比如50把细碎的红色干扰点直接滤掉。第二是圆度只靠面积不够还需要判断这个色块像不像圆形靶心。圆度打分我用的公式是circle_score 1 - abs(area - pi * w * h / 4) / (w * h)原理很直观一个完美圆形的面积占其外接矩形的比例是π/4约78.5%。色块的面积越接近这个比例它就越像圆。这个打分在K230上直接用浮点运算就能跑开销很小但能干掉大量长条状的反光干扰。验同心则是利用靶面的结构特征。红色靶心周围是白色环区再往外是黑色数字环。如果我只检测到红色块还不能100%确认它就是要找的靶心因为背景里可能有红色标识。但如果在红色块外围检测到一圈大面积的白色区域或者再外围检测到黑色圆环的边那么“这是靶面”的置信度就大大提高了。检测时我把多个blob放到一个数组里按圆度得分排序同时检查它们的外接矩形是否有包含关系。被一个更大的环状白色块包含的那个红色块就是靶心候选。这个“嵌套验证”在赛场上帮我滤掉了至少一半误检。3.4 质心计算与亚像素精修find_blobs返回的色块对象自带cx、cy也就是质心坐标单位是像素。这个质心是二值化后的几何重心对靶心这种对称图形来说已经非常接近真实中心。但如果你追求更高精度可以考虑两种精修方式。第一种是做多帧平均连续采样三帧取质心的平均值可以抵消一部分空气扰动和传感器噪声。第二种是做边缘亚像素修正把靶心边缘的像素梯度信息用起来拟合出更精确的中心位置不过这个在MicroPython环境里跑起来比较慢我比赛时没有用。实际项目里真正影响瞄准精度的往往不是那一两个像素而是后面的标定系数。像素坐标就算精确到0.5个像素标定不准云台还是会跑偏。所以我把主要精力放在了第四部分的标定上。3.5 可落地的MicroPython检测代码下面这一段是我在亚博K230上验证过的核心检测流程按我们的场景写的简化版本import time import math import sensor import image sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.set_vflip(True) sensor.set_hmirror(True) sensor.set_auto_exposure(False, exposure_us12000) sensor.set_auto_whitebal(False, rgb_gain_db(65, 63, 65)) sensor.skip_frames(60) ROI (60, 40, 200, 160) # 靶面所在区域 RED_LAB (20, 90, 20, 90, 0, 60) def select_target(blobs): best None best_score 0.0 for b in blobs: w b.w() h b.h() if w 8 or h 8: continue if w 2 * h or h 2 * w: continue # 宽高比异常过滤长条形 area b.area() circle 1.0 - abs(area - math.pi * w * h / 4) / (w * h) if circle best_score: best_score circle best b return best, best_score while True: img sensor.snapshot() blobs img.find_blobs([RED_LAB], roiROI, pixels_threshold50, area_threshold50, mergeTrue) if blobs: target, score select_target(blobs) if target and score 0.6: img.draw_cross(target.cx(), target.cy(), color(0, 255, 0)) img.draw_rectangle(target.rect(), color(0, 255, 0)) img.draw_string(target.cx() 5, target.cy() - 10, %.2f % score, color(255, 255, 0))两点提醒。第一find_blobs这个函数在不同固件里细节略有差异有的版本还额外返回别的内容写代码时先用一个空场景打印一次blobs的结构看看。第二mergeTrue会把分裂的小色块合并成完整的靶心如果靶心被压出白色高光合并标志可以让分割结果更完整。4. 从像素坐标到瞄准角度标定比算法更重要4.1 直接用比例映射为什么不准很多队伍走到这里就开始“填鸭”摄像头分辨率320x240靶心在(160, 120)就是画面中心那我让云台往左转一个角度转多少呢直接拿像素值做线性比例映射看起来能转实际上误差很大。误差主要来源有三个。一是镜头畸变广角镜头在画面边缘的放大率和中位置不一样画面中间的10像素和边缘的10像素对应角度不同。二是摄像头光轴和云台转轴不重合这个在机械装配上几乎无法避免。三是靶面和摄像头平面不绝对平行有倾斜角成像就会带透视变形。所以正确的思路是做一个标定先记录一系列“像素坐标和云台角度”的对应关系再用这些对应关系算出换算系数。这个思路比任何高级算法都值钱赛场上帮我省了大量调参时间。4.2 多点标定与最小二乘拟合具体标定步骤是这样的打印一张靶面固定放在正前方。手动通过云台控制程序让摄像头十字准星对准靶心。记录此时云台的pitch角度和yaw角度同时记录画面中靶心的像素坐标。改变云台角度让靶心出现在画面的不同位置重复记录4到6组数据。然后假设像素坐标和云台角度之间满足线性关系pitch a * x b * y c yaw d * x e * y f用最小二乘法拟合出这六个系数。整个过程可以写个几十行的Python脚本在电脑上离线算算完把系数写进K230代码里。这里要特别注意采集数据时要覆盖画面的四个角落不要只采一条线否则拟合出来的系数在画面边缘会明显失真。拟合公式不复杂本质是解线性方程组。我用的伪代码如下# 每组数据格式: (x, y, pitch, yaw) # 构造矩阵 A [[x, y, 1], ...] # 解 (A^T A) beta A^T pitch # 即可得到 a, b, cyaw同理得到 d, e, f拟合完一定要留几组数据做校验把没参与拟合的像素坐标代进去看算出来的角度和实际记录的角度差多少。我们当时的指标是角度误差控制在0.5度以内靶心在画面边缘时也能保持这个水平。如果你的校验点偏差很大要么是采集时云台没对准准星要么是数据点太少、太集中。4.3 把角度送给云台PWM映射与闭环微调标定系数算出的角度是云台应该转到的目标角度要真正驱动舵机还要把角度映射成PWM脉宽。普通舵机工作在50Hz脉宽500到2500微秒对应的角度范围一般是0到180度。我的处理方式是在主控STM32里定义好pwm_pulse center_pulse angle * pulse_per_degree不同舵机的center_pulse和pulse_per_degree不一样一定要在联调时实测。有个更快的办法手动发一个脉宽值量一下云台的绝对角度多测几组写个表格就能算出这个舵机的映射系数。光有前馈映射还不够云台机械间隙、皮带打滑都会带来误差。我加了一个视觉闭环微调云台先转到目标角度摄像头重新采一帧看看靶心还在不在画面中心。如果偏差大于3个像素就把偏差折算成角度修正量继续微调。这个闭环一加整个系统的抗机械偏差能力立刻上来也是我们在赛前检测中表现稳定的关键原因。5. 串口协议设计告诉主控“看到了什么”5.1 一帧数据怎么设计视觉部分算出了靶心的像素坐标和云台目标角度接下来要通过串口告诉主控。串口协议设计得好不好直接关系联调效率。我定义的数据帧格式是帧头 AA 55 帧长 0B (固定11字节) 状态 valid target_num 坐标X 2字节小端 坐标Y 2字节小端 角度P 2字节小端 角度Y 2字节小端 校验 sum 0xFF之所以加状态字段是因为“没检测到目标”也是一种有效信息。valid0时主控保持云台当前位置不让云台乱动valid1时主控才响应坐标和角度。这个字段帮我解决了大问题识别偶尔丢帧时云台不会猛地复位到原点吓评委一跳。K230发送端的逻辑类似这样from machine import UART uart UART(3, baudrate115200, bits8, parityNone, stop1) def send_target(valid, x, y, pitch, yaw): data bytearray(11) data[0] 0xAA data[1] 0x55 data[2] valid data[3] int(x) 0xFF data[4] (int(x) 8) 0xFF data[5] int(y) 0xFF data[6] (int(y) 8) 0xFF data[7] int(pitch) 0xFF data[8] (int(pitch) 8) 0xFF data[9] int(yaw) 0xFF data[10] (sum(data[:10]) 0xFF) uart.write(data)校验用的是累加和简单可靠。不要在协议里追求CRC16那套电赛场景累加和已经足够防住绝大部分偶发干扰。5.2 与STM32联调时的三个坑第一个坑是共地。第一次联调时K230发的数据STM32经常收到错字排查了半天发现是两个板子没有共地。串口通信本质上是电平比较两个设备参考地不一致数据当然会乱。联调第一步就是让GND先接上再去管TX RX。第二个坑是电平。K230的UART是3.3V电平我们用的STM32F103也是3.3V直连没有大问题。但如果你的主控是5V单片机或者用了一些电平转换模块务必先确认两边的逻辑电平兼容。3.3V的TX接5V单片机的RX一般能识别但反向情况下5V TX接3.3V RX就危险了可能烧引脚最好加电平转换。第三个坑是粘包和半包。串口数据是字节流没有“帧”的概念如果接收端只按固定长度读很容易在发送节奏变化时读到半截数据或拆开两帧。正确的做法是状态机解析先找帧头收到完整的11字节后校验校验通过才处理数据校验失败直接丢弃、重新搜索帧头。接收端伪代码大概是state 0 recv_len 0 frame [0] * 11 # state 0: 等待帧头AA # state 1: 等第二个帧头55 # state 2: 接收后续数据直到recv_len 11 # 最后校验通过后重置这样不管发送端怎么断流接收都不会卡死。发送频率也要控制。我实测过K230全流程处理一帧35到40毫秒串口发送本身不到2毫秒所以完全没必要每帧都发。目标坐标变化不大时可以每50毫秒发一次让主控有平滑的时间窗口来处理舵机控制。如果识别突然丢失连续发几帧valid0通知主控进入保持状态而不是只发一帧就没了下文。6. 现场实测数据与最容易翻车的细节6.1 光照变化是头号杀手我敢说电赛视觉题90%的现场翻车都出在光照上。我们在自己实验室调试时用的是LED顶灯识别快准稳。到了比赛场地灯光变成了偏暖的场馆顶灯再加上场地窗户的自然光同一套阈值下靶心直接找不到了。这个问题的解决方案分三层。第一层是硬件和参数层固定曝光和白平衡这是我前面反复强调的。只要画面整体亮度稳定LAB阈值就不会出现大范围漂移。第二层是策略层多存几套阈值现场按键切换。我在代码里做了三个阈值槽位比赛场地下发指令前先用现场环境分别试一遍选识别最稳的那套。这个功能不复杂但能救命。第三层是动态自适应。如果你时间充裕可以让系统启动时自动采集当前画面的靶心区域像素值动态生成阈值。具体做法是赛前让云台转到靶面附近的初始位置程序采集一帧统计红色候选区域的L、A、B均值把阈值中心设成这个均值再给一个固定的半径范围。这个方法比固定阈值更耐环境变化但需要额外写几百行代码我们当时因为时间紧没上属于可扩展项。6.2 误检与丢帧的兜底策略现场除了光照还经常出现各种红色物体队友的红色水杯、墙上的红色贴纸、灭火器。如果单纯依赖红色分割这些全是误检来源。我的兜底策略是三重验证。第一重ROI限定靶面区域红色水杯就算出现在画面里也不在检测区域内。第二重圆度打分圆形靶心比大多数杂物更像圆。第三重同心靶环判定只有被白色外圈包围的红色块才被认定为靶心。三重验证下来我们现场基本没有再出现误检。丢帧的处理同样重要。赛场上云台转动速度快时画面运动模糊偶尔会连续两三帧找不到靶心。这时候要注意不能发送一组无意义的“0,0”否则主控会把云台转到某个奇怪的位置。我们的协议里valid0就是干这个的主控收到后保持现有角度等下一帧有效数据来了再更新。如果遇到单帧坐标跳变特别大的情况比如上一帧x120下一帧突然x240那大概率是误检。可以在主控侧加一个一阶低通滤波或者限幅逻辑相邻两帧坐标差超过阈值就丢弃新值。这个逻辑放在主控里因为K230已经够忙了。6.3 实测耗时的参考数据下面是我们实测的一个性能参考不同固件版本、不同代码写法会有差异但量级应该差不多处理模式分辨率单帧耗时帧率适用场景基础颜色分割QVGA 320x240约35ms约28fps静态瞄准多环判定形态学QVGA 320x240约42ms约24fps含误检过滤全流程含串口发送QVGA 320x240约40ms约25fps比赛实跑基础颜色分割VGA 640x480约70ms约14fps高精度标定我在比赛时用的是QVGA加多环判定模式。可能有人担心QVGA分辨率不够但实际靶面占画面比例足够大QVGA下靶心直径都能有30到50像素完全满足精度要求。帧率从14fps提到25fps云台转动的跟手感完全不一样这个取舍很值得。K230还有一个可优化的点是双核并行。C908是双核处理器可以把图像采集放到一个核算法处理放到另一个核用队列通信理论上能进一步压缩帧间隔。我们比赛时间紧没有做这个优化如果你有时间值得研究一下官方SDK里的多核示例。6.4 赛前必跑的检查清单最后列一下我们赛前那晚的检查清单照着做能少踩很多坑固件版本和SD卡备份至少备份两张烧好固件的SD卡防止现场卡损坏。阈值表确认三套阈值都要在场地实际光线下一一验证标记好现场亮度对应的槽位。串口线和转接板多带几根USB转TTL、杜邦线现场线材经常莫名其妙损坏。供电稳定性K230和舵机最好分开供电舵机启动瞬间的电流尖峰容易拉低K230的电压导致重启。云台机械紧固比赛前检查舵机臂螺丝跑一晚上测试后螺丝松掉是常见事。标定系数文件K230里存一份电脑里再留一份现场如果改了云台安装结构要重新标定。备用摄像头K230的摄像头排线脆弱拔插几次可能接触不良备一根排线或整个摄像头模组更稳妥。现场快速自测脚本准备一段3秒自检程序上电后自动检测阈值是否生效屏幕上打印识别状态快速确认系统正常再提交作品测评。最后再多说一句关于现场的心态比赛时不要临时改算法不要想着再加大一点阈值范围去“适应”现场。所有改动都应该是赛前场景切换的预案而不是临时调参。我们的三套阈值方案在真正的比赛场地也只用了其中一套剩下的时间都在处理云台机械和舵机供电的问题这也印证了一件事视觉算法的稳定性一半靠代码一半靠机械和供电的底子。