ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

电赛视觉节点设计:从HSV目标提取到稳定脱靶量输出

电赛视觉节点设计:从HSV目标提取到稳定脱靶量输出 简介这份资源是2023年全国大学生电子设计竞赛E题视觉部分的完整代码包面向备战电赛视觉赛题的高校参赛队与指导教师也适合想入门嵌入式视觉的开发者参考。方案基于Sipeed Maix BitK210开发板实现图像采集、预处理、特征提取与目标检测跟踪等视觉功能并配套主从云台控制代码主控采用STC32G12K128形成视觉与运动控制协同的完整框架。压缩包共352个文件、约19.47MB以C与H源码、Keil工程文件uvproj、uvopt、编译中间文件obj、lst、crf、map及hex烧录文件为主另有少量Python脚本、lib库、asm汇编、PDF与md说明文档可直接在Keil环境下打开编译。目前已有619人学习下载。读者可从中获得赛题视觉部分的工程组织方式、K210与STC32G的通信与控制分工、云台步进电机与MPU6050姿态模块的驱动代码以及可复用的图像处理函数便于快速搭建稳定的视觉系统并在其基础上做二次开发与赛前调试。1. 需求先立住视觉部分要交付的是稳定脱靶量不是一张识别截图拿到某届电子设计竞赛 E 题视觉部分代码这个题目很多人第一反应是先把摄像头点亮、把目标框出来截一张识别成功的图就算交差。真正上云台一跑问题立刻暴露光斑在画面里跳、云台来回摆、串口数据断断续续。E 题视觉部分要交付的不是一张识别截图而是一串稳定、有坐标系、有时序约束的脱靶量。适合谁正在做电子设计竞赛视觉题、课程设计里要做目标跟踪、毕设需要搭一套摄像头到云台执行链路的人。关键词里写的是软件/插件实际这套代码更像独立视觉节点它不替主控做控制决策只负责把目标位置、置信状态和帧序号讲清楚。先把需求立住后面的方案才不会变成调参玄学。2. 总体方案设计采集、识别、解算、通信四层链路怎么拆视觉部分最容易写成一坨取图、阈值、找轮廓、算中心、发串口全塞在一个while True里。调参时看着能跑换一个光照或者换一个云台就翻车。我一般把电赛视觉题拆成四层每层只回答一个问题采集层回答“画面是否稳定且低延迟”识别层回答“目标在哪里、可不可信”解算层回答“像素偏差对应云台要转多少”通信层回答“主控什么时候拿到、拿到的数据是否完整”。四层拆开之后参数有归属问题有边界答辩时也能把链路讲清楚。2.1 视觉链路拆分四个层各自只做一件事采集层不参与识别只负责相机索引、分辨率、帧率、曝光和增益。竞赛现场常见的问题是自动曝光把光斑压暗或者自动白平衡让颜色漂移所以相机参数通常要手动锁定。分辨率不是越高越好640×480 在 60 帧下往往比 1280×720 在 30 帧下更适合跟踪因为控制环需要的是低延迟而不是一张好看的图。采集层还要记录帧时间戳后面做丢帧判断和延迟统计都靠它。识别层只输出候选目标列表不直接决定最终目标。候选列表里包含中心点、面积、外接圆半径、圆形度、置信分数。最终目标选择放在解算层因为选择逻辑会用到上一帧位置和跟踪状态。这样拆的好处是识别层可以单独拿静态图片回放测试不用接云台解算层可以拿录好的像素坐标离线跑不用反复上电。解算层负责坐标映射、死区、限幅和丢帧保持。它不直接驱动舵机只输出角度增量或者归一化偏差。通信层负责帧头、长度、校验、序号和发送频率。视觉节点只发“偏差状态”不背控制锅主控愿意用 PID 还是比例控制是主控的事。职责一混出现振荡时就会互相甩锅视觉说主控参数太猛主控说视觉数据太跳。把接口定死排查才有黑匣子。目录上我习惯分成camera/、detect/、solve/、comm/、config/五块配置文件单独放现场改阈值不用翻代码。下面这份参数表是视觉节点的起点不同场地要改的是 HSV 和曝光不是整个结构。camera: index: 0 width: 640 height: 480 fps: 60 exposure: -6 # 手动曝光现场按光斑亮度微调 gain: 8 # 增益过高会放大噪声 target: hsv_lower: [0, 120, 80] hsv_upper: [10, 255, 255] min_area: 80 max_area: 8000 min_circularity: 0.55 blur_ksize: 5 track: kp_x: 0.08 kp_y: 0.08 deadband_px: 3 lost_hold_frames: 5 serial: port: /dev/ttyUSB0 baud: 115200 frame_head: 0xAA55这段配置把相机、目标、跟踪、串口分开。exposure和gain需要现场锁定自动模式在光斑移动时会反复调整导致阈值跟着漂。min_area和max_area不是拍脑袋写的先用静态靶在目标距离上量出光斑像素面积再取 0.5 倍到 3 倍作为范围。deadband_px用来抑制中心点抖动太小会让云台追着噪声走太大又会出现明显滞后。lost_hold_frames是丢帧保持帧数目标短暂被遮挡时不要立刻发丢失否则主控会急停再启动机械结构容易抖。2.2 坐标系对齐图像中心、光轴中心和云台零位不能混视觉题翻车最多的地方不是识别而是坐标系。图像中心是(width/2, height/2)光轴中心是相机镜头实际瞄准的点云台零位是舵机或电机安装后的机械零位。这三个点不重合时画面里目标在中心云台却偏一边。我的做法是先做静态对中把靶点固定在远处手动把云台零位对准再读图像中心偏差把这个偏差记成offset_x、offset_y后面所有脱靶量都先减掉它。像素到角度的映射有两种常用做法。小孔模型适合快速起步已知水平视场角HFOV焦距像素近似为f (width/2) / tan(HFOV/2)像素偏差dx对应角度theta atan(dx / f)。但镜头畸变和安装误差会让边缘不准所以正式验收前通常要做一张标定表在画面横向和纵向各取 5 个点记录像素偏差和云台实际角度用分段线性或者二次拟合修正。标定表不用很复杂但一定要有否则中心准、边缘飘追踪快速目标时就会画圈。提示先把云台零位、相机光轴、图像中心三件事在同一张静态图上对齐再谈跟踪算法。顺序反了后面调参全是玄学。2.3 通信帧设计视觉节点输出偏差、状态和序号视觉节点和主控之间最常见的问题是“数据看起来发了主控解析出来是乱码”。原因通常不是串口坏了而是帧边界不清楚只发浮点数没有帧头没有长度没有校验主控一上电从中间开始读就把半个浮点当成帧头。我一般用固定帧结构帧头两字节、长度一字节、两个浮点偏差、一个状态字节、一个序号字节、一个校验字节。浮点用小端打包主控和视觉端约定死。import struct def pack_frame(dx, dy, state, seq): # 帧头固定 0xAA55长度表示 payload 字节数 head b\xAA\x55 # ffBB 表示小端两个 float、两个 unsigned char payload struct.pack(ffBB, dx, dy, state, seq) length len(payload) checksum sum(payload) 0xFF return head bytes([length]) payload bytes([checksum])这段代码只做打包不负责发送。dx、dy是解算后的像素偏差或归一化偏差单位要在协议里写清楚state用位标志表示“目标有效、目标丢失、帧超时、饱和”seq每帧加一主控可以用它判断有没有丢帧。checksum用简单累加和就够竞赛现场干扰没有大到必须上 CRC但帧头和长度必须有。发送频率通常跟图像帧率一致50 到 60 帧足够主控控制环再快也不能拿旧帧硬补。3. 目标提取实现HSV 阈值、轮廓筛选与中心点稳定的三组参数目标提取是视觉部分最直观的一层也是最容易被“能识别”骗过去的一层。静态图上阈值一拉光斑清清楚楚现场灯光一打白墙反光、地面高光、队友衣服上的颜色全跑进来。HSV 比 RGB 稳是因为色调 H 和亮度 V 分开光照变化主要影响 V颜色类别主要看 H 和 S。但 HSV 不是万能药阈值范围给太宽会把背景吃进来给太窄目标稍微暗一点就丢。我的习惯是先用固定曝光拍几张现场图再用滑动条调 H、S、V 上下限最后把范围收窄到刚好包住目标而不是包住所有可能。3.1 颜色空间选择HSV 阈值要按现场光而不是屏幕光调调阈值时不要只看摄像头预览窗口那个窗口经过显示器和环境光和实际保存下来的帧不是一回事。我一般先抓一帧原图存成 PNG用离线脚本读图调参调完再把参数写回配置文件。H 通道在 OpenCV 里是 0 到 179红色通常跨 0 和 179 两端所以红色目标往往要做两段掩膜再合并绿色和蓝色一般一段就够。S 下限用来压掉白色和灰色背景V 下限用来压掉暗部噪声。若现场有强反光V 上限也可以适当限制避免高光区域被当成目标。阈值不是一劳永逸的。比赛现场从上午到下午窗边自然光会变LED 补光灯角度也会变。我的做法是准备两套阈值一套“保守阈值”用于正式跟踪宁可丢几帧也不要乱框一套“宽松阈值”只用于调试和找目标。运行中如果连续多帧丢失再短暂切到宽松阈值搜目标搜到后切回保守阈值。这样比全程宽松稳也比全程保守不容易丢。3.2 轮廓筛选面积、长宽比、圆形度三板斧二值化之后直接取最大轮廓是另一个常见翻车点。现场最大的白色区域可能是反光板不是目标。轮廓筛选我一般看三个量面积、外接矩形长宽比、圆形度。面积用来排除噪点和过大背景长宽比用来排除长条形反光圆形度4πA/P²用来排除不规则亮斑。光斑通常是近似圆形圆形度在 0.5 以上比较合理。三个条件不必同时卡死可以做成评分面积越接近期望值、圆形度越高、离上一帧位置越近分数越高。import cv2 import numpy as np def extract_target(frame, cfg): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) lower np.array(cfg[hsv_lower], dtypenp.uint8) upper np.array(cfg[hsv_upper], dtypenp.uint8) mask cv2.inRange(hsv, lower, upper) # 先开后闭去掉孤立噪点再把断裂的光斑连起来 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations1) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel, iterations2) cnts, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) best None for c in cnts: area cv2.contourArea(c) if area cfg[min_area] or area cfg[max_area]: continue perimeter cv2.arcLength(c, True) if perimeter 1e-6: continue circularity 4 * np.pi * area / (perimeter * perimeter) if circularity cfg[min_circularity]: continue (cx, cy), radius cv2.minEnclosingCircle(c) if radius 3: continue score area * circularity if best is None or score best[score]: best { x: float(cx), y: float(cy), r: float(radius), area: float(area), score: float(score), } return best, mask这段函数只返回一个最佳候选和掩膜方便调试时把掩膜显示出来看。min_area和max_area决定哪些轮廓进入评分min_circularity卡掉不规则亮斑score area * circularity是简单评分实际项目里还可以加上“离上一帧距离”的权重避免两个相似光斑之间来回跳。注意minEnclosingCircle给的是外接圆中心不是质心光斑被部分遮挡时外接圆中心更稳但形状被拉长时会偏所以后面还要滤波。3.3 中心点解算与滤波滑动平均不能替代丢帧保护中心点解算常用质心、外接圆中心、最小二乘拟合圆心。质心对形状敏感外接圆对遮挡敏感拟合圆心计算量大一些。竞赛场景里目标通常是圆形光斑我一般用外接圆中心加滑动平均起步够用且计算量小。滤波窗口不要太大5 帧左右窗口越大越平滑但相位滞后越明显云台追快速目标时会画弧。更好的做法是自适应滤波目标静止时窗口大一点目标快速移动时窗口小一点用中心点位移速度判断。丢帧保护比滤波更重要。目标短暂被遮挡时如果立刻发“丢失”主控会急停再出现时又猛追机械结构容易抖。我一般设lost_hold_frames 5丢失后的前 5 帧继续发上一帧偏差但状态位标记“预测/保持”主控可以选择降速跟踪。超过 5 帧再发丢失主控进入搜索模式。这个数字不是固定的云台惯量大就设大一点惯量小就设小一点。滤波和保持都要在解算层做不要塞进识别函数否则离线回放时很难复现。4. 坐标映射与云台接口像素偏差到角度指令的闭环设计识别到目标只是半成品真正决定追踪效果的是映射和控制接口。像素偏差 100 在画面中心可能对应 5 度在边缘可能对应 7 度镜头畸变越大差异越明显。云台控制环的输入如果是“像素偏差”主控就要自己猜比例如果视觉节点直接发“角度增量”主控的 PID 更容易调。我一般让视觉节点输出归一化偏差或者角度增量单位在协议里写死主控只做闭环。这样视觉部分可以单独用录制的像素序列回放不用每次都接云台。4.1 相机标定与像素-角度映射小孔模型起步分段标定收尾小孔模型适合快速搭链路。已知相机水平视场角焦距像素f (width / 2) / tan(HFOV / 2)像素偏差dx对应的角度近似atan(dx / f)。这个公式在画面中心附近准边缘会受畸变影响。如果镜头是广角边缘误差可能大到让云台追偏。我的做法是先用小孔模型跑通再在画面横向取 -240、-120、0、120、240 五个像素点记录云台实际角度做分段线性修正。纵向同理。标定表不用很密五点足够覆盖竞赛场地。标定时要注意云台角度读数和图像采集的时间同步。如果云台还在动就读数标定表会带滞后。正确做法是云台转到位置后停 200 毫秒等画面稳定再采图记录当前角度和光斑中心。标定完成后把offset_x、offset_y和分段斜率写进配置运行时先减偏移再查表。若现场换了镜头或者相机安装角度标定表必须重做不能沿用旧参数。4.2 串口协议实现帧头、长度、校验、浮点打包串口协议在第 2 章已经给了打包函数这里补接收端和状态位。主控解析时先找0xAA55再读长度再按长度收 payload最后校验。校验不过直接丢帧不要尝试修复。浮点数用struct.unpack(ffBB, payload)解析字节序必须和发送端一致。状态位建议定义清楚bit0 目标有效bit1 目标丢失bit2 帧超时bit3 输出饱和。主控看到 bit3 时应该限幅而不是继续加大控制量。import struct def pack_frame(dx, dy, state, seq): # dx/dy 单位为角度增量保留浮点state 为位标志 head b\xAA\x55 payload struct.pack(ffBB, dx, dy, state, seq) checksum sum(payload) 0xFF return head bytes([len(payload)]) payload bytes([checksum]) def unpack_frame(buf): # buf 至少包含帧头长度payload校验 if len(buf) 4 or buf[0] ! 0xAA or buf[1] ! 0x55: return None length buf[2] if len(buf) 3 length 1: return None payload buf[3:3 length] checksum buf[3 length] if (sum(payload) 0xFF) ! checksum: return None if length ! 10: return None dx, dy, state, seq struct.unpack(ffBB, payload) return {dx: dx, dy: dy, state: state, seq: seq}发送端每帧调用pack_frame接收端把串口缓冲喂给unpack_frame。length ! 10是防御性判断防止协议版本不一致时解析错位。实际项目里还可以加时间戳但竞赛现场主控和视觉端通常共用一个电源时间戳同步要求不高序号足够判断丢帧。若串口出现连续校验失败先查波特率和地线再查发送频率是否超过主控解析能力。4.3 跟踪策略比例控制、死区和丢帧保护视觉节点不直接做 PID但可以给主控一个建议控制量。比例控制u kp * error简单有效kp太大云台会来回摆太小追不上快速目标。死区deadband_px用来忽略中心附近的小抖动通常 2 到 5 像素。输出限幅必须有否则目标突然出现在画面边缘时云台会猛冲机械结构容易撞限位。丢帧保护前面说过保持帧数内继续发上一帧偏差并标记预测状态超过后发丢失。调参顺序我一般这样走先把云台断开只看视觉输出的偏差曲线确认目标静止时偏差不跳再接云台给很小的kp让云台慢慢追确认方向对再逐步加大kp直到出现轻微过冲然后回调 20%。不要一上来就上大kp云台撞限位一次齿轮间隙可能就变了后面调参全是血泪经验。死区也不要设太大否则目标在中心附近移动时云台不动看起来像“识别丢了”其实是控制没响应。5. 避坑与排查视觉部分最容易翻车的五个现场问题视觉代码在实验室跑通到现场翻车通常不是算法不够先进而是几个工程细节没兜住。下面五条是电赛视觉题里最常见的现场问题每条按“现象、原因、解决”写照着排查能省很多时间。5.1 光照一变阈值全废现象是上午能跟下午丢目标现象同一套 HSV 阈值上午在窗边能稳定跟踪下午太阳角度变了目标要么丢要么背景反光被当成目标。原因自动曝光和自动白平衡没有锁定环境光变化让 H、S、V 整体漂移阈值范围又给得太宽背景一亮就进入掩膜。解决相机曝光、增益、白平衡全部手动锁定现场先用静态靶抓图按实际帧调阈值准备保守和宽松两套阈值丢失时短暂切宽松搜索搜到后切回保守跟踪。阈值范围收窄到刚好包住目标不要追求“所有光照都能识别”。5.2 反光与同色背景导致轮廓乱跳现象是目标框在两个亮斑之间跳现象画面里目标明明只有一个识别框却在两个位置之间来回跳云台跟着左右摆。原因现场有同色反光或者高光区域面积和圆形度都接近目标评分函数只按面积选最大谁大选谁。解决在评分里加入“离上一帧中心距离”的权重近距离候选优先用跟踪门限限制候选范围超过上一帧一定像素半径的候选直接排除实在分不开时用形状和亮度分布做二次筛选。不要只靠面积面积最大的不一定是目标。5.3 坐标没对齐导致越追越偏现象是目标在画面中心云台却往一边跑现象静态看目标已经在画面中心云台零位也调过但一追动态目标就慢慢偏出画面。原因图像中心、光轴中心、云台零位没有完全对齐或者像素角度映射只用了中心点比例边缘误差被放大。解决先做静态对中记录offset_x、offset_y再用五点标定做分段修正运行时先减偏移再映射。若换了相机安装角度标定表必须重做。这个坑很隐蔽因为静态图看起来一切正常。5.4 分辨率太高帧率掉现象是识别准但云台反应慢半拍现象高分辨率下识别很准但云台总是慢半拍快速移动目标追不上。原因图像分辨率和帧率、处理耗时是矛盾的大图找轮廓耗时增加控制环拿到的是旧帧。解决跟踪阶段降到 640×480 或 320×240先保证 50 到 60 帧识别只在 ROI 里做ROI 根据上一帧中心外扩关闭不必要的显示窗口和调试输出。帧率比单帧精度更重要控制环吃的是延迟。5.5 串口丢包与浮点解析错现象是数据偶尔飞点云台猛抖一下现象大部分时间正常偶尔云台猛抖日志里出现一个非常大的偏差值。原因串口帧没有帧头或者校验接收端从中间开始读浮点字节序不一致发送频率超过主控解析能力。解决固定帧头、长度、校验和序号接收端先找帧头再收长度校验失败直接丢帧浮点统一小端发送频率和主控解析频率匹配。出现飞点时先看seq是否连续再看校验失败计数不要先怀疑算法。6. 验收与答辩用五组可复现测试讲清视觉部分的边界视觉部分做完验收不要只说“能跟踪”。我一般准备五组可复现测试每组都有操作和通过标准答辩时直接拿数据说话。静态靶看中心误差和抖动阶跃看响应和过冲匀速看跟踪滞后遮挡看丢帧保持和恢复光照变化看阈值鲁棒性。测试前把相机曝光锁定把云台零位对齐把串口日志打开记录帧序号和偏差序列。没有日志的验收评委一问细节就只能凭感觉答。测试项操作通过标准静态靶目标固定在画面中心附近 30 秒中心误差小于 5 像素偏差峰峰值小于 3 像素阶跃目标从中心快速移到画面一侧云台 1 秒内跟上过冲小于 10%无持续振荡匀速目标沿画面横向匀速移动跟踪滞后稳定不出现周期性画圈遮挡手动遮挡目标 0.5 秒保持帧内不报丢失恢复后 0.5 秒内重新锁定光照改变补光灯角度和亮度保守阈值下不误框背景丢失后能切宽松阈值找回答辩讲法我建议按“约束、链路、数据、边界”四段走。先讲现场约束光照不可控、云台有惯量、串口有延迟再讲链路采集、识别、解算、通信各做什么然后拿表格里的数据讲静态误差、阶跃响应和丢帧恢复最后主动讲失败边界比如强反光下会误判、快速目标在画面边缘会有滞后。主动讲边界比被评委问出来更加分。代码不用逐行念挑extract_target的筛选逻辑和pack_frame的协议设计讲清楚足够说明你懂工程。从那以后我每次交付视觉代码都强制先跑一遍静态靶和阶跃测试再谈追踪效果。没有这两组数据调参就是碰运气。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表