采集高质量、有代表性的数据
从真实部署环境采集图像,在每个文件名中保留采集元数据,根据规格跟踪覆盖情况,并对视频抽帧,避免数据集过度膨胀。
大多数数据集问题都源于采集环节。团队用一个摄像头在一周内采集干净样本,验证结果看起来很不错,但模型到了第二个地点就表现不佳。采集质量决定了上限:无论标注质量、数据增强还是超参数设置得多好,都无法让模型学会数据集中从未出现过的场景。
实践操作
多样性胜过数量#
比较两个针对相同部署场景的数据集:
| 数据集 | 验证结果 | 在生产环境中的表现 |
|---|---|---|
| 10,000 帧几乎相同的画面,只来自一个摄像头,且仅在中午采集 | 高:验证帧看起来和训练帧相似 | 换一个摄像头或到了黄昏时,表现就会下降 |
| 1,500 帧,分布在规格中的 12 种场景里 | 表现相对普通 | 表现稳定,因为生产环境与数据相似 |
第一个数据集的验证分数很高,只是因为验证集只评估了它覆盖的那种情况。在生产环境中,模型的表现取决于它遇到的覆盖最不足的场景。采集时应首先优化规格覆盖情况,其次才是数据量。
按合适的顺序选择数据源#
数据集的大部分内容应来自真实的生产环境。数据采集与标注指南详细介绍了每种数据源:
| 来源 | 适用情况 | 不适用情况 |
|---|---|---|
| 生产摄像头,实时采集 | 最接近部署环境 | 速度慢;可能需要签订数据协议 |
| 生产摄像头,历史录像 | 能快速获得大量多样化数据 | 旧录像可能与当前配置不匹配 |
| 公开数据集(COCO、Open Images、VOC) | 为常见类别构建初始数据 | 与实际场景存在域偏移 |
| 摆拍采集 | 按需采集特定边缘情况 | 场景过于干净;容易过度依赖 |
| 合成或生成图像 | 采集罕见的危险和故障情况,无需等待它们自然发生 | 模型可能会学到生成器的风格 |
| 网络抓取 | 低成本获取多样化数据 | 质量不一致,且存在许可风险 |
使用摆拍、公开和合成图像填补规格中明确指出的特定缺口,并跟踪它们在数据集中的占比,以便衡量这些图像是否有帮助。在 Ultralytics Platform 中,生成相似图像会为数据集中的现有图像生成变体;这些图像未经标注,可直接用于标注。
将元数据写入文件名#
每张图像至少需要包含摄像头 ID、采集日期,以及帧号或片段编号。没有这些信息,你就无法在之后按组划分数据、追溯回归问题的来源,或补采代表性不足场景的数据。最可靠的载体是文件名:cam2_2026-05-01_000124.jpg 能在所有工具、导出和上传过程中保留下来。Ultralytics Platform 数据集搜索支持匹配文件名和自定义元数据,因此上传后仍可搜索这些字段。
此脚本会构建本课程其余部分所用的练习数据集。它会复制 COCO128 中已标注的图像,并将其重命名,模拟四个摄像头在四天内拍摄的情况,其中 cam1 的采集量会偏多,就像通常更方便使用的摄像头一样:
import shutil
from pathlib import Path
from ultralytics.data.utils import check_det_dataset, img2label_paths
data = check_det_dataset("coco128.yaml") # 128 COCO images stand in for your camera captures
out = Path("datasets/practice")
(out / "images").mkdir(parents=True, exist_ok=True)
(out / "labels").mkdir(parents=True, exist_ok=True)
for i, image in enumerate(sorted(Path(data["train"]).glob("*.jpg"))):
camera = ("cam1", "cam1", "cam2", "cam3", "cam4")[i % 5] # cam1 over-collected, like a real site
name = f"{camera}_2026-05-0{i // 32 + 1}_{i:06d}" # camera, capture day, frame number
shutil.copy(image, out / "images" / f"{name}.jpg")
label = Path(img2label_paths([str(image)])[0])
if label.exists():
shutil.copy(label, out / "labels" / f"{name}.txt")
print(len(list((out / "images").glob("*.jpg"))), "images written to", out)128 images written to datasets/practice
上传到 Ultralytics Platform 后,这些文件名会保留下来。上传对话框也支持视频和完整数据集,设置中还包含用于保护隐私的面部模糊处理选项。
根据规格跟踪覆盖情况#
文件名中包含元数据后,覆盖情况就可以通过计数得出。根据规格设置配额,并让脚本指出接下来需要采集什么:
from collections import Counter
from pathlib import Path
QUOTA = {"cam1": 30, "cam2": 30, "cam3": 30, "cam4": 30} # images per camera, from your spec
images = sorted(Path("datasets/practice/images").glob("*.jpg"))
cameras = Counter(p.stem.split("_")[0] for p in images)
days = Counter(p.stem.split("_")[1] for p in images)
for camera, quota in QUOTA.items():
gap = quota - cameras[camera]
print(f"{camera}: {cameras[camera]:3d} / {quota} {'ok' if gap <= 0 else f'collect {gap} more'}")
print("days:", dict(sorted(days.items())))cam1: 52 / 30 ok
cam2: 26 / 30 collect 4 more
cam3: 25 / 30 collect 5 more
cam4: 25 / 30 collect 5 more
days: {'2026-05-01': 32, '2026-05-02': 32, '2026-05-03': 32, '2026-05-04': 32}判断规则:停止从任何超出配额的数据源采集,把精力投入缺口。这里,cam1 已占图像总数的 40%。再采集一周这类视频,只会增加数据量,不会扩大覆盖范围。
对视频抽帧,避免数据集过度膨胀#
视频是成本最低的数据源,也是最容易被滥用的数据源。以每秒 30 帧计算,一分钟就会产生 1,800 帧几乎相同的画面;重复标注同一时刻既浪费资金,也会抬高指标。只有在场景与上一张保留帧相比发生变化时,才保留当前帧:
from pathlib import Path
import cv2
from ultralytics.utils.downloads import safe_download
video = "solutions_ci_demo.mp4" # a short public demo clip; swap in your own footage
safe_download(f"https://github.com/ultralytics/assets/releases/download/v0.0.0/{video}")
Path("frames").mkdir(exist_ok=True)
cap = cv2.VideoCapture(video)
fps = cap.get(cv2.CAP_PROP_FPS)
read = kept = 0
last = None
while True:
ok, frame = cap.read()
if not ok:
break
thumb = cv2.cvtColor(cv2.resize(frame, (64, 64)), cv2.COLOR_BGR2GRAY).astype(float)
if last is None or abs(thumb - last).mean() > 10: # keep a frame only when the scene has changed
cv2.imwrite(f"frames/clip01_{read:06d}.jpg", frame) # the clip ID stays in the filename
kept, last = kept + 1, thumb
read += 1
print(f"{read} frames at {fps:.0f} FPS -> {kept} kept")62 frames at 30 FPS -> 6 kept这段两秒的视频包含 62 帧,但只有 6 个视觉上明显不同的时刻。脚本会比较尺寸很小的灰度缩略图,因此处理数小时的视频素材也很快。对于静态摄像头,提高 10 的阈值以减少保留帧;对于快速移动的场景,则降低阈值。还要将采集预算分散到多个片段:从 50 个片段中各取 20 帧,胜过从一个片段中取 1,000 帧。
相隔一秒的两帧共享光照和背景,而且通常包含相同的物体。如果它们同时出现在训练集和验证集中,验证时评估的就是模型实际上已经见过的图像。请在每个文件名中保留视频片段 ID,这样正确划分数据集就能将同一视频片段放在一起。
避免数据采集中的常见陷阱#
| 陷阱 | 症状 | 解决方法 |
|---|---|---|
| 只在一周内采集 | 验证集 mAP 很高,但在其他季节或班次中漏检 | 将采集分散到几周或几个月内 |
| 只用一台摄像头 | 换到第二台摄像头时就会失效 | 从你部署的每种摄像头采集数据 |
| 只有摆拍数据 | 在真实杂乱场景中失效 | 将摆拍图像控制在少数,并做好跟踪 |
| 重复帧 | 指标虚高,标注工作白费 | 按场景变化抽样;Ultralytics Platform 上传时也会跳过完全相同的副本 |
| 只有一名操作员或一个班次 | 出现未预料到的外观和类别不平衡 | 跨班次、地点和人员进行抽样 |
| 缺少罕见案例 | 漏掉最重要的安全事件 | 定向采集、摆拍或补充合成数据 |
偏见与同意#
偏见是数据采集问题,而不是建模问题:如果数据中从未包含某个群体,任何超参数都无法弥补。数据采集指南列出了四种方法:使用多样化的数据来源、平衡不同群体的代表性、定期重新审查覆盖情况,以及通过过采样代表性不足的群体等方式进行缓解。
如果图像中包含人脸、车牌或其他个人数据,你需要在开始采集前取得同意并制定数据保留政策。数据隐私词汇表条目是很好的起点,而隐私、区域与合规介绍了如何通过 dataRegion 将数据保留在正确的司法管辖区内。
显示解答
对于练习数据集,覆盖情况报告会给出明确的计划:
| 来源 | 数量 / 配额 | 操作 |
|---|---|---|
cam1 | 52 / 30 | 停止采集;当前数量已占全部图像的 40% |
cam2 | 26 / 30 | 再采集 4 张 |
cam3 | 25 / 30 | 再采集 5 张 |
cam4 | 25 / 30 | 再采集 5 张 |
每台摄像头都覆盖了全部四天,因此缺口按摄像头计算,而不是按天计算。将新增的 14 张图像分散到至少两个新日期,这样它们就能增加新的条件,而不是重复同一天下午的场景。对于无法补足缺口的摄像头(例如始终只能拍到夜间场景的摄像头),请为缺失条件添加一行规格,并记录缺口由摆拍图像还是生成图像填补,以便日后衡量其影响。