1. 从“集装箱方向分类”这个不起眼的任务说起
我在自动化码头和智能吊装项目里头泡了几年,会发现一个规律:越不起眼的小任务,越容易成为整套系统的瓶颈。集装箱侧面方向分类就是一个典型例子。所谓“方向分类”,就是输入一张集装箱侧面的图像,让模型判断这个箱子当前是以什么姿态出现在画面里。这个数据集本身只有2408张图,划分为5个类别,体量不大,但它解决的是真实作业场景里一个非常棘手的难题——集装箱外观高度重复,正面和背面乍一看几乎一样,左侧和右侧更是完全镜像,如果机器认错了方向,后面的吊具对位、堆场盘点、箱门朝向判断全都会跟着错。
放在实际业务里看,这个方向分类模块的上游是监控相机,下游是自动吊具或者调度系统。想象一台龙门吊要从船上抓箱子,系统先是框出目标集装箱,然后需要知道它到底是正面朝向摄像机,还是箱门在远端,或者箱子已经转动了一个倾斜角度。只有得到这个“方向标签”,自动吊具才能决定旋转多少度、对准哪条边下钩。如果方向搞反,吊具就会选择错误的一侧进行锁扣,轻则抓空,重则损伤箱体。因此,方向分类不是可有可无的辅助功能,而是很多视觉控制链路上的第一块积木。
这个数据集选用的5个类别是这样划分的:正面、背面、左侧、右侧,以及倾斜视角。正面和背面依靠箱门锁杆、门缝特征来区分;左侧和右侧以箱体纵向长边作为参考;倾斜视角则涵盖了前面四类之外的各种偏转状态。有人会问,为什么单独把倾斜视角列成一类,而不是直接回归出角度值?从工程落地角度讲,离散分类的鲁棒性更高,对相机安装角度、畸变、模糊都有更强的容错能力,而且输出信号可以直接给PLC层面的逻辑使用。如果你正在做港口机械视觉、集装箱堆场自动化、物流货车装载识别的算法工作,这套思路可以直接复用。
2. 数据是怎么来的:2408张图的采集、清洗与标注细节
2.1 五个类别到底怎么界定
很多数据集最模糊的就是类别定义,定义不清晰,标注员和算法工程师就会互相折磨。我们当时定的标准很明确,核心是“以箱体特征为准,而不是以相机位置为准”。
- 正面:画面中能看到箱门端面,门框、锁杆、门铰链这些细节可见。判断标准是“锁杆特征是否存在”,而不是单纯看箱号。
- 背面:画面中是封闭端板,看不到锁杆和门缝。端板表面平整,通常有角件、铭牌或残留的油漆痕迹。
- 左侧:箱体的纵向长边构成画面主体,长边延伸方向从画面右上到左下,或者长边整体处在画面中线左侧。
- 右侧:同样的纵向长边,但长边主体压向画面中线右侧。
- 倾斜视角:箱子与摄像机的光轴之间存在明显偏转角,包括左前斜、右前斜、左后斜、右后斜等,不属于正对端面或正对侧面的任何一种标准姿态。
这里最容易出错的是正面和背面。我去观察标注过程的时候发现,标注员如果只看缩略图,很容易把有箱号的一端当成正面。但实际上箱号可以印在端面也可以印在侧面,用它做标准会让两个类彻底混乱。后来我们强制标注界面里显示大图,并且给了一个判断题:“锁杆是否可辨认?”只有这个条件成立才允许标成正面。
2.2 采集场景与数量分布
2408张图不是一次性抓拍的,而是分了三轮整理出来的。第一轮是从堆场高杆摄像机和龙门吊小车相机拍摄的视频里抽帧,这部分占了一大半。第二轮专门在阴天、早晚、雨雾天气下补拍,因为低照度和均匀光照下集装箱表面颜色会变得非常“平”,算法容易失去纹理参照。第三轮加入了手持相机在不同仰角、不同距离下的拍摄画面,用来模拟巡检机器人和人工复核设备看到的视角。
最终样本分布大致是:正面520张左右,背面大约480张,左侧480张,右侧430张,倾斜视角将近500张。这张表列出来大家可以看看:
| 类别 | 样本数量 | 占比 | 典型特征 |
|---|---|---|---|
| 正面 | 520 | 21.6% | 锁杆、门缝、铰链可见 |
| 背面 | 480 | 19.9% | 封闭端板、铭牌或角件 |
| 左侧 | 480 | 19.9% | 长边成为画面主体、压迫中线左侧 |
| 右侧 | 430 | 17.8% | 长边成为画面主体、压迫中线右侧 |
| 倾斜视角 | 498 | 20.7% | 箱体与光轴存在明显偏转 |
整体看每类数量基本均衡,不需要做特别复杂的采样策略。但我额外强调的是,2424和2408这种数字本身说明数据量处在“勉强够用”的边界上。如果项目要覆盖的场景更杂,比如集装箱船在码头里多种箱型混排、不同涂装颜色的箱子交替出现,这个量还需要再往上加。
2.3 标注流程里的几个实务细节
标注工业目标跟标马路上的行人车辆不太一样。集装箱表面图案简单,但是重复性极高,没有“人形”这种天然的语义强锚点,标注员需要靠几何特征去判断。我们的做法是在标注工具上叠加一条辅助参考线,让标注员把线的方向拖到箱体的纵向长边方向,系统再根据这条线与水平轴的夹角自动映射到左、右两个类别。这个半自动方式,比人工直接点“左侧”“右侧”的准确率要高不少。
另外一个心得是:弱光图、运动模糊图、雨雾遮挡图不要轻易删。很多团队做数据清洗时,优先生成“好看”的干净数据,把模糊图直接丢掉,这是个大坑。因为这些困难样本恰恰是上线之后真实环境里最常见的输入。集装箱识别系统在夜间、雨天出问题的概率远大于晴天白天,训练集里没有这些数据,模型就永远不会适应这些场景。最后数据里大概留了12%左右的低质量样本作为困难样本,对最终指标提升起到了关键作用。
2.4 数据目录与基本参数
数据集目录结构按训练、验证、测试拆分,每个子集内部直接以类别名作为子文件夹,这个结构无论用PyTorch还是用Keras都能零改造直接读取。
container_side_dataset/ ├── train/ │ ├── front/ │ ├── back/ │ ├── left/ │ ├── right/ │ └── oblique/ ├── val/ │ ├── front/ │ ├── back/ │ ├── left/ │ ├── right/ │ └── oblique/ └── test/ ├── front/ ├── back/ ├── left/ ├── right/ └── oblique/图片统一为JPG格式RGB图像,短边长度不低于640像素,长边尽量控制在1920像素以内。这个尺寸既能保留箱体波状纹理、锁杆、角件的细节,又不会把训练显存撑爆。如果原始视频帧超过这个范围,写入前就做等比缩放。我实际跑下来,这个分辨率在常规硬件上训练速度很快,效率远高于动不动就上4096像素的做法。
3. 动手训练前,先把数据策略想清楚
3.1 关于增广,最忌讳的是水平翻转
普通图像分类任务里,水平翻转是再基础不过的增广手段,但在方向分类里,它就是灾难。你想,一张左侧面图像水平翻转之后,就变成右侧面了;一张正面图翻转后可能看起来接近背面。我第一次实验时没想那么多,直接在transforms里加了RandomHorizontalFlip,结果验证集的loss和准确率漂亮得离谱,心里正高兴,拿到真实监控画面一测才发现方向几乎全错。原因就是增广把类别语义给翻转了,模型等于同时学了“左”和“右”两种矛盾的标签。
正确的做法是保留部分旋转增广,但角度必须控制在正负10度以内。这个范围可以模拟相机安装偏差和震动带来的轻微角度变化,又不会把箱体的姿态语义磨模糊。超过15度,倾斜视角和左右侧面的边界就会不可逆地被污染。
我比较推荐加入的增广组合是:随机旋转(±10°)、随机裁切后再缩放回原尺寸、亮度对比度抖动、高斯噪声、少量运动模糊。色彩抖动在集装箱这种低色彩饱和度目标上尤其重要,因为雨水、灰尘会让箱体表面颜色发生整体偏移,不做色彩增广,模型会把某种灰度当成了类别特征。
3.2 训练集和验证集不能按帧随机切分
很多人抽帧之后直接按照7:2:1随机分配train、val、test,短时间看指标不错,但坑在后面。视频抽帧有一个特性叫时间连续性,相邻帧之间差距很小,如果同一个视频片段里的帧被同时分进了训练集和验证集,验证集里就会出现大量和训练样本几乎一致的图像,这叫“数据泄漏”。这种情况下验证集指标会上涨三到五个百分点,但算法到了新的拍摄视角和机位,准确率瞬间掉回原型。
我建议的做法是按“视频片段”或“拍摄批次”切分。同一台相机、同一时间段、同一个场景拍出来的所有帧看成一个整体,要么全部进训练集,要么全部进验证集。只有让验证集遇到的视角和场景完全独立于训练集,评估结果才有说服力,也才能真实反映模型在未知工况下的表现。
3.3 类别不平衡的处理优先级
这个数据集的类别分布相对均匀,没有到需要重采样或者合成样本的程度。但要注意倾斜视角这一类的特殊性。它内部其实包含好几种子姿态,比如左前斜和右后斜,视觉特征差得远,只是被归到了一个标签里。所以倾斜视角类在训练中loss往往偏高,这不一定代表数据标注有问题,只是类内方差大。我的建议是训练时监控每个类别的单独准确率,而不是只看整体准确率,因为整体指标会被容易的类别拉高,掩盖困难类别的问题。
4. 用2408张图训练一个实用的方向分类模型
4.1 选什么网络结构
在中小规模数据集上做图像分类,我的第一选择永远是带预训练权重的模型,而不是自己从头搭一个卷积网络。ResNet18、EfficientNet-B0、MobileNetV3这几个都很合适。我更常用EfficientNet-B0作为主力基线,原因是它在相同参数规模下,对纹理类特征的提取更细腻,集装箱表面的波状板纹、角件边缘、锁杆阴影这些细节都能被更好捕获。MobileNetV3则更适合以后部署到边缘设备,可以在训练完成后通过蒸馏从大模型迁移过来。
不建议一上来就直接在MobileNet上从零训练,因为它的骨干网络容量相对小,而集装箱虽然外观单一,但几何结构比较特殊,需要用预训练特征作为起点才能更快收敛。
4.2 数据加载与训练基本流程
这是一个可以直接复用的PyTorch数据加载类,比较简单,适合作为起步模板。
import os import torch import torch.nn as nn import torchvision.transforms as transforms from torch.utils.data import Dataset from PIL import Image class ContainerDirectionDataset(Dataset): def __init__(self, root_dir, transform=None): self.classes = sorted(os.listdir(root_dir)) self.class_to_idx = {c: i for i, c in enumerate(self.classes)} self.paths = [] self.labels = [] for c in self.classes: folder = os.path.join(root_dir, c) if not os.path.isdir(folder): continue for name in os.listdir(folder): if name.lower().endswith(('.jpg', '.jpeg', '.png')): self.paths.append(os.path.join(folder, name)) self.labels.append(self.class_to_idx[c]) def __len__(self): return len(self.paths) def __getitem__(self, idx): img = Image.open(self.paths[idx]).convert('RGB') if self.transform: img = self.transform(img) return img, self.labels[idx]训练时的关键超参数,我给出一个被验证过比较稳定的组合:
- 输入尺寸:224×224
- 优化器:SGD + 余弦退火,初始学习率0.01
- Weight Decay:5e-4
- Batch Size:32~64
- Epoch:50
- 损失函数:CrossEntropyLoss,加上label smoothing 0.1
推荐分两个阶段微调。前10个epoch只训练最后的全连接分类头,特征提取骨干保持冻结,因为预训练模型已经学到了丰富的通用纹理特征,集装箱的波状板纹和普通物体纹理在底层是相通的。等分类头loss下降到平台区后,再解锁骨干网络,用更低的学习率(比如初始学习率的十分之一)做整体微调。如果一开始就用大学习率全网络微调,很容易把预训练特征破坏掉,在2408张这种规模的数据上,往往表现为过拟合提前到来,验证集指标不升反降。
4.3 训练过程中的监控指标
不要只盯着验证集Top-1准确率。对于这个任务,我更推荐同时记录每个类别的准确率和召回率。特别是正面和背面这一对,很容易出现“平均准确率高但正面召回率低”的情况。因为如果你把一些正面误判为背面,而被背面整体数量足够大时,整体准确率不明显受损,但对实际吊装作业来说,把正面当背面的后果可能很严重,因为箱门方向直接关系到货物装卸流程。
推理端就简单很多。模型输出5维logits,经过Softmax之后取最大值对应索引即类别。真正部署时还要设置一个置信度阈值,比如低于0.6就判定“不确定”,交给调度系统走人工复核。方向分类的置信度本身就是一个有用的质量信号。
5. 实验结果和现场表现:到底能跑到什么水平
5.1 一组有代表性的实验数据
不同初始化种子和增广组合会带来一些波动,但整体表现相对稳定。我在测试集上得到过这样一组结果,可以作为参考:
| 指标 | 数值 |
|---|---|
| Top-1准确率 | 94.8% |
| Precision(加权) | 0.95 |
| Recall(加权) | 0.94 |
| F1 Score(加权) | 0.94 |
从混淆矩阵来看,错误分布有明显规律。正面和背面相互混淆约占总错误数的50%,左和右相互混淆占大约25%,剩下的是倾斜视角被误判成了左右侧面。前两类错误意味着,如果你在生产环境里需要更高的安全裕度,单纯堆数据未必是最优解,更好的做法是专门增强箱门锁杆区域的细节,让模型更容易抓住判别性特征。
5.2 数据量的消融实验
我专门做过一次数据消融,用来评估“2408张”这个量级是多了还是少了。把每类样本量从400张压缩到200张再训练,Top-1准确率从接近95%降到91%左右;继续压缩到100张,就只有87%左右。说明这个任务对数据数量还是比较敏感的,2408张虽然不算多,但正处于一个“再少就明显恶化”的临界区。如果想让准确率往97%以上走,不能光靠增加随机样本,应该去补困难样本:端面锁杆特写、低照度、镜头污损、箱体大面积反光、堆场货物遮挡等。
5.3 实际链路里的“置信度护栏”
在某次模拟项目里,我把方向分类模块接进吊具对位流程,整个链路是相机抓拍、方向分类、箱体角点检测、吊具偏移量解算。方向分类一旦出错,角点检测会找到对称错位的那条边,计算出来的补偿量方向正好相反。为了避免这种问题,我加了一个很简单的护栏逻辑:当分类置信度低于0.6时,不启用自动对位,自动切换到人工确认界面。这个阈值看起来粗糙,但在试运行阶段把误操作率压降低到几乎可以忽略。方向分类这种模块很少成为演示里的亮点,但它是控制链路上最容易放大风险的一环,必须对低置信度保持“敬畏”。
5.4 留存困难样本的价值
所有训练过程中模型反复出错的图像,我都会单独建一个hard_negative目录留存。这些图包括:雨滴附着镜头导致局部模糊、强逆光下锁杆阴影消失、集装箱表面贴有巨大广告纸、箱子之间互相遮挡导致只有局部轮廓。后期做模型更新时,先把这些hard样本拿来跑一轮预测,如果错误率明显下降,说明优化方向是对的。如果没有下降,那可能说明新增的训练数据并没有覆盖到它们的特征分布,还需要继续采集。
6. 实操中经常踩的坑和排查办法
6.1 训练集表现很好,现场数据一测就崩
这个现象在视觉项目里太常见了。多数原因是训练图像和现场图像存在“视角差”。比如训练集里大量是平视近景,而现场相机是高点俯拍,集装箱顶面会占画面的一定比例,箱体侧面的几何形态完全不同。遇到这种情况,先把现场图片缩小后和训练图片一起跑一个特征降维可视化,看看现场数据是否大量落在训练数据分布的边缘。如果确实如此,单纯调模型参数没有意义,要采集现场视角的增量数据补进训练集。
6.2 左侧和右侧总是互相混淆
第一反应先检查增广里是否有水平翻转。如果确认没有,再看是不是左右两类的角度分布不对称。比如左侧图的平均倾斜角是15度,而右侧图只有8度,网络很可能学到的是倾斜角度而非真正的箱体长边方向。解决办法是让左右两类的角度范围尽量一致,可以通过分层采样让每类样本在角度轴上均匀分布。实际测试下来,这个操作对减少左右混淆的作用非常直接,比单纯增加图片数量更有效。
6.3 倾斜视角类别的内部差异太大
一整个倾斜视角类别里包含多个子姿态,网络需要学习“不同角度的统一语义”,这本身就很拧巴。如果你想进一步压榨准确率,可以把倾斜视角拆成四个子标签:左前斜、右前斜、左后斜、右后斜,在训练时按子标签计算损失,输出时再把四个子标签合并成倾斜视角一个大类。子标签帮助模型学到更细的边界,对外输出保持不变,这是比较实用的一招。
6.4 小数据集过拟合的标准信号
当你看到训练集准确率一路冲到99%而验证集卡在93%左右不动,十有八九是过拟合。按我的习惯,先做三件事:把预训练骨干冻结得更久,让分类头在低层特征不变的情况下更好地收敛;把Weight Decay从5e-4往上调到1e-3;最后用早停,验证集连续8个epoch不提升就回滚到最佳权重。在这个规模的数据上,Dropout加在分类头后面比加在骨干里效果更显著。
6.5 验证集指标虚高的另一个隐蔽来源
除了视频帧数据泄漏,还有一种情况是同一张图像经过不同后处理产生了多个副本,被分别分配到了训练集和验证集。比如原始采集是1080P视频,抽帧时既保存了完整帧又保存了裁剪区域,结果两个文件内容高度重合。做数据集清理时,对相似图像可以计算感知哈希值,把哈希相近的样本统一放到同一侧,避免无意间造成泄漏。这类问题不排查,验证集指标基本等于摆设。
7. 从方向分类出发,还能继续往哪个方向扩展
方向分类作为基础能力,天然适合跟其他模块串联。最直接的是接一个箱号OCR识别,做到“输入一张侧面图,输出箱号、朝向、箱门位置”这样的结构化信息。另一个有价值的方向是预测箱门锁定状态,比如锁杆是否到位、门锁是否闭合,这个任务的输入域和方向分类几乎完全一样,完全可以共享特征提取网络,只需要在分类头上增加一个分支,省掉重新采集标注的大量成本。
如果要往更大场景推进,比如无人化堆场全场调度,可以把离散方向分类升级成姿态估计,不再输出五选一的标签,而是输出集装箱在相机坐标系下的旋转角度和近似位置。现有数据集可以作为姿态估计的弱监督来源,用来生成初始锚点和验证边界。不过那时候就要特别注意数据同步问题:图像时间戳、相机内外参、吊具编码器数值必须严格对齐,否则采集的数据再多也只是噪声堆叠,算法效果反而更差。
我自己在这个方向上的体会是,数据集的规模永远不是评判一个任务价值的唯一标准。2408张图看似小,却能在实际业务里支撑起一条完整的自动化链路,关键看你怎么组织类别、怎么设计增广、怎么设置置信度护栏。方向分类这类任务不像大模型那样自带热度,但它是视觉感知与机械控制之间连接最紧密的一环,容错空间最小。踩过几次坑之后,我现在拿到一套新工业数据,第一反应永远是问清楚机位、光照、视角的分布,而不是急着改网络结构。把数据和评估方式理顺,模型训练本身就是水到渠成的事。
最后分享一个我自己保留了很久的习惯:每次训练结束,把错误样本按误判类型导出成带缩略图的表格,逐张过一遍。这个过程能帮你快速判断问题出在标注噪声、类别边界模糊还是视角缺失,远比盯着混淆矩阵猜来得高效。方向分类这个任务本身不复杂,但它能逼你把数据工程里的每一个细节都做到位,这份经验换到任何其他视觉项目里都是值钱的。