1. 为什么我们需要打通PaddleX和Halcon的数据标注在工业视觉项目里尤其是做语义分割比如检测产品表面的划痕、瑕疵或者分割出复杂的零部件我们常常会遇到一个头疼的问题选哪个框架好PaddleX作为国产开源框架的代表上手快、生态友好特别适合快速原型验证和算法迭代。而Halcon呢作为老牌的商业视觉软件在工业现场的稳定性、算法执行效率以及和硬件的集成度上往往有它的独到之处。很多团队在项目前期评估时都希望能在PaddleX上跑一跑也在Halcon上测一测综合对比精度、速度和部署成本。但这就引出了一个核心痛点数据壁垒。你辛辛苦苦用Labelme标注了几百张、几千张图片生成了PaddleX需要的格式。结果想换到Halcon里试试发现格式完全不认难道要重新标注一遍这无疑是时间和人力的巨大浪费。我经历过不止一个项目因为前期没规划好数据流导致后期切换框架时焦头烂额甚至不得不妥协框架选择。所以今天我想跟你分享的就是一套我实战中总结出来的“数据流水线”。我们以最通用的标注工具Labelme为起点构建一套自动化脚本实现数据在PaddleX格式、Halcon格式之间的自由转换。目标很简单一次标注多处使用彻底解放生产力让你能心无旁骛地专注于模型本身的调优和评估。下面我们就从最基础的环节开始一步步搭建这条流水线。2. 从Labelme开始统一的标注起点无论你最终用PaddleX还是Halcon甚至其他任何框架我都强烈建议将Labelme作为你的标注起点。原因有三第一它是开源且基于Python的跨平台谁都能用第二它标注后保存的是JSON文件这是一种结构化的文本格式里面包含了原始的图像信息Base64编码和所有的多边形标注点坐标这是最“原始”和“丰富”的数据形态便于我们向任何格式转换第三社区庞大遇到问题容易找到解决方案。2.1 安装与标注实战安装Labelme非常简单一条命令搞定。我建议创建一个独立的Python虚拟环境来做数据标注和转换相关的工作避免污染你的主开发环境。# 创建并激活虚拟环境以conda为例 conda create -n labelme_env python3.8 conda activate labelme_env # 安装labelme pip install labelme安装完成后直接在命令行输入labelme就能打开图形界面。标注时对于语义分割任务我们使用“多边形Polygon”工具沿着目标轮廓进行勾勒。比如我们要分割电路板上的不同元件就为“电容”、“电阻”、“芯片”分别创建不同的标签Label然后用多边形把它们一个个框出来。这里有个小技巧标注时尽量让多边形闭合得精准一些虽然后续可以处理但好的源头数据能省去很多麻烦。标注完一张图片保存后你会得到一个同名的.json文件。这个文件就是我们的“数据金矿”。2.2 理解Labelme JSON的结构要想灵活转换必须知道我们手里有什么。用文本编辑器打开一个Labelme的JSON文件你会看到类似下面的结构我做了简化{ version: 5.1.1, flags: {}, shapes: [ { label: capacitor, points: [[102.5, 150.3], [155.7, 150.3], ...], // 多边形的顶点坐标 group_id: null, shape_type: polygon, flags: {} }, { label: resistor, points: [[...]], shape_type: polygon, ... } ], imagePath: board_001.jpg, imageData: iVBORw0KGgoAAAANSUhEUgAAASw... // 图片的base64编码 }关键字段是shapes它是一个列表里面每个元素对应一个标注对象。label是类别名points是多边形顶点坐标。imageData包含了完整的图片数据这意味着即使原始图片移动了位置单靠JSON文件也能复原出图片这对于数据管理非常有用。3. 转换为PaddleX训练格式PaddleX为语义分割任务准备的数据格式非常直观一个文件夹放原图例如JPEGImages另一个文件夹放标注图例如Annotations。标注图是单通道的PNG图像标注区域用不同的灰度值填充背景通常为0黑色。3.1 使用PaddleX官方工具转换PaddleX贴心地提供了数据转换工具。假设我们所有的Labelme JSON文件和对应的原图如果JSON里不含imageData则需要原图都放在同一个目录./labelme_data下转换命令如下paddlex --data_conversion --source labelme --to SEG --pics ./labelme_data --annotations ./labelme_data --save_dir ./paddlex_data解释一下参数--source labelme指定源格式为Labelme。--to SEG指定目标格式为语义分割Segmentation。--pics和--annotations这里因为Labelme的JSON和图片可能在同一目录所以指向同一个路径。如果分开存放则分别指定。--save_dir转换后数据的保存目录。执行后./paddlex_data目录下会生成两个子文件夹JPEGImages存放原图和Annotations存放标注的灰度图。你可以用图片查看器打开标注图看看不同的类别会显示为不同灰度的区域。3.2 理解转换逻辑与自定义灰度值有时候PaddleX默认的灰度值映射可能不符合你的习惯或者你需要和Halcon的索引值对齐。这时候我们就需要了解其底层逻辑甚至自己写脚本。本质上这个转换过程就是读取JSON中的每一个shape根据其label在一张全黑的画布背景上将points描述的多边形区域填充为一个特定的灰度值。PaddleX内部会按label首次出现的顺序自动分配灰度值例如123...。如果你想控制这个映射关系比如让“缺陷”类别始终对应灰度值128你就需要自己动手。下面是一个简化版的自定义转换脚本核心思路import json import os import numpy as np from PIL import Image, ImageDraw # 定义你的标签到灰度值的映射 label_to_gray { background: 0, # 背景 scratch: 64, # 划痕 stain: 128, # 污渍 crack: 255 # 裂纹 } def convert_json_to_mask(json_path, output_mask_path): with open(json_path, r) as f: data json.load(f) # 获取图片尺寸从imageData或原图 # ... 解析图片尺寸的代码 ... height, width img_shape # 创建全零背景的掩码图 mask np.zeros((height, width), dtypenp.uint8) for shape in data[shapes]: label shape[label] points shape[points] # 多边形顶点列表 gray_value label_to_gray.get(label, 0) # 获取对应的灰度值默认为背景 # 将多边形顶点转换为PIL需要的格式 polygon_tuple [(p[0], p[1]) for p in points] # 创建一张临时单通道图像绘制多边形 img_pil Image.fromarray(mask) draw ImageDraw.Draw(img_pil) draw.polygon(polygon_tuple, fillgray_value) mask np.array(img_pil) # 保存掩码图 Image.fromarray(mask).save(output_mask_path)通过这样的自定义脚本你就完全掌控了灰度值的分配这为后续转换到Halcon格式打下了坚实的基础。4. 转换为Halcon训练格式Halcon的深度学习工具MVTec Deep Learning Tool, DLT标注后导出的数据格式和PaddleX有本质区别。Halcon的标注图Label Image也是单通道PNG但它填充的不是肉眼可区分的灰度值而是类别的索引值。通常背景索引为0第一个目标类别为1第二个为2以此类推。4.1 理解Halcon DLT的数据结构当你从DLT导出语义分割数据集时会得到类似这样的结构project_folder/ ├── images/ # 存放所有原始图像 │ ├── image_001.png │ └── ... ├── labels/ # 存放所有标注图像索引图 │ ├── image_001.png │ └── ... └── example.hdict # 数据集字典文件包含类别信息等元数据关键点在于labels文件夹下的PNG文件。如果你用普通的图片查看器打开很可能看到一片漆黑因为像素值可能是1、2这样非常小的数字接近黑色。你需要用编程方式读取像素值才能看到差异。4.2 从PaddleX格式转换到Halcon格式假设我们已经有了PaddleX格式的数据JPEGImages和Annotations现在需要转换成Halcon格式。这里的核心操作就是像素值映射将PaddleX标注图中的各种灰度值如64128255替换成Halcon需要的类别索引如123。我们需要建立一个映射表。例如灰度值64划痕 - 索引 1灰度值128污渍 - 索引 2灰度值255裂纹 - 索引 3其他背景- 索引 0下面是一个使用OpenCV实现的批量转换脚本import cv2 import os import numpy as np def convert_paddlex_to_halcon(paddlex_anno_dir, halcon_label_dir, gray_to_index_map): 将PaddleX标注图转换为Halcon格式的索引图。 Args: paddlex_anno_dir: PaddleX Annotations文件夹路径。 halcon_label_dir: 要输出的Halcon labels文件夹路径。 gray_to_index_map: 字典键为PaddleX中的灰度值值为Halcon中的索引值。 os.makedirs(halcon_label_dir, exist_okTrue) for img_name in os.listdir(paddlex_anno_dir): if not img_name.endswith(.png): continue img_path os.path.join(paddlex_anno_dir, img_name) # 以灰度模式读取PaddleX标注图 img_gray cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) # 创建输出图像初始化为0背景 halcon_label np.zeros_like(img_gray, dtypenp.uint8) # 根据映射表替换像素值 for gray_val, index_val in gray_to_index_map.items(): # 找到所有等于当前灰度值的像素位置 mask (img_gray gray_val) # 将这些位置赋值为对应的索引值 halcon_label[mask] index_val # 保存Halcon格式的标注图 output_path os.path.join(halcon_label_dir, img_name) cv2.imwrite(output_path, halcon_label) print(fConverted: {img_name}) # 定义映射关系根据你的PaddleX标注实际情况修改 gray_to_index { 64: 1, # paddlex灰度值64 - halcon索引1 128: 2, # paddlex灰度值128 - halcon索引2 255: 3, # paddlex灰度值255 - halcon索引3 # 注意灰度值0在PaddleX中通常也是背景会自动被halcon_label的初始值0覆盖 } # 执行转换 convert_paddlex_to_halcon(./paddlex_data/Annotations, ./halcon_data/labels, gray_to_index)重要提示别忘了同时把JPEGImages文件夹里的原图复制到Halcon格式的images文件夹下。原图是不需要任何改变的。4.3 创建Halcon数据集字典.hdict文件转换好图像和标签后还需要一个.hdict文件来告诉Halcon数据集的组成和类别信息。这个文件本质是一个JSON格式的文本。你可以手动创建也可以用Halcon的DLT工具“导入”已有的images和labels文件夹来自动生成。不过在纯脚本化流水线中我们也可以自己生成一个简单的版本import json dataset_info { type: segmentation_data, class_ids: { background: 0, scratch: 1, stain: 2, crack: 3 }, class_names: [background, scratch, stain, crack], # 以下路径是相对于.hdict文件位置的或者使用绝对路径 image_dir: ./images, label_dir: ./labels } hdict_path ./halcon_data/example.hdict with open(hdict_path, w) as f: json.dump(dataset_info, f, indent2)这样一个完整的、Halcon DLT可读的数据集就准备好了。5. 终极自动化从Labelme直接到双格式流水线上面我们分步讲解了转换过程。在实际项目中我们肯定希望一个脚本就能搞定所有事情实现“Labelme JSON输入PaddleX和Halcon格式数据同时就绪”的自动化流程。这个完整脚本的思路是遍历所有Labelme JSON文件。为每个JSON文件 a. 解析并生成PaddleX格式的标注图使用自定义灰度值映射。 b.同时利用同样的解析结果和另一套映射规则灰度值-索引值直接生成Halcon格式的索引图。复制原图到两个格式各自的图像文件夹。生成Halcon所需的.hdict文件。这样做的好处是效率最高且避免了中间格式转换可能带来的信息损失或误差。下面给出这个一体化脚本的核心框架import os import json import base64 import shutil from PIL import Image, ImageDraw import numpy as np import cv2 # 配置部分 labelme_dir ./labelme_data paddlex_output_dir ./paddlex_data halcon_output_dir ./halcon_data # 标签定义必须包含_background_ label_config { _background_: {gray: 0, index: 0}, scratch: {gray: 64, index: 1}, stain: {gray: 128, index: 2}, crack: {gray: 255, index: 3} } # def create_dirs(): 创建所有必要的输出目录 os.makedirs(os.path.join(paddlex_output_dir, JPEGImages), exist_okTrue) os.makedirs(os.path.join(paddlex_output_dir, Annotations), exist_okTrue) os.makedirs(os.path.join(halcon_output_dir, images), exist_okTrue) os.makedirs(os.path.join(halcon_output_dir, labels), exist_okTrue) def process_single_json(json_path): 处理单个Labelme JSON文件 with open(json_path, r) as f: data json.load(f) # 1. 获取图像数据并保存原图 image_data data[imageData] img img_b64_to_arr(image_data) # 需要实现或引用此函数 img_pil Image.fromarray(img) base_name os.path.splitext(os.path.basename(json_path))[0] # 保存原图到两个地方 img_pil.save(os.path.join(paddlex_output_dir, JPEGImages, base_name .jpg)) img_pil.save(os.path.join(halcon_output_dir, images, base_name .png)) height, width img.shape[:2] # 创建两张空白画布 paddlex_mask np.zeros((height, width), dtypenp.uint8) halcon_mask np.zeros((height, width), dtypenp.uint8) # 2. 遍历所有标注形状 for shape in data[shapes]: label shape[label] points shape[points] if label not in label_config: print(f警告在文件{base_name}中跳过未定义的标签 {label}) continue gray_val label_config[label][gray] index_val label_config[label][index] # 将多边形绘制到PaddleX掩码上 polygon_pil [(p[0], p[1]) for p in points] img_tmp Image.fromarray(paddlex_mask) draw ImageDraw.Draw(img_tmp) draw.polygon(polygon_pil, fillgray_val) paddlex_mask np.array(img_tmp) # 将多边形绘制到Halcon掩码上相同的多边形不同的填充值 img_tmp2 Image.fromarray(halcon_mask) draw2 ImageDraw.Draw(img_tmp2) draw2.polygon(polygon_pil, fillindex_val) halcon_mask np.array(img_tmp2) # 3. 保存两张掩码图 Image.fromarray(paddlex_mask).save(os.path.join(paddlex_output_dir, Annotations, base_name .png)) Image.fromarray(halcon_mask).save(os.path.join(halcon_output_dir, labels, base_name .png)) print(f已处理: {base_name}) def main(): create_dirs() json_files [f for f in os.listdir(labelme_dir) if f.endswith(.json)] for json_file in json_files: process_single_json(os.path.join(labelme_dir, json_file)) print(所有数据转换完成) # 可选在此处自动生成Halcon的.hdict文件 if __name__ __main__: main()这个脚本就是一个完整的自动化流水线核心。你只需要配置好输入路径、输出路径和标签映射关系运行一次PaddleX和Halcon所需的数据就都准备好了。6. 实战中的坑与经验分享这条路我踩过不少坑这里分享几个关键点希望能帮你绕过去。第一个坑标签一致性。这是最重要的。在Labelme里标注时标签名必须严格一致不能有时用“scratch”有时用“Scratch”有时用“划痕”。建议在项目开始前就定好一个标签名称列表并写入配置文件中所有标注人员统一遵守。否则在转换时会出现类别匹配错误导致标注信息丢失。第二个坑图像编码与颜色通道。Labelme的JSON里存的是RGB图像的Base64编码。而PaddleX和Halcon的标注图都是单通道灰度图。在转换时务必确保你操作的是灰度数组不要不小心当成三通道来处理否则会得到错误的掩码。第三个坑Halcon索引图的查看。就像前面说的Halcon的索引图用普通看图软件看是黑的。调试时我常用一个简单的方法用Python的OpenCV读入后将像素值乘以50或60再显示这样不同索引的差异就能肉眼可见了便于检查转换是否正确。import cv2 halcon_label cv2.imread(halcon_data/labels/image_001.png, cv2.IMREAD_GRAYSCALE) # 放大像素值以便可视化 vis_img halcon_label * 60 cv2.imshow(Halcon Label (Visualized), vis_img) cv2.waitKey(0)第四个坑数据集.hdict文件的路径。在Halcon DLT中创建项目并导入数据集时如果选择这个.hdict文件要确保hdict文件中image_dir和label_dir指向的路径是有效的。最好使用相对于.hdict文件的相对路径这样移动整个数据集文件夹时不会出错。最后建议把整个转换流程脚本化、模块化。你可以将“标签映射配置”单独写成一个config.yaml文件将不同的转换步骤写成函数。这样当你的标注类别增加或者需要支持新的框架比如MMDetection、TensorFlow时只需要修改配置和增加新的输出模块整个数据流水线就能快速适配真正实现“一次标注终身受用”。数据准备不再是瓶颈你才能把更多精力投入到模型设计和调参这些更有创造性的工作中去。