Ultralytics YOLO27: 加入候补名单。
Get Started
构建高性能 YOLO 数据集·采集·课程 3/10
课程初级

采集高质量、有代表性的数据

从真实部署环境采集图像,在每个文件名中保留采集元数据,根据规格跟踪覆盖情况,并对视频抽帧,避免数据集过度膨胀。

大多数数据集问题都源于采集环节。团队用一个摄像头在一周内采集干净样本,验证结果看起来很不错,但模型到了第二个地点就表现不佳。采集质量决定了上限:无论标注质量、数据增强还是超参数设置得多好,都无法让模型学会数据集中从未出现过的场景。

学习成果

构建一个文件名包含摄像头、日期和帧信息的数据集,根据规格配额报告覆盖情况,并从视频中抽取彼此不同的帧。

快速通道
如果你已经熟悉相关内容,以下是简要版。
  1. 使用模型实际运行时所用的生产摄像头进行采集。

  2. 将每个文件命名为 camera_day_frame,以便后续步骤对文件分组并进行审计。

  3. 按照规格,统计每个摄像头、日期和场景的图像数量,并采集缺少的部分。

  4. 对视频抽帧,只保留场景发生变化的帧。

  5. 仅使用公开、摆拍或合成图像来填补已明确指出的缺口。

实践操作

多样性胜过数量#

比较两个针对相同部署场景的数据集:

数据集验证结果在生产环境中的表现
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 新建数据集对话框:拖入 YOLO、COCO 或 NDJSON 格式的图像、视频或数据集,然后设置数据集名称、URL、描述、任务类型、许可、面部模糊处理和可见性

上传到 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 将数据保留在正确的司法管辖区内。

试试看

运行 make_practice_set.py,然后编辑 coverage.py 中的配额,使其符合你自己的规格(或保留练习配额),再运行一次。对于你的实际项目,将 50 张近期图像重命名为 camera_day_frame 模式,并对这些图像运行相同的覆盖情况统计。交付内容:一份覆盖情况报告和一份采集计划,列出所有超出配额的数据来源,以及每个缺口及其规模。

完成条件
满足以下所有条件,即表示你已完成本课程。
  • 每个图像文件名都包含摄像头(或数据来源)、采集日期以及帧号或片段编号。

  • 覆盖情况报告会将每台摄像头、每天或每种场景的图像数量与规格中的配额进行比较。

  • 视频按场景变化进行抽样,保留的帧会保留其片段 ID。

显示解答

对于练习数据集,覆盖情况报告会给出明确的计划:

来源数量 / 配额操作
cam152 / 30停止采集;当前数量已占全部图像的 40%
cam226 / 30再采集 4 张
cam325 / 30再采集 5 张
cam425 / 30再采集 5 张

每台摄像头都覆盖了全部四天,因此缺口按摄像头计算,而不是按天计算。将新增的 14 张图像分散到至少两个新日期,这样它们就能增加新的条件,而不是重复同一天下午的场景。对于无法补足缺口的摄像头(例如始终只能拍到夜间场景的摄像头),请为缺失条件添加一行规格,并记录缺口由摆拍图像还是生成图像填补,以便日后衡量其影响。

接下来做什么

图像采集完成后,下一课将教你如何首次就为图像添加一致的标签。

登录以保存学习进度