交通标志数据集裁剪与标准化全流程

🚀 YOLOv8项目实战|交通标志数据集裁剪与标准化全流程(含完整Python脚本)

在本专栏上一篇里,我们已经讲过了 Kaggle 上这个交通标志数据集的下载和目录重组方法。本篇就接着做数据预处理的“第二步”——裁剪成小图,方便 YOLOv8 直接使用。

我们会用 Python 编写一个完整的脚本:

✅ 读取现成的 YOLO 格式数据
✅ 按照指定大小(640×640)平铺裁剪
✅ 同步生成对应的标签
✅ 输出为 YOLOv8 标准目录结构

非常适合「交通标志识别」这类任务——因为大部分的标志在原图里尺寸很小,不裁剪的话很难让模型学得好。


📌 本文内容

  • 背景介绍
  • 输入和输出目录结构
  • 裁剪逻辑与标签同步
  • 完整Python脚本
  • 运行和调试
  • YOLOv8训练可直接调用

✅ 1️⃣ 背景介绍

很多交通场景的照片分辨率都挺大,里面的标志只占很小的一部分。如果你直接把整张大图喂给 YOLOv8,模型可能很难收敛,或者检测小目标效果很差。

解决办法就是——把大图裁剪成小块训练。

比如下面这种示意:

整张大图(1360x800)
↓
裁剪成多个 640x640 小图
↓
每块只含 1~2 个目标

优点:
✅ 更集中地学习特征
✅ 显存占用低
✅ 提高小目标检测效果


✅ 2️⃣ 输入目录结构

上一篇我们已经把 Kaggle 数据集重组好,变成了 YOLO 格式:

traffic/
  images/
    train/
    val/
  labels/
    train/
    val/
  • images/:原始图片

  • labels/:YOLO格式标签,每行:

    <class_id> <x_center> <y_center> <width> <height>
    

✅ 3️⃣ 目标输出结构

裁剪完成后,我们要得到:

traffic_crops/
  images/
    train/
    val/
  labels/
    train/
    val/
  • 每张大图会被裁成多个 640x640 的小图
  • 标签也会自动同步更新成新裁剪坐标
  • 训练集、验证集的划分会保持不变

这样可以直接喂给 YOLOv8 的 data.yaml


✅ 4️⃣ 裁剪和标签同步逻辑

关键点在于两步:

① 平铺滑窗裁剪:

  • 从左到右、从上到下,按 640x640 切图
  • 步长可以等于640(无重叠)或小于640(有重叠)

② 同步标签变换:

  • 只保留中心点落在当前裁剪块内的目标
  • 重新换算坐标到裁剪后的小图,并归一化到YOLO格式

这样做的好处是:
✅ 保证标签和图片完全对齐
✅ 自动丢弃跨裁剪块但不在视野内的框


✅ 5️⃣ 主要参数

在脚本里可以轻松调整这些参数:

CROP_SIZE = 640    # 裁剪图大小
STEP = 640         # 滑动步长
KEEP_EMPTY = False # 是否保留没有目标的小图
  • 步长 = 裁剪大小 → 无重叠
  • 步长 < 裁剪大小 → 有重叠

✅ 6️⃣ 🔥 完整Python脚本

👇 复制即可用:

import os
import cv2
import numpy as np
from tqdm import tqdm

# ========== 配置参数 ==========
INPUT_ROOT = "traffic"          # 输入目录
OUTPUT_ROOT = "traffic_crops"   # 输出目录
CROP_SIZE = 640
STEP = 640
KEEP_EMPTY = False
# =============================

def load_yolo_label(txt_path, img_w, img_h):
    boxes = []
    if not os.path.exists(txt_path):
        return boxes
    with open(txt_path, 'r') as f:
        for line in f:
            cls, cx, cy, bw, bh = map(float, line.strip().split())
            x1 = int((cx - bw / 2) * img_w)
            y1 = int((cy - bh / 2) * img_h)
            x2 = int((cx + bw / 2) * img_w)
            y2 = int((cy + bh / 2) * img_h)
            boxes.append((int(cls), x1, y1, x2, y2))
    return boxes

def crop_and_save(split, img_path, label_path, out_img_dir, out_lbl_dir, base_name):
    img = cv2.imread(img_path)
    if img is None:
        print(f"⚠️ 无法读取图像: {img_path}")
        return 0

    img_h, img_w = img.shape[:2]
    boxes = load_yolo_label(label_path, img_w, img_h)
    count = 0

    for y in range(0, img_h - CROP_SIZE + 1, STEP):
        for x in range(0, img_w - CROP_SIZE + 1, STEP):
            crop = img[y:y + CROP_SIZE, x:x + CROP_SIZE]
            label_lines = []

            for box in boxes:
                cls, x1, y1, x2, y2 = box
                cx = (x1 + x2) / 2
                cy = (y1 + y2) / 2
                if x <= cx < x + CROP_SIZE and y <= cy < y + CROP_SIZE:
                    x1_new = max(0, x1 - x)
                    y1_new = max(0, y1 - y)
                    x2_new = min(CROP_SIZE, x2 - x)
                    y2_new = min(CROP_SIZE, y2 - y)

                    bw = x2_new - x1_new
                    bh = y2_new - y1_new
                    if bw < 1 or bh < 1:
                        continue

                    cx_new = (x1_new + x2_new) / 2 / CROP_SIZE
                    cy_new = (y1_new + y2_new) / 2 / CROP_SIZE
                    bw /= CROP_SIZE
                    bh /= CROP_SIZE
                    label_lines.append(f"{cls} {cx_new:.6f} {cy_new:.6f} {bw:.6f} {bh:.6f}")

            if label_lines or KEEP_EMPTY:
                new_name = f"{base_name}_{count}"
                cv2.imwrite(os.path.join(out_img_dir, new_name + ".jpg"), crop)
                with open(os.path.join(out_lbl_dir, new_name + ".txt"), 'w') as f:
                    f.write("\n".join(label_lines))
                count += 1

    return count

def process_split(split):
    in_img_dir = os.path.join(INPUT_ROOT, "images", split)
    in_lbl_dir = os.path.join(INPUT_ROOT, "labels", split)
    out_img_dir = os.path.join(OUTPUT_ROOT, "images", split)
    out_lbl_dir = os.path.join(OUTPUT_ROOT, "labels", split)
    os.makedirs(out_img_dir, exist_ok=True)
    os.makedirs(out_lbl_dir, exist_ok=True)

    img_files = [f for f in os.listdir(in_img_dir) if f.lower().endswith(('.jpg', '.png'))]
    total = 0

    for fname in tqdm(img_files, desc=f"🔄 裁剪 {split}"):
        base = os.path.splitext(fname)[0]
        img_path = os.path.join(in_img_dir, fname)
        label_path = os.path.join(in_lbl_dir, base + ".txt")
        count = crop_and_save(split, img_path, label_path, out_img_dir, out_lbl_dir, base)
        total += count

    print(f"✅ {split} 裁剪完成,共生成 {total} 张")

if __name__ == "__main__":
    for split in ["train", "val"]:
        process_split(split)

    print("\n🎯 全部分割完成!可以用于YOLOv8训练 ✅")

✅ 7️⃣ 运行方法

python crop_traffic_yolo_dataset.py

⚡ 运行完后,你会得到:

traffic_crops/
  images/train/*.jpg
  images/val/*.jpg
  labels/train/*.txt
  labels/val/*.txt

这个目录可以直接写到 YOLOv8 的 data.yaml 里训练。


✅ 8️⃣ YOLOv8 训练调用示例

# data.yaml
train: traffic_crops/images/train
val: traffic_crops/images/val

nc: 4
names: [prohibitory, danger, mandatory, other]

✅ ❤️ 总结

在本篇里,我们:

✅ 讲解了为什么需要做大图裁剪
✅ 设计了标准的输入输出格式
✅ 给出了可直接用的Python脚本
✅ 保证标签同步到裁剪块

🔜 下一篇预告
✨ 第三篇: YOLOv8 的训练配置和全流程命令教程(Python API 版本)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值