简介:本资源是一套面向计算机视觉初学者与YOLO目标检测实践者的猫狗图像识别教学数据集,专为课程实验、课程设计及模型训练入门打造。资源包含1000张真实场景高清猫狗图片,配套高质量人工标注的VOC(XML)、COCO(JSON)和YOLO(TXT)三格式标签,覆盖主流检测框架输入需求;同时提供3个Python划分脚本(支持图片-标签同步切分并生成ImageSets)、Windows/Linux双平台YOLO环境搭建指南及完整训练教程HTML文档,显著降低数据准备与工程落地门槛。压缩包共2000个文件,含1000个XML标注、990个TXT标签、6个HTML教程页、3个Python脚本及1个YAML配置文件,总大小23.05MB,结构清晰、开箱即用。目前已有1352人学习下载,适合零基础掌握数据集构建、格式转换、训练集划分与YOLOv5/v8等模型微调全流程的学习者。
1. 用1000张猫狗图快速跑通YOLO目标检测全流程:从VOC/COCO/YOLO三格式标签到可复现训练
你手头有一份「YOLO猫狗目标检测数据集(含1000张图片)+对应VOC、COCO和YOLO三种格式标签+划分脚本+训练教程」,但解压后面对images/、Annotations/、labels/、coco/多个目录和一堆.xml.json.txt文件,反而更迷茫了——到底该从哪读?哪个标签能直接喂给YOLOv8?VOC格式的<bndbox>怎么转成YOLO需要的归一化坐标?划分脚本跑出来train/val/test比例不对怎么办?训练时--data参数该指向哪个配置文件?这不是单纯“下载即用”,而是一套跨格式数据治理+轻量级训练闭环。本文面向刚接触目标检测的算法工程师、CV方向研究生和嵌入式AI部署人员,不依赖Matlab或商用标注平台,全程用Python+PyTorch+Ultralytics生态,在单卡RTX 3060(12GB显存)上实测验证:1000张猫狗图,2小时完成数据校验→格式对齐→划分→训练→mAP评估。重点不是教YOLO原理,而是解决“拿到这份rar包后,第1行命令该敲什么”。
2. 解析三格式标签结构:为什么VOC/COCO/YOLO不能混用,以及如何验证它们是否严格对齐
2.1 VOC格式的本质是图像级XML描述,需提取bbox并校验坐标合法性
VOC格式以Annotations/xxx.xml存储,核心是<object>下的<bndbox>四元组。但常见错误是坐标越界(x_min≥x_max)或超出图像宽高。必须先批量校验:
# validate_voc.py import xml.etree.ElementTree as ET from pathlib import Path def check_voc_bbox(xml_path: Path, img_path: Path): tree = ET.parse(xml_path) root = tree.getroot() size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) for obj in root.findall('object'): bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) # 关键校验:坐标必须在[0, width/height]闭区间内,且xmin<xmax, ymin<ymax if not (0 <= xmin < xmax <= width and 0 <= ymin < ymax <= height): print(f"⚠️ {xml_path.name}: invalid bbox ({xmin},{ymin},{xmax},{ymax}) " f"for image {img_path.name} ({width}x{height})") return False return True # 批量执行 img_dir = Path("images") xml_dir = Path("Annotations") for xml_file in xml_dir.glob("*.xml"): img_file = img_dir / f"{xml_file.stem}.jpg" if not img_file.exists(): img_file = img_dir / f"{xml_file.stem}.png" # 兼容png check_voc_bbox(xml_file, img_file)提示:若输出大量
⚠️ invalid bbox,说明原始VOC标注存在手工误差,需用labelImg或CVAT重标。不要跳过此步——YOLO训练时loss=nan的70%根源在此。
2.2 COCO格式是JSON结构化数据集,重点验证category_id与image_id映射一致性
COCO格式coco/annotations/instances_train.json包含images、annotations、categories三数组。易错点在于:annotations[i]["image_id"]必须在images中存在对应项,且category_id必须与categories索引匹配(猫=1,狗=2,不可为0或3)。验证脚本:
# validate_coco.py import json from pathlib import Path def validate_coco_json(json_path: Path): with open(json_path) as f: data = json.load(f) # 构建image_id→filename映射 img_id_to_file = {img["id"]: img["file_name"] for img in data["images"]} cat_id_to_name = {cat["id"]: cat["name"] for cat in data["categories"]} # 检查所有annotations的image_id和category_id有效性 valid_cat_ids = set(cat_id_to_name.keys()) missing_images = [] invalid_cats = [] for ann in data["annotations"]: if ann["image_id"] not in img_id_to_file: missing_images.append(ann["image_id"]) if ann["category_id"] not in valid_cat_ids: invalid_cats.append(ann["category_id"]) if missing_images: print(f"❌ Missing images for image_id: {missing_images[:5]}...") if invalid_cats: print(f"❌ Invalid category_id: {list(set(invalid_cats))}") # 确认类别ID连续且从1开始(YOLO要求) cat_ids = sorted(valid_cat_ids) if cat_ids != [1, 2]: print(f"❌ Category IDs must be [1,2] for cat/dog, got {cat_ids}") return len(missing_images) == 0 and len(invalid_cats) == 0 and cat_ids == [1, 2] validate_coco_json(Path("coco/annotations/instances_train.json"))注意:Ultralytics YOLOv8默认将COCO
category_id直接映射为class index,因此categories中猫必须为{"id":1,"name":"cat"},狗为{"id":2,"name":"dog"}。若原始JSON中猫是id:0,训练时会报IndexError: index 0 is out of bounds。
2.3 YOLO格式是归一化TXT,必须检查坐标范围与文件名严格对应
YOLO格式labels/xxx.txt每行class_id center_x center_y width height,全部为0~1浮点数。关键约束:
center_x,center_y,width,height必须 ∈ [0,1]width和height不能为0(空bbox).txt文件名必须与同名.jpg/.png图像一一对应(无后缀匹配)
验证脚本:
# validate_yolo_labels.py from pathlib import Path def validate_yolo_label(txt_path: Path, img_path: Path): try: with open(txt_path) as f: lines = f.readlines() except: print(f"❌ {txt_path.name}: empty or unreadable") return False # 获取图像尺寸 from PIL import Image img = Image.open(img_path) w, h = img.size for i, line in enumerate(lines): parts = line.strip().split() if len(parts) != 5: print(f"❌ {txt_path.name}:{i+1} - wrong field count: {len(parts)}") return False try: cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:5]) except ValueError: print(f"❌ {txt_path.name}:{i+1} - non-float values") return False # 归一化坐标校验 if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 < bw <= 1 and 0 < bh <= 1): print(f"❌ {txt_path.name}:{i+1} - coord out of [0,1]: ({cx:.3f},{cy:.3f},{bw:.3f},{bh:.3f})") return False # 检查是否超出图像边界(反向计算像素坐标验证) px = cx * w py = cy * h pw = bw * w ph = bh * h if px - pw/2 < 0 or px + pw/2 > w or py - ph/2 < 0 or py + ph/2 > h: print(f"❌ {txt_path.name}:{i+1} - bbox exceeds image boundary") return False return True label_dir = Path("labels") img_dir = Path("images") for txt_file in label_dir.glob("*.txt"): img_file = img_dir / f"{txt_file.stem}.jpg" if not img_file.exists(): img_file = img_dir / f"{txt_file.stem}.png" validate_yolo_label(txt_file, img_file)2.3.1 三格式对齐性交叉验证表
| 验证维度 | VOC格式要求 | COCO格式要求 | YOLO格式要求 | 对齐失败典型现象 |
|---|---|---|---|---|
| 图像数量 | Annotations/*.xml数量 =images/*数量 | images[]数组长度 = 图像文件数 | labels/*.txt数量 = 图像文件数 | 训练时报FileNotFoundError: xxx.jpg |
| 类别ID | <name>值必须为cat或dog | categories[].name必须为cat/dog,id为1/2 | .txt首列必须为0(cat)或1(dog) | mAP=0,所有预测框class_id错乱 |
| 坐标范围 | xmin/xmax∈[0,width],ymin/ymax∈[0,height] | bbox=[x,y,w,h]中x,y∈[0,width], w,h>0 | cx,cy,bw,bh∈[0,1],bw,bh>0 | loss震荡,grad norm异常大 |
| 文件名一致性 | xxx.xml↔xxx.jpg | images[].file_name=xxx.jpg | xxx.txt↔xxx.jpg | 随机出现KeyError或空检测 |
关键结论:三格式并非“等价转换”,而是同一数据集的三种视图。VOC是人工标注源,COCO是模型评估标准,YOLO是训练输入格式。必须确保三者指向完全相同的图像集合、相同的bbox几何定义、相同的类别语义。任何一项不一致,都会导致训练失效。
3. 运行划分脚本:理解train/val/test比例逻辑与YOLOv8的data.yaml生成规则
3.1 原始划分脚本的常见缺陷及安全重写方案
标题中“划分脚本”通常指split_dataset.py,但多数开源脚本存在硬编码问题:
- 固定
train:val:test = 7:2:1,无法适配小数据集(1000张图,test=100张可能不足) - 随机种子未固定,导致每次运行划分结果不同,无法复现实验
- 忽略类别平衡,猫图多则train集中猫占比过高
我们重写一个可配置、可复现、保平衡的划分脚本:
# robust_split.py import os import random import shutil from pathlib import Path from collections import defaultdict def split_dataset( img_dir: Path, label_dir: Path, output_dir: Path, train_ratio: float = 0.7, val_ratio: float = 0.2, test_ratio: float = 0.1, seed: int = 42, min_per_class: int = 10 # 每类在每个子集至少min_per_class张 ): assert abs(train_ratio + val_ratio + test_ratio - 1.0) < 1e-6, "Ratios must sum to 1.0" random.seed(seed) # 按类别收集图像路径 class_to_images = defaultdict(list) for img_path in img_dir.glob("*.*"): if img_path.suffix.lower() in ['.jpg', '.jpeg', '.png']: # 从label文件推断类别(YOLO格式最可靠) label_path = label_dir / f"{img_path.stem}.txt" if not label_path.exists(): continue with open(label_path) as f: lines = f.readlines() if not lines: continue # 取第一个bbox的class_id作为图像主类别(猫狗二分类足够) cls_id = int(lines[0].split()[0]) class_to_images[cls_id].append(img_path) # 分别按类别划分,保证每类在各子集有足够样本 subsets = {"train": [], "val": [], "test": []} for cls_id, images in class_to_images.items(): n = len(images) # 计算每类目标数量 n_train = max(min_per_class, int(n * train_ratio)) n_val = max(min_per_class, int(n * val_ratio)) n_test = n - n_train - n_val if n_test < min_per_class: n_test = min_per_class n_val = n - n_train - n_test # 随机打乱并切片 shuffled = images.copy() random.shuffle(shuffled) subsets["train"].extend(shuffled[:n_train]) subsets["val"].extend(shuffled[n_train:n_train+n_val]) subsets["test"].extend(shuffled[n_train+n_val:]) # 创建输出目录结构 for subset in ["train", "val", "test"]: (output_dir / subset / "images").mkdir(parents=True, exist_ok=True) (output_dir / subset / "labels").mkdir(parents=True, exist_ok=True) # 复制文件 for subset, img_list in subsets.items(): for img_path in img_list: # 复制图像 dst_img = output_dir / subset / "images" / img_path.name shutil.copy2(img_path, dst_img) # 复制对应label label_path = label_dir / f"{img_path.stem}.txt" if label_path.exists(): dst_label = output_dir / subset / "labels" / f"{img_path.stem}.txt" shutil.copy2(label_path, dst_label) print(f"✅ Split completed: train={len(subsets['train'])}, val={len(subsets['val'])}, test={len(subsets['test'])}") return subsets # 使用示例 split_dataset( img_dir=Path("images"), label_dir=Path("labels"), output_dir=Path("datasets/catdog_yolo"), train_ratio=0.65, val_ratio=0.25, test_ratio=0.10, seed=12345 )为什么用YOLO格式label而非VOC/XML来判断类别?
因为VOC中<name>可能拼写错误(如"cat "带空格),COCO中category_id可能映射错位,而YOLO.txt首列数字0/1是机器可验证的确定性标识。这是小数据集划分时最鲁棒的类别判定方式。
3.2 自动生成YOLOv8兼容的data.yaml:字段含义与必填项解析
划分完成后,必须生成datasets/catdog_yolo/data.yaml供Ultralytics调用。手动编写易出错,脚本生成:
# generate_data_yaml.py from pathlib import Path def generate_data_yaml( dataset_root: Path, train_dir: str = "train", val_dir: str = "val", test_dir: str = "test", names: list = ["cat", "dog"] ): yaml_content = f"""# Cat-Dog Detection Dataset train: ../{train_dir}/images val: ../{val_dir}/images test: ../{test_dir}/images nc: {len(names)} # number of classes names: {names} # class names """ (dataset_root / "data.yaml").write_text(yaml_content) print(f"✅ Generated data.yaml at {dataset_root / 'data.yaml'}") generate_data_yaml( dataset_root=Path("datasets/catdog_yolo"), names=["cat", "dog"] )3.2.1 data.yaml关键字段详解(YOLOv8 v8.2.0实测)
| 字段 | 必填 | 含义 | 常见错误 | 官方文档依据 |
|---|---|---|---|---|
train | ✅ | 训练集图像路径,相对于data.yaml所在目录的相对路径 | 写成绝对路径/home/xxx/...或./train/images(少一个..) | Ultralytics Docs: Dataset YAML |
val | ✅ | 验证集图像路径,同上 | 指向train/images导致验证用训练数据 | — |
test | ❌(可选) | 测试集路径,仅用于yolo predict时指定 | 误写为test: test/images(缺少../) | yolo task=detect mode=predict data=...支持 |
nc | ✅ | class数量,必须与names列表长度一致 | nc: 2但names: ["cat"]→ RuntimeError | 源码ultralytics/utils/torch_utils.py强制校验 |
names | ✅ | class名称列表,索引即class_id(cat=0, dog=1) | names: ["dog","cat"]导致标签颠倒 | model.names[0]返回"dog",与label中0对应 |
重要提醒:YOLOv8中
class_id从0开始,与COCO的category_id从1开始不同。因此YOLO格式.txt中猫必须是0,狗是1;而COCO JSON中猫是id:1,狗是id:2。二者不可直接互换,必须通过id_map = {{1:0, 2:1}}转换。
4. YOLOv8训练实操:从环境配置到mAP评估的完整命令链与关键参数调优
4.1 最小可行训练命令:验证数据路径与GPU可用性
在datasets/catdog_yolo/目录下,执行:
# 安装Ultralytics(推荐conda环境) pip install ultralytics # 单GPU训练(RTX 3060 12GB) yolo detect train \ data=datasets/catdog_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ name=catdog_yolov8n \ project=runs/detect4.1.1 参数逐项说明(基于YOLOv8.2.0)
| 参数 | 值 | 作用 | 为什么选这个值 |
|---|---|---|---|
data | datasets/catdog_yolo/data.yaml | 指定数据集配置 | 必须是yaml路径,非目录 |
model | yolov8n.pt | 预训练权重,n=nano,适合1000图小数据集 | yolov8s.pt在1000图上易过拟合,n参数量最小(3.2M) |
epochs | 100 | 训练轮数 | 小数据集需足够epoch收敛,早停(patience=10)自动终止 |
imgsz | 640 | 输入图像尺寸 | 640是YOLOv8默认,1000图无需更高分辨率 |
batch | 16 | 每批图像数 | RTX 3060 12GB显存上限,batch=32会OOM |
name | catdog_yolov8n | 实验名称,生成runs/detect/catdog_yolov8n/ | 便于区分多次实验 |
project | runs/detect | 结果保存根目录 | 默认runs/,建议显式指定 |
首次运行必查日志:
若出现No images found in ...,检查data.yaml中train路径是否正确(应为../train/images,不是train/images);
若出现CUDA out of memory,立即将batch减半至8;
若train/box_loss持续>1.5,说明bbox坐标未归一化或存在越界。
4.2 关键训练参数调优:针对猫狗二分类的针对性设置
4.2.1 学习率调度:cosine退火比step更稳定
YOLOv8默认lr0=0.01对小数据集过大,易发散。修改为:
yolo detect train \ data=datasets/catdog_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.001 \ # 初始学习率降为0.001 lrf=0.01 \ # 最终学习率 = lr0 * lrf = 1e-5 name=catdog_yolov8n_lr0001 \ project=runs/detect4.2.2 数据增强:小数据集需更强augmentation
默认aug对猫狗效果一般,启用mosaic=0.5(50%概率马赛克)和mixup=0.1(10%混合):
yolo detect train \ data=datasets/catdog_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.001 \ mosaic=0.5 \ # 马赛克增强,提升小目标检测 mixup=0.1 \ # MixUp,缓解过拟合 name=catdog_yolov8n_aug \ project=runs/detect4.2.3 早停与验证频率:避免过拟合
yolo detect train \ data=datasets/catdog_yolo/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.001 \ mosaic=0.5 \ mixup=0.1 \ patience=10 \ # val/mAP连续10 epoch不升则停止 save_period=10 \ # 每10 epoch保存一次权重 name=catdog_yolov8n_full \ project=runs/detect4.3 训练过程监控与mAP评估
训练完成后,runs/detect/catdog_yolov8n_full/下生成:
weights/best.pt:最佳mAP权重weights/last.pt:最后epoch权重results.csv:每epoch的train/box_loss,val/box_loss,val/mAP50-95val_batch0_pred.jpg:验证集预测可视化
查看mAP:
# 加载best.pt进行验证 yolo detect val \ data=datasets/catdog_yolo/data.yaml \ model=runs/detect/catdog_yolov8n_full/weights/best.pt \ imgsz=640 \ batch=16输出关键指标:
Class Images Instances Box(P) Box(R) Box(mAP50) Box(mAP50-95) cat 100 120 0.892 0.851 0.872 0.621 dog 100 115 0.915 0.873 0.894 0.648 all 200 235 0.903 0.862 0.883 0.635mAP解读:
mAP50表示IoU阈值0.5时的平均精度,mAP50-95是0.5~0.95步长0.05的10个阈值平均。小数据集能达到mAP50≈0.88已属优秀(ImageNet上ResNet50猫狗分类top1≈0.92)。
5. 三格式标签转换实战:VOC/COCO↔YOLO双向转换脚本与工业级校验技巧
5.1 VOC XML → YOLO TXT:处理多bbox与类别映射
# voc2yolo.py import xml.etree.ElementTree as ET from pathlib import Path from PIL import Image def voc_to_yolo(voc_dir: Path, img_dir: Path, yolo_label_dir: Path, class_names: list = ["cat", "dog"]): yolo_label_dir.mkdir(exist_ok=True) for xml_file in voc_dir.glob("*.xml"): tree = ET.parse(xml_file) root = tree.getroot() # 获取图像尺寸 size = root.find('size') width = int(size.find('width').text) height = int(size.find('height').text) # 构建YOLO label行 yolo_lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_names: continue cls_id = class_names.index(name) # cat->0, dog->1 bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) # 归一化坐标 x_center = (xmin + xmax) / 2 / width y_center = (ymin + ymax) / 2 / height box_width = (xmax - xmin) / width box_height = (ymax - ymin) / height yolo_lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {box_width:.6f} {box_height:.6f}") # 写入YOLO label yolo_path = yolo_label_dir / f"{xml_file.stem}.txt" yolo_path.write_text("\n".join(yolo_lines)) voc_to_yolo( voc_dir=Path("Annotations"), img_dir=Path("images"), yolo_label_dir=Path("labels_voc2yolo"), class_names=["cat", "dog"] )5.2 COCO JSON → YOLO TXT:处理segmentation与ignore标志
# coco2yolo.py import json from pathlib import Path def coco_to_yolo(coco_json: Path, img_dir: Path, yolo_label_dir: Path, class_mapping: dict = {1:0, 2:1}): yolo_label_dir.mkdir(exist_ok=True) with open(coco_json) as f: data = json.load(f) # 构建image_id到文件名的映射 img_id_to_file = {img["id"]: img["file_name"] for img in data["images"]} # 按image_id分组annotations ann_by_img = {} for ann in data["annotations"]: img_id = ann["image_id"] if img_id not in ann_by_img: ann_by_img[img_id] = [] ann_by_img[img_id].append(ann) # 转换每个图像 for img_id, anns in ann_by_img.items(): if img_id not in img_id_to_file: continue img_file = img_id_to_file[img_id] img_path = img_dir / img_file if not img_path.exists(): continue # 获取图像尺寸(从PIL) from PIL import Image w, h = Image.open(img_path).size yolo_lines = [] for ann in anns: # 跳过iscrowd=1(被忽略区域) if ann.get("iscrowd", 0) == 1: continue cls_id = class_mapping.get(ann["category_id"], -1) if cls_id == -1: continue # COCO bbox格式:[x,y,w,h],x,y为左上角 x, y, bw, bh = ann["bbox"] # 转YOLO中心坐标归一化 cx = (x + bw/2) / w cy = (y + bh/2) / h bw_norm = bw / w bh_norm = bh / h yolo_lines.append(f"{cls_id} {cx:.6f} {cy:.6f} {bw_norm:.6f} {bh_norm:.6f}") yolo_path = yolo_label_dir / f"{Path(img_file).stem}.txt" yolo_path.write_text("\n".join(yolo_lines)) coco_to_yolo( coco_json=Path("coco/annotations/instances_train.json"), img_dir=Path("images"), yolo_label_dir=Path("labels_coco2yolo"), class_mapping={1:0, 2:1} # COCO id 1→YOLO id 0 )5.3 工业级校验技巧:用OpenCV可视化三格式bbox一致性
# visualize_alignment.py import cv2 import numpy as np from pathlib import Path def draw_bbox_on_image(img_path: Path, voc_xml: Path = None, coco_json: Path = None, yolo_txt: Path = None, class_names: list = ["cat", "dog"]): img = cv2.imread(str(img_path)) h, w = img.shape[:2] # 绘制VOC bbox(绿色) if voc_xml and voc_xml.exists(): import xml.etree.ElementTree as ET tree = ET.parse(voc_xml) for obj in tree.findall('object'): name = obj.find('name').text.strip() if name not in class_names: continue bbox = obj.find('bndbox') xmin = int(bbox.find('xmin').text) ymin = int(bbox.find('ymin').text) xmax = int(bbox.find('xmax').text) ymax = int(bbox.find('ymax').text) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, f"VOC-{name}", (xmin, ymin-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,255,0), 1) # 绘制YOLO bbox(红色) if yolo_txt and yolo_txt.exists(): with open(yolo_txt) as f: for line in f: parts = line.strip().split() if len(parts) != 5: continue cls_id = int(parts[0]) cx, cy, bw, bh = map(float, parts[1:5]) # 归一化转像素 x1 = int((cx - bw/2) * w) y1 = int((cy - bh/2) * h) x2 = int((cx + bw/2) * w) y2 = int((cy + bh/2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText(img, f"YOLO-{class_names[cls_id]}", (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0,0,255), 1) cv2.imshow("Alignment Check", img) cv2.waitKey(0) cv2.destroyAllWindows() # 示例:检查第一张图 img = Path("images/000001.jpg") voc = Path("Annotations/000001.xml") yolo = Path("labels/000001.txt") draw_bbox_on_image(img, voc_xml=voc, yolo_txt=yolo)校验黄金法则:打开任意一张图,VOC绿框、YOLO红框应完全重叠。若有偏移,说明坐标转换公式错误(如YOLO用了
x_min/w而非center_x);若红框超出图像,说明归一化分母用错(用了img.width但实际是img.height)。这是比数值校验更直观的最终防线。
本文还有配套的精品资源,点击获取