YOLOX实战:从Labelme标注到模型训练的全链路避坑指南
如果你正在尝试用YOLOX训练自己的数据集,大概率已经踩过不少坑了。从标注工具的选择,到数据格式转换,再到训练配置的调整,每一步都可能遇到意想不到的问题。我最近刚完成一个工业质检项目,用YOLOX训练了一个检测微小缺陷的模型,整个过程走下来,发现网上很多教程都只讲了“应该怎么做”,却没告诉你“为什么这么做”以及“出错了怎么办”。
这篇文章就是我在实战中积累的经验总结,重点解决从Labelme标注到COCO格式转换这个最让人头疼的环节。我会分享完整的Python脚本、常见报错的解决方案,还有那些只有真正踩过坑才知道的细节。
1. 数据标注:Labelme的最佳实践与常见陷阱
很多人觉得标注就是画框,但实际项目中,标注的质量直接决定了模型的上限。我用Labelme标注了超过5000张工业零件图像,总结出几个关键点。
1.1 标注前的准备工作
在打开Labelme之前,先做好这些准备:
-
统一图像格式:将所有图像转换为JPG格式,避免PNG的透明通道带来问题
-
规范命名规则:使用有意义的文件名,比如
part_001.jpg,不要用IMG_20240101.jpg这种无意义的名字 -
创建类别映射表:提前定义好所有类别及其ID,比如:
class_mapping = { "scratch": 1, # 划痕 "dent": 2, # 凹陷 "crack": 3, # 裂纹 "stain": 4, # 污渍 "missing_part": 5 # 缺件 }
注意:COCO格式要求类别ID从1开始,0预留给背景。很多转换脚本出错就是因为没处理好这个细节。
1.2 Labelme标注时的技术细节
标注时容易忽略但很重要的点:
- 矩形框的绘制顺序:Labelme默认支持多边形,但YOLOX只需要矩形框。绘制矩形时,建议从左上角开始,到右下角结束,保持一致性
- 标签命名规范:使用英文小写,避免空格和特殊字符。比如用
scratch而不是划痕或scratch_01 - 复杂目标的处理:对于被遮挡的目标,标注可见部分即可,不要猜测被遮挡的部分
标注完成后,你会得到这样的文件结构:
labelme_data/
├── image_001.jpg
├── image_001.json
├── image_002.jpg
├── image_002.json
└── ...
每个JSON文件包含这样的结构:
{
"version": "5.1.1",
"flags": {},
"shapes": [
{
"label": "scratch",
"points": [[100, 150], [300, 250]],
"group_id": null,
"shape_type": "rectangle",
"flags": {}
}
],
"imagePath": "image_001.jpg",
"imageData": "base64编码的图像数据",
"imageHeight": 1080,
"imageWidth": 1920
}
1.3 标注质量检查
标注完成后,一定要做质量检查。我写了一个简单的检查脚本:
import json
import os
from pathlib import Path
def check_labelme_annotations(labelme_dir):
"""检查Labelme标注文件的质量"""
issues = []
for json_file in Path(labelme_dir).glob("*.json"):
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# 检查图像文件是否存在
image_path = Path(labelme_dir) / data["imagePath"]
if not image_path.exists():
issues.append(f"❌ {json_file.name}: 图像文件不存在 - {data['imagePath']}")
continue
# 检查标注数量
if len(data["shapes"]) == 0:
issues.append(f"⚠️ {json_file.name}: 没有标注框")
# 检查每个标注框
for i, shape in enumerate(data["shapes"]):
if shape["shape_type"] != "rectangle":
issues.append(f"⚠️ {json_file.name}: 标注{i}不是矩形框")
points = shape["points"]
if len(points) != 2:
issues.append(f"❌ {json_file.name}: 标注{i}坐标点数量错误")
# 检查坐标是否在图像范围内
x1, y1 = points[0]
x2, y2 = points[1]
if x1 < 0 or y1 < 0 or x2 > data["imageWidth"] or y2 > data["imageHeight"]:
issues.append(f"❌ {json_file.name}: 标注{i}超出图像范围")
return issues
# 使用示例
issues = check_labelme_annotations("./labelme_data")
for issue in issues:
print(issue)
这个脚本能帮你发现标注中的常见问题,避免训练时出现奇怪的错误。
2. Labelme转COCO:完整脚本与深度解析
网上能找到很多转换脚本,但大多数都有各种问题。我结合多个项目的经验,重写了一个更健壮的版本。
2.1 完整的转换脚本
#!/usr/bin/env python3
"""
Labelme转COCO格式转换脚本
支持矩形框标注,自动处理类别映射,支持训练集/验证集划分
"""
import os
import json
import numpy as np
import glob
import shutil
import argparse
from pathlib import Path
from sklearn.model_selection import train_test_split
from datetime import datetime
import base64
import cv2
class LabelmeToCOCOConverter:
"""将Labelme标注转换为COCO格式"""
def __init__(self, class_mapping):
"""
初始化转换器
Args:
class_mapping: 类别名称到ID的映射字典,如 {"scratch": 1, "dent": 2}
"""
self.class_mapping = class_mapping
self.coco_data = {
"info": {
"description": "Custom dataset",
"url": "",
"version": "1.0",
"year": datetime.now().year,
"contributor": "",
"date_created": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
},
"licenses": [{
"url": "",
"id": 0,
"name": ""
}],
"images": [],
"annotations": [],
"categories": []
}
# 初始化类别信息
self._init_categories()
# 计数器
self.image_id = 0
self.annotation_id = 0
def _init_categories(self):
"""初始化COCO类别信息"""
for class_name, class_id in self.class_mapping.items():
self.coco_data["categories"].append({
"id": class_id,
"name": class_name,
"supercategory": "none"
})
def convert_single_file(self, json_path, image_dir=None):
"""
转换单个Labelme JSON文件
Args:
json_path: JSON文件路径
image_dir: 图像目录,如果为None则从JSON同目录读取
Returns:
bool: 转换是否成功
"""
try:
# 读取JSON文件
with open(json_path, 'r', encoding='utf-8') as f:
labelme_data = json.load(f)
# 获取图像路径
if image_dir:
image_filename = labelme_data["imagePath"]
image_path = Path(image_dir) / image_filename
else:
image_path = Path(json_path).parent / labelme_data["imagePath"]
# 读取图像获取尺寸
if image_path.exists():
img = cv2.imread(str(image_path))
height, width = img.shape[:2]
else:
# 如果图像文件不存在,尝试从base64数据解码
if "imageData" in labelme_data:
img_data = base64.b64decode(labelme_data["imageData"])
nparr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
height, width = img.shape[:2]
else:
print(f"⚠️ 警告: 无法获取图像尺寸,使用JSON中的尺寸")
height = labelme_data.get("imageHeight", 0)
width = labelme_data.get("imageWidth", 0)
# 添加图像信息
image_info = {
"id": self.image_id,
"file_name": Path(labelme_data["imagePath"]).name,
"height": height,
"width": width,
"license": 0,
"date_captured": None
}
self.coco_data["images"].append(image_info)
# 处理每个标注框
for shape in labelme_data.get("shapes", []):
if shape["shape_type"] != "rectangle":
print(f"⚠️ 跳过非矩形标注: {shape['label']}")
co

305

被折叠的 条评论
为什么被折叠?



