一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

图像识别入门练习:标注转换与数据集整理

时间:2026-09-20 13:34:01 编辑:袖梨 来源:一聚教程网

图像识别模型能否顺利训练,往往先取决于标注格式和数据集划分是否可靠。面对 LabelMe 导出的 JSON 文件,需要正确换算矩形框坐标、处理缺失图片和无效标注,还要避免同一图片的数据落入不同集合。下面从格式转换与训练验证集整理两个环节入手,梳理一套可复现的 Python 实现。

自动化实现LabelMe JSON转YOLO txt 格式

# labelme_to_yolo.py —— LabelMe JSON → YOLO txt(含图片归集、配对检查、日志)
import json, shutil
from pathlib import Path
from collections import Counter

# ==========================================
# 1. 路径与类别配置
# ==========================================
# 原始数据目录:存放 LabelMe 导出的 .json 标注文件和对应的 .jpg/.png 图片
SRC = Path(r"/hy-tmp/cv_data/images")
# YOLO 输出目录:归集后的图片文件夹
OUT_IMG = Path(r"/hy-tmp/cv_data/yolo/images")
# YOLO 输出目录:转换后的 .txt 标签文件夹
OUT_LBL = Path(r"/hy-tmp/cv_data/yolo/labels")

# 自动创建输出目录(parents=True 递归创建,exist_ok=True 已存在不报错)
OUT_IMG.mkdir(parents=True, exist_ok=True)
OUT_LBL.mkdir(parents=True, exist_ok=True)

# 类别列表(顺序固定!索引即 YOLO 标签中的 class_id)
CLASSES = ["person", "operatingbar", "grove", "wrongglove", "badge"]
# 构建 类别名 -> ID 的映射字典,方便后续快速查表
cls2id = {c: i for i, c in enumerate(CLASSES)}

# 支持匹配的图片后缀(覆盖大小写差异)
IMG_EXT = [".jpg", ".JPG", ".jpeg", ".JPEG", ".png", ".PNG"]
# 统计计数器:记录各类别数量及处理的图片总数
stat, skipped = Counter(), []

# ==========================================
# 2. 遍历并转换标注文件
# ==========================================
# 按文件名排序遍历源目录下的所有 JSON 文件
for jp in sorted(SRC.glob("*.json")):
    # 解析 JSON 标注数据
    data = json.loads(jp.read_text(encoding="utf-8"))
    # 获取原图的宽高,用于后续坐标归一化
    W, H = data["imageWidth"], data["imageHeight"]

    # --- 步骤 1:寻找同名图片 ---
    # 忽略后缀大小写差异,尝试匹配对应的图片文件
    img_path = next((SRC / f"{jp.stem}{e}" for e in IMG_EXT if (SRC / f"{jp.stem}{e}").exists()), None)
    if img_path is None:
        # 若找不到对应图片,记录跳过原因并处理下一个文件
        skipped.append((jp.name, "缺少对应图片")); continue

    # --- 步骤 2:逐框转换坐标格式 ---
    lines = []
    for s in data["shapes"]:
        # 仅处理矩形框(rectangle),跳过 polygon 等其他形状
        if s["shape_type"] != "rectangle":
            continue
        
        label = s["label"].strip()
        # 检查类别是否在预设列表中,未登记的类别直接跳过
        if label not in cls2id:
            skipped.append((jp.name, f"未登记类别 {label}")); continue
        
        # 提取矩形的左上角 (x1, y1) 和右下角 (x2, y2) 坐标
        (x1, y1), (x2, y2) = s["points"]
        # 关键:强制排序,防止标注时手滑导致 x1 > x2 或 y1 > y2
        x1, x2 = sorted((float(x1), float(x2)))
        y1, y2 = sorted((float(y1), float(y2)))
        
        # 边界裁剪:确保坐标不超出图片范围 [0, W] 和 [0, H]
        x1, x2 = max(0.0, min(x1, W)), max(0.0, min(x2, W))
        y1, y2 = max(0.0, min(y1, H)), max(0.0, min(y2, H))
        
        # 过滤退化框:宽或高小于 1 像素的无效框直接丢弃
        if x2 - x1 < 1 or y2 - y1 < 1:
            skipped.append((jp.name, "退化框(宽或高<1px)")); continue
        
        # 核心转换:LabelMe (x1,y1,x2,y2) -> YOLO (class_id, cx, cy, bw, bh)
        # cx, cy: 归一化后的中心点坐标 (0~1)
        # bw, bh: 归一化后的框宽和框高 (0~1)
        cx, cy = (x1 + x2) / 2 / W, (y1 + y2) / 2 / H
        bw, bh = (x2 - x1) / W, (y2 - y1) / H
        
        # 格式化为一行 YOLO 标签字符串,保留 6 位小数以保证精度
        lines.append(f"{cls2id[label]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}")
        # 更新类别统计计数
        stat[label] += 1

    # --- 步骤 3:落盘保存 ---
    # 复制图片到输出目录,并统一将后缀转为小写(方便后续训练脚本读取)
    shutil.copy2(img_path, OUT_IMG / f"{jp.stem}{img_path.suffix.lower()}")
    # 将转换好的多行标签写入同名 .txt 文件
    (OUT_LBL / f"{jp.stem}.txt").write_text("n".join(lines), encoding="utf-8")
    # 更新图片处理总数统计
    stat["图片"] += 1

# ==========================================
# 3. 打印日志与生成类别文件
# ==========================================
print("完成。统计:", dict(stat))
print("跳过明细:", skipped)
# 打印最终的类别索引映射,方便核对
for i, c in enumerate(CLASSES):
    print(f"  {i}: {c}")

# 生成 classes.txt 文件(YOLOv8/v11 等新版训练时通常需要此文件来映射 class_id -> 类别名)
Path(r"/hy-tmp/cv_data/yolo/classes.txt").write_text("n".join(CLASSES), encoding="utf-8")

image.png

训练集 / 验证集划分

划分原则

  1. 图片级划分:同一张图的所有框必须落在同一集合,绝不能按框随机分(会造成严重泄漏)。
  2. 固定随机种子:保证可复现(seed=42)。
  3. 验证集要有代表性:小类别(badge 8 框、wrongglove 12 框)在验证集里至少出现 1~2 次,否则该类 mAP 直接是 0 或 NaN。
  4. 场景不串场:同一工位/同一时段的连拍图尽量整体进同一集合。
  5. 比例:本数据集 59 张,8:2 ≈ 47 / 12;数据量 < 100 张时建议交叉验证(见 4.3)。
import random, shutil, json
from pathlib import Path
from collections import Counter

# ==========================================
# 1. 基础配置
# ==========================================
# 数据集根目录(存放 images 和 labels 文件夹)
ROOT = Path(r"/hy-tmp/cv_data/yolo")

# 类别列表(顺序必须与训练时的 names 映射保持一致)
CLASSES = ["person", "operatingbar", "grove", "wrongglove", "badge"]

VAL_RATIO = 0.2   # 验证集划分比例:20%
SEED = 42         # 随机种子:固定后每次运行划分结果一致,保证实验可复现
random.seed(SEED)

# ==========================================
# 2. 获取样本列表并打乱
# ==========================================
# 读取 labels 目录下所有 .txt 文件的文件名(不带后缀)
stems = sorted(p.stem for p in (ROOT / "labels").glob("*.txt"))

# 随机打乱文件顺序
random.shuffle(stems)

# 按比例计算验证集数量(至少保留 1 张,防止数据集过小时验证集为空)
n_val = max(1, round(len(stems) * VAL_RATIO))

# 切分:前 n_val 个作为验证集,剩余作为训练集,转为 set 方便后续遍历
val_set = set(stems[:n_val])
train_set = set(stems[n_val:])

# ==========================================
# 3. 辅助函数:统计单张图片的类别分布
# ==========================================
def cls_of(stem):
    """
    读取指定样本的 .txt 标签文件,统计其中包含的各类别标注框数量
    返回格式示例:Counter({'person': 2, 'badge': 1})
    """
    c = Counter()
    txt_path = ROOT / "labels" / f"{stem}.txt"
    if txt_path.exists():
        for line in txt_path.read_text().splitlines():
            if line.strip():  # 跳过空行
                class_id = int(line.split()[0])  # 取每行第一个数字,即类别 ID
                c[CLASSES[class_id]] += 1        # 通过 ID 映射回类别名称并计数
    return c

# ==========================================
# 4. 创建标准 YOLO 目录结构
# ==========================================
# 自动创建 images/train、images/val、labels/train、labels/val 四个子文件夹
for sub in ("images/train", "images/val", "labels/train", "labels/val"):
    (ROOT / sub).mkdir(parents=True, exist_ok=True)

# ==========================================
# 5. 执行文件复制与类别统计
# ==========================================
split_stat = {}  # 用于记录训练集和验证集的类别分布统计结果

for name, group in (("train", train_set), ("val", val_set)):
    class_counter = Counter()  # 当前分组的类别计数器
    
    for s in group:
        # 在 images 目录下查找与标签同名的图片(自动兼容 .jpg/.png/.JPG 等任意后缀)
        img = next((ROOT / "images").glob(f"{s}.*"))
        
        # 复制图片到对应的分组目录(如 images/train/xxx.jpg)
        shutil.copy2(img, ROOT / "images" / name / img.name)
        # 复制标签到对应的分组目录(如 labels/train/xxx.txt)
        shutil.copy2(ROOT / "labels" / f"{s}.txt", ROOT / "labels" / name / f"{s}.txt")
        
        # 统计该图片包含的类别,累加到分组计数器中
        class_counter.update(cls_of(s))
    
    # 保存当前分组的统计结果
    split_stat[name] = dict(class_counter)
    # 打印日志:图片数量、标注框总数、各类别分布
    print(f"[{name}] 图片 {len(group)} 张,框数 {sum(class_counter.values())},类别分布 {dict(class_counter)}")

# ==========================================
# 6. 安全检查:验证集小类别告警
# ==========================================
# 遍历所有预设类别,检查验证集中是否包含该类别的样本
for cls in CLASSES:
    # 如果验证集中某类别数量为 0,会导致训练时该类别的 mAP 无法计算
    if split_stat["val"].get(cls, 0) == 0:
        print(f"!! 警告:验证集中没有 {cls},该类指标无法计算,请调整 seed 或手动指定")

# ==========================================
# 7. 保存统计结果到 JSON 文件
# ==========================================
# 将训练集和验证集的类别分布统计写入 split_stat.json,方便后续查看数据分布
json.dump(
    split_stat, 
    open(ROOT / "split_stat.json", "w", encoding="utf-8"),
    ensure_ascii=False,  # 保证中文类别名正常显示,不转义为 uXXXX
    indent=2             # 格式化缩进,提升可读性
)

模型训练

按照依赖:pip install ultralytics==8.0.138 -i https://pypi.tuna.tsinghua.edu.cn/simple


跑不通改东西:
# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: /hy-tmp/cv_data/yolo # dataset root dir
train: images/train # train images (relative to 'path') 128 images
val: images/val # val images (relative to 'path') 128 images
test: # test images (optional)

# Classes
names:
  0: person
  1: operatingbar
  2: grove
  3: wrongglove
  4: badge


from ultralytics import YOLO


if __name__=="__main__":
    model = YOLO("yolov8s.pt") # nsmlx
    model.train(data ="coco128.yaml",epochs = 100,batch =16,imgsz =640,device="0")

增强版

from ultralytics import YOLO
if __name__ == "__main__":
    # 1. 选用预训练模型 (根据显存情况,yolov8s 或 yolov8m 均可)
    model = YOLO("/root/cv/yolov8n.pt")

    # 2. 高精度训练配置
    results = model.train(
        # --- 基础配置 ---
        data="/root/cv/coco128.yaml",    # 替换为你的数据配置文件 (如 data.yaml)
        epochs=100,             # 训练轮次
        batch=16,               # 批次大小 (若显存不足可降至 8)
        imgsz=800,              # ? 关键提分点 1: 将输入分辨率从 640 提升至 800 (大幅提升路牌细节识别)
        device="0",             # GPU 设备
        amp=False,  # 关闭 AMP,全程使用 FP32
        # --- 优化器与学习率配置 ---
        optimizer="AdamW",      # ? 关键提分点 2: 使用 AdamW 优化器,比默认 SGD 收敛更好
        cos_lr=True,            # ? 关键提分点 3: 开启余弦退火学习率 (Cosine Annealing)
        lr0=0.001,              # 初始学习率
        lrf=0.01,               # 最终学习率衰减比例
        weight_decay=0.0005,    # 权重衰减,防止过拟合
        warmup_epochs=3.0,      # 预热轮次,保护预训练权重
        
        # --- 数据增强配置 (针对路牌/小目标优化) ---
        mosaic=1.0,             # ? 关键提分点 4: 开启 Mosaic 增强,提升小目标及多目标检测
        close_mosaic=10,        # 最后 10 个 epoch 关闭 Mosaic,让模型在真实图像上平稳收敛
        mixup=0.15,             # 图层混合增强
        degrees=10.0,           # 随机旋转 (路牌微小倾斜适应)
        scale=0.5,              # 图像缩放增强
        fliplr=0.5,             # 左右翻转
        
        # --- 结果保存与打印 ---
        project="hy-tmp/task2_result",
        name="train_run_high_acc",
        save=True
    )

    print("? 训练完成,正在进行测试时增强 (TTA) 高精度验证...")

image.png

模型验证

 # 3. 评估时开启 TTA (Test-Time Augmentation)
 # ? 关键提分点 5: 验证时开启 augment=True,通常能直接带来 1%~3% 的 mAP 提升
from ultralytics import YOLO
if __name__ == "__main__":
    val_model = YOLO("/task2_result/train_run_high_acc2/weights/best.pt")
    metrics = val_model.val(
        data="/root/cv/coco128.yaml",
        imgsz=800,
        augment=True,           # 开启 TTA 推理增强
        split="val"
    )

    print(f"✅ TTA 验证完毕!mAP50: {metrics.box.map50:.4f}, mAP50-95: {metrics.box.map:.4f}")

模型测试

from ultralytics import YOLO

model = YOLO("/tmp/pycharm_project_662/runs/detect/train/weights/best.pt")

results = model.predict(
    source="yolo/images/train",   # 图片 / 目录 / 视频 / 0(摄像头)
    imgsz=640,
    conf=0.25,            # 推理阈值,可参考 F1_curve 调
    iou=0.45,             # NMS 阈值
    device=0,
    save=True,            # 保存画框后的图片

    project="runs/detect", exist_ok=True,
)


模型量化展示(三种常用做法)

from ultralytics import YOLO

model = YOLO("runs/detect/v8_exp1/weights/best.pt")

# 1) 导出 FP32 ONNX
model.export(format="onnx", imgsz=640, opset=12, simplify=True, dynamic=False)

# 2) 导出 FP16 ONNX(GPU 推理用,体积减半,精度基本无损)
model.export(format="onnx", imgsz=640, half=True)

# 3) 导出 INT8 ONNX —— 必须提供 data 做校准!
model.export(format="onnx", imgsz=640, int8=True, data="data.yaml")

热门栏目