RF-DETR训练自己的数据集

 

  长期以来,YOLO 系列凭借其出色的速度优势,一直是实时目标检测领域的绝对主导者。然而,2026 年,这一格局正在被一个名为 RF-DETR 的新模型彻底改变。由 Roboflow 开发的 RF-DETR,是首个在 COCO 数据集上突破 60 AP 的实时目标检测模型,其 2XL 版本达到了 60.1 AP50:95 的惊人成绩。这不仅标志着 Transformer 架构在实时检测领域的重大突破,也为整个计算机视觉社区带来了新的选择。RF-DETR模型基于 Transformer 架构,融合 DINOv2 视觉骨干网络,支持目标检测、实例分割和关键点检测任务,提供从 Nano 到 2XLarge 共 6 种尺寸规格。‌‌

1. DINOv2 

  与许多依赖 CNN 的检测器不同,RF-DETR 采用了 DINOv2 作为其视觉骨干网络。DINOv2 是一个基于 Transformer 的自监督学习模型,在多达 1.42 亿张图像上进行过预训练。这使得 RF-DETR 拥有了极其强大的视觉先验知识,能够更好地理解和迁移到新的、未见过的领域(即 domain adaptation)。在衡量模型迁移能力的 RF100-VL 基准测试中,RF-DETR 同样取得了领先地位

2. 神经架构搜索(NAS)

  传统的模型系列(如 YOLO)通常采用统一缩放的方式,即对一个大模型进行深度和宽度的缩减来生成小模型,这可能会继承大模型的低效设计。RF-DETR 则引入了 神经架构搜索(NAS) 技术 。它通过训练一个“超网络”(supernet),在训练过程中随机采样不同的内部配置(如层深度、通道数),最终可以为特定的硬件速度限制(如要求在 T4 GPU 上快于 2.5ms)搜索出最优的子网络结构,从而在硬件限制下获得最佳精度 。

3. NMS-Free 设计

  这是 RF-DETR 最核心的优势之一。传统的 CNN 检测器(如 YOLOv8、YOLOv9)在推理后,需要使用 非极大值抑制(NMS) 来过滤掉重复的预测框。NMS 不仅增加了计算开销,更重要的是,其计算量会随着图像中物体数量的增加而大幅上升,导致延迟不稳定

  RF-DETR 作为一个端到端的 Transformer 模型,其解码器天生就能输出干净、唯一的预测结果,彻底摆脱了对 NMS 的依赖。这意味着,无论场景是空旷还是拥挤,RF-DETR 都能提供稳定且可预测的推理延迟,这对于自动驾驶、工业质检等要求严苛的实时应用至关重要 。

4. 检测与分割的无缝融合

  自 v1.3.0 版本起,RF-DETR 通过统一的 API 原生支持目标检测和实例分割两种任务 。这意味着你可以使用几乎相同的代码来训练检测模型和分割模型,极大地降低了学习和使用成本 。

image

 

5. 训练代码

  安装rfdetr

pip install rfdetr

  RF-DETR 的训练接口 model.train() 支持 COCO JSON 和 YOLO 两种主流数据格式,并会自动识别 。如果你的数据集是 YOLO 格式,确保根目录包含一个 data.yaml 文件,结构如下:

  

image

   如果想使用coco数据集格式,这里也给出转换代码:

import os
import json
from tqdm import tqdm
import shutil
from PIL import Image


def yolo_to_coco(label_dir, image_dir, output_dir, classes_txt_path=None, include_neg_samples=True):
    """
    将 YOLO 格式数据集转换为 COCO 格式,支持负样本(无标注图像),用于 RT-DETR 等模型训练。
    Args:
        label_dir (str): YOLO 标签文件 (.txt) 所在目录。
        image_dir (str): 对应图像文件 (.jpg/.png) 所在目录。
        output_dir (str): 输出目录,包含图像副本和 _annotations.coco.json。
        classes_txt_path (str, optional): classes.txt 路径。若未提供,自动尝试在 label_dir/../classes.txt 查找。
        include_neg_samples (bool): 是否包含无标注的负样本图像,默认开启。
    """
    os.makedirs(output_dir, exist_ok=True)

    # 自动推断 classes.txt 路径
    if classes_txt_path is None:
        default_classes = os.path.join(os.path.dirname(label_dir.rstrip('/')), 'classes.txt')
        if os.path.exists(default_classes):
            classes_txt_path = default_classes
        else:
            raise FileNotFoundError("未找到 classes.txt,请通过 classes_txt_path 指定路径。")

    # 读取类别
    with open(classes_txt_path, 'r', encoding='utf-8') as f:
        class_names = [line.strip() for line in f if line.strip()]
    categories = [{"id": i, "name": name, "supercategory": "object"} for i, name in enumerate(class_names)]

    # 获取所有标签文件(.txt)
    label_files = [f for f in os.listdir(label_dir) if f.endswith('.txt')]
    if not label_files:
        raise ValueError(f"在 {label_dir} 中未找到任何 .txt 标签文件!")

    # 提取已标注的图像basename(用于区分负样本)
    labeled_basenames = {os.path.splitext(f)[0] for f in label_files}

    coco_data = {
        "images": [],
        "annotations": [],
        "categories": categories,
        "info": {"description": "Converted from YOLO to COCO format with negative samples support"},
        "licenses": []
    }

    annotation_id = 1
    image_id = 1

    # 支持的图像扩展名
    IMG_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.bmp', '.webp'}

    # --------------------------
    # 第一步:处理有标注的正样本
    # --------------------------
    print("📌 开始处理有标注的正样本...")
    for label_file in tqdm(label_files, desc="Processing labeled images"):
        label_path = os.path.join(label_dir, label_file)
        base_name = os.path.splitext(label_file)[0]

        # 查找对应图像
        img_file = None
        for ext in IMG_EXTENSIONS:
            candidate = base_name + ext
            if os.path.exists(os.path.join(image_dir, candidate)):
                img_file = candidate
                break
        if img_file is None:
            print(f"⚠️ 警告:找不到与 {label_file} 对应的图像,跳过。")
            continue

        src_img_path = os.path.join(image_dir, img_file)
        dst_img_path = os.path.join(output_dir, img_file)
        shutil.copy(src_img_path, dst_img_path)

        # 使用 PIL 读取图像尺寸
        try:
            with Image.open(dst_img_path) as img:
                width, height = img.size
        except Exception as e:
            print(f"❌ 读取图像 {img_file} 失败: {str(e)},跳过。")
            os.remove(dst_img_path)  # 清理损坏的图像副本
            continue

        # 添加图像信息
        coco_data["images"].append({
            "id": image_id,
            "file_name": img_file,
            "width": width,
            "height": height
        })

        # 读取标签并转换
        with open(label_path, 'r') as lf:
            for line in lf:
                parts = line.strip().split()
                if len(parts) < 5:
                    continue
                try:
                    cls_id = int(parts[0])
                    x_center, y_center, w_norm, h_norm = map(float, parts[1:5])
                except ValueError:
                    print(f"⚠️ 标签文件 {label_file} 格式错误,跳过该行。")
                    continue

                # 转换为绝对坐标 (x_min, y_min, width, height)
                bbox_width = w_norm * width
                bbox_height = h_norm * height
                x_min = (x_center - w_norm / 2) * width
                y_min = (y_center - h_norm / 2) * height

                # 确保 bbox 不越界(关键:避免训练时报错)
                x_min = max(0.0, x_min)
                y_min = max(0.0, y_min)
                bbox_width = min(bbox_width, width - x_min)
                bbox_height = min(bbox_height, height - y_min)

                area = bbox_width * bbox_height

                coco_data["annotations"].append({
                    "id": annotation_id,
                    "image_id": image_id,
                    "category_id": cls_id,
                    "bbox": [x_min, y_min, bbox_width, bbox_height],
                    "area": area,
                    "iscrowd": 0,
                    "segmentation": []
                })
                annotation_id += 1

        image_id += 1

    # --------------------------
    # 第二步:处理无标注的负样本
    # --------------------------
    if include_neg_samples:
        print("\n📌 开始处理无标注的负样本...")
        # 获取所有图像文件
        all_image_files = [f for f in os.listdir(image_dir)
                           if os.path.splitext(f)[1].lower() in IMG_EXTENSIONS]

        # 筛选出无对应标签的负样本
        neg_sample_files = [f for f in all_image_files
                            if os.path.splitext(f)[0] not in labeled_basenames]

        if not neg_sample_files:
            print("ℹ️ 未找到无标注的负样本图像。")
        else:
            for img_file in tqdm(neg_sample_files, desc="Processing negative samples"):
                src_img_path = os.path.join(image_dir, img_file)
                dst_img_path = os.path.join(output_dir, img_file)

                # 避免重复复制(如果正样本中已有同名文件)
                if os.path.exists(dst_img_path):
                    print(f"⚠️ 负样本 {img_file} 已存在于输出目录,跳过。")
                    continue

                shutil.copy(src_img_path, dst_img_path)

                # 读取图像尺寸
                try:
                    with Image.open(dst_img_path) as img:
                        width, height = img.size
                except Exception as e:
                    print(f"❌ 读取负样本 {img_file} 失败: {str(e)},跳过。")
                    os.remove(dst_img_path)
                    continue

                # 仅添加图像信息,无标注
                coco_data["images"].append({
                    "id": image_id,
                    "file_name": img_file,
                    "width": width,
                    "height": height
                })
                image_id += 1

    # 保存 COCO JSON
    output_json = os.path.join(output_dir, "_annotations.coco.json")
    with open(output_json, 'w', encoding='utf-8') as f:
        json.dump(coco_data, f, indent=2, ensure_ascii=False)

    # 统计信息
    total_images = len(coco_data["images"])
    total_annotations = len(coco_data["annotations"])
    neg_samples_count = total_images - len(labeled_basenames)

    print(f"\n✅ 转换完成!")
    print(f"📊 统计信息:")
    print(f"   - 总图像数: {total_images} (正样本 {len(labeled_basenames)} + 负样本 {neg_samples_count})")
    print(f"   - 总标注数: {total_annotations}")
    print(f"📁 输出目录: {output_dir}")
    print(f"📄 标注文件: {output_json}")


if __name__ == "__main__":
    # === 请根据你的实际情况修改以下路径 ===
    LABEL_DIR = "./test_coco/labels"  # YOLO 标签目录(.txt 文件)
    IMAGE_DIR = "./test_coco/images"  # 包含正样本和负样本的图像目录
    OUTPUT_DIR = "./coco_with_neg_samples"  # 输出目录
    CLASSES_TXT = "./classes.txt"  # 类别文件(可选)

    yolo_to_coco(
        label_dir=LABEL_DIR,
        image_dir=IMAGE_DIR,
        output_dir=OUTPUT_DIR,
        classes_txt_path=CLASSES_TXT,
        include_neg_samples=True  # 开启负样本支持
    )

image

   划分数据集:

  

import json
import os
import random
import shutil
from tqdm import tqdm


def split_coco_dataset(input_json, image_dir, output_dir, split_ratio=0.8):
    # 1. 创建输出目录结构
    train_dir = os.path.join(output_dir, 'train')
    val_dir = os.path.join(output_dir, 'valid')
    os.makedirs(train_dir, exist_ok=True)
    os.makedirs(val_dir, exist_ok=True)

    # 2. 读取原始 JSON
    with open(input_json, 'r', encoding='utf-8') as f:
        data = json.load(f)

    images = data['images']
    annotations = data['annotations']
    categories = data['categories']

    # 3. 随机打乱图片顺序
    random.seed(42)  # 固定随机种子,保证结果可复现
    random.shuffle(images)

    # 4. 计算拆分点
    split_idx = int(len(images) * split_ratio)
    train_images = images[:split_idx]
    val_images = images[split_idx:]

    def save_split(split_images, split_name, target_dir):
        # 提取当前拆分对应的图片 ID 集合
        image_ids = {img['id'] for img in split_images}

        # 过滤对应的标注
        split_annos = [anno for anno in annotations if anno['image_id'] in image_ids]

        # 构造新的 JSON 数据
        split_data = {
            "images": split_images,
            "annotations": split_annos,
            "categories": categories,
            "info": data.get("info", {}),
            "licenses": data.get("licenses", [])
        }

        # 保存 JSON 文件
        json_path = os.path.join(target_dir, "_annotations.coco.json")
        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(split_data, f, indent=2, ensure_ascii=False)

        # 物理复制图片到对应目录
        print(f"正在处理 {split_name} 图片...")
        for img in tqdm(split_images):
            src_path = os.path.join(image_dir, img['file_name'])
            dst_path = os.path.join(target_dir, img['file_name'])
            # 如果图片存在则复制
            if os.path.exists(src_path):
                shutil.copy(src_path, dst_path)
            else:
                print(f"⚠️ 警告:找不到图片 {src_path}")

    # 5. 执行保存逻辑
    save_split(train_images, "训练集", train_dir)
    save_split(val_images, "验证集", val_dir)

    print(f"\n✅ 划分完成!")
    print(f"📂 训练集: {len(train_images)} 张图,存放在 {train_dir}")
    print(f"📂 验证集: {len(val_images)} 张图,存放在 {val_dir}")


if __name__ == "__main__":
    # === 请根据你的实际路径修改以下变量 ===
    INPUT_JSON = "/data/coding/coco/_annotations.coco.json"
    IMAGE_DIR = "/data/coding/coco"  # 你图片所在的目录
    OUTPUT_DIR = "/data/coding/coco_train"  # 划分后的新根目录

    split_coco_dataset(INPUT_JSON, IMAGE_DIR, OUTPUT_DIR, split_ratio=0.8)

  开始训练:

from rfdetr import RFDETRBase, RFDETRNano, RFDETRSmall, RFDETRMedium

# n/s/m/l
# model = RFDETRNano()
model = RFDETRSmall()
# model = RFDETRMedium()
# model = RFDETRBase()

model.train(

    dataset_dir="/data/coding/coco_train",
    # 加载数据地址文件夹

    epochs=200,

    batch_size=8,

    grad_accum_steps=4,

    lr=1e-4,

    resolution=512,

    output_dir="rf-detr_weight",  # 模型训练完成的输出地址

    # resume = "",

    early_stopping=True,

    tensorboard=True,
    pin_memory=True,
    num_workers=8

)

  导出onnx:

from rfdetr import RFDETRSmall

# 1. 实例化你训练时使用的模型缩放
# 2. pretrain_weights 指向你训练得到的 .pth 文件
model = RFDETRSmall(pretrain_weights="checkpoint_best_regular.pth")

# 3. 执行导出 (默认会生成在当前目录下)
model.export()
print("ONNX 导出完成!")

6. 推理代码与效果

import time
import cv2
import numpy as np
import onnxruntime as ort


def letterbox(im, new_shape=(512, 512), color=(114, 114, 114)):
    """保持比例缩放并进行补白 (Letterbox),防止图像变形导致坐标偏移"""
    shape = im.shape[:2]  # 当前形状 [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # 计算缩放比例
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])

    # 计算缩放后的未填充形状
    new_unpad = (int(round(shape[1] * r)), int(round(shape[0] * r)))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # 填充的宽高

    dw /= 2  # 两侧平分
    dh /= 2

    if shape[::-1] != new_unpad:  # 调整大小
        im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR)

    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))

    im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # 填充边框
    return im, r, (left, top)


def preprocess_image(image, input_size=512):
    """预处理:Letterbox -> 归一化 -> CHW"""
    if image is None:
        raise ValueError("输入图片为空")

    # 1. Letterbox 缩放 (保持比例)
    img_padded, ratio, (pad_w, pad_h) = letterbox(image, new_shape=input_size)

    # 2. 归一化
    img_data = img_padded.astype(np.float32) / 255.0

    # 3. HWC 转 CHW
    img_data = np.transpose(img_data, (2, 0, 1))
    img_data = np.expand_dims(img_data, axis=0)

    return img_data, image.shape[:2], ratio, (pad_w, pad_h)


def box_cxcywh_to_xyxy(x):
    """将 [center_x, center_y, width, height] 转换为 [x1, y1, x2, y2]"""
    cx, cy, w, h = x
    x1 = cx - 0.5 * w
    y1 = cy - 0.5 * h
    x2 = cx + 0.5 * w
    y2 = cy + 0.5 * h
    return np.array([x1, y1, x2, y2])


def inference_image(image, model_path="./output/inference_model.onnx", input_size=512, conf_threshold=0.5):
    """
    对单张图片进行推理

    Args:
        image: numpy数组格式的图片 (BGR)
        model_path: ONNX模型路径
        input_size: 模型输入尺寸
        conf_threshold: 置信度阈值

    Returns:
        detections: 检测结果列表,每个元素包含 [x1, y1, x2, y2, score, label]
        visualized_image: 绘制了检测框的图片
    """
    # --- 初始化 ONNX 会话 ---
    session = ort.InferenceSession(model_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
    input_name = session.get_inputs()[0].name

    # --- 预处理 ---
    img_data, (orig_h, orig_w), ratio, (pad_w, pad_h) = preprocess_image(image, input_size)

    # --- 推理 ---
    t1 = time.time()
    outputs = session.run(None, {input_name: img_data})
    inference_time = time.time() - t1

    # 识别输出节点 (2个节点的情况)
    if outputs[0].shape[-1] == 4:
        out_boxes, out_logits = outputs[0][0], outputs[1][0]
    else:
        out_logits, out_boxes = outputs[0][0], outputs[1][0]

    # --- 解析结果 ---
    detections = []
    visualized_image = image.copy()

    for i in range(len(out_boxes)):
        probs = out_logits[i]
        label = np.argmax(probs)
        score = probs[label]

        if score > conf_threshold:
            # 1. 坐标格式转换 [cx,cy,w,h] -> [x1,y1,x2,y2]
            box = box_cxcywh_to_xyxy(out_boxes[i])

            # 2. 还原 Letterbox 偏移和缩放
            # 模型输出的是相对于 input_size 补齐图的归一化坐标 (0~1)
            x1 = (box[0] * input_size - pad_w) / ratio
            y1 = (box[1] * input_size - pad_h) / ratio
            x2 = (box[2] * input_size - pad_w) / ratio
            y2 = (box[3] * input_size - pad_h) / ratio

            # 3. 边界检查和四舍五入
            x1, y1, x2, y2 = max(0, int(x1)), max(0, int(y1)), min(orig_w, int(x2)), min(orig_h, int(y2))

            # 保存检测结果
            detections.append([x1, y1, x2, y2, float(score), int(label)])

            # 绘制检测框
            cv2.rectangle(visualized_image, (x1, y1), (x2, y2), (0, 255, 0), 2)
            cv2.putText(visualized_image, f"Class:{label} {score:.2f}", (x1, y1 - 10),
                        cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)

    # 打印推理信息
    print(f"推理耗时: {inference_time:.4f}s")
    print(f"检测到 {len(detections)} 个目标")
    for i, det in enumerate(detections):
        print(f"  目标 {i + 1}: 位置=({det[0]},{det[1]},{det[2]},{det[3]}), 置信度={det[4]:.3f}, 类别={det[5]}")

    return detections, visualized_image


if __name__ == "__main__":
    # --- 配置 ---
    model_path = "./output/inference_model.onnx"
    image_path = r"./crop/test/images/capture_20250823_173410_896_original_840_840_1800_1800.jpg"
    input_size = 512
    conf_threshold = 0.5

    # --- 读取图片 ---
    image = cv2.imread(image_path)
    if image is None:
        print(f"错误: 无法读取图片 {image_path}")
        exit(1)

    # --- 推理 ---
    detections, result_image = inference_image(
        image,
        model_path=model_path,
        input_size=input_size,
        conf_threshold=conf_threshold
    )

    # --- 保存结果 ---
    cv2.imwrite("./onnx_test_image4.png", result_image)
    print(f"结果已保存至: ./onnx_test_image4.png")

image

 

 

  小结:借助AI,简单介绍了RF-DETR,并测试了自己的数据集,效果还不错。

 

posted @ 2026-09-04 22:15  wancy  阅读(61)  评论(0)    收藏  举报