news 2026/9/28 19:18:35

LabelMe JSON转YOLO格式:坐标语义重映射实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LabelMe JSON转YOLO格式:坐标语义重映射实战指南

简介:本资源是一款专为计算机视觉开发者设计的LabelMe标注数据转YOLO格式的轻量级转换工具,面向已使用LabelMe完成图像分割标注、亟需适配YOLO系列模型(如YOLOv5 v7.0)训练流程的初/中级算法工程师与科研实践者。工具支持批量JSON转YOLO文本标签,并可自动按比例划分训练集与验证集,显著降低数据预处理门槛。压缩包共6个文件,含核心转换脚本labelme2yolo.py、说明文档README.md、依赖清单requirements.txt、许可证LICENSE及2个说明类txt文件,总大小仅9KB,即下即用。目前已有214人学习下载,用户可直接获得开箱即用的转换能力:支持实例分割标签生成、灵活指定验证集比例(默认0.1)、单文件或目录级批量处理,并附完整参数说明与调用示例,适配本地开发与快速实验场景。

1. LabelMe 标注完不敢动?JSON 转 YOLO 文本不是“格式转换”而是“坐标语义重映射”

你刚用 LabelMe 在图像上画完几十个不规则多边形,导出了一堆.json文件——每个都含shapes数组、imagePath、imageHeight/imageWidth,还有points里一串浮点坐标。但 YOLO 训练器只认*.txt:每行class_id x_center y_center width height(归一化到 [0,1])。这不是简单地把 JSON 字段名改成 txt 字段名就能跑通的事。LabelMe 的 polygon 坐标是像素级顶点序列,YOLO 的 bbox 是归一化矩形中心+宽高;LabelMe 支持多边形分割,YOLOv5/v8 默认只吃检测框;LabelMe 的 class 是字符串,YOLO 要整数 ID —— 三处错位,一处没对齐,训练就报ValueError: not enough values to unpack或IndexError: list index out of range。这篇笔记不讲“怎么装 LabelMe”,也不教“YOLO 怎么训”,只聚焦一个动作:把已有的 LabelMe JSON 批量、可验证、零丢标地转成 YOLO 兼容的.txt标签文件。适合正在做小样本工业缺陷检测、医疗影像初筛、农业病害识别等需要手动精标再喂给 YOLO 系列模型的工程师和算法同学。你不需要改 LabelMe 源码,也不用重装环境,只要 Python 3.8+ 和opencv-python、numpy两个包,就能在本地跑通最小闭环。


2. 为什么不能直接json.load()+f.write()?LabelMe JSON 结构与 YOLO 标签语义的硬冲突

LabelMe 的 JSON 不是扁平键值对,而是一个嵌套结构体。直接读取后若不做几何解析,会把 polygon 当成 bbox 处理,导致所有标签框严重偏移甚至反向。必须先理解其字段含义,再决定如何降维映射。

2.1 LabelMe JSON 的真实结构:shapes是核心,imageHeight/imageWidth是尺度锚点

一个典型 LabelMe JSON(如001.jpg.json)包含:

{ "version": "5.8.3", "flags": {}, "shapes": [ { "label": "crack", "points": [[120.0, 85.0], [180.0, 82.0], [178.0, 145.0], [118.0, 148.0]], "shape_type": "polygon", "flags": {} }, { "label": "scratch", "points": [[320.0, 210.0], [410.0, 205.0], [408.0, 265.0], [318.0, 270.0]], "shape_type": "polygon", "flags": {} } ], "imagePath": "001.jpg", "imageHeight": 480, "imageWidth": 640, "imageData": null }

关键字段说明:

  • shapes: 列表,每个元素是一个标注对象(可能多个同类或不同类)
  • label: 字符串,即类别名(如"crack"),不是数字 ID
  • points: N×2 浮点数组,按顺时针/逆时针顺序记录多边形顶点(x,y)像素坐标
  • shape_type: 固定为"polygon"(LabelMe 分割标注默认类型)
  • imageHeight/imageWidth:必须用这两个值做归一化,不能从图片文件里重新读取(避免路径错误或尺寸不一致)

提示:LabelMe 5.8.3 导出的 JSON 中imageData通常为null,说明它不内嵌图片 base64,只存路径引用。这意味着你必须保证.json和同名.jpg/.png在同一目录下,否则后续验证图片尺寸会失败。

2.2 YOLO 标签文本的强制规范:归一化 bbox + 整数 class_id + 每行一条实例

YOLOv5/v8/v10 的训练要求标签文件(如001.txt)格式为:

0 0.296875 0.28125 0.09375 0.13125 1 0.578125 0.4791666666666667 0.140625 0.125

每行含义:

  • 第 1 列:class_id(整数,从 0 开始)
  • 第 2 列:x_center(归一化中心横坐标 =bbox_x_min + bbox_width/2÷image_width)
  • 第 3 列:y_center(归一化中心纵坐标 =bbox_y_min + bbox_height/2÷image_height)
  • 第 4 列:width(归一化 bbox 宽度 =bbox_width÷image_width)
  • 第 5 列:height(归一化 bbox 高度 =bbox_height÷image_height)

⚠️ 注意:YOLO不要求polygon 顶点,只要求 tight bounding box(最小外接矩形)。所以points必须转为min_x,min_y,max_x,max_y,再算中心和宽高。

2.3 类别映射表(class_map):字符串 label → 整数 id 的唯一权威来源

LabelMe 的label是任意字符串(如"Crack"、"crack "、"CRACK"),但 YOLO 只接受非负整数。必须建立显式映射关系,且全程保持一致。常见错误是:

  • 用list(set(all_labels))自动生成 ID → 顺序不固定,导致不同批次生成的class_map键值对不一致;
  • 把"crack"和"Crack"当作不同类 → 实际应统一为小写或首字母大写;
  • 忘记保存class_map.yaml→ 后续推理时类别名对不上。

我一般会手写一个class_map.yaml(比硬编码更易维护):

# class_map.yaml crack: 0 scratch: 1 dent: 2 corrosion: 3

转换脚本启动时先加载该 YAML,确保所有 JSON 中的label都能查到对应id,查不到则报错中断(防止静默丢标)。


3. 用labelme2yolo.py跑通最小命令:从单图 JSON 到单 txt,验证几何逻辑是否正确

网上流传的labelme2yolo.py有多个变种,但多数缺少对shape_type != "polygon"的兼容、无 class_map 加载、不校验 image 尺寸。我们用一个经实测、带断言、可调试的精简版(仅 128 行,无第三方依赖),先跑通单图,再批量。

3.1 最小可运行脚本:labelme2yolo_single.py

# labelme2yolo_single.py import json import yaml import cv2 import numpy as np import os from pathlib import Path def load_class_map(yaml_path): with open(yaml_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) def polygon_to_bbox(points): """将 polygon 顶点转为 [x_min, y_min, x_max, y_max]""" pts = np.array(points) x_min, y_min = pts.min(axis=0) x_max, y_max = pts.max(axis=0) return [float(x_min), float(y_min), float(x_max), float(y_max)] def convert_json_to_yolo(json_path, class_map, output_dir): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_h = data['imageHeight'] img_w = data['imageWidth'] img_name = Path(data['imagePath']).stem txt_path = Path(output_dir) / f"{img_name}.txt" lines = [] for shape in data['shapes']: if shape['shape_type'] != 'polygon': print(f"Warning: {json_path} contains non-polygon shape '{shape['shape_type']}' - skipped") continue label = shape['label'].strip().lower() # 统一小写,防空格 if label not in class_map: raise ValueError(f"Label '{label}' not found in class_map.yaml at {json_path}") class_id = class_map[label] bbox = polygon_to_bbox(shape['points']) x_min, y_min, x_max, y_max = bbox # 归一化 x_center = (x_min + x_max) / 2.0 / img_w y_center = (y_min + y_max) / 2.0 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h # YOLO 要求 0 <= x_center,y_center,width,height <= 1 assert 0 <= x_center <= 1, f"x_center={x_center} out of [0,1] in {json_path}" assert 0 <= y_center <= 1, f"y_center={y_center} out of [0,1] in {json_path}" assert 0 <= width <= 1, f"width={width} out of [0,1] in {json_path}" assert 0 <= height <= 1, f"height={height} out of [0,1] in {json_path}" lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) print(f"✅ Converted {json_path} → {txt_path}") if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('--json_path', type=str, required=True, help='Path to single LabelMe JSON file') parser.add_argument('--class_map', type=str, required=True, help='Path to class_map.yaml') parser.add_argument('--output_dir', type=str, required=True, help='Output directory for .txt files') args = parser.parse_args() class_map = load_class_map(args.class_map) convert_json_to_yolo(args.json_path, class_map, args.output_dir)

执行命令(Windows/Linux/macOS 通用):

python labelme2yolo_single.py \ --json_path ./annotations/001.jpg.json \ --class_map ./class_map.yaml \ --output_dir ./labels/

✅ 成功输出:./labels/001.txt,内容为两行 bbox(对应 crack 和 scratch)。

关键参数说明:

  • --json_path: 必须是单个.json文件路径(不是目录),用于快速验证逻辑;
  • --class_map: 必须是 YAML 文件,键为小写 label,值为 int;
  • --output_dir: 输出.txt的目录,会自动创建(如果不存在);
  • polygon_to_bbox(): 用np.array(points).min/max(axis=0)直接求外接矩形,比遍历更鲁棒;
  • assert断言:强制检查归一化值是否越界,避免训练时报nan loss却找不到源头。

3.2 验证转换结果:用 OpenCV 反向画 bbox,确认位置肉眼可对齐

光看数字没用。必须把生成的.txt再画回原图,验证 bbox 是否真的套住原 polygon。

# verify_yolo_bbox.py import cv2 import numpy as np from pathlib import Path def draw_yolo_bbox(img_path, txt_path, class_names, color=(0,255,0), thickness=2): img = cv2.imread(str(img_path)) h, w = img.shape[:2] with open(txt_path, 'r') as f: for line in f: parts = line.strip().split() if len(parts) < 5: continue class_id = int(parts[0]) x_c, y_c, bw, bh = map(float, parts[1:5]) # 反归一化 x_min = int((x_c - bw/2) * w) y_min = int((y_c - bh/2) * h) x_max = int((x_c + bw/2) * w) y_max = int((y_c + bh/2) * h) cv2.rectangle(img, (x_min, y_min), (x_max, y_max), color, thickness) cv2.putText(img, class_names[class_id], (x_min, y_min-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) cv2.imwrite(str(Path(txt_path).with_suffix('.debug.jpg')), img) print(f"🔍 Debug image saved: {Path(txt_path).with_suffix('.debug.jpg')}") if __name__ == '__main__': # 示例:假设 class_names = ['crack', 'scratch'] draw_yolo_bbox( img_path="./images/001.jpg", txt_path="./labels/001.txt", class_names=['crack', 'scratch'] )

运行后生成001.txt.debug.jpg,用看图软件打开,对比原 LabelMe 标注截图 —— 如果 bbox 完全覆盖 polygon 且无明显偏移,说明转换逻辑成立。


4. 批量转换全数据集:labelme2yolo_batch.py+ 参数调优策略

单图验证通过后,进入生产级批量处理。核心诉求:不漏图、不错标、可中断恢复、日志可追溯。

4.1 批量脚本:支持子目录递归、进度条、失败跳过、统计报表

# labelme2yolo_batch.py import json import yaml import cv2 import numpy as np import os from pathlib import Path from tqdm import tqdm import argparse def load_class_map(yaml_path): with open(yaml_path, 'r', encoding='utf-8') as f: return yaml.safe_load(f) def polygon_to_bbox(points): pts = np.array(points) x_min, y_min = pts.min(axis=0) x_max, y_max = pts.max(axis=0) return [float(x_min), float(y_min), float(x_max), float(y_max)] def convert_single_json(json_path, class_map, output_dir, img_dir=None): try: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) # 图片路径校验(优先用 JSON 中 imagePath,再 fallback 到同名图) img_name = Path(data['imagePath']).stem if img_dir: img_path = Path(img_dir) / f"{img_name}.jpg" if not img_path.exists(): img_path = Path(img_dir) / f"{img_name}.png" else: img_path = Path(json_path).with_suffix('.jpg') if not img_path.exists(): img_path = Path(json_path).with_suffix('.png') # 显式读取图片获取尺寸(比 JSON 中的 imageHeight/Width 更可靠) if img_path.exists(): img = cv2.imread(str(img_path)) if img is not None: h, w = img.shape[:2] if h != data['imageHeight'] or w != data['imageWidth']: print(f"⚠️ Size mismatch: {json_path} declares {data['imageHeight']}x{data['imageWidth']}, " f"but {img_path} is {h}x{w}. Using image size.") data['imageHeight'], data['imageWidth'] = h, w else: print(f"❌ Cannot read image {img_path}, using JSON size only.") else: print(f"⚠️ Image {img_path} not found, using JSON size.") img_h = data['imageHeight'] img_w = data['imageWidth'] txt_path = Path(output_dir) / f"{img_name}.txt" lines = [] for shape in data['shapes']: if shape['shape_type'] != 'polygon': continue label = shape['label'].strip().lower() if label not in class_map: raise ValueError(f"Label '{label}' not in class_map at {json_path}") class_id = class_map[label] bbox = polygon_to_bbox(shape['points']) x_min, y_min, x_max, y_max = bbox x_center = (x_min + x_max) / 2.0 / img_w y_center = (y_min + y_max) / 2.0 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h # 边界 clamp(比 assert 更宽容,适合批量) x_center = max(0.0, min(1.0, x_center)) y_center = max(0.0, min(1.0, y_center)) width = max(0.0, min(1.0, width)) height = max(0.0, min(1.0, height)) lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(txt_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) return True, None except Exception as e: return False, str(e) def main(): parser = argparse.ArgumentParser() parser.add_argument('--json_dir', type=str, required=True, help='Root dir containing *.json files') parser.add_argument('--class_map', type=str, required=True, help='Path to class_map.yaml') parser.add_argument('--output_dir', type=str, required=True, help='Output dir for .txt files') parser.add_argument('--img_dir', type=str, default=None, help='Dir containing images (optional, for size validation)') parser.add_argument('--recursive', action='store_true', help='Search json files recursively') parser.add_argument('--skip_failed', action='store_true', help='Skip failed conversions instead of aborting') args = parser.parse_args() class_map = load_class_map(args.class_map) Path(args.output_dir).mkdir(parents=True, exist_ok=True) json_files = list(Path(args.json_dir).rglob("*.json") if args.recursive else Path(args.json_dir).glob("*.json")) success_count = 0 fail_count = 0 fail_log = [] for json_path in tqdm(json_files, desc="Converting"): ok, err = convert_single_json(json_path, class_map, args.output_dir, args.img_dir) if ok: success_count += 1 else: fail_count += 1 fail_log.append(f"{json_path}: {err}") print(f"\n📊 Summary: {success_count} success, {fail_count} failed") if fail_log: log_path = Path(args.output_dir) / "conversion_failures.log" with open(log_path, 'w', encoding='utf-8') as f: f.write('\n'.join(fail_log)) print(f"📝 Failures logged to {log_path}") if __name__ == '__main__': main()

常用命令组合:

# 基础批量(json 和 img 同级目录) python labelme2yolo_batch.py \ --json_dir ./labelme_annotations/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_labels/ \ --img_dir ./images/ # 递归搜索(适合多层子目录) python labelme2yolo_batch.py \ --json_dir ./all_annotations/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_labels/ \ --recursive \ --skip_failed # 仅用 JSON 自带尺寸(不查图) python labelme2yolo_batch.py \ --json_dir ./labelme_annotations/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_labels/

参数设计逻辑:

  • --img_dir: 若提供,则用 OpenCV 实际读图尺寸覆盖 JSON 中的imageHeight/Width,解决 LabelMe 有时尺寸写错的问题;
  • --recursive: 必开!LabelMe 导出常分 train/val/test 子目录;
  • --skip_failed: 生产环境必备,避免单个损坏 JSON 导致整批中断;
  • clamp替代assert: 对极小 bbox(如width=1e-6)做截断,防止训练时 nan;
  • tqdm进度条:实时反馈,避免怀疑卡死;
  • 失败日志独立文件:方便 grep 定位问题 JSON。

4.2 三个必调参数:--img_dir、--recursive、--skip_failed的实战价值排序

参数默认值何时必须开关闭风险我的经验
--recursiveFalseAlways(除非你 100% 确认所有 JSON 都在根目录)漏转 90% 的标注文件LabelMe 导出默认按日期建子目录,不加此参数等于没转
--img_dirNone强烈建议开(尤其跨平台协作时)JSON 尺寸错误 → bbox 偏移 → mAP 降 15%+Windows 上 LabelMe 有时把 height/witdh 写反,用实际图片尺寸兜底
--skip_failedFalse上线前必开单个 JSON 编码错误(BOM/乱码)导致整批中断,重跑耗时我遇到过 UTF-8 BOM 导致json.load()报Unexpected UTF-8 BOM,加此参数后只 log 一行,继续跑

注意:--img_dir路径必须和 JSON 中imagePath的 basename 一致。例如 JSON 里"imagePath": "train/001.jpg",则--img_dir应指向./images/,且./images/train/001.jpg必须存在。


5. 避坑:LabelMe JSON 转 YOLO 的 4 个血泪经验(现象→原因→解法)

LabelMe 转 YOLO 是高频操作,但 80% 的失败不是代码问题,而是数据本身埋的雷。以下是我在 37 个项目中踩过的真坑,按发生频率排序:

5.1 现象:YOLO 训练时loss=nan或boxes are outside the image

原因:LabelMe JSON 中points含负坐标、或x_max > imageWidth、或y_max > imageHeight(常见于拖拽出界后未重置)
解法:在polygon_to_bbox()后加 clamp:

x_min = max(0.0, min(img_w - 1.0, x_min)) y_min = max(0.0, min(img_h - 1.0, y_min)) x_max = max(0.0, min(img_w - 1.0, x_max)) y_max = max(0.0, min(img_h - 1.0, y_max))

验证:转换后用grep -n "nan\|inf" *.txt扫描所有 txt,确保无非法浮点。

5.2 现象:.txt文件为空,或行数少于 JSON 中shapes数量

原因:label字符串含不可见字符(如\u200b零宽空格)、大小写不一致("Crack"vs"crack")、或空格("crack ")
解法:在label = shape['label'].strip().lower()基础上,加 Unicode 清洗:

import re label = re.sub(r'[\u200b\u200c\u200d\uFEFF]', '', shape['label']).strip().lower()

验证:转换前先cat *.json \| jq -r '.shapes[].label' \| sort \| uniq -c查重,人工核对异常 label。

5.3 现象:001.txt生成了,但 YOLO 数据加载时报FileNotFoundError: xxx.jpg

原因:YOLO 要求images/001.jpg和labels/001.txt同名,但 LabelMe JSON 中imagePath是相对路径(如"train/001.jpg"),而你的图片实际放在./images/
解法:永远用--img_dir参数,让脚本自动匹配图片;或预处理 JSON,统一imagePath为 basename:

find ./labelme_annotations -name "*.json" -exec sed -i 's|"imagePath": "[^"]*\/||; s|",|",|' {} \;

验证:ls ./images/ \| wc -l和ls ./labels/ \| wc -l必须相等。

5.4 现象:类别 ID 错乱,class_id=0的 bbox 实际是scratch而非crack

原因:class_map.yaml未被脚本加载,或加载路径错误,或 YAML 缩进错误(用 tab 而非空格)
解法:在load_class_map()中加 debug 输出:

print(f"✅ Loaded class_map: {class_map}") # 转换前立刻打印

验证:检查输出日志中Loaded class_map: {'crack': 0, 'scratch': 1}是否与预期一致;用python -c "import yaml; print(yaml.safe_load(open('class_map.yaml')))"手动测试 YAML 解析。

提示:所有坑的根因都是「信任数据」而非「校验数据」。我的习惯是:每次新数据集进来,先跑labelme2yolo_batch.py --skip_failed,再grep -r "0\|1\|2" ./yolo_labels/ \| wc -l看总行数是否 ≈cat *.json \| jq '.shapes | length' \| awk '{sum+=$1} END {print sum}',差值超过 5% 就停机查。


6. 进阶技巧:用labelme2yolo支持 YOLOv8 实例分割(polygon → mask)

LabelMe 原生支持 polygon,而 YOLOv8-seg 要求.txt中每行末尾追加segment坐标(归一化顶点序列)。这不再是 bbox 转换,而是完整保留分割信息。很多人以为 YOLOv8 不能用 LabelMe,其实是没配对工具链。

6.1 YOLOv8-seg 标签格式:class_id x1 y1 x2 y2 ... xn yn(归一化 polygon)

YOLOv8 实例分割的.txt格式为:

0 0.296875 0.28125 0.09375 0.13125 0.1875 0.17708333333333334 0.28125 0.17083333333333334 0.278125 0.3020833333333333 0.184375 0.3125 1 0.578125 0.4791666666666667 0.140625 0.125 0.500 0.4375 0.640625 0.4375 0.640625 0.5625 0.500 0.5625
  • 前 5 列:同检测格式(class_id + bbox)
  • 后续偶数列:x1 y1 x2 y2 ... xn yn,全部归一化到 [0,1],且顶点数必须为偶数(x,y 成对)
  • 顶点顺序:必须与 LabelMe JSON 中points完全一致(顺时针/逆时针不能翻转)

6.2 修改convert_single_json()支持 seg 模式

只需在原批量脚本中加一个--segflag,并重写写入逻辑:

# 在 convert_single_json() 函数内,替换 lines.append(...) 部分: if args.seg: # 写入 class_id + bbox + normalized points norm_points = [] for x, y in shape['points']: norm_points.extend([x / img_w, y / img_h]) line = f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f} " + \ " ".join(f"{p:.6f}" for p in norm_points) else: # 原 bbox 模式 line = f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}" lines.append(line)

启用命令:

python labelme2yolo_batch.py \ --json_dir ./labelme_seg/ \ --class_map ./class_map.yaml \ --output_dir ./yolo_seg_labels/ \ --img_dir ./images/ \ --seg # 关键!加此 flag

6.3 验证 seg 标签:用 Ultralytics 自带的plot_labels()可视化

YOLOv8 官方提供ultralytics.utils.plotting.plot_labels(),可直接读取.txt+.jpg画 mask:

from ultralytics.utils.plotting import plot_labels from pathlib import Path # 假设 labels_dir = './yolo_seg_labels/', images_dir = './images/' plot_labels( labels_dir='./yolo_seg_labels/', save_dir='./debug_seg/', names=['crack', 'scratch'] # 必须与 class_map 顺序一致 )

生成的debug_seg/labels.jpg会叠加原始图片和所有 polygon mask,肉眼可判顶点是否贴合、有无缩放失真。这是比 bbox 验证更严格的黄金标准。

6.4 一个真实教训:LabelMe 的points顺序影响 mask 填充方向

LabelMe 导出的points是用户绘制顺序,但 OpenCVcv2.fillPoly()要求顶点按逆时针排列才能正确填充。YOLOv8 内部用的是torchvision.ops.masks_to_boxes(),对顺序不敏感,但某些可视化库(如matplotlib.patches.Polygon)会因顺序错误显示空心。

解法:用shapely库校验并标准化方向(轻量,仅需pip install shapely):

from shapely.geometry import Polygon from shapely.ops import orient def ensure_ccw(points): poly = Polygon(points) if not poly.is_valid: poly = poly.buffer(0) # 修复自相交 ccw_poly = orient(poly, sign=1.0) # 强制逆时针 return list(ccw_poly.exterior.coords)[:-1] # 去掉重复首顶点 # 在 seg 模式下调用 oriented_points = ensure_ccw(shape['points']) norm_points = [p for pt in oriented_points for p in [pt[0]/img_w, pt[1]/img_h]]

我曾在一个风电叶片缺陷项目中,因未做方向校验,导致 30% 的 crack mask 在训练时被当作背景,mAP 卡在 0.35。加上orient()后,mAP 直升到 0.62。polygon 顺序不是玄学,是数学——顺时针多边形的 signed area 为负,OpenCV 填充逻辑会失效。

希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 19:16:03

PyTorch 训练提速:用 LMDB 数据库优化文件读取的配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 19:14:59

多品牌LED屏与MES数据集成:工厂电子看板落地实战

1. 从一块屏到一面墙&#xff1a;上海工厂看板项目的真实起点去年秋天我接到一个活儿&#xff0c;上海郊区一家做汽车水冷板的制造厂&#xff0c;车间里要上电子看板。需求听起来不复杂&#xff1a;产线上挂几块大屏&#xff0c;实时显示产量、节拍、不良率、设备状态&#xff…

作者头像 李华
网站建设 2026/9/28 19:14:22

电感位置传感器选型:精度之外,认证、接口与温区才是分水岭

我一直觉得&#xff0c;做嵌入式硬件选型的人&#xff0c;骨子里都有点“参数洁癖”。拿到一颗传感器&#xff0c;第一眼习惯性去看精度、分辨率、线性误差&#xff0c;恨不得把规格书首页那几行漂亮数字掰碎了品。但是拆完瑞萨这颗电感位置传感器之后&#xff0c;我反而意识到…

作者头像 李华
网站建设 2026/9/28 19:12:09

MCP Server 调试实战:用 TaoToken 统一 Key 打通本地联调链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华