简介:本资源为托盘实例分割数据集,面向物流自动化、工业机器人视觉集成及制造业质量检测等场景,适合算法工程师与研究人员用于目标检测与实例分割模型的训练验证。数据共676张JPEG图片,按训练、验证、测试集划分,包含palletfront(托盘正面)与palletpocket(托盘口袋)两类关键部件的多边形标注,格式为YOLO实例分割格式,便于直接接入主流框架。压缩包内共有1354个文件,主要包括676个jpg图像、676个txt标签文件,另附yaml配置文件与docx说明文档,整体包体约29.48MB,结构清晰便于快速加载。目前已有62人学习浏览。该数据集的价值在于提供真实物流与工业场景下的精准实例分割标注,帮助开发者构建能够精确识别和分割托盘部件的AI模型,并进一步应用于堆叠、分类、库存管理及机器人抓取放置等自动化任务,也可用于工业环境下实例分割算法的研究与性能提升。
1. 托盘实例分割数据集:从拿到 zip 到能训练模型的完整路径
仓库里一排托盘堆叠在一起,普通目标检测只能给出一堆互相打架的矩形框,而实例分割能把每个托盘的边缘像描边一样单独勾出来,直接数出“这一垛到底有几个”。托盘实例分割数据集_20251120_043045.zip从命名看就是一次带时间戳的数据集快照,里面的核心资产不是训练好的权重,而是图片和对应的轮廓标注。拿到这个包的人,多半是想在 YOLO 这类分割框架上做托盘实例分割,用来做托盘计数、AGV 调度或者货位占用判断。这篇笔记按落地顺序走一遍:先把实例分割数据集的概念和格式理清,再解压、检查、转换成训练目录,然后跑一次分割训练,最后把最容易翻车的几个坑单独列出来。
2. 托盘实例分割数据集到底存的什么:语义、格式与标注字段
2.1 先分清实例分割、语义分割和目标检测
很多做托盘项目的人一开始会把语义分割和实例分割混在一起,实际差别非常直接。语义分割对图像里每个像素做类别判断,把所有“托盘”像素归成一类,输出是一张颜色掩膜,同类物体之间不做区分。一台相机对着堆叠的三层托盘,语义分割会输出一整块连通区域,告诉你“这里有托盘”,但没法告诉你“这里有三层”。目标检测退一步,输出每个托盘的矩形框,虽然有了“几个目标”的概念,但框会包含托盘之间的空隙和背后的货架,而且倾斜、错位的托盘在框里只有很小的面积占比。
实例分割正好补上这两个缺口。它既对每个像素做语义分类,又给每个同类目标分配独立的实例编号,输出是 mask 轮廓、bbox 和类别。放在托盘场景里,实例分割可以直接输出“第 1 个托盘的外轮廓坐标”“第 2 个托盘的外轮廓坐标”,每个托盘即是一个实例。这也是这类名为“托盘实例分割数据集”的压缩包最常见的使用场景:训练一个能数得清堆叠层数、能给出每个托盘轮廓的视觉模型。
在 YOLO 系列里,实例分割任务被实现为 segment 体系,训练阶段多一条 mask 分支,输出既包含检测框也包含二值掩膜。很多人误以为实例分割只是把语义分割的模型改个输出头,实际差别在于标签格式和损失计算完全不同。语义分割的标签是一张和原图等大的 PNG 像素图,实例分割的标签则是一组多边形坐标或者按实例编码的 RLE,理解这个差异,才能看懂数据集里面的内容。
2.2 托盘数据集的“交付三格式”
一个打包好的实例分割数据集,市面上最常见的交付格式是三种:COCO JSON、YOLO seg txt、以及 LabelMe JSON。文件名里的20251120_043045更像打包历史版本的时间戳,不是标注格式标志,真正决定你能不能直接训练的是压缩包内部结构。
COCO JSON 是把所有图片路径、标注框、segmentation 多边形、类别映射放在一个 JSON 里,适合作为中转格式,很多开箱数据集的标注文件就叫instances.json。YOLO seg txt 是 Ultralytics 系列训练时实际读取的格式,每张图片名对应一个同名.txt文件,每行保存一个实例的类别 ID 和归一化多边形坐标。LabelMe JSON 是标注工具产生的原始格式,通常每张图一个 JSON,需要二次转换。
还有少部分托盘数据集会用 PNG mask 做标签,每个实例用不同颜色索引区分,但这类格式在 YOLO 上不能直接训练,也缺乏拓扑校验,我一般不建议收这种。收到 zip 后第一件事不是解压训练,而是打开文件列表确认它是哪种。
| 格式 | 优点 | 常见问题 |
|---|---|---|
| COCO JSON | 信息完整、有 bbox/area/iscrowd,生态工具多 | 文件大、解析复杂 |
| YOLO seg txt | 训练直接读、每图独立、方便增量标注 | 缺少校验字段,异常不易发现 |
| LabelMe JSON | 标注工具原生,可逆编辑 | 需要转换,漏点会生成畸形 mask |
2.3 COCO 标注里一个托盘到底记了什么
如果 zip 里是 COCO 格式,核心文件instances.json的结构和公开的 COCO2017 数据集结构一致。顶层通常有三个数组:images、annotations、categories。images里记录每张图片的 id、宽、高和文件名;categories里是id与类别名的映射,托盘数据集这里一般只有一个pallet类;annotations是真正要关心的部分。
单个托盘实例的 annotation 大概长这样:
{ "id": 1, "image_id": 1, "category_id": 1, "bbox": [320, 180, 210, 140], "area": 16800.5, "iscrowd": 0, "segmentation": [[320, 180, 510, 180, 530, 320, 300, 300]] }这里的segmentation是二维数组,内层按x1, y1, x2, y2, ...交替存储一个封闭多边形的像素坐标,不是从 0 到 1 的归一化值。bbox是[左上角 x, 左上角 y, 宽度, 高度],注意不是中心点表示。area最好用多边形真实面积,有些转换工具偷懒填了 bbox 面积,训练时对 mask 损失权重会有微小影响。iscrowd在托盘场景几乎应该全是 0,如果有 1 表示该区域是人群或不可数目标,YOLO seg 转换时会直接丢弃,避免污染。
我见过不少由 LabelMe 导出的 COCO 文件,segmentation的外层字段结构会出现两条坐标链,例如[[x1,y1,x2,y2,...,x1,y1]]结尾重复起点。转换脚本应在上游兼容这种闭合写法,否则后续归一化时会多出一个点,YOLO 训练不会报错,但 mask 形状会有一道无谓的割线。
2.4 YOLO seg txt 格式与归一化规则
把 COCO JSON 转换成 YOLO seg txt 时,每一张图片的标注会写进和图片同名的.txt文件。对托盘实例来说,每一行格式是:
0 0.5123 0.2812 0.8125 0.2810 0.8301 0.4980 0.4700 0.4880第一个数字是类别 ID,接下来所有坐标按x, y交替排列,且全部除以图片宽度和高度做归一化。这里有个老生常谈的坑:分母必须用图片实际像素尺寸,而不是 COCO JSON 里images字段的宽高。如果拍照后有 EXIF 旋转,或标注工具对宽高做了缩放,JSON 里记录的分辨率和cv2.imread读出来的不一致,转换出来的坐标全都会偏移。
归一化后的坐标是浮点数,但 YOLO 训练读取时会对 mask 做再缩放。坐标精度保留到 6 位小数通常够用,保留更多不会显著提升效果,保留太少会出现多边形锯齿。托盘这类规则矩形物体,点数量一般在 8 到 20 个之间,如果某个标注多边形有上百个点,先怀疑是不是工具把 mask 边缘自动拟合成了密集折线,这种文件后续训练会拖慢数据加载。训练前的数据检查脚本里,可以顺手把单实例点数超过 100 的标注列出来人工复查。
3. 从 zip 到训练目录:解压检查、转 YOLO 格式、场景拆分
3.1 解压前先看清单,确认目录名和标注是否匹配
收到托盘实例分割数据集_20251120_043045.zip这类文件,我习惯先不解压,用unzip -l看压缩包内部结构,避免直接解压时把一堆零散图片撒得满磁盘都是。
unzip -l 托盘实例分割数据集_20251120_043045.zip | head -50 mkdir -p /data/pallet_dataset unzip -q 托盘实例分割数据集_20251120_043045.zip -d /data/pallet_datasethead -50只看前 50 行,足够判断顶层是images/和annotations/两个目录,还是所有图片平铺在根目录。如果看到文件名有中文或空格,建议解压后统一重命名,因为训练脚本和后续 shell 命令都可能被空格坑到。解压到/data/pallet_dataset这类固定路径,比直接在用户目录解压更利于 data.yaml 里的路径保持稳定。-q参数抑制解压输出,不然几百张图片会把终端刷满。
3.2 用 Python 做一次体检:类别、数量、多边形合法性
拿到目录后先写一个几行的 Python 脚本做检查,不要急着训练。常见做法是直接加载 COCO JSON 并统计图片数、标注数、类别分布,然后抽样画 mask 和原图对比。
import json from collections import Counter with open("/data/pallet_dataset/annotations/instances.json", "r", encoding="utf-8") as f: coco = json.load(f) cat_map = {cat["id"]: cat["name"] for cat in coco["categories"]} img_map = {img["id"]: img["file_name"] for img in coco["images"]} anns = coco["annotations"] print("图片数:", len(coco["images"])) print("标注数:", len(anns)) print("类别分布:", Counter(cat_map[a["category_id"]] for a in anns)) for a in anns[:20]: seg = a["segmentation"][0] if len(seg) < 6: print("疑似错误标注:", a["id"], img_map[a["image_id"]])这段脚本的意义是把肉眼不好发现的结构性问题暴露出来。len(seg) < 6意味着多边形少于三个点,不可能构成有效轮廓,这类标注进入训练集后网络会学到噪声。下行打印的annotations id可以帮助回原图定位问题标注。category 分布这里也能提示类别失衡:如果托盘只有一类,但另一个类别出现在历史数据中,说明交付版本混入了不同批次的标注规则。
如果脚本输出异常,先修格式再谈训练,否则后面所有指标都是假象。COCO JSON 校验这块,我一般只看三点:segmentation是否闭合、坐标是否越界、类别 ID 是否连续从 0 开始。YOLO 训练要求类别 ID 从 0 连续编号,如果 JSON 里类别 ID 从 1 开始,转换时得像class_id = cat_id - 1这样规整。
3.3 把 COCO JSON 转成 YOLO seg txt:一个可复用脚本
转换是训练前最关键的工序,不要用网上抄来的随手脚本。下面这个脚本经过托盘项目反复使用,兼容坐标闭合、越界和多边形点数过滤,保存成coco_to_yolo_seg.py后可以直接套用。
import json import os def convert(coco_path, out_dir, min_area=50): with open(coco_path, "r", encoding="utf-8") as f: coco = json.load(f) img_map = {img["id"]: img for img in coco["images"]} ann_map = {} for ann in coco["annotations"]: img_id = ann["image_id"] ann_map.setdefault(img_id, []).append(ann) os.makedirs(out_dir, exist_ok=True) for img_id, anns in ann_map.items(): img = img_map[img_id] base_name = os.path.splitext(img["file_name"])[0] w, h = int(img["width"]), int(img["height"]) if w <= 0 or h <= 0: continue lines = [] for ann in anns: if ann.get("iscrowd", 0): continue if ann["area"] < min_area: continue seg = ann["segmentation"][0] if len(seg) < 6: continue points = [] for i in range(0, len(seg), 2): x = max(0.0, min(1.0, seg[i] / w)) y = max(0.0, min(1.0, seg[i + 1] / h)) points.append(f"{x:.6f} {y:.6f}") class_id = ann["category_id"] lines.append(f"{class_id} " + " ".join(points)) with open(os.path.join(out_dir, base_name + ".txt"), "w") as f: f.write("\n".join(lines)) convert("/data/pallet_dataset/annotations/instances.json", "/data/pallet_dataset/labels_yolo", min_area=50)脚本逻辑分四步。先建立image_id到图片信息的映射,再建立image_id到标注列表的映射;随后逐图生成标注行;最后落盘。这里的关键参数是min_area,它过滤掉太小的碎片标注。托盘在画面里即便是远景也不会只有几十个像素,设 50 比较安全,如果你要识别极小目标,再调小。
归一化时用了max(0.0, min(1.0, seg[i] / w)),把越界坐标强制裁剪回 0 到 1 区间。这个操作对少量越界是安全的,但如果一张图超过一半坐标都被裁剪,说明原始标注坐标系和图片分辨率不匹配,需要回头处理,不能靠裁剪掩盖。转换完成后,可以随机打开三个 txt 文件,确认每行首列是类别号、坐标数量是偶数。
3.4 按库位和场景拆分,别随机打乱
很多教程教你把所有图片随机划分成 train 和 val,这在托盘数据集上会导致严重的数据泄漏。同一批托盘、同一角度的连续帧如果同时出现在训练集和验证集,验证结果会虚高,一旦换到真实仓库立即失效。
我一般先按场景前缀拆分。托盘数据的命名常包含库位号或拍摄日期,例如A0101_001.jpg这样的前缀。下面命令按前缀分组,把每个前缀下的文件尽量只进一个集合。
cd /data/pallet_dataset python3 - <<'EOF' import glob, os, random files = glob.glob("images/*.jpg") groups = {} for fp in files: prefix = os.path.basename(fp).split("_")[0] groups.setdefault(prefix, []).append(fp) keys = list(groups.keys()) random.shuffle(keys) split = int(len(keys) * 0.8) train_keys = set(keys[:split]) val_keys = set(keys[split:]) os.makedirs("images/train", exist_ok=True) os.makedirs("images/val", exist_ok=True) for fp in files: prefix = os.path.basename(fp).split("_")[0] dest = "images/train" if prefix in train_keys else "images/val" os.makedirs(f"labels/{dest.split('/')[1]}", exist_ok=True) base = os.path.basename(fp) os.rename(fp, os.path.join(dest, base)) lb = os.path.join("labels", base.replace(".jpg", ".txt")) if os.path.exists(lb): os.rename(lb, os.path.join("labels", dest.split("/")[1], base.replace(".jpg", ".txt"))) EOF这段脚本按首段文件名分组,把 80% 的前缀进 train,20% 进 val。注意移动图片的同时要移动同名 txt。更严谨的做法还要检查一个前缀下的图片数量,如果某个前缀只有一两张,又刚好被分进 val,验证集会缺失那个场景,这种情况就把它强制并入 train。
拆分完成后,生成pallet.yaml:
path: /data/pallet_dataset train: images/train val: images/val names: 0: palletpath要写成数据集根目录的绝对路径,train和val是相对根目录的子目录。如果训练机和数据存储不在同一台机器,把path改成相对路径会踩大坑,后面避坑章节会展开。到这里,数据集才真正具备了进入 YOLO 训练管线的条件。
4. 用 YOLO 实例分割训练托盘模型:命令、参数与评估
4.1 选哪个模型:从 YOLOv8-seg 开始
实例分割模型可选的不少,Mask R-CNN 在论文里强,但工程部署重;而现在做托盘项目最顺手的还是 YOLO 的 segment 系列。它的训练接口统一,输出格式直接,还自带 mask 可视化和评估指标,所以这里就用 Ultralytics YOLOv8-seg 作为基线。模型大小可以从yolov8s-seg.pt起步,如果显卡显存够,托盘特征又明显,m或l也不是不能上,但第一次实验我建议用s,把链路跑通。
托盘本身是刚性物体,形状规则、颜色统一,不是难识别的目标,难点只在堆叠遮挡和相似纹理,因此大模型带来的收益往往没有想象中大。优先保证数据质量和 imgsz,比直接换x模型更划算。
4.2 训练命令与最少必须调的参数
进入数据集根目录后,跑下面这条命令是标准做法。
cd /data/pallet_dataset yolo task=segment mode=train \ model=yolov8s-seg.pt \ data=pallet.yaml \ imgsz=640 \ epochs=150 \ batch=16 \ device=0 \ project=runs/pallet \ name=seg_v1task=segment告诉框架走实例分割分支;model=yolov8s-seg.pt表示加载 COCO 预训练权重,而不是从随机初始化开始,对托盘这种单一类别目标,预训练权重能显著加快收敛。data指向刚才生成的 yaml。imgsz=640是训练时的输入边长,实际会做等比缩放再 padding,但不代表原始图片分辨率不重要。epochs=150是上限,训练开了早停的话一般到 80 到 120 轮就会停。batch=16要看显存,8GB 显存跑s模型 640 输入,批量降到 8 更稳,否则会 OOM。project和name决定训练日志和权重的落盘目录,方便后续同时跑多版对比实验。
训练时输出到终端的每一行都包含当前 epoch 的 loss。新手容易踩的一个坑是只盯box_loss和cls_loss,这两个值下降不意味着分割效果好。要重点看seg_loss,它才反映 mask 分支是否收敛。如果seg_loss一直徘徊在 2.0 不下,大概率是标签有问题,而不是训练轮数不够。
训练前还有一个值得调的参数叫overlap_mask。它控制训练时是否让重叠实例的 mask 共享像素。托盘堆叠场景经常出现两个实例的 mask 在边缘重叠,建议显式设为False,让每个实例单独计算损失,避免梯度互相干扰。命令加在参数尾部即可:
yolo task=segment mode=train \ model=yolov8s-seg.pt \ data=pallet.yaml \ imgsz=640 \ epochs=150 \ batch=16 \ overlap_mask=False4.3 训练日志要看 mask mAP,不是只看框 mAP
训练结束会在runs/pallet/seg_v1下生成一堆结果。打开results.png,里面有七个曲线,其中metrics/mAP50(B)和metrics/mAP50-95(B)是检测框的 mAP;metrics/mAP50(M)和metrics/mAP50-95(M)才是 mask 的 mAP。这两个 M 系列指标直接决定模型能不能满足托盘计数需求。
托盘这种规则物体,bbox 很容易达到 0.95 以上,mask mAP 反而是真实水平。如果 M 系列比 B 系列低超过 15 个点,多数情况下说明 mask 边缘还不够贴合,要么是标注多边形太粗糙,要么是 imgsz 不够。此时不要盲目加 epoch,先可视化预测,找到 mask 崩坏的具体位置。
推理验证用下面命令:
yolo task=segment mode=predict \ model=runs/pallet/seg_v1/weights/best.pt \ source=/data/pallet_dataset/samples \ device=0 \ save_txt=True \ save_conf=True \ conf=0.5这里save_txt=True会保存每个检测到的实例的类别、置信度和多边形坐标,conf=0.5是置信度阈值。托盘项目部署时阈值设 0.5 左右比较合理,太低会出现大量误检,太高会漏掉远处小托盘。预测结果中_seg.txt文件的第一行是类别 ID、第二列是置信度,后面是按输入图尺寸归一化的 mask 坐标,这套东西可以直接接到后台计数程序里,不需要再解析 mask 图像。
4.4 验证时的第三个参数:图像分辨率
很多人在训练和推理时只用一个imgsz,但实际部署场景里,摄像头出图可能是 200 万像素或 500 万像素,如果推理时直接按原始分辨率跑,显存占用会爆炸。常见做法是训练用 640,推理也保持 640,让模型适应下采样后的特征。托盘不是微小目标,640 已经能数清楚堆叠层数。
如果托盘在画面里只占很小区域,比如几十米外的库位,那 640 不够。这时优先做切图推理,把原图裁成多个 640 的 patch 分别预测,而不是推高全局分辨率。全局提高分辨率会把远处小目标和近处大目标放在同一个尺度,模型对尺度变化的压力更大,反而不稳。
5. 托盘实例分割训练最常见的 5 个坑:现象、原因、对策
5.1 mask 跑到画面外,训练图出现大面积黑边
现象:转换格式后做可视化,发现不少托盘 mask 一半在图像外侧,或者训练时的增强图出现不规则黑边,模型推理在图像边缘区域频繁漏检。最初以为模型问题,反复调参都没改善,后来直接打开 label 文件对比原图,才知道是坐标已经越界。
原因:COCO JSON 里segmentation的坐标是像素值,但 JSON 中images字段记录的宽高和实际图片解码后的尺寸不一致。常见来源有两个:一是标注工具加了 EXIF 旋转,二是工具预处理阶段做了缩略图,写进 JSON 的宽高是缩小后的值,而训练读的是原图。坐标分母用错,整个 mask 全盘偏移。
解决:转换脚本不要用 JSON 里的img["width"]和img["height"],统一用 Python 读取图片实际尺寸,cv2.imread后的shape[1]和shape[0]为准。转换前对全部图片抽样检查,如果发现有一张宽高不匹配,先修正数据集,再进训练。这个坑最容易悄无声息地吃掉精度,因为它不报错,只看 val 图才能发现。
5.2 相邻托盘贴太近,推理输出连体 mask
现象:单独拍一个托盘识别正常,两三个托盘紧挨着摆放时,模型把它们预测成一个连通的大 mask,计数直接少了一个。检查训练数据,标注上两个托盘其实是分开的两个多边形,但中间只隔了一条细缝隙。
原因:实例分割模型的 mask 分支在特征图上做上采样,相邻实例的边缘距离小于几个像素时,特征图上的两个实例天然会粘连。加上 Mosaic 增强会把不同图片的托盘拼在一起,边缘互相切分后更加模糊。类别只有一个时,网络对“区分不同实例”的压力本来就小,出现连体 mask 是常见表现。
解决:标注时在两个托盘相邻处留出至少两个像素宽的背景间隔,不要为了贴合边缘让多边形贴到零距离。训练阶段适当降低mosaic的启用权重,或者关闭让它为 0,减少目标边缘被拉伸的概率。推理阶段也可以把nms的 IoU 阈值稍微调低,让相邻 mask 更难被合并。
5.3 mask mAP 远低于 box mAP,小托盘全部报废
现象:训练结果里metrics/mAP50(B)到 0.93,但metrics/mAP50(M)只有 0.6 左右。可视化后发现远处 30 到 50 像素大小的托盘几乎检测不到,近处大托盘却很好。
原因:检测框对目标中心响应敏感,即使目标只有十几个像素也能给出候选框;mask 分支需要在特征图上恢复精确边缘,小目标在 32 倍下采样后只剩一个点,掩膜自然丢失。托盘项目里相机安装高度一高,远景托盘就会触发这个问题。
解决:把imgsz从 640 提到 960,小目标特征会多保留一层;不行就对原图切 patch 训练和推理。我后来在类似场景选择切图方案,把 200 万像素源图裁成四份 640 patch,推理后再把坐标拼回原图坐标系,mask AP 提升超过 10 个点。对应成本是多一道拼图逻辑,但效果稳定。
5.4 训练不报错,但推理出来所有标签都是同一个类
现象:模型训练过程完全正常,loss 下降,可视化图片上 mask 也贴合,但预测输出的类别全部是同一类。如果数据集只有一个托盘类别,看不出明显异常,而一旦混入第二类,错乱立刻暴露。
原因:YOLO seg 的 label 文件第一列是从 0 开始的类别 ID,转换脚本从 COCO 的category_id直接写入,而 COCO 数据集里category_id从 1 开始,导致类别偏移一位。更隐蔽的情况是 txt 文件第一列误写成了其他字段,模型把“类别 ID”当成坐标之一训练,最终全乱。
解决:转换脚本里显式做class_id = ann["category_id"],同时检查数据集的 categories 是否从 0 开始。如果原始标注里category_id只有一个托盘,可以把转换脚本里 class 写死为 0,排除一切意外。训练前随机打印三个 txt 文件的第一行,确认首列只有一个 0 且后面坐标数量正确,再去跑训练。这一步十秒钟,能省掉一整天的排查。
5.5 换了机器后 data.yaml 路径失效,模型白练
现象:在带 GPU 的服务器上训练完,把runs/pallet和数据集拷到另一台机器做推理,mode=predict提示数据集路径不存在,或者直接加载不到 yaml。
原因:data.yaml 里path写的是第一台机器的绝对路径/data/pallet_dataset,拷贝后根目录变了,而 Ultralytics 读取时会优先用 yaml 里的 path 拼接 train 和 val 路径。如果用相对路径如path: ../pallet_dataset,换到不同目录层级也会失效。
解决:data.yaml 里的路径在训练完成后就不再需要,推理时只用model=best.pt,不需要 yaml。如果训练和推理在一套环境,固定根目录是最省事的。我现在的习惯是训练前把 data.yaml 路径和数据集目录结构一起写进 README,所有同事拿到压缩包先按 README 重建目录结构,再跑命令,这样不会因为换人换机器导致历史实验不可复现。数据集文件名里带20251120_043045的好处就在这里,版本可追溯,环境可复现,才谈得上继续迭代。
6. 进阶:用 mask 做托盘计数、姿态估计和半自动回标
6.1 从 mask 到托盘数量、中心点和旋转角度
实例分割训练完成后,最大的价值不只是画图好看,而是可以直接从 mask 数出托盘数量、算出中心坐标和姿态。常见做法是提取每个实例的轮廓点,再用 OpenCV 做几何计算。下面是一个配合save_txt=True推理结果的后处理片段。
import cv2 import numpy as np def tray_stats(points, img_w, img_h): pts = np.array(points, dtype=np.float32) pts[:, 0] *= img_w pts[:, 1] *= img_h cnt = pts.astype(np.int32).reshape(-1, 1, 2) area = cv2.contourArea(cnt) if area < 2000: return None rect = cv2.minAreaRect(cnt) (cx, cy), (w, h), angle = rect return {"center": (round(cx, 1), round(cy, 1)), "size": (round(w, 1), round(h, 1)), "angle": round(angle, 2), "area": int(area)}cv2.minAreaRect返回的外接旋转矩形很适合托盘这种长方体目标,w与h的比值可以初判托盘朝向,angle用来对齐 AGV 取货姿态。area过滤把面积小于 2000 像素的碎片 mask 丢掉,这类 mask 通常来自远处的误检。实际部署时,我会把每个托盘实例的结果组装成 JSON,推给调度系统做下一步决策。
这套后处理逻辑是实例分割模型真正“落地”的一步,有了中心点和角度,托盘计数、位置引导、货位占用判断才能变成具体业务能力。如果检测型号有误差,再通过卡尔曼滤波对连续帧的中心点做平滑,托盘姿态输出会更稳定。
6.2 半自动回标:用训练结果补数据集,第二次迭代更省力
托盘实例分割数据集每迭代一次,都会面临标注成本。与其从头手动描多边形,我现在的习惯是先跑一次模型,把置信度低于 0.6 的预测结果转成 LabelMe JSON,然后让标注员在这些预标注基础上修正。预标注把 80% 的点位摆好,人工只需拖动边缘点,标注时间能压缩一半以上。
这个技巧特别适合托盘这种形状可预测的目标。第一版模型可能只有 0.8 的 mask AP,但它的误检大多集中在边缘偏移,轮廓的大方向是正确的。把这类结果导入标注工具,人工修正后补进训练集,第二轮模型通常能涨两到三个点的 mask mAP。
我最后养成的一个习惯是:每个版本的数据集打包命名都保留时间戳,就像托盘实例分割数据集_20251120_043045.zip这样,训练前写一行命令记录数据版本和模型版本,出了问题能快速回退。最后一版迭代里,即使模型短期效果不够,我也能靠数据版本管理和半自动回标快速追回来,而不是推倒重来。希望这篇笔记能帮你在托盘实例分割数据集上少踩几个坑。
本文还有配套的精品资源,点击获取