资讯动态

YOLO标签可视化与标注质检实战:从坐标换算到批量流水线

发布时间:2026/10/26 13:16:40 来源:尧图企业网站定制
做目标检测的人几乎都绕不开这么一段经历标注结果交回来随手挑一张图配上标签一画框不是偏了半格就是整片挤在图像左上角还有的类别编号对不上明明标的是“安全帽”画出来却显示一个 2。这类问题如果不在训练前发现后面就是几个小时的训练白跑指标怎么调都上不去。yolo 格式的标签可视化说白了就是把每张图对应的 txt 里那几行归一化数字还原成看得见的矩形框、多边形或者关键点再盖回原图上用眼睛做一次人肉质检。这件事听起来简单真正落地的时候坑不少坐标系是中心点还是左上角、数值是归一化还是像素、类别索引从 0 起还是从 1 起、图片读进来有没有被 EXIF 转过、分割标签的点数对不上、空标签文件要不要跳过。这篇就把我这些年画框、验标注、查数据集的经验整理出来从一行坐标换算讲到批量流水线从矩形框讲到分割和关键点代码可以直接抄参数怎么定也一并说清楚。适合刚拿到 YOLO 格式数据集准备训练的人也适合已经跑起来但怀疑标注有问题、想回头做数据体检的人。1. 先把 YOLO 标签文件里的内容拆开看1.1 一行五个数每个数都在说什么YOLO 检测格式的标签文件是纯文本一张图对应一个同名 txt文件里每一行代表一个目标。标准的一行是五个数值中间用空格隔开0 0.481250 0.533333 0.126563 0.244444依次是类别索引、中心点 x、中心点 y、框宽、框高。这里有两个关键点必须记牢。第一坐标不是左上角加右下角而是中心点加宽高这是 YOLO 系列从一开始就定下的约定是为了让输出层直接预测偏移量训练时更稳定。第二后四个数全部是归一化到 0 到 1 之间的小数也就是相对于图片宽高的比例不是像素值。上面那行里的 0.481250 乘以图片宽度才是真正的像素横坐标。这两个设计在训练侧很优雅模型输入会被统一缩放到固定尺寸比如 640×640归一化坐标不受原始分辨率影响同一套标签在 416、640、1280 输入下都能直接用。但在可视化侧就变成了一道必须手动做的换算题很多人第一次画框画歪八成就是漏了“乘回图片宽高”这一步或者把中心点当成了左上角。1.2 归一化带来的便利和隐藏的陷阱归一化的好处很直接标签和图片解耦。你把原图从 1920×1080 缩放成 640×360标签文件一个字节都不用改。但这份便利也藏了几个陷阱我在实际项目里都撞过。第一个陷阱是宽高比失真。如果你的数据准备环节把图片直接 resize 成正方形而没有做 letterbox 填充标签仍然按原图归一化保存那画出来的框在拉伸后的图上是歪的。反过来如果你在可视化时用cv2.resize把图强行拉成正方形然后拿原始标签去画框会沿着一个方向被压扁看起来“框住了一半”。这不是标签错了是你的绘制流程和目标检测预处理流程不一致。第二个陷阱是浮点精度与边界溢出。归一化坐标理论上最大是 1.0实际标注工具偶尔会输出 1.000001 这种值乘以宽度后超出图像边界一两个像素用 OpenCV 画框时不会报错但框的边会贴着图边看不见。更常见的是小目标的框宽度只有 0.003乘以 1280 像素是 3.84 像素取整之后可能变成 3 像素甚至 2 像素肉眼看就是一个小点很容易被误判成“标注错了”。第三个陷阱是不同工具的归一化基准。绝大多数标注工具按图像实际宽高归一化但有些工具在图片被旋转手机拍摄的 JPEG 带 EXIF Orientation之后按未旋转的原始尺寸算出标签PIL 读进来会自动应用 EXIF 旋转OpenCV 的imread默认不会。同一张图用两个库读宽高可能互换标签就整体错位 90 度。这个坑后面第 5 章会专门讲。1.3 为什么必须做可视化三类看不见的标注问题有人觉得标注文件只要能读、训练能跑就说明数据没问题。实际情况是标签格式合法和标注质量合格完全是两件事。可视化能帮你抓到三类问题这三类问题仅靠看 loss 曲线基本发现不了。第一类是系统性偏移。比如整个批次的框都往左上偏了大约 5%原因可能是标注工具在缩放预览图时算错了比例。这种偏差不会让训练崩溃模型照样收敛但检测框会稳定地偏一点mAP 掉几个点你调学习率、换增强策略都没用。第二类是类别错标与索引错位。标注规范里“0 是行人、1 是骑行者”结果某个标注员从 1 开始数整批数据错位一格。训练出来的模型会把行人判成骑行者混淆矩阵上两个类互相污染得很严重但你在损失值上完全看不出来。第三类是冗余框与漏标。同一辆车被画了两个几乎重合的框或者一张图里明显有五个目标只标了两个。重复框会让模型学到模糊的回归目标漏标则变成“把正样本当背景”是检测任务里最伤模型的一类噪声。这类问题唯一的发现手段就是把框画回去看批量扫一遍几十张图里总能揪出问题样本。2. 工具选型从二十行脚本到现成库2.1 三种常见方案的实际对比可视化这件事从完全手写到调用现成接口中间有好几个档位。我把常用的几种方案列个表按自己的使用场景挑就行。方案依赖上手成本灵活度适合场景OpenCV 手写绘制opencv-python、numpy低二十行代码极高想加什么加什么日常验标注、定制输出、批量流水线Pillow 绘制Pillow低高字体渲染更漂亮需要中文标签、出报告图matplotlib 叠加matplotlib中中适合做统计图论文配图、分布分析检测框架自带的绘制接口对应框架很低低样式固定快速看一眼不想写代码通用可视化工具库额外第三方库中中需要统一风格、画关键点和轨迹我自己的习惯是验标注用 OpenCV 手写出材料用 Pillow做统计分析用 matplotlib。框架自带的绘制接口确实省事一行就能出图但它的样式颜色、线宽、字体基本写死标签多的时候文字互相遮挡而且它内部往往还会做一次图像缩放你很难判断看到的结果和原始标签是否严格对应。做数据体检这种需要“眼见为实”的事情我宁可自己写二十行可控性高得多。2.2 什么时候该自己写什么时候别重复造轮子判断标准其实很简单你要不要控制输出的每一个像素。如果只是想知道“这批标注大概长什么样”用现成接口完全够几分钟出结果。但如果你要做的是批量生成几千张带框图压缩打包给标注团队返工把类别名、置信度、目标尺寸一起写在框上按类别过滤只画某一类看分布输出带框视频做演示那就自己写。核心逻辑无非三步读标签、换算坐标、画。真正花时间的从来不是绘制本身而是文件路径匹配、异常行容错、类别名映射这些边角料这些恰恰是现成接口不管的部分。2.3 环境与依赖准备基础环境很轻三条命令搞定pip install opencv-python numpy pyyaml # 需要漂亮中文字体渲染时再加 pip install pillowpyyaml是用来读data.yaml的这个文件里存着类别名列表是可视化时把数字翻译成人话的唯一依据。很多数据集会把类别名写在classes.txt里一行一个顺序即索引那就更简单直接读文本。注意opencv-python和opencv-python-headless不要同时装后者没有 GUI 但适合服务器批量跑。如果脚本要在无显示器的机器上跑务必用 headless 版本并且全程不要调用cv2.imshow一律cv2.imwrite落盘。3. 坐标换算与绘制逻辑的核心细节3.1 从归一化中心宽高还原到像素左上右下这是整件事的地基我把它单独抽成函数所有脚本都调它避免每处各写一遍导致不一致。换算分两步先乘回像素再从中点展开成左上右下。def yolo_to_xyxy(xc, yc, w, h, img_w, img_h): 归一化中心宽高 - 像素左上右下 x1 (xc - w / 2.0) * img_w y1 (yc - h / 2.0) * img_h x2 (xc w / 2.0) * img_w y2 (yc h / 2.0) * img_h return int(round(x1)), int(round(y1)), int(round(x2)), int(round(y2))写法上有几个细节值得说。先减后乘还是先乘后减结果一样但先算(xc - w/2)再乘宽度浮点误差更小也更好读。用round而不是intint是截断会让所有框系统性地往左上偏半像素小目标上这个偏差肉眼可见。最后一定要做边界夹取把坐标限制在[0, img_w-1]和[0, img_h-1]范围内否则 OpenCV 画线时超出部分会直接裁掉看起来像框被切了一角。那要不要顺手算出框的面积和长宽比建议要。验标注的时候我经常在框上同时打印w×h的像素尺寸一眼就能看出哪些是小到没法训练的目标比如 8×6 像素哪些是几乎覆盖整图的大框往往是标注员误把背景框进去了。3.2 颜色调度、线宽和文字遮挡的处理颜色这块千万别用随机色。同一张图刷新两次颜色不一样做前后对比时会怀疑自己看错了不同图之间颜色也不统一回头看根本记不住哪个颜色对应哪类。正确做法是按类别索引取固定调色板PALETTE [ (56, 56, 255), (151, 157, 255), (31, 112, 255), (29, 178, 255), (49, 114, 49), (0, 165, 255), (255, 128, 0), (128, 0, 128), (0, 200, 200), (200, 0, 100), ] def color_of(cls_id): return PALETTE[cls_id % len(PALETTE)]注意这里给的是BGR 顺序因为 OpenCV 内部就是 BGR直接用 RGB 值喂进去红色和蓝色会对调看着也别扭但不容易意识到错在哪。调色板选色时尽量拉开色相差距避免相邻两个类颜色接近——类别一多10 个颜色必然不够分取模复用是可以接受的妥协但相邻索引一定要不同色。线宽按图片尺寸动态定公式大概是max(1, round(min(img_w, img_h) / 400))。1920 宽的图算出来 4~5 像素640 宽的图算出来 1~2 像素视觉上粗细一致。写死线宽 2 的话大图上细得像头发丝小图上粗得盖住目标。文字标签最容易被忽略的是遮挡。默认往框上方写框贴着图顶的时候文字就飞到画面外了。我的处理是文字基线 y 坐标取max(y1 - 6, 14)如果框顶部空间不够就改写到框内部顶端。再给文字加一层深色实心底衬用cv2.rectangle填一个矩形再写字比加轮廓线可读性好得多尤其是浅色背景图。3.3 一个可直接复用的最小可视化函数把上面这些揉成一个函数日常验标注直接调它import cv2 def load_classes(yaml_pathNone, txt_pathNone): if txt_path: with open(txt_path, r, encodingutf-8) as f: return [line.strip() for line in f if line.strip()] if yaml_path: import yaml with open(yaml_path, r, encodingutf-8) as f: cfg yaml.safe_load(f) names cfg.get(names, []) if isinstance(names, dict): # 有些 yaml 写成 {0: a, 1: b} return [names[k] for k in sorted(names)] return list(names) return [] def draw_one(img_path, lbl_path, classes, save_path, min_vis2): img cv2.imread(str(img_path)) if img is None: print(f[skip] 读不到图: {img_path}) return False H, W img.shape[:2] lw max(1, round(min(W, H) / 400)) fs max(0.4, min(W, H) / 1200) n 0 if lbl_path.exists(): with open(lbl_path, r, encodingutf-8) as f: for lineno, line in enumerate(f, 1): parts line.strip().split() if len(parts) 5: if line.strip(): print(f[warn] {lbl_path.name} 第{lineno}行字段不足: {line.strip()}) continue try: c int(float(parts[0])) xc, yc, w, h map(float, parts[1:5]) except ValueError: print(f[warn] {lbl_path.name} 第{lineno}行解析失败) continue x1, y1, x2, y2 yolo_to_xyxy(xc, yc, w, h, W, H) if x2 - x1 min_vis or y2 - y1 min_vis: pass # 仍然画但心里有数这是极小目标 x1, y1 max(0, x1), max(0, y1) x2, y2 min(W - 1, x2), min(H - 1, y2) col color_of(c) cv2.rectangle(img, (x1, y1), (x2, y2), col, lw) name classes[c] if 0 c len(classes) else fid_{c} text f{name} {x2 - x1}x{y2 - y1} (tw, th), base cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, fs, lw) ty y1 - 6 if y1 - th - 6 0 else y1 th 6 cv2.rectangle(img, (x1, ty - th - 3), (x1 tw 4, ty base), col, -1) cv2.putText(img, text, (x1 2, ty), cv2.FONT_HERSHEY_SIMPLEX, fs, (255, 255, 255), lw, cv2.LINE_AA) n 1 save_path.parent.mkdir(parentsTrue, exist_okTrue) cv2.imwrite(str(save_path), img) return n这个函数里我刻意做了两件事。一是所有异常行都跳过但不静默打印文件名和行号方便回头批量修二是空标签文件也照样输出原图因为空标签意味着这张图是负样本纯背景负样本在检测任务里是必要的不能因为没框就跳过——恰恰相反你要通过看图确认它确实没有目标而不是标注员偷懒忘了标。4. 从单张到整批搭一条可视化流水线4.1 单张快速验框先确认坐标逻辑是对的拿到新数据集第一件事永远是挑一张图、画一遍、看一眼。别急着批量跑坐标系搞错方向的话批量跑完几千张再发现问题返工成本高。挑图有个技巧不要挑最典型的那张挑目标最多、目标最小、有大目标贴边的那张。目标多的能暴露遮挡和文字重叠问题目标小的能暴露取整误差贴边的能暴露边界夹取。一张图三种情况都覆盖基本等于把绘制逻辑跑了一遍全路径测试。画的时候顺手在控制台打印几行信息图片实际宽高、标签行数、每个框的像素尺寸。这三个数和图对不上的时候问题基本一眼就定位了。img_path Path(dataset/images/val/sample_001.jpg) lbl_path Path(dataset/labels/val/sample_001.txt) classes load_classes(txt_pathdataset/classes.txt) print(classes , classes) n draw_one(img_path, lbl_path, classes, Path(out/sample_001_vis.jpg)) print(drawn boxes:, n)drawn boxes这个数字很关键。标签文件里有 7 行打印出来是 6说明有一行被跳过了回去看 warn 日志打印出来是 7 但图上只数出 5 个说明有两个框完全重合或者跑到了画面外。4.2 批量生成与目录规划批量跑的逻辑不复杂难点在图片和标签的配对。YOLO 数据集的标准结构是图片和标签分开放目录名一一对应dataset/ ├── images/ │ ├── train/ │ │ ├── a.jpg │ │ └── b.png │ └── val/ └── labels/ ├── train/ │ ├── a.txt │ └── b.txt └── val/配对规则是把图片路径里的images段替换成labels后缀换成.txt。这个规则看起来土但比任何花哨的匹配都可靠因为它是官方约定的结构。实现上我用Path.relative_to保结构避免同名图片在不同子目录里撞车def pair_label(img_path, img_root, lbl_root): rel img_path.relative_to(img_root) return lbl_root / rel.with_suffix(.txt)from pathlib import Path def batch_visualize(img_dir, lbl_dir, classes, out_dir, exts(.jpg, .jpeg, .png, .bmp)): img_dir, lbl_dir, out_dir Path(img_dir), Path(lbl_dir), Path(out_dir) imgs [p for p in img_dir.rglob(*) if p.suffix.lower() in exts] print(ffound {len(imgs)} images) miss, empty, total 0, 0, 0 for p in imgs: lbl pair_label(p, img_dir, lbl_dir) if not lbl.exists(): miss 1 print(f[no-label] {p.name}) elif lbl.stat().st_size 0: empty 1 rel p.relative_to(img_dir) draw_one(p, lbl, classes, out_dir / rel.with_suffix(.jpg)) total 1 print(fdone{total} missing_label{miss} empty_label{empty})这里输出的三个统计量其实已经是一次小规模的数据体检了。missing_label大于 0说明有一批图没标注empty_label占比过高要想想是不是某批数据批量漏标两个都为 0 但图里明显有目标没框那就是标注质量问题得进人工复核。目录规划上我习惯把输出镜像原目录结构out/train/xxx.jpg这样返回去和原图对照时路径完全对应不用记忆映射关系。文件名后缀统一成.jpg也行但要注意原图是 PNG 带透明通道的时候cv2.IMREAD_COLOR读进来会丢掉 alpha背景变黑框在图上看不清这种情况老实实保持原后缀。4.3 类别名对不上时怎么办data.yaml里的names写法有好几种变体实际项目里我见过至少四种列表、字典、带nc字段的、以及干脆没有 yaml 只有classes.txt。加载逻辑必须做兼容否则一个KeyError就把整个批处理卡住。names cfg.get(names) if names is None and nc in cfg: names [fclass_{i} for i in range(int(cfg[nc]))]类别名缺失时退化成class_0、class_1这样的占位名比直接崩掉强得多。还有一种情况是标签里的类别索引超出了类别表长度比如只有 5 个类标签里出现了 7。这通常是两批数据集合并时索引没统一绘制时用id_7兜底显示比抛异常好至少你能从图上看出哪些样本是脏数据。4.4 分割标签和关键点标签的画法不一样YOLO 分割格式一行是类别 一串 x y 坐标对全部归一化点数不固定0 0.312 0.441 0.338 0.462 0.371 0.498 0.355 0.521画法是把坐标两两一组还原成像素组装成numpy数组然后cv2.polylines或cv2.fillPoly。关键是点数必须是偶数奇数说明有一行被截断了直接跳过pts list(map(float, parts[1:])) if len(pts) % 2 ! 0: continue arr np.array(pts, dtypenp.float32).reshape(-1, 2) arr[:, 0] * W arr[:, 1] * H poly arr.astype(np.int32).reshape((-1, 1, 2)) cv2.polylines(img, [poly], isClosedTrue, colorcol, thicknesslw)想做半透明填充看覆盖效果就复制一份图用fillPoly填色再cv2.addWeighted叠回去透明度取 0.3 左右既能看清轮廓又不遮住底下的目标细节。关键点格式更复杂一些一行通常是类别 框的四个数 N 组x, y, 可见性0 0.52 0.61 0.18 0.42 0.501 0.503 2 0.512 0.548 2 0.498 0.601 1那个可见性标志0 未标注、1 被遮挡、2 清晰可见一定要区别对待值为 0 的点不要画否则图上一堆点堆在原点位置看着像标签全错了。我一般用不同形状区分——清晰点画实心圆遮挡点画空心圆颜色统一跟着类别走。5. 常见问题速查与排查实录5.1 框全挤在左上角或者整体缩小成一团这是最高频的现象几乎每个新手都会遇到一次。原因基本锁定在两类坐标没乘回图片尺寸或者标签本身是像素坐标却被当成归一化处理。前者的表现是所有框集中在一个很小的区域因为 0.5 这样的归一化值没乘宽高直接当作像素用了1280 宽的图上坐标 0.5 就落在左上角。后者的表现完全相反框会大得离谱、超出画面因为像素值 640 被当成了 0.64乘回宽度后是 800 多像素位置全跑偏。排查方法很直接打印一行标签的原始数值看一眼四个数字是不是都在 0 到 1 之间。如果出现了 300、680 这种数那就是像素坐标格式有些转换脚本会把 VOC 转成这种需要在换算函数里改成直接取左上右下而不是中心宽高归一化。5.2 框整体偏移或者转了 90 度偏移通常是 EXIF 惹的祸。手机拍的图经常带旋转标记标注工具按“显示方向”标了框但你用 OpenCV 读进来是“存储方向”两者差 90 度或 180 度。判断方法看图片实际宽高和你的预期是否一致一张竖拍的图读出来是 1280×720 还是 720×1280一眼就能看出来。稳妥的解法是固定用同一个库读图和取尺寸并且在读图前统一把 EXIF 清掉、物理旋转过去。批量预处理时做一次这个动作后续所有环节都用同一份规整过的图能省掉大量玄学问题。from PIL import Image, ImageOps def read_image_safe(path): with Image.open(path) as im: im ImageOps.exif_transpose(im).convert(RGB) return cv2.cvtColor(np.array(im), cv2.COLOR_RGB2BGR)还有一种偏移是letterbox 导致的比例错位。训练时图片会被等比缩放后填充到 640×640两侧有灰边而你可视化时直接按原始尺寸画框看着是对的但和模型看到的输入对不上就会误判“模型检测偏了”。遇到这种比较必须把 letterbox 的缩放比和填充偏移量带进换算里。5.3 类别名显示成数字、显示成乱码或者文字重叠显示数字说明类别表没加载成功或者索引超范围走了兜底分支。先确认classes列表长度和标签里的最大索引是否匹配再看 yaml 的names是不是字典格式被当列表用了。显示成问号方块是字体问题。OpenCV 的putText只支持 ASCII中文标签必然是问号。要标中文就换 Pillow 渲染指定一个支持中文的字体文件路径注意字号要按图片尺寸换算别在小图上用 30 号字糊成一片。文字重叠没有完美解只有几个实用的缓解手段一是文字只写类别名和序号尺寸信息只在控制台输出二是同一位置检测到已有文字占位就往上平移一行三是类别多的时候干脆关闭文字用颜色图例代替另出一张图例说明图。我一般选第三种尤其是做给非技术同事看的材料时干净比信息全更重要。5.4 一张表覆盖大部分排查场景现象最可能的原因处理方式框挤在左上角一小片归一化坐标当像素用了换算时乘回图片宽高框巨大超出画面像素坐标当归一化用了改换算分支或重新导出标签框整体偏半格用了 int 截断而非四舍五入改用 round 并夹取边界框转了 90 度EXIF 方向未处理读图时统一 exif_transpose框沿一个方向被压扁图片被非等比 resize用 letterbox或按原比例画图上框数量少于标签行数有解析失败的行看 warn 日志定位行号图上框数量多于标签行数标签重复行或完全重合框去重后再画或加偏移错开类别名全是问号字体不支持该文字换 Pillow 中文字体所有图框都对但训练效果差漏标、错标、重复标抽查 统计框数分布部分图黑底看不清框PNG 透明通道被丢弃保持原后缀读 PNG 时注意 alpha5.5 几个不写在文档里但很省时间的习惯给输出图加文件名水印。批量跑几千张返回去找原图时全靠文件名把文件名直接写在图的角落小字号、半透明后续沟通时截图就能定位省掉大量“你说的哪张图”的来回。控制台一定要打印统计汇总而不只是逐条日志。总数、缺标签数、空标签数、解析失败行数、每类目标数五个数字一屏看完比翻 200 行滚动日志有效得多。采样抽检不要全量看。几千张图全看完不现实。按类别分层抽样每类抽 20 张再额外把所有“框数特别多”和“框数特别少”的极端样本抽出来看。异常样本往往集中在长尾上均匀随机抽反而容易漏。保存一份原始标签的副本。清理重复框、修正越界坐标这类操作会改标签改之前一定先复制一份。我吃过一次亏批量去重脚本跑完发现阈值设错了把正常的小目标框也当成重复框删了只能从备份恢复。注意任何会修改标签文件的批量脚本先在 100 张的临时目录上跑一遍把它输出的图和原图并排看确认没误伤再上全量。6. 让可视化产出更多价值6.1 顺手把类别分布和框尺寸分布统计出来画框的同时那几行数字其实已经被解析出来了多存一份统计结果几乎零成本。类别分布能立刻暴露出长尾问题某一类占了 60% 的框另一类只有 30 个后者基本训不好要么补充数据要么在采样和损失加权上做文章。框尺寸分布更值得看。把所有框的像素宽度排序取 5%、50%、95% 分位数你会很直观地知道这个数据集“难在哪”。如果 5% 分位数只有 6 个像素说明大量目标小于模型的检测下限得考虑切图推理、提高输入分辨率或者干脆放弃这些样本。如果 95% 分位数接近整图宽度的 80%那基本是标注错误一个目标不可能占满画面还叫“目标”。长宽比分布同样有用。同一个类别里长宽比从 0.2 横跨到 5.0说明标注粒度不一致有人在标整辆车有人在标车的局部这种数据会让回归头学得很混乱。6.2 并排对比原图、标注、模型预测三栏合一看只画标注只能看标注质量把模型预测叠上去才是完整的问题定位。我习惯出一张三联图左栏原图、中栏标注框、右栏预测框。看一眼就能区分是数据问题还是模型问题标注框错位、预测框也跟着错位那是数据的问题标注框是对的、预测框偏了那是模型或后处理的问题标注框漏标了目标、预测框标出来了说明模型在这个样本上比标注强属于“被噪声拖累”的情况预测框一大堆重叠召回看着高但精度差回头调 NMS 阈值。用np.hstack横向拼三张同尺寸的图输出成一张对比图做周报和复盘材料特别省事。注意拼接前要把三张图统一到同一尺寸不然hstack会直接报错。6.3 输出带框视频做动态预览静态图看久了会疲劳做成视频能抓住一些时间维度上的问题连续帧里同一目标框时有时无标注漏帧或者框在帧间跳动明显人工标注手抖。做法和图片完全一样只是把imread换成VideoCapture逐帧读标签按帧号匹配文件名frame_000123.jpg对应frame_000123.txt再VideoWriter写出去。编码器选mp4v兼容性最好但注意帧率要和原视频一致否则预览看起来快进或慢放容易误判“框漂移”。曾经有同事因为帧率设错看着框一跳一跳的怀疑标注质量有问题实际是预览视频本身 30 帧写成了 15 帧。6.4 可视化脚本本身也该进版本管理最后提一件容易被忽略的事可视化脚本不要随手写在临时文件里用完就丢。它其实是数据质检流程的一部分长期会用而且会被不同的人复用。我现在的习惯是把load_classes、yolo_to_xyxy、draw_one、batch_visualize这几个函数放在一个固定的小模块里参数用命令行传进去参数默认值写死在代码里谁拿到都能直接跑。这样换一批数据时只需要改三个路径参数几分钟就能出一轮体检结果。我在实际使用中体会最深的一点是可视化真正省下的时间不在画图这一步而在它把问题暴露在了训练之前。以前是训完看指标不对再回头一行行查标签一轮排查两三天现在跑一次批量可视化十几分钟就能定位到具体的类别和样本返工范围小得多。踩过几次“训练白跑”的坑之后我现在的固定流程是拿到任何一批新标注先做可视化抽样确认无误再进训练。这个习惯本身比任何脚本都值钱。

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

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

免费获取报价 →
↑