写在前面
这篇文章写的是我最近在搭的一个合成数据处理工具。它目前还处在内部实验阶段,没有开源——代码、配置、样例数据和文档都还没整理到适合公开的状态。这篇文章不会贴仓库链接,只聊工程搭建的思路和踩坑过程。

一、问题:生成视频不等于训练数据
最近在做的工作,简单说是想用视频生成模型产一些自动驾驶/无人清扫车场景里的长尾样本。比如小动物穿行、行人突然靠近、路面上出现障碍物——这些场景在真实数据里很少见,但对检测模型来说很重要。
最开始的想法很直接:用视频生成模型生成这些场景的视频,然后从视频里抽帧,拿去训练 YOLO。
但真的开始做之后,很快发现一个问题——视频本身不是训练数据。一段视频生成出来,只能说明模型"会画"。但目标检测训练需要的是另一种东西:每张图片里有哪些类别,每个对象的位置框在哪里,哪些图进训练集、哪些进验证集。没有这些结构化标注,视频只是素材,不是数据集。
具体来说,生成完视频之后还缺这些步骤:
- 从视频中稳定抽帧,同时避免抽出大量重复画面
- 过滤掉模糊、过曝、没有目标的无效帧
- 给每张图标注类别和 bbox
- 统一类别命名,别让同一个东西在不同帧里叫不同的名字
- 人工检查和修正 VLM 给出的错误标注
- 导出 COCO 或 YOLO 能直接消费的数据集格式
- 最后才能进入训练流程
每一步单独看都不难,但串起来之后工程量就上来了。
二、目标:先跑通最小闭环
今天的目标是先把从视频到数据集的整条链路跑通。具体来说:
AI 生成视频
↓
抽帧(OpenCV)
↓
本地 VLM 自动标注(Qwen3-VL)
↓
每帧缓存(.vlm.json)
↓
人工审核 / 修框
↓
COCO 数据集导出
↓
准备 YOLO 训练
每一步拆开来看都很朴素,但放在一起就是一个完整的数据生产线。有一件事我觉得挺重要:最小闭环比单点效果更重要。单帧标注效果好不代表管线能用,一个能用的管线至少要做到——能批量跑、能中途停下来、能恢复、能人工修、能导出标准格式。

从图上看,这条链路不是"生成视频之后直接训练",而是中间多了一层数据生产过程:抽帧、预标注、人工审核、格式导出,每一步都要留下能复查的中间产物。
三、为什么选本地 VLM
标注这一步,我没有用云端 API,而是在本地用 Ollama 部署了 Qwen3-VL 8b。
原因有几个。最主要的是成本,seedance价格太高。
至于为什么是Ollama,原因是部署成本,Ollama 一条命令就能把模型拉下来跑起来,比折腾完整的 HuggingFace 或 conda 环境轻得多。Qwen3-VL 通过 Ollama 的本地 HTTP API 就能调用,后端只需要发一个 localhost 请求。

对于企业来说,还有一个理由就是数据避免出网,本地运行更安全。
还有一个考虑:对原型阶段来说,先把管线跑通比追求最强模型更重要。后续如果想换模型——比如换更大的 Qwen3-VL 版本或者其他 VLM——只要新模型能返回类似结构的 JSON 检测结果,管线就不用大改。模型成了一个可替换的组件,后续迭代会省很多事。
四、管线设计:五个模块
管线拆成五个模块来写。每个模块单独看都不复杂,但放在一起要考虑的边界条件就多了。
1. 抽帧:从视频到图片
这一层用 OpenCV 做,逻辑比较直白——读视频,按目标 FPS 跳帧,写 JPEG。但不直白的是参数选择。
FPS 设太高会产生大量重复帧,徒增后续 VLM 标注的时间和算力开销;设太低又可能刚好跳过目标出现的关键瞬间。目前我的做法是设一个偏保守的值(比如 1-2 FPS),后续如果发现某些场景漏掉了关键帧,再针对性地调整。
另外抽帧产出的图片需要按 batch/task 组织好目录结构。因为后面 VLM 标注的缓存文件要和图片放在一起,目录结构如果一开始没想清楚,后面会很乱。
2. VLM 标注
VLM 这一层的定位很明确:它只负责出候选标注。最终真值要等人工审完才算。
输入是一张抽帧图片加上一个目标检测 prompt,要求模型返回 JSON。输出是一个 objects 数组,每个对象包含 class、bbox(normalized xyxy,0~1 范围)和 confidence:
{
"objects": [
{
"class": "dog",
"bbox": [0.12, 0.35, 0.28, 0.62],
"confidence": 0.82
}
],
"summary": "a dog appears on the road"
}
坐标归一化
这里的话我为什么要输入归一化坐标呢,因为这里踩过一个坑。VLM输出的(x,y)坐标普遍往右下偏移,

原因可能是内部会有缩放,所以这里采用输出归一化坐标的方式规避:(这两图对比框少了很多,只能说vlm还是有点推理不稳定的问题)

而且VLM 也会犯错。实际应用qwen3 vl:8b,就算是输出归一化坐标,有时候打的框位置还是会偏。
这些问题不是"调调 prompt 就能解决"的——VLM 作为预标注工具有它的本质局限。所以 VLM 的输出只是候选,不能直接当训练集用。还得人工审核。
3. 缓存:每张图一个 .vlm.json
这个模块看起来不起眼,但它决定了标注这件事能不能真正工程化。
VLM 推理不是瞬时操作。一张图跑 Qwen3-VL,经过我的测试,8b在4090上体感时间大概2-3分钟,4b比8b快很多,如果是几十上百张图批量跑就更久了。中间前端可能刷新、服务可能重启、模型可能卡住——如果标注结果只存在内存里,出任何问题都得从头跑。
所以每张图片旁边保存一个对应的 .vlm.json 缓存文件,里面记录了模型名、时间戳、原始响应、解析后的 objects、人工审核状态。有缓存的帧可以直接跳过,Export 页面可以直接读缓存,不需要重新调 VLM。
这一个小文件把标注过程从"一次性脚本"变成了"可恢复的工程流程"。这个设计在后面批量标注时发挥了很大的作用。

4. 人工审核:VLM 的候选框需要人确认
如果直接把 VLM 输出转成 COCO 格式送进训练,那些错框、漏框、错类都会被 YOLO 当成真值来学习。这会让整个合成数据管线变成"污染训练集管线"。
所以我在导出之前加了一层人工审核。这层需要支持的操作不少:
- 自动聚合当前 batch 里所有 VLM 检测出的类别,按出现频率排序
- 选择哪些类别进入最终数据集(有些 VLM 检出的东西可能不关心)
- 逐帧查看 bbox 叠加在图片上的效果
- 拖动已有框、缩放四角、删除错误框
- 手动画新框
- 修改框对应的类别
- 把明显有问题的帧标记为 rejected
- 所有修改自动保存回
.vlm.json,翻页即存
简单说就是:VLM 负责降低人工成本——把人从"从零画框"变成"检查和修正候选框"——但它不能消灭人工审核这个环节。
5. COCO 导出:从标注结果到训练数据
导出这一步需要把缓存里的标注结果和人工修改转成 COCO detection 格式。
我没有把 train/val 拆成两套复杂的目录,而是用一个扁平结构:所有图片放在一个 images/ 目录里,文件名用导出时间戳做前缀保证唯一性,annotations.json 里记录每张图的 split 字段(train 或 val)。这样数据集是自包含的,后续打包下载、转 YOLO 格式、送给训练脚本都更方便。
COCO 导出时有几个容易出错的地方需要注意:category id 要稳定、image id 要唯一、annotation id 全局递增、bbox 要从 VLM 的 normalized xyxy 转成 COCO 的像素 xywh、rejected 帧不能导出、图片和标注打包后要能独立使用。这些细节如果没处理好,后面训练脚本报错时会很难排查。

五、今天的软件工程量
今天实际完成的东西列出来大概是这样:
- 本地部署 Ollama + Qwen3-VL
- 后端封装 VLM Runner,接入 Ollama HTTP API
- Dataset 页面:选 batch、预览抽帧、调 prompt、单帧测试 VLM(打磨这个页面感觉是最费事的,主要是想做的成熟一点)
- 批量标注:SSE 推送进度,后台异步逐帧标注
- 标注结果落盘缓存(.vlm.json)
- BBox 可视化(Canvas 只读模式 + SVG 交互编辑模式)
- Export 页面:类别聚合面板、Review Grid、逐帧全尺寸审核
- 人工修框:拖动、缩放角、删除、手动画新框
- 人工审核状态:pending / reviewed / approved / rejected
- COCO 格式导出 + train/val split
- 数据集 tar.gz 打包下载
软件工程量其实比模型部署本身大得多。真正花时间的地方,是围绕一次 VLM 调用补齐一整套工程能力:批量任务管理、缓存落盘、前端可视化、交互式编辑、状态保存、数据导出。这些东西单拿出来都不难,但少一个管线就断一个环节。
界面架构:Persistent Shell
前端这一块花了不少时间打磨,中间还按一个设计模式重构过一次。这个模式叫 persistent shell,想法很简单:应用的骨架(顶部导航、侧边栏、主内容区)始终挂在那里,切换页面的时候只换内容区,骨架不卸载。
这个工具一共有四个页面——Chat、Batch、Dataset、Export。如果每次切页都把整个 React 组件树拆了重建,有几个明显的问题。侧边栏状态会丢。Dataset 和 Export 共享同一个 batch 上下文,切来切去要反复重新加载数据。BBox 标注组件在两个页面里都要用(Dataset 里只读预览,Export 里交互编辑),拆成两套实现会多出很多重复代码。
重构后的结构是这样的:App 作为 shell,TopNav 和 Sidebar 始终挂载。每个页面通过一个 onSetSidebar 回调往侧边栏注入自己的面板内容。Dataset 页面注入帧浏览器,Export 页面注入审核列表,Chat 页面注入对话列表。切页的时候,侧边栏的框架不动,只换里面那块内容。

BBox 标注组件也收拢成一个共享组件,通过 mode 参数切换两种形态:view 模式走 Canvas 渲染,只读展示检测框;edit 模式走 SVG 渲染,支持点击选中、拖动位移、四角缩放、拉框新建和键盘删除。
这次重构省掉的代码量不算夸张,但逻辑收敛之后,加新功能和修 bug 都顺了很多。Dataset 里调好的 prompt 切到 Export 就能直接看结果,不用再想"这两个页面的框到底是不是同一套逻辑"。
六、目前效果和局限
目前最小闭环已经跑通了:AI 生成视频 → 抽帧 → 本地 Qwen3-VL 自动标注 → 人工修框 → 导出 COCO 。
但局限也很明显:
- VLM 标注质量还没有系统评估过,目前只能靠人眼逐帧看
- 类别命名还不稳定,同一个东西在不同帧里可能被 VLM 叫成不同的名字
- 生成视频和真实场景之间存在 domain gap,合成数据训练的模型在真实场景下表现如何还是未知数
- 没有做过对比实验:用合成数据训练出来的 YOLO,在真实测试集上到底是变好还是变差
制作的数据集只能先发给同事,让他看看效果了。
七、小结
目前这个工具还不能说是成熟产品。VLM 标注仍然会错,bbox 还需要人工修,类别也需要继续收敛,合成数据对真实训练到底有没有提升也是只能给同事验证。但至少这个闭环已经跑通了:
AI 生成视频 → 抽帧 → 本地 Qwen3-VL 自动标注 → 人工修框 → 导出 COCO
这一步跑通之后,后面才有资格讨论"合成数据到底能不能提升真实检测模型"。后面的路还很长,但至少已经有了一个可以继续往前走的起点。