资讯动态

电赛E题视觉方案:红点定位、靶框识别与云台闭环

发布时间:2026/10/26 3:17:23 来源:尧图企业网站定制
简介面向参加全国大学生电子设计竞赛E题、准备机器视觉赛题原型的团队与指导教师这套工程代码围绕K210视觉模块、云台执行机构与STC32G主控的协同控制展开。视觉端基于sipeed Maix Bit开发板主控采用STC32G12K128包内包含视觉代码、主机云台与从机云台工程便于按模块理解整套方案并对照硬件联调。压缩包共352个文件、约19.47MB涵盖C/H源码、Python脚本、Keil工程文件uvproj/uvopt、hex烧录文件及pdf、docx、md说明文档兼顾算法实现、嵌入式工程配置与调试记录。代码覆盖图像采集、预处理、特征提取、目标检测与跟踪并提供颜色识别、物体分类、距离测量等应用案例可作为赛题调试、方案复盘和二次开发参考。已有619人学习下载适合需要完整视觉框架、工程组织方式与排错思路的参赛者和自学者。1. 从一颗打偏的光点说起电赛 E 题视觉部分到底要交出什么场地灯一开调了一下午的阈值全废了。红点从画面里消失云台开始在原地画圈屏幕上的绿色十字准星还稳稳停在上一次的位置不动。这是很多人拿到电赛 E 题视觉部分代码之后遇见的第一次翻车问题并不在算法有多难而在于没人告诉你这套代码究竟该对外交出什么。E 题的视觉部分本质上是三件事把红色光点从背景里稳定抠出来并给出亚像素级坐标把靶纸的矩形边框认出来并排序成四个角点把像素坐标换算成云台该转的角度并且在光点丢失、被遮挡、跑出视野时有一套能自恢复的状态逻辑。这三件事任何一件没做扎实现场演示就会变成“能跑但打不准”。这套方案对应的是一份可直接参考的视觉代码工程适合正在准备电子设计竞赛控制类題目的人也适合把它当作机器视觉 云台闭环的课设或毕设骨架来改。下面按“需求拆解、算法流水线、控制通信、避坑、验收”的顺序把我在这类题目上真正会走的流程摊开讲。2. 需求拆解与硬件选型三道视觉任务与一套光轴几何动手写第一行代码之前我一般先在纸上把“看见什么”和“打到哪里”这两件事分开。视觉部分最容易犯的错是把识别精度和落点精度混为一谈图上算得再准只要摄像头的轴和激光笔的轴不平行打出去的光点就永远差那么一两厘米而且这个偏差随距离变化靠调 PID 是调不回来的。2.1 三道视觉任务红点、绿点、靶框各自的精度预算第一道任务是红色光点定位。它是追踪系统的唯一输入输出必须是一个稳定的、带时间戳的像素坐标。这里的关键词是“稳定”单帧算得准不难难的是连续 200 帧不跳变。我通常要求同一个静止光点在 3 秒内的坐标抖动标准差小于 1 个像素否则后面不管怎么调 PID云台都会持续微抖。第二道任务是绿色光点自检。追踪系统自己打出去的绿点其实是一个免费的闭环验证信号把绿点的像素位置和红点的像素位置画在同一张图上两者重合度直接反映映射矩阵标得准不准。很多队伍跳过这一步结果现场只能凭肉眼判断“差不多对准了”一旦评委要求报偏差就说不出来。第三道任务是靶框识别。矩形四点不只是用来“画边框”它更重要的作用是建立像素坐标到物理平面的映射基准。有了四个角点就能算出靶纸的实际边长对应多少像素从而把“偏差 15 像素”翻译成“偏差 6 毫米”这句话在答辩时非常值钱。精度预算要提前算清楚。假设摄像头视野覆盖 1.2 m 宽度输出 640 像素宽那么每像素约 1.875 mm此时要求 2 mm 的落点精度理论上就已经不够用了。换到 1280 像素宽每像素 0.94 mm留出一倍余量才合理。如果算力跟不上与其全图 1280不如 640 全图做粗定位、再在目标附近裁 160×160 的 ROI 做精定位等效分辨率直接翻四倍这个技巧在嵌入式平台上比换摄像头划算得多。2.2 摄像头选型分辨率、帧率、曝光三者的取舍三条主流路线的差别其实不在“谁更强”而在“谁的延迟更可控”。路线典型输出延迟可控性适合场景一体化视觉模组自带脚本环境320×240 至 640×48030~60 fps好图像不出板只要红点坐标、不想碰驱动单板计算机 USB 摄像头640×480 至 1280×72030~60 fps中受 USB 与驱动缓冲影响需要同时跑识别与串口调度工控机 / 笔记本 工业相机1280×1024 以上全局快门差链路最长桌面调试、算法验证选型上我有一个很固执的偏好宁可分辨率低一点也要把帧率拉到 60 fps 并且把曝光锁死。原因在于控制环的延迟预算里舵机本身就是大头——普通舵机从收到指令到到位一百多毫秒是常态如果图像端再贡献 66 ms30 fps 的一帧周期加上自动曝光带来的抖动整个环路的相位裕度就被吃光了。暴露在自动模式下还有个隐蔽的坑红点进入视野时画面整体亮度被拉低红点的饱和度跟着掉阈值分割当场失效。手动曝光 手动增益 手动白平衡是这套代码默认的前提条件。快门类型也值得多说一句。卷帘快门在光点快速横扫时会拉出一条斜线二值化之后这不是一个圆点而是一段月牙质心位置自然偏了。如果预算允许全局快门是首选只能用卷帘快门时就得靠后面 3.2 节讲的亮度加权质心来补偿同时尽量缩短曝光时间、提高增益把拖影压到可接受范围。2.3 光轴几何为什么“看到的不等于打到的”摄像头和激光笔装在同一块云台上但它们的轴线几乎不可能重合。假设两者水平间距 3 cm目标在 1 m 外这个几何关系带来的视差本身不大真正麻烦的是激光笔有一定发散角近距离时打出的光斑直径可能达到 6~8 mm而质心算法给出的是光斑中心两者在斜射到白板上时会呈现椭圆长轴方向随入射角变化于是“看到的中心”和“人眼认为的中心”又差了一点。机械上能做的优化是尽量缩短两轴间距把激光笔贴着镜头边缘安装并且让激光笔的出射方向与镜头光轴大致平行可以用远处墙面做粗调在 3 m 外用一张纸同时承接镜头中心和光点反复微调支架。但机械只能做到“大致平行”剩下的偏差必须交给软件标定。我一般的标定流程是五步。第一步把靶纸固定好摄像头也固定之后全程不再碰支架。第二步让云台在两个轴上各走固定的几个角度比如水平 -10°、-5°、0°、5°、10°垂直同理组成九宫格。第三步每个角度位置停留 500 ms 等云台稳住记录下当时的绿色光点像素坐标——注意不是红点是自己打出去的绿点因为标定的是“指令角度到落点像素”的映射。第四步用这九组数据拟合一个二次多项式或者 3×3 单应矩阵把残差控制在 2 像素以内。第五步把矩阵固化到配置里跑一遍验证随机给十个角度看绿点落到哪里误差超标的点单独记录通常是因为该区域落到了镜头畸变较大的边缘。提示标定数据一定要存盘成文本并随代码一起保留。现场换个场地、换张靶纸很多时候只需要重跑一次标定而不是重调所有参数。3. 图像处理流水线从一帧画面到一组角度增量流水线的结构我习惯分成四段取帧、分割、特征提取、坐标解算。每一段的输出都是下一段的输入段与段之间用最简单的数据结构传递掩膜、坐标元组、角度元组这样任何一段出问题都能单独打印出来看不至于变成一个黑匣子。3.1 颜色空间选择与阈值分割HSV 为什么比 RGB 稳激光光点的最大特点是亮度极高、饱和度极高而白色靶纸和场地灯光在亮度上会随环境剧烈变化。RGB 三个通道都会被环境光整体抬升或压低阈值一改就得全改HSV 把色相 H 单独拎出来环境光变化主要影响 V色相基本不动这就是它更稳的根本原因。import cv2 import numpy as np # 红色在 HSV 色环上横跨 0 度必须拆成两段阈值再合并 LOWER_RED_A np.array([0, 120, 200]) UPPER_RED_A np.array([10, 255, 255]) LOWER_RED_B np.array([170, 120, 200]) UPPER_RED_B np.array([180, 255, 255]) def red_mask(bgr): hsv cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) m1 cv2.inRange(hsv, LOWER_RED_A, UPPER_RED_A) m2 cv2.inRange(hsv, LOWER_RED_B, UPPER_RED_B) mask cv2.bitwise_or(m1, m2) k np.ones((3, 3), np.uint8) # 先开后闭开运算去掉孤立噪点闭运算把光斑补成一个实心块 mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, k) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k) return mask三个参数的含义和调法S 下限设 120是为了把白纸、白墙这类低饱和高亮区域排除掉V 下限设 200只保留接近饱和的高亮像素因为激光光斑中心一定是过曝的H 拆成 0~10 和 170~180 两段是因为纯红的色相在 0 附近卷绕之后单段阈值必然漏掉一半光斑。开闭运算的核用 3×3 就够核太大在光点快速移动时会把拖影当成光斑主体反而放大误差。改阈值的正确姿势是打印中间图而不是凭感觉改数字。我通常会在调试模式下把掩膜叠加回原图一半原画面、一半掩膜光斑有没有被吃掉、噪点有没有被连通一眼就能看出来。3.2 光点质心提取与抗拖影处理二值化之后直接取轮廓中心是大多数人写的第一版也是精度最差的一版。原因在于光斑边缘在二值化时被切得参差不齐而光斑中心是过曝的、亮度梯度最大的地方。用亮度做权重算加权质心相当于让中心像素说话边缘像素闭嘴位置自然更靠近真实中心。def spot_centroid(bgr, mask): hsv cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) # 用 V 通道当权重越亮权重越高 w cv2.bitwise_and(hsv[:, :, 2], mask).astype(np.float64) total w.sum() if total 1e-6: return None ys, xs np.nonzero(mask) cx float((w[ys, xs] * xs).sum() / total) cy float((w[ys, xs] * ys).sum() / total) area len(xs) # 面积窗口过滤太小是反光碎点太大是灯光或白色反光板 if area 6 or area 2000: return None return cx, cy, area这段代码有三个可调的地方。权重通道用 V 是最省事的做法如果发现光斑边缘有严重红色溢出可以换成 S 通道效果会更接近几何中心。面积窗口的上下限必须结合自己的分辨率和场地来定640×480 下1 m 外的一个激光点大约 10~40 像素面积窗口设 6~2000 是很宽松的如果场地里有白色反光板上限要往下压到 300 左右。返回值带 area 不是为了显示而是给状态机判断用的——面积突然翻倍往往意味着视场里进来了另一束光或者镜头被正对光源照到了。拖影的处理在这里体现得很直接。光点静止时 iterative 结果非常稳一旦快速移动掩膜的形心会落后于光斑头部形成系统性滞后。缓解办法不是加滤波而是把曝光时间压到 4~8 ms让单帧内的位移尽可能小同时允许面积窗口适当放宽因为短曝光会让光斑变小。3.3 靶框识别轮廓筛选与四点排序靶框的作用是提供物理尺度基准所以稳定性要求比光点低但绝对不能认错。认错一次整个坐标系就翻了后面的角度全错而且这种错误从云台动作上看不出来——它照样在动只是越动越偏。def find_target_quad(bgr, min_area3000): gray cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) blur cv2.GaussianBlur(gray, (5, 5), 0) edges cv2.Canny(blur, 50, 150) # 膨胀一次把断裂的边线接上否则四边形拟合会失败 edges cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations1) cnts, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) best None for c in cnts: area cv2.contourArea(c) if area min_area: continue peri cv2.arcLength(c, True) # 0.02 是经验系数太大拟合不出四边太小会被噪点干扰 approx cv2.approxPolyDP(c, 0.02 * peri, True) if len(approx) ! 4 or not cv2.isContourConvex(approx): continue if best is None or area best[0]: best (area, approx.reshape(4, 2)) return None if best is None else order_quad(best[1]) def order_quad(pts): # 左上角 xy 最小右下角 xy 最大右上角 x-y 最大左下角 x-y 最小 s pts.sum(axis1) d pts[:, 0] - pts[:, 1] return np.array([pts[np.argmin(s)], pts[np.argmax(d)], pts[np.argmax(s)], pts[np.argmin(d)]], dtypenp.float32)这里最容易翻车的是 approxPolyDP 的系数和面积下限。系数 0.02 是相对周长而言的意味着允许边线上有 2% 的锯齿如果靶纸边框被光点压住一角轮廓会缺一块拟合出来的就不是四边形而是五边形这段逻辑会直接返回 None状态机需要据此进入“靶框丢失”分支而不是报错退出。面积下限 3000 对应的是“靶纸在画面里至少占这么大”如果摄像头拉得很远、靶纸只占画面一小块这个值要按实测调小否则每次上电都要等半天才认出框。四点的顺序必须固定否则后续算映射矩阵时角点错配算出来的矩阵看上去数值正常实际把画面翻了个个儿。上面这个 sum/diff 排序法对轻微旋转的矩形都成立只有在矩形旋转超过 45 度时才会失效——场地里靶纸是贴平的不用担心。3.4 像素坐标到云台角度一套能说清楚的映射这一步是视觉与控制的分界线也是答辩时最容易被追问的地方。原理上是个针孔模型像素偏移除以焦距取反正切就是角度。听起来简单但直接套用会有一个系统性偏差——摄像头和激光笔不同轴映射的中心并不是图像中心。import math def pixel_to_angle(px, py, cx, cy, fx, fy): # 针孔模型偏移量 / 焦距 tan(角度) yaw_err math.degrees(math.atan2(px - cx, fx)) pitch_err math.degrees(math.atan2(py - cy, fy)) return yaw_err, pitch_err def angle_with_bias(px, py, cfg): # 在针孔模型基础上叠加标定得到的零点偏置与一阶修正 y (px - cfg[cx]) / cfg[fx] p (py - cfg[cy]) / cfg[fy] yaw math.degrees(math.atan(y)) * cfg[kx] cfg[bx] pitch math.degrees(math.atan(p)) * cfg[ky] cfg[by] return yaw, pitchfx、fy 是焦距的像素表示标定方法很朴素把靶纸放在已知距离量出靶纸实际宽度 W 毫米再数出它在图像里占多少像素 w那么 fx ≈ w × 距离 / W。cx、cy 不是图像中心而是“激光笔光轴指向的那一点”通过 2.3 节的九宫格标定反推出来通常偏离图像中心几十个像素这一步省不得。kx、ky 是标定后残余误差的一阶修正系数一般落在 0.95~1.05 之间如果超出这个范围说明机械安装有问题应该回去调支架而不是靠系数硬掰。注意角度误差在小角度范围内近似线性超过 ±15 度之后 atan 的非线性就明显了。如果云台行程较大必须保留 atan不能偷懒用线性比例替代否则边缘区域会持续偏。4. 云台控制与上下位机通信把角度变成光点的落点视觉算出的是一组角度增量真正让云台动起来的是下位机。这一段的设计目标只有两个指令别丢、时序别乱。听起来平淡但现场演示时云台突然抽风抽搐九成问题出在这。4.1 双环结构视觉环给目标舵机环给输出我的做法是把控制拆成两层。外环是视觉环跑在视觉端输出的是“云台应该指向的角度”内环是舵机环跑在下位机负责把角度变成 PWM 并保证舵机平滑到位。外环频率低15~30 Hz跟着摄像头帧率走内环频率高50~100 Hz两者用串口解耦。为什么外环不用高频因为摄像头就那么多帧视觉环跑再快也只是在重复计算同一帧。反而是在外环里加一个死区很有用当角度误差小于 0.3 度时直接输出零让舵机别再修正。这一步能把可见的抖动压掉一大半很多人调不好 PID 其实是缺了死区。class GimbalTracker: def __init__(self, cfg): self.cfg cfg self.kp, self.ki, self.kd 0.85, 0.05, 0.08 self.integ_yaw self.integ_pitch 0.0 self.prev_yaw self.prev_pitch 0.0 self.lost_frames 0 self.DEADBAND_DEG 0.3 def update(self, spot, dt): if spot is None: self.lost_frames 1 # 丢帧时清积分避免重新捕获瞬间积分项爆炸 self.integ_yaw self.integ_pitch 0.0 return None self.lost_frames 0 yaw_err, pitch_err pixel_to_angle( spot[0], spot[1], self.cfg[cx], self.cfg[cy], self.cfg[fx], self.cfg[fy]) if abs(yaw_err) self.DEADBAND_DEG and \ abs(pitch_err) self.DEADBAND_DEG: return 0.0, 0.0 self.integ_yaw yaw_err * dt self.integ_pitch pitch_err * dt d_yaw (yaw_err - self.prev_yaw) / dt if dt 0 else 0.0 d_pitch (pitch_err - self.prev_pitch) / dt if dt 0 else 0.0 self.prev_yaw, self.prev_pitch yaw_err, pitch_err out_yaw self.kp * yaw_err self.ki * self.integ_yaw \ self.kd * d_yaw out_pitch self.kp * pitch_err self.ki * self.integ_pitch \ self.kd * d_pitch return out_yaw, out_pitch三个参数的作用要分清楚kp 决定跟随速度太大就会过冲然后来回摆ki 用来消除静态偏差但在有死区的情况下容易积攒所以丢帧时必须清零kd 抑制超调但它对图像噪声极其敏感噪声大的时候宁可把 kd 降到 0.03 以下。我一般的调参顺序是先只开 kp 到刚好不振荡再把 kp 降到 70%然后加 ki 到静态偏差消失最后用 kd 收拾残余的超调。4.2 串口协议定长帧 异或校验协议设计上我踩过最大的坑是“用换行符分隔的文本协议”。调试时看着舒服实际跑起来一旦有半帧丢失或者粘包解析端就会拿着半截数据去控制云台动作看起来像抽风。定长二进制帧虽然不好看但可靠得多。import struct import serial FRAME_HEAD b\xAA\x55 def pack_cmd(cmd, yaw_deg, pitch_deg, laser_on): # 角度乘 100 转成 int16省带宽也避免浮点精度歧义 yaw max(-32768, min(32767, int(round(yaw_deg * 100)))) pitch max(-32768, min(32767, int(round(pitch_deg * 100)))) body struct.pack(BhhB, cmd, yaw, pitch, 1 if laser_on else 0) chk 0 for b in body: chk ^ b return FRAME_HEAD body bytes([chk]) def open_port(name/dev/ttyUSB0, baud115200): return serial.Serial(name, baud, timeout0.02)帧结构是2 字节帧头 AA 551 字节命令字2 字节偏航角小端 int16单位 0.01 度2 字节俯仰角1 字节激光开关1 字节异或校验一共 9 字节。下位机按字节喂状态机解析typedef enum { CMD_IDLE 0, CMD_SET_ANGLE 1, CMD_LASER 2, CMD_RESET 3 } cmd_t; static uint8_t buf[9]; static uint8_t idx 0; static uint8_t in_frame 0; void uart_rx_byte(uint8_t b) { if (!in_frame) { if (b 0xAA) { buf[0] b; idx 1; in_frame 1; } return; } buf[idx] b; if (idx 2 buf[1] ! 0x55) { in_frame 0; idx 0; return; } if (idx 9) return; uint8_t sum 0; for (uint8_t i 2; i 8; i) sum ^ buf[i]; if (sum buf[8]) { int16_t yaw (int16_t)((buf[4] 8) | buf[3]); int16_t pitch (int16_t)((buf[6] 8) | buf[5]); gimbal_set_angle(yaw / 100.0f, pitch / 100.0f); gimbal_set_laser(buf[7]); } in_frame 0; idx 0; }解析端的两个细节值得说。一是第二个帧头字节校验失败时必须把 idx 清零而不是回退一位否则遇到连续 0xAA 会陷入死循环二是校验通过之后立刻复位状态不要在里面做耗时操作比如直接驱动舵机走完整段行程那样下一帧就来不及收了。舵机指令我一般写进一个全局目标值由定时器中断去平滑输出串口中断只负责解析和赋值。提示波特率先上 115200如果发现丢帧严重先查线材和共地再考虑降波特率。很多丢帧是地线没接好导致的电平漂移跟波特率没关系。4.3 状态机搜索、锁定、复位、丢失重捕视觉代码看起来是流水线实际运行时是个状态机。我一般定义四个状态SEARCH没找到红点云台按预设路径扫、TRACK锁定正常闭环、LOST连续丢帧但还在尝试、RESET收到复位指令回零点并关激光。状态迁移条件写死不要用一堆 if 散在代码里。关键是 LOST 和 SEARCH 的边界。连续丢 3 帧以内我倾向继续在 TRACK 里保持上一帧的指令不动因为激光点在靶纸上偶尔会被手指或者工具挡住一两帧就恢复了此时乱动反而丢了目标。连续丢超过 8 帧才切到 LOST让云台回到最后一次有效位置附近小范围搜索。这个阈值的确定方法是实测让人用手快速划过光点 10 次统计从遮挡到恢复的帧数分布取 95 分位往上一点。RESET 状态要特别注意时序——先关激光、再回零、最后等 300 ms 稳定这三步不能并行。我见过有人把回零和关激光同时发结果云台回零过程中激光还在画线在靶纸上拖出一道长长的划痕评委看得一清二楚。5. 避坑与排查调了一整晚还是打不中的五个原因这一节是全篇最该先看的部分。下面五条每一条我都在不同场合见过至少两次而且都不是“代码写错了”这么简单靠读代码是找不出来的。5.1 现象白天调好的阈值晚上一开灯就失效原因几乎总是自动曝光和自动白平衡没关。摄像头看到画面整体变亮会自动压低曝光时间红点在图像里从过曝的白心红边变成一个暗淡的小红点V 通道掉到 200 以下直接被阈值切掉。白天在窗边调参时这个机制是隐形的因为环境光稳定摄像头不需要调整。解决办法是把曝光、增益、白平衡、伽马全部设为手动写在代码初始化里而不是靠摄像头的默认行为。初始化之后打印一次实际生效的曝光值如果它跟你设的不一致说明驱动层覆盖了你的设置需要换 API 或者换设备。另外建议在阈值之外加一道保险如果连续 10 帧检测不到任何候选点且掩膜总面积异常大就判定为“环境突变”主动把 V 下限临时下调 20 再试这个自适应逻辑救过我一次外场演示。5.2 现象云台一直在小幅抖动静止时尤其明显这通常不是 PID 参数的问题而是死区没设或者死区太小。图像坐标是整数像素光点静止时质心也会在 ±0.5 像素之间游走换算成角度大概 0.05 度看起来微不足道但 PID 会把这个误差放大并持续输出修正量舵机就在两个相邻位置之间来回跳。解决办法是引入角度死区误差小于 0.3 度直接输出零同时把 kd 降下来因为 kd 对噪声的放大最厉害。还有一个常被忽略的点舵机的机械回差。便宜舵机在目标位置附近有 0.5~1 度的空程你让它走 0.1 度它根本不动但指令一直在发下一个周期误差还在积分项就越积越大最后突然跳一下。所以积分限幅一定要加我一般限制在输出量的 30% 以内。5.3 现象换个场地就得重调所有参数原因在于阈值是绝对阈值而不同场地的环境光色温差别很大。日光灯偏绿、白炽灯偏黄靶纸的白色在不同光源下的 HSV 值能差出一大截。用固定阈值去适配所有场地注定要重调。解决办法有两个层次。浅层的做法是把所有阈值、焦距、中心点、死区全部外置到配置文件里现场改文件不改代码改完重启进程即可。深层的做法是在开机时做一次快速标定让云台把绿点打到靶框四个角用这四组数据现场重算映射矩阵同时统计靶纸区域的 V 通道均值用它来动态调整光点检测的 V 下限。这个自动标定流程跑一次大概 8 秒换来的是换场地不用改代码。我一般会把它做成一个独立的标定模式通过串口命令触发而不是每次上电都跑免得演示时多等十秒。5.4 现象中间准边缘偏越靠边越离谱这是典型的映射非线性或者镜头畸变问题。要么是 3.4 节里的线性比例替代了 atan要么是镜头本身畸变没校正。分辨方法是把光点打到九个均匀分布的位置记下每个位置的指令角度和实际落点像素反算像素到角度的关系。如果偏差随位置单调增大且方向一致是模型问题如果四角偏差大、中间小、方向各异是畸变问题。模型问题就老老实实用 atan 加标定系数畸变问题需要做一次相机标定拿内参和畸变系数然后用 undistort 校正。很多人嫌相机标定麻烦想跳过但如果靶纸在画面里只占中间一小块区域畸变影响确实可以忽略——所以我的建议是能用中心区域就别用边缘把云台的机械零位调到让工作区域落在画面中央比事后校正省事得多。5.5 现象串口通着但云台每隔几秒抽一下原因通常是粘包或者半帧。视觉端以 30 Hz 发下位机如果解析不及时缓冲区里会堆两三帧某一帧正好被截断解析出来的角度是个随机值云台就抽一下。另一个来源是视觉端的多线程写串口没加锁两帧数据交错写入粘在一起。解决办法有三条一起上串口中断里只做解析不做业务业务放到主循环解析端严格按帧长度滑动校验失败也必须把整个 9 字节跳过不能只跳 1 字节视觉端发数据的线程和算数据的线程之间用队列队列长度限制为 1新帧覆盖旧帧——反正旧的角度指令已经没有意义了宁可丢也不要排队。注意调试期间建议在视觉端记录每一帧的发送时间戳和角度下位机记录接收时间戳和校验结果两边日志一对是发的问题还是收的问题五分钟就能定位。6. 验收与答辩用一份自测记录证明视觉部分真的能用到了最后一步很多人卡在“我知道它能用但我不知道怎么证明”上。答辩场上评委不会看你的代码写得多漂亮他们只问三个问题精度多少、延迟多少、丢了怎么办。这三个问题都有办法用数据回答前提是你提前测了。我的做法是准备一张自测表在正式演示前一天跑一遍结果直接打印出来贴在旁边。测试项不用多五项足够每一项都要有可复现的操作步骤和明确的判据。测试项操作方法合格判据静态定位精度光点静止在靶框四个角和中点各采 100 帧坐标标准差小于 1 像素落点偏差给定九个角度量取实际落点与理论位置距离最大偏差小于靶纸边长的 2%跟踪延迟光点做阶跃移动统计视觉输出响应帧数小于 5 帧60 fps 下约 83 ms丢失恢复手动遮挡光点 200 ms 后松开重复 10 次10 次全部重新捕获平均恢复时间小于 0.5 s连续运行闭环跟踪 5 分钟不干预无状态机异常跳转、无串口丢帧报警测完之后把数据整理成一句话“静态抖动 0.7 像素最大落点偏差 4.1 毫米阶跃延迟 4 帧十次遮挡全部恢复。”这句话在答辩时比任何架构图都管用因为它把抽象的能力变成了可核对的数字。演示顺序也有讲究。我一般先做复位让所有人看到系统能回到确定状态再慢速走一圈靶框让人看清光点是贴着边框走的最后才做快速跟踪和遮挡恢复。反过来的顺序风险很大——一上来就高速跟踪万一第一下没锁上后面所有演示都在解释“刚才那个是特殊情况”。从那以后我每次做这类带视觉闭环的项目都会强制走一遍“先测数据、再排演示顺序、最后才碰参数”的流程哪怕时间再紧也不跳步。参数是最后才调的东西一旦在演示前十分钟改了阈值又没记下来出了问题是查不出来的。希望这份流程能帮到你把视觉部分从“能跑”做到“敢演示”。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑