简介2023年全国大学生电子设计竞赛E题视觉部分代码包面向参加电赛的参赛队伍与学生围绕K210视觉识别与双云台协同控制给出可复用的工程实现。视觉端基于sipeed Maix Bit开发板涵盖图像采集、预处理、特征提取与目标检测跟踪等关键函数并配有颜色识别、物体分类、距离测量等实战案例控制侧提供主机云台、从机云台代码主控采用STC32G12K128便于快速搭建稳定的视觉系统并在此基础上进行改造和创新。压缩包共352个文件、约19.47MB以C/H源码、Python脚本、Keil工程文件uvproj、uvopt、lst、obj等及hex固件、pdf说明与md文档为主视觉端与单片机端并存目录按模块划分便于检索。已有619人学习适合视觉新手与有基础的学生对照工程结构理解赛题方案、复用算法函数并梳理调试思路。1. 电赛 E 题视觉部分先让光斑坐标可信再谈云台跟得快2023 年电赛 E 题视觉部分代码包最容易让人误判的一点是它看起来在“找红色光斑”真正决定成绩的却是坐标输出是否稳定。摄像头画面里红点一闪一闪阈值稍微一改就丢目标后面 PID 再漂亮也只能跟着抖。这份资源适合正在复现电赛 E 题视觉链路、手里有 OpenMV 或 OpenCV 摄像头加二维云台的同学也适合想把颜色跟踪做成可调参软件模块的开发者。它解决的是从图像采集、阈值分割、轮廓筛选到串口帧输出这一段不是完整云台工程拿到后先跑最小闭环再逐项换参数比一上来改控制环靠谱。2. 拆开视觉代码包从图像采集到串口帧的六段链路拿到这份视觉代码先别从 main 一路往下读。颜色跟踪类项目的入口往往藏在“坐标出口”附近谁把 cx、cy 变成串口字节谁就定义了整条链路的节奏。先找这个出口再反推检测和采集阅读成本会低很多。2.1 目录结构与模块职责先找“坐标出口”这类代码包常见命名不一定统一但职责大体固定。下面这张表按职责列不按文件名列你手里的包叫 detector.py 还是 vision.py 都不重要能对上输入输出就行。模块职责常见输入常见输出先检查什么参数配置无阈值、ROI、串口参数参数是否集中能否不改代码图像采集摄像头索引帧、时间戳是否锁曝光、分辨率是否固定目标检测单帧图像候选轮廓、质心面积和圆度筛选是否合理坐标解算质心、ROI归一化偏差死区、限幅、丢失状态串口发送偏差、状态字节帧帧头、长度、校验离线回放视频、日志复算结果能不能复现现场问题坐标出口通常是一个打包函数。串口帧不要直接发浮点像素坐标下位机还得再做一次映射调试时两边容易打架。常见做法是上位机先归一化到固定整数范围下位机只做比例缩放。下面这段是可直接抄的帧格式示例字段长度和校验方式可以改但思路建议保留。import struct FRAME_HEAD 0xAA55 def pack_frame(x: int, y: int, valid: bool, seq: int) - bytes: # x/y 先归一化到 [-1000, 1000]下位机按云台角度范围缩放 x max(-1000, min(1000, int(x))) y max(-1000, min(1000, int(y))) payload struct.pack( hhBBH, x, # int16水平偏差 y, # int16垂直偏差 1 if valid else 0, # uint8本帧是否有效 0, # uint8预留状态位 seq 0xFFFF # uint16帧序号排查丢帧用 ) body struct.pack(H, FRAME_HEAD) bytes([len(payload)]) payload checksum 0 for b in body: checksum ^ b return body bytes([checksum])逻辑说明帧头占 2 字节长度占 1 字节payload 固定 6 字节最后 1 字节异或校验。x、y 用 int16 而不是 float是为了避免上下位机浮点解析差异valid 单独占一字节丢目标时下位机能立刻进入保持或回中策略不用靠坐标 0,0 猜。seq 每帧自增联调时看日志就能判断是视觉没出帧还是串口丢了帧。参数上唯一容易翻车的是字节序表示小端下位机如果是大端解析收到负数会变成很大的正数云台会直接打偏。2.2 颜色阈值 轮廓筛选为什么不是简单的 inRange红色在 HSV 里跨了 0 度边界只写一段inRange很容易漏掉偏紫的红光斑。常见做法是低 H 段和高 H 段各做一次掩膜再按位或。掩膜出来之后也不能直接取最大轮廓赛场上红色衣服、指示灯、反光贴都可能进来必须加面积、圆度、长宽比这类几何筛选。import cv2 import numpy as np def red_mask(hsv): # 红色跨 0 度低段和高段都要保留 lower1 np.array([0, 80, 60]) upper1 np.array([10, 255, 255]) lower2 np.array([170, 80, 60]) upper2 np.array([180, 255, 255]) m1 cv2.inRange(hsv, lower1, upper1) m2 cv2.inRange(hsv, lower2, upper2) mask cv2.bitwise_or(m1, m2) kernel np.ones((3, 3), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) return mask def find_spot(mask, min_area30, max_area2000): contours, _ cv2.findContours( mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) best None for c in contours: area cv2.contourArea(c) if not (min_area area max_area): continue (cx, cy), r cv2.minEnclosingCircle(c) if r 0: continue circularity area / (3.1415926 * r * r) if circularity 0.35: continue if best is None or area best[0]: best (area, cx, cy, r) return best逻辑说明red_mask只负责把红色区域留下find_spot负责从候选里挑最像光斑的那个。min_area 和 max_area 跟分辨率强相关QVGA 下 30 到 2000 像素是个起点换到 640×480 要按面积比例放大。circularity 用面积除以最小外接圆面积光斑越圆越接近 1阈值 0.35 偏宽松现场误检多就往上抬到 0.5 左右。形态学开运算去小噪点闭运算补光斑内部空洞核大小别超过 5×5否则小光斑会被吃掉。这里不要把检测函数写死成只返回一个坐标最好同时返回面积和圆度后面排查误检时就是黑匣子数据。2.3 坐标解算与帧格式像素坐标怎么变成云台增量像素坐标直接送下位机不是不行但下位机要知道当前分辨率和 ROI 偏移一旦视觉端改窗口控制端就得跟着改。更稳的做法是在视觉端做归一化以 ROI 中心为原点把水平、垂直偏差压到 [-1,1]再乘 1000 转成整数。死区也要在视觉端先做掉否则光斑在中心附近轻微抖动云台会一直来回修正。def pixel_to_norm(cx, cy, roi, dead_zone4): x0, y0, w, h roi center_x x0 w / 2 center_y y0 h / 2 ex (cx - center_x) / (w / 2) ey (cy - center_y) / (h / 2) if abs(cx - center_x) dead_zone: ex 0.0 if abs(cy - center_y) dead_zone: ey 0.0 ex max(-1.0, min(1.0, ex)) ey max(-1.0, min(1.0, ey)) return int(ex * 1000), int(ey * 1000)逻辑说明roi 传的是当前裁剪窗口的(x, y, w, h)不是整幅图像。dead_zone 单位是像素4 像素在 320 宽画面里大约对应 1% 出头太小没效果太大中心会出现明显停顿。归一化后乘 1000 只是定标不是物理角度真正的角度映射要么在下位机做要么在视觉端用四点标定拟合。帧格式里的 valid 要跟这个函数配合找不到轮廓时不要发 (0,0)要发 valid0并保持上一帧或回中具体策略看云台机械结构。血泪经验是很多“云台抖”不是 PID 参数问题而是丢目标瞬间坐标突然归零控制器以为目标跳到中心实际是视觉在撒谎。3. 把代码跑起来OpenCV 桌面调试与 OpenMV 移植的两套环境同一套视觉逻辑桌面端和嵌入式端要分开跑。桌面端用 OpenCV 方便看掩膜、拖阈值、录视频OpenMV 或 K210 端负责最终上机算力和内存都紧。两边保留同样的接口调参结果才能搬过去。3.1 桌面端最小可运行环境依赖、摄像头索引与首帧检查先建独立环境避免和系统里的 OpenCV 打架。依赖不多opencv-python、numpy、pyserial 够用。摄像头索引 0 和 1 经常被笔记本内置摄像头占掉插上 USB 摄像头后先枚举一遍别默认 0 就是外置。python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install opencv-python numpy pyserial装完后先跑一个只读帧的小脚本确认画面、分辨率、帧率都正常再谈检测。下面这段设置分辨率后打印 fps按 Esc 退出。import cv2 import time cap cv2.VideoCapture(0, cv2.CAP_DSHOW) # Linux 可去掉 CAP_DSHOW必要时换 CAP_V4L2 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 60) if not cap.isOpened(): raise RuntimeError(摄像头没打开先换 index 或检查占用) t0 time.time() n 0 while True: ok, frame cap.read() if not ok: break n 1 if n % 30 0: print(fps, round(n / (time.time() - t0), 2), frame.shape) cv2.imshow(raw, frame) if cv2.waitKey(1) 27: break cap.release() cv2.destroyAllWindows()逻辑说明CAP_DSHOW在 Windows 下对 USB 摄像头兼容性通常更好Linux 下用 V4L2 或默认后端。分辨率设置不一定生效打印frame.shape确认实际值有些摄像头只支持 640×480设 1280×720 会静默回退。fps 打印用于判断采集是不是瓶颈如果显示 15 帧但设置 60后面调 PID 前先换线或换 USB 口。首帧检查还要看画面有没有过曝红色光斑过曝后 HSV 的 S 会掉下去阈值再宽也救不回来。3.2 从视频流到调参面板用 Trackbar 做阈值粗调阈值不要靠猜。做一个最小 Trackbar 面板只调 H 下限、H 上限、S 下限、V 下限实时看掩膜。先固定曝光和白平衡再拖阈值自动曝光开着的时候同一组参数在画面明暗变化后就会漂。import cv2 import numpy as np def nothing(x): pass cap cv2.VideoCapture(0) cv2.namedWindow(tune) for name, val, maxv in [ (hmin, 0, 180), (hmax, 10, 180), (smin, 80, 255), (vmin, 60, 255) ]: cv2.createTrackbar(name, tune, val, maxv, nothing) while True: ok, frame cap.read() if not ok: break hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) hmin cv2.getTrackbarPos(hmin, tune) hmax cv2.getTrackbarPos(hmax, tune) smin cv2.getTrackbarPos(smin, tune) vmin cv2.getTrackbarPos(vmin, tune) mask cv2.inRange( hsv, np.array([hmin, smin, vmin]), np.array([hmax, 255, 255]) ) cv2.imshow(raw, frame) cv2.imshow(mask, mask) if cv2.waitKey(1) 27: break逻辑说明这个面板只调红色低 H 段高 H 段固定 170 到 180 先看效果如果光斑偏紫把 hmax 往上抬如果偏橙把 hmin 往下压。S 下限压白墙和灰色物体V 下限压暗处反光。调的时候把光斑移到靶面四个角各看一次取四角都能稳定出掩膜的交集不要只盯中心。参数记到配置表里别只留在 Trackbar 窗口。窗口缩小到 320×240 看掩膜更清楚但调参用原分辨率否则小光斑会被缩放吃掉。3.3 移植到 OpenMV/K210 时保留的三个接口桌面端调好后移植不要重写整套逻辑。保留三个接口取帧、检测目标、发坐标。OpenMV 上用 MicroPython颜色空间和 OpenCV 不同阈值一般是 LAB 六元组不是 HSV 三元组。下面这段是 OpenMV 找红色色块的骨架重点是锁自动增益和白平衡否则现场灯一变阈值就废。import sensor import image import time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time2000) sensor.set_auto_gain(False) sensor.set_auto_whitebal(False) red (30, 100, 15, 127, 15, 127) # LAB 阈值按现场光重调 clock time.clock() while True: clock.tick() img sensor.snapshot() blobs img.find_blobs( [red], pixels_threshold30, area_threshold30, mergeTrue ) if blobs: b max(blobs, keylambda x: x.pixels()) img.draw_cross(b.cx(), b.cy()) # 坐标出口保持独立串口打包不要塞进检测循环 print(b.cx(), b.cy(), clock.fps())逻辑说明pixels_threshold和area_threshold是 OpenMV 侧的面积门槛和桌面端的 min_area 对应但不能直接照搬QVGA 下 30 像素起步。mergeTrue会把相邻色块合并光斑碎裂时有用但两个红色目标靠得近时也会粘在一起现场要关掉对比。set_auto_gain(False)和set_auto_whitebal(False)是必须的不锁的话画面会自己找平衡阈值调了等于没调。串口发送建议单独开一个函数按固定周期发最新坐标不要每检测到一帧就写串口否则帧率一高下位机串口缓冲会积压。4. 调参不是玄学阈值、ROI、曝光与坐标标定的可复现步骤视觉调参最容易变成“看起来差不多”。可复现的做法是每次只改一类参数改完记录画面和日志。阈值、ROI、曝光、坐标映射四类分开调云台控制环先别动等坐标输出稳定了再进 PID。4.1 阈值怎么设HSV 三通道的边界与光照余量HSV 阈值不是越窄越准。窄阈值抗干扰但光斑稍微偏色就丢宽阈值不容易丢但误检多。建议先用宽阈值保召回再用轮廓筛选压误检。下面这张表是桌面端红色光斑的起点不是万能值换摄像头和光源都要重调。参数起点值现场调整方向作用H 低段0 到 10偏橙往下压偏紫往上抬覆盖红色低色相H 高段170 到 180与低段合并看掩膜覆盖红色高色相S 下限80误检白墙就抬高压低饱和背景V 下限60暗处反光多就抬高压暗部噪声面积下限30 像素按分辨率比例缩放去小噪点圆度下限0.35误检多抬到 0.5保留圆形光斑调阈值时先把摄像头曝光锁死再把光斑打在靶面中心、四角、最远点各看一次。四角都能稳定出掩膜才算过了光照余量这一关。赛场灯和实验室灯色温不一样S 和 V 的下限要留出至少 20 的余量不要卡在临界值上。红色激光点在摄像头里容易过曝过曝区域会变成白色H 还在但 S 掉到很低这时候要么降曝光要么加 ND 片不要硬调 S 下限。4.2 ROI 与分辨率把算力花在靶面而不是整幅画面整幅 640×480 做 HSV 转换和轮廓查找桌面端无所谓OpenMV 上帧率会掉得很难看。靶面通常只占画面中间一块先固定 ROI再考虑动态 ROI。固定 ROI 简单可靠动态 ROI 跟踪上一帧坐标能进一步省算力但丢目标时容易把窗口带偏。def get_roi(frame, last_xyNone, base(80, 60, 480, 360)): h, w frame.shape[:2] x, y, rw, rh base if last_xy is not None: cx, cy last_xy rw, rh 240, 180 x max(0, min(w - rw, int(cx - rw / 2))) y max(0, min(h - rh, int(cy - rh / 2))) return frame[y:y rh, x:x rw], (x, y, rw, rh)逻辑说明base是固定 ROI起点取画面中间偏下给云台运动留边。last_xy有值时切到 240×180 的跟踪窗窗口跟着上一帧光斑走。max(0, min(...))防止窗口越界。注意 ROI 坐标要跟着检测结果一起传给坐标解算否则归一化中心会错。动态 ROI 的窗口不要小于 160×120太小时光斑一出窗就彻底丢失连重捕获的机会都没有。固定 ROI 和动态 ROI 的切换阈值要写进配置别在代码里散落魔法数。4.3 像素到角度四点标定与畸变补偿的简化做法像素偏差到云台角度不是线性关系镜头有畸变摄像头光轴和云台轴也不一定平行。精确标定要拍多张棋盘格赛场上更常用简化四点法把光斑打到靶面四个已知点记录像素坐标和对应的云台角度拟合一个仿射关系。精度够用标定十分钟能做完。import numpy as np # 采样光斑在靶面四点时的像素坐标与云台角度 P np.array([[100, 80], [540, 80], [100, 420], [540, 420]], dtypenp.float64) A np.array([[-8, -6], [8, -6], [-8, 6], [8, 6]], dtypenp.float64) M np.hstack([P, np.ones((4, 1))]) coef_x, *_ np.linalg.lstsq(M, A[:, 0], rcondNone) coef_y, *_ np.linalg.lstsq(M, A[:, 1], rcondNone) def pixel_to_angle(px, py): v np.array([px, py, 1.0]) return float(v coef_x), float(v coef_y)逻辑说明P 是像素点A 是对应角度单位按你的云台协议来示例用度。np.linalg.lstsq解最小二乘四个点刚好能拟合仿射点多于四个就变成超定误差更稳。这个模型假设角度和像素近似线性靶面小、角度范围小时够用角度范围大就要加二次项或分象限拟合。标定采样时云台先回中记录角度用下位机反馈值不要用指令值指令和实际角度可能有静态误差。标定完拿靶面九个点验一遍最大误差超过 0.5 度就要查镜头畸变或安装角。4.4 帧率与延迟时间戳、缓冲队列和丢帧策略视觉链路最怕“看起来帧率很高控制端拿到的却是旧坐标”。采集线程、检测线程、串口发送如果各跑各的中间不加时间戳和队列长度限制延迟会越积越大。常见做法是只保留最新帧旧帧直接丢控制端永远拿当前画面算出来的坐标。import collections import time class FrameBuffer: def __init__(self, maxlen2): self.q collections.deque(maxlenmaxlen) def push(self, frame, tsNone): self.q.append((frame, ts if ts is not None else time.time())) def latest(self): return self.q[-1] if self.q else (None, None)逻辑说明maxlen2表示最多存两帧第三帧进来挤掉最旧的天然防积压。ts用time.time()或time.perf_counter()都行关键是全链路统一。串口发送前取latest()不要遍历队列逐帧发。如果发现控制端响应慢半拍先打印now - ts超过两帧周期就说明链路有积压优先查串口写阻塞和检测耗时不要先调 PID。帧率不是越高越好30 帧稳定比 60 帧抖动强尤其是颜色跟踪这种受光照影响的检测。5. 避坑与排查光斑丢了、云台抖了、串口乱码分别查什么视觉部分翻车八成不是算法多复杂而是几个工程细节没锁死。下面这几条是这类代码包联调时反复出现的现象按“现象→原因→解决”记现场能省很多时间。5.1 现象阈值白天能用赛场灯一变就丢目标现象很典型实验室里掩膜干净换到赛场灯下红色光斑只剩几个像素或者整幅画面都变成候选。原因通常不是阈值本身而是摄像头自动曝光和自动白平衡还开着。画面一亮S 和 V 整体漂移昨天调的参数今天就不成立。另一个原因是阈值卡得太窄H 低段只设了 0 到 5光斑偏橙就出界。解决分三步先锁曝光和白平衡再重调阈值最后留余量。OpenCV 下不同后端控制项取值不一样下面这段是起点具体数值看画面。cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 0.25) # 部分后端 0.25 表示手动 cap.set(cv2.CAP_PROP_EXPOSURE, -6) # 先降曝光避免光斑过曝 cap.set(cv2.CAP_PROP_AUTO_WB, 0) # 关自动白平衡逻辑说明自动曝光关闭后画面整体亮度固定HSV 阈值才可复现。曝光值 -6 不是通用值要看摄像头支持范围过暗会把红色压没。锁完再跑一遍 Trackbar把 H 低段放宽到 0 到 10高段保留 170 到 180S 下限留 20 的余量。现场如果灯特别暖H 低段可以收到 0 到 8灯偏冷高段往上抬。阈值改完把配置写死别在比赛时开自动。5.2 现象云台低频摆动PID 越调越糟云台像钟摆一样来回晃加大 P 晃得更快加大 D 又变成高频抖。很多人第一反应是 PID 参数实际常见原因是视觉坐标在跳。光斑质心每帧差几个像素归一化后放大成几十个整数单位控制器以为目标在动。另一个原因是丢帧时坐标归零云台被拉回中心又弹出去。解决先看日志不看 PID。把每帧的 cx、cy、valid、时间戳写进 CSV回放看坐标序列是不是毛刺。如果是毛刺先加死区和限幅再做一阶低通如果是丢帧归零改成 valid0 时保持上一帧或缓慢回中。滤波不要加太重截止频率太低会引入相位滞后云台反而更晃。调 PID 前先把视觉坐标的抖动压到 2 个像素以内再动控制参数。5.3 现象串口能收数但云台响应慢半拍串口助手里能看到坐标在变云台动作却明显滞后或者动一下停一下。原因通常是发送频率高于下位机解析能力串口缓冲积压也可能是视觉端每检测一帧就写一次串口检测帧率不稳定下位机收到的帧间隔忽长忽短。还有一种情况是帧头校验太弱下位机把数据段里的 0xAA 当成帧头解析错位。解决用“只发最新帧”加“固定发送周期”。检测线程更新共享坐标发送线程按 50Hz 或 100Hz 取最新值打包旧坐标直接丢。帧格式加长度和异或校验下位机解析时先找帧头再验长度再验校验三步都过才更新目标。波特率不够就往上提但先确认线材和串口芯片支持。不要用print代替串口调试打印本身会拖慢主循环。5.4 现象程序跑十分钟后帧率断崖下跌刚启动 30 帧跑十分钟掉到 10 帧重启又恢复。常见原因是日志列表无限增长、视频窗口没释放、串口对象反复打开或者每帧都创建大数组。Python 里列表 append 不删内存慢慢涨GC 一抖帧率就掉。OpenMV 上更明显图像对象没复用也会吃内存。解决给主循环加帧率计数每秒打印一次日志用固定长度队列或按文件滚动写VideoCapture和VideoWriter在异常分支也要 release。检测函数里尽量复用掩膜和核矩阵不要在每帧里np.ones新建。跑长时间测试至少二十分钟看帧率曲线是不是平的。如果帧率缓慢下降但内存没涨查串口写是不是阻塞写超时会拖住整个循环。5.5 现象同一靶纸左半边准右半边偏光斑在靶面中心跟得很准移到左边或右边误差变大换另一边又反向偏。原因通常是镜头畸变、摄像头光轴和云台转轴不平行或者四点标定只取了中心附近。像素到角度的线性模型在边缘误差最大角度范围一大就暴露。解决先把标定点铺开至少覆盖靶面四角和中心有条件做九点。检查摄像头安装镜头平面尽量平行靶面云台水平轴和垂直轴没有明显夹角。标定后用九点验边缘误差超过中心误差两倍就考虑分象限拟合或加二次项。机械安装的偏差不要靠软件硬补补出来的模型换个场地就废。每次拆装摄像头后重新标定不要沿用旧参数。6. 进阶验证用日志回放和误差统计把视觉链路压到可交付视觉部分能不能上场不看单帧截图好不好看看的是连续运行时的误差分布和丢帧率。赛前我一般会录一段带时间戳的原始视频再把检测参数离线跑一遍至少能分清是算法问题还是现场光照问题。6.1 录一段带时间戳的原始视频离线复算现场调试时间紧录原始视频是最便宜的后悔药。把摄像头画面和每帧时间戳一起落盘赛后用同一套检测代码复算改阈值、改 ROI 都能重跑不用把云台再搭一遍。import cv2 import csv import time cap cv2.VideoCapture(0) vw cv2.VideoWriter( raw_capture.avi, cv2.VideoWriter_fourcc(*MJPG), 30, (640, 480) ) log open(raw_capture.csv, w, newline) writer csv.writer(log) writer.writerow([ts, fps]) t0 time.time() n 0 while n 30 * 20: # 录 20 秒按需改 ok, frame cap.read() if not ok: break n 1 vw.write(frame) writer.writerow([f{time.time() - t0:.3f}, round(n / (time.time() - t0), 2)]) vw.release() log.close() cap.release()逻辑说明MJPG 编码对 CPU 压力小帧率稳缺点是文件大录短片段够用。CSV 记的是写入时刻不是曝光时刻精确分析要用硬件时间戳或至少用perf_counter。复算时按视频帧号对齐日志比较不同阈值下的丢帧率和质心轨迹。录的时候把现场灯、云台位置、靶纸距离记在文件名里后面回看不会混。6.2 用误差椭圆和丢帧率判断“能不能上场”单看平均误差会骗人边缘几个大误差点被平均掉场上偏偏就是那几下打偏。至少看四个数平均误差、95 分位误差、最大误差、丢帧率。下面这段统计可以直接接在标定采样后面。import numpy as np err np.array(errors) # 单位度来自九点验证 print(mean, round(float(err.mean()), 3)) print(p95, round(float(np.percentile(err, 95)), 3)) print(max, round(float(err.max()), 3))逻辑说明p95 比均值更能反映“大部分时候够不够准”最大误差决定极端情况会不会出界。丢帧率用总帧数减有效帧数再除以总帧数超过 5% 就要查光照或阈值。判断标准按题目要求倒推比如允许偏差 1 度p95 至少压到 0.5 度以内留余量。误差统计要分区域做中心和边缘分开看不然边缘问题会被中心数据稀释。6.3 赛前固化参数一份可回滚的配置表调参过程中最怕“越调越回不去”。我习惯把关键参数集中到一张表里每次改动前备份旧值赛前只认最后一份。下面这张表是这类视觉链路的最小集合。配置项起点允许范围作用摄像头分辨率640×480320×240 到 1280×720影响算力和最小面积曝光模式手动手动锁亮度H 低段0 到 100 到 12红色低色相H 高段170 到 180168 到 180红色高色相S 下限8060 到 140压背景饱和面积范围30 到 2000按分辨率缩放轮廓筛选死区4 像素2 到 8中心防抖发送周期20 ms10 到 50 ms串口节流这张表不要只放在代码注释里导出成独立配置文件改完跑一遍最小闭环再上场。从那以后我每次拆装摄像头、换场地灯光都会强制走一遍“锁曝光、重录视频、九点验证、备份配置”的本文还有配套的精品资源点击获取