☰
基于YOLOv8的智慧码头集装箱箱号自动识别系统:从训练到部署全流程
2026/9/28 14:54:19 网站建设 项目流程

简介:这是一套面向计算机视觉方向的毕业设计与课程设计资源,围绕智慧码头场景下的集装箱箱号自动识别任务,基于YOLOv8目标检测框架实现完整系统。资源适合计算机、人工智能、通信工程、自动化等专业学生及教师使用,也可供入门深度学习的小白进阶学习,拿来即可作为毕设、课设或大作业的保底方案。压缩包共8个文件,包含3个py源码文件、3个pt模型权重文件与2个txt说明文档,整体约15.91MB,涵盖模型训练、视频检测、可视化界面等核心模块,并附有完整数据集与部署说明。项目可生成核心指标曲线图、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果及标签分布图,便于答辩展示与结果分析。目前已有35人学习下载,代码均经测试运行成功,下载后按README说明即可快速部署,也可在此基础上修改扩展实现其他功能。

1. 码头箱号识别为什么总在光线和角度上翻车

做过港口项目的人都有一个共识:集装箱箱号识别这件事,实验室里跑通不难,难的是在真实堆场里活下来。龙门吊下面的逆光、箱体表面的锈蚀、侧面字符被吊具遮挡、雨雾天摄像头起雾,这些场景会让一个在 COCO 上 mAP 跑到 0.5 的模型直接掉到没法用。而《基于YOLOv8的智慧码头集装箱箱号自动识别系统》这个标题,本质上要解决的就是这条链路:从一张堆场图片里先定位到箱体上的字符区域,再把字符逐个识别出来拼成完整箱号,最后通过一个可视化界面把结果呈现给操作员。

这套方案适合三类人:一是做毕设或课程设计的学生,需要一套能跑通、有界面、有数据集、能写进论文的完整工程;二是刚接触 YOLOv8 想找一个真实场景练手的算法工程师;三是港口信息化团队里需要快速验证箱号识别可行性的开发人员。它不追求 SOTA 精度,追求的是端到端可复现——拿到源码和数据集,配好环境就能跑起来看到结果。这也是为什么标题里强调“简单部署即可运行”,因为大部分同类项目死在环境配置和数据集格式对不上这两步上。

2. 箱号识别链路拆解:从整图到字符串的四个阶段

2.1 为什么不能一步到位做端到端识别

很多人第一反应是拿 YOLOv8 直接训练一个检测模型,把“箱号”当成一个类别框出来,然后接一个 OCR 模型读框里的文字。这个思路在箱号规整、背景干净时能work,但在码头场景下会翻车。原因是箱号在整张图里占比极小,通常只有几十个像素宽,YOLOv8 的检测头在 stride 8 的特征图上对这么小的目标响应很弱,框都框不准,后面 OCR 再强也没用。

常见做法是拆成两阶段:第一阶段用 YOLOv8 检测箱体上的字符行区域(不是单个字符,是一整行),第二阶段对字符行做透视矫正和字符分割,再用一个轻量级 CNN 或 CRNN 做单字符分类。这样每个阶段的输入尺度都合理,YOLOv8 只需要学会“哪里有一行字”,不需要学会“这行字是什么”,任务难度大幅下降。

我一般会把字符行检测的类别设成两类:box_code(箱号行)和box_type(箱型代码行,比如 45G1)。因为这两行在箱体上的位置和字体大小不同,分开检测比合并成一类更稳。数据集标注时用 labelme 画矩形框,导出成 YOLO 格式的 txt,每行class_id x_center y_center width height,坐标归一化到 0-1。

2.2 YOLOv8 字符行检测的训练参数怎么设

拿到数据集后,第一步是确认目录结构符合 YOLOv8 的要求:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── data.yaml

data.yaml内容如下:

path: ./dataset train: images/train val: images/val nc: 2 names: ['box_code', 'box_type']

然后写训练脚本。我一般用yolov8n.pt做 baseline,因为码头场景的字符行特征比较固定,不需要太大的模型,n 版本在 1080Ti 上 batch=16 能跑起来,显存占用约 4GB。

from ultralytics import YOLO # 加载预训练权重,n 版本够用,s 版本精度略高但推理慢 30% model = YOLO('yolov8n.pt') # 训练参数说明: # epochs=100:字符行检测收敛快,100 轮足够,再多容易过拟合 # imgsz=640:输入尺寸,箱号行在 640 下大约 40-80 像素宽,够检测 # batch=16:根据显存调整,8GB 显存用 8,16GB 用 16 # lr0=0.01:初始学习率,YOLOv8 默认值,字符行任务不需要大改 # patience=20:20 轮无提升就早停,省时间 model.train( data='data.yaml', epochs=100, imgsz=640, batch=16, lr0=0.01, patience=20, device=0, project='runs/train', name='container_code_det' )

训练完成后,用model.val()看 mAP50 和 mAP50-95。字符行检测的 mAP50 一般能到 0.9 以上,如果低于 0.85,优先检查标注框有没有把字符行框全,以及有没有把箱体边缘的干扰文字(比如自重、容积)也标进去。

2.3 字符分割与透视矫正的工程处理

检测到字符行框之后,不能直接把框里的图丢给分类模型,因为箱体有透视变形,字符会倾斜。我一般用 OpenCV 做两步处理:先根据框的四个角点做透视变换,把字符行拉成水平矩形;再用自适应阈值做二值化,然后垂直投影切分单个字符。

import cv2 import numpy as np def rectify_and_split(img, box): """ img: 原始整图 BGR box: [x1, y1, x2, y2] 字符行检测框 返回: 单个字符图像列表 """ x1, y1, x2, y2 = map(int, box) roi = img[y1:y2, x1:x2] # 转灰度 + 自适应阈值,应对光照不均 gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) binary = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 15, 8 ) # 垂直投影:统计每列白色像素数 proj = np.sum(binary, axis=0) / 255 # 找到字符间的空白间隔,阈值设为行高的 5% h = binary.shape[0] threshold = h * 0.05 chars = [] in_char = False start = 0 for i, v in enumerate(proj): if v > threshold and not in_char: in_char = True start = i elif v <= threshold and in_char: in_char = False if i - start > 3: # 过滤太窄的噪声 chars.append(binary[:, start:i]) return chars

这段代码的关键参数是adaptiveThreshold的blockSize=15和C=8。blockSize 太小会把字符内部也二值化出空洞,太大则对光照变化不敏感。C 是常数项,用来微调阈值,光照偏暗时调大 C,偏亮时调小。垂直投影的阈值h * 0.05是经验值,字符间距大的箱号可以调到 0.08,间距小的调到 0.03。

2.4 单字符分类模型的选择与训练

字符分类我试过三种方案:CRNN 端到端、CNN 单字符分类、模板匹配。模板匹配在字体固定时最快,但码头箱号字体有十几种,模板匹配泛化太差。CRNN 需要大量序列标注数据,训练成本高。最终我选的是轻量级 CNN 单字符分类,因为字符集固定(数字 0-9 + 字母 A-Z 去掉 I、O、Q 避免混淆,共 33 类),每个字符单独分类,准确率能到 99% 以上。

网络结构用 4 层卷积 + 2 层全连接,输入 32x32 灰度图,输出 33 类。训练数据从字符行里切出来,每类至少 200 张,总共约 7000 张。数据增强用随机旋转 ±5 度、随机亮度 ±20%、随机高斯噪声,模拟码头光照变化。

import torch import torch.nn as nn class CharCNN(nn.Module): def __init__(self, num_classes=33): super().__init__() self.features = nn.Sequential( nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(128, 128, 3, padding=1), nn.ReLU(), nn.AdaptiveAvgPool2d(1) ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, num_classes) ) def forward(self, x): return self.classifier(self.features(x))

训练时用 CrossEntropyLoss,Adam 优化器,lr=1e-3,batch=64,30 轮收敛。注意输入要归一化到 [0,1],并且做标准化(均值 0.5,标准差 0.5)。如果某类字符准确率明显低,比如数字 8 和字母 B 混淆,就针对这两类多补 100 张训练图,或者在损失函数里给这两类加权。

3. 数据集制作与 YOLOv8 训练避坑

3.1 标注箱号行时最容易犯的三个错

第一个错是把整张图里所有文字都标成箱号行。码头图片里除了箱号,还有自重、容积、箱型代码、危险品标识,这些如果都标进去,模型会学到错误的特征。正确做法是只标箱号行(通常是 4 个字母 + 7 个数字,比如 CSQU3054383)和箱型代码行(比如 45G1),其他文字不标。

第二个错是框贴得太紧。字符行检测框如果刚好贴着字符边缘,训练时数据增强的随机裁剪会把字符切掉。我一般让框比字符行外扩 5-10 个像素,给模型一点上下文。

第三个错是忽略小目标。箱号行在整图里可能只占 1% 面积,如果标注时漏标了远处的箱号,模型会学到“远处没有箱号”的错误先验。解决办法是标注时把图片放大到 200% 再标,确保不漏。

3.2 数据集划分与增强策略

数据集按 8:1:1 划分训练、验证、测试。注意同一个箱体的多张图片不能同时出现在训练集和验证集里,否则验证集精度会虚高。我一般按箱体 ID 划分,同一个箱体的所有图片只进一个集合。

YOLOv8 内置的增强包括 mosaic、mixup、随机翻转、HSV 调整。对于箱号检测,mosaic 增强要慎用,因为四张图拼在一起会引入大量无关文字,模型容易混淆。我一般把 mosaic 关掉(mosaic=0.0),只保留 HSV 增强和随机平移。HSV 的hsv_h=0.015, hsv_s=0.7, hsv_v=0.4是默认值,码头场景可以把hsv_v调到 0.5,增强对暗光的鲁棒性。

3.3 训练过程中的损失曲线怎么看

YOLOv8 训练完会在runs/train/container_code_det/下生成results.csv,里面有每轮的 box_loss、cls_loss、dfl_loss 和 mAP。正常情况 box_loss 从 1.5 降到 0.3 左右,cls_loss 从 2.0 降到 0.5 左右。如果 box_loss 震荡不降,检查学习率是不是太大,或者标注框有没有问题。如果 cls_loss 降不下去,说明类别不平衡,箱型代码行的样本可能太少,需要补数据。

用 matplotlib 画损失曲线:

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/train/container_code_det/results.csv') df.columns = df.columns.str.strip() # 列名可能有空格 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(df['epoch'], df['train/box_loss'], label='box_loss') axes[0].plot(df['epoch'], df['train/cls_loss'], label='cls_loss') axes[0].set_xlabel('epoch') axes[0].set_ylabel('loss') axes[0].legend() axes[1].plot(df['epoch'], df['metrics/mAP50(B)'], label='mAP50') axes[1].plot(df['epoch'], df['metrics/mAP50-95(B)'], label='mAP50-95') axes[1].set_xlabel('epoch') axes[1].set_ylabel('mAP') axes[1].legend() plt.savefig('loss_curve.png', dpi=150)

如果 mAP50 在 50 轮后还在涨,说明 100 轮不够,加到 200 轮。如果 50 轮就平了,说明模型容量不够,换yolov8s.pt试试。

4. 可视化界面与部署:从脚本到可交互系统

4.1 用 Gradio 搭一个能上传图片看结果的界面

毕设和课程设计最需要的是一个能演示的界面。Gradio 是最快的方式,十几行代码就能做出上传图片、显示检测框和识别结果的效果。

import gradio as gr from ultralytics import YOLO import cv2 import numpy as np det_model = YOLO('runs/train/container_code_det/weights/best.pt') char_model = ... # 加载字符分类模型 def recognize(image): # image 是 numpy 数组,RGB 格式 img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) results = det_model(img, conf=0.5, iou=0.45) full_code = '' for box in results[0].boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() cls_id = int(box.cls[0]) if cls_id == 0: # box_code 类 chars = rectify_and_split(img, [x1, y1, x2, y2]) for ch in chars: # 预处理 ch 成 32x32,送 char_model 分类 ... full_code += predicted_char # 画框 cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) # 把箱号写在图片上方 cv2.putText(img, full_code, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2) return cv2.cvtColor(img, cv2.COLOR_BGR2RGB), full_code demo = gr.Interface( fn=recognize, inputs=gr.Image(type='numpy'), outputs=[gr.Image(type='numpy'), gr.Textbox(label='识别结果')], title='集装箱箱号识别系统' ) demo.launch(server_name='0.0.0.0', server_port=7860)

conf=0.5是置信度阈值,低于 0.5 的框不输出。如果漏检多,降到 0.3;如果误检多,升到 0.6。iou=0.45是 NMS 的 IoU 阈值,箱号行之间不会重叠,这个值影响不大。

4.2 部署到 Ubuntu 20.04 CPU 版本的注意事项

很多毕设环境没有 GPU,只能用 CPU 推理。YOLOv8n 在 CPU 上单张图推理约 200-300ms,加上字符分类,整条链路约 500ms,勉强能接受。安装时注意:

# 创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装 PyTorch CPU 版本,不要装 CUDA 版本 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # 安装 ultralytics 和 gradio pip install ultralytics gradio opencv-python # 验证 python -c "from ultralytics import YOLO; print('ok')"

如果pip install ultralytics报错,大概率是 numpy 版本冲突,先pip install numpy==1.24.4再装。CPU 推理时把model.predict的device参数设成'cpu',不要留空,否则会尝试找 GPU 然后报错。

4.3 用 ONNX 导出加速 CPU 推理

PyTorch 模型在 CPU 上推理有额外开销,导出成 ONNX 能快 20-30%。导出命令:

from ultralytics import YOLO model = YOLO('runs/train/container_code_det/weights/best.pt') model.export(format='onnx', imgsz=640, simplify=True)

导出的best.onnx用 onnxruntime 推理:

import onnxruntime as ort import numpy as np session = ort.InferenceSession('best.onnx') input_name = session.get_inputs()[0].name # 预处理:resize 到 640x640,归一化,HWC 转 CHW,加 batch 维度 blob = np.random.randn(1, 3, 640, 640).astype(np.float32) outputs = session.run(None, {input_name: blob})

注意 ONNX 模型的输出格式和 PyTorch 不同,需要自己写后处理(解码框、NMS)。如果不想写,直接用 ultralytics 的YOLO('best.onnx')也能跑,它会自动处理。

5. 避坑与排查:箱号识别系统上线前必须过的五道关

5.1 检测框漂移导致字符切分错位

现象:字符行检测框位置基本对,但切分出来的字符要么多一个要么少一个,识别结果串位。原因通常是检测框的宽高比和实际字符行不匹配,透视矫正时把字符拉变形了。解决办法是在rectify_and_split里加一步:根据框的宽高比判断是否需要旋转。箱号行的宽高比一般在 6:1 到 10:1 之间,如果检测框宽高比小于 4:1,说明框歪了,先做最小外接矩形矫正再切分。

5.2 字符分类模型对相似字符混淆

现象:数字 0 和字母 O、数字 1 和字母 I、数字 8 和字母 B 经常认错。原因有两个:一是训练数据里这些相似字符的样本不够,二是字符分割时把相邻字符的边角切进来了。解决办法:在字符集里直接去掉 I、O、Q 这三个字母(箱号标准里本来就不用),把 33 类降到 30 类;然后在训练时对 0/8/B 这几类做重点增强,每类补到 500 张。

5.3 低光照图片检测全漏

现象:白天图片正常,傍晚或阴天图片一个框都检测不出来。原因是训练集里低光照样本太少,模型没学过暗光特征。解决办法:在训练前用 Albumentations 做离线增强,对每张训练图随机降低亮度 30%-50%,模拟暗光;或者在 YOLOv8 训练时把hsv_v调到 0.6,让模型在训练中见到更多暗光变化。如果已经训练完了,推理前用 CLAHE 做直方图均衡化也能救急。

5.4 Gradio 界面并发上传时显存溢出

现象:一个人用没问题,两个人同时上传图片就报 CUDA out of memory。原因是 Gradio 默认多线程,每个请求都加载一次模型。解决办法:把模型加载放在函数外面,全局只加载一次;然后在demo.launch()里设max_threads=1,强制串行处理。如果还是溢出,把 batch 设成 1,并且每处理完一张图手动torch.cuda.empty_cache()。

5.5 部署到新机器时 OpenCV 报错

现象:在开发机上跑得好好的,换一台机器cv2.imshow报cannot connect to X server。原因是新机器没有图形界面,而代码里用了imshow。解决办法:把所有cv2.imshow删掉,改成cv2.imwrite保存到文件,或者直接用 Gradio 的 Image 组件显示。另外 OpenCV 的版本也要注意,opencv-python和opencv-python-headless选一个装,服务器上装 headless 版本,避免依赖图形库。

6. 把识别率从 85% 拉到 95% 的两个后处理技巧

第一个技巧是箱号校验位修正。集装箱箱号最后一位是校验位,按 ISO 6346 标准计算:前 10 位字母数字映射成数值(A=10, B=12, ..., Z=38,跳过 11 的倍数),加权求和后取模 11,再模 10。如果识别结果的校验位对不上,说明某一位认错了。这时候不要直接输出,而是把置信度最低的那一位替换成候选字符里第二高的,重新算校验位,如果对上了就采纳。这个后处理能把准确率拉高 3-5 个百分点,因为校验位相当于一个免费的纠错码。

第二个技巧是多帧投票。码头摄像头通常是视频流,同一个箱号会在连续多帧里出现。把连续 5 帧的识别结果收集起来,每一位字符取众数,如果某一位有 3 帧以上一致就采纳。这个技巧对运动模糊和瞬时遮挡特别有效,代价是延迟增加 5 帧,对于龙门吊场景完全可接受。

from collections import Counter def multi_frame_vote(results_list): """ results_list: 连续多帧的识别结果字符串列表 返回: 投票后的最终箱号 """ if not results_list: return '' # 取最短长度,避免长度不一致 min_len = min(len(r) for r in results_list) final = [] for i in range(min_len): chars = [r[i] for r in results_list] # 取出现次数最多的字符 most_common = Counter(chars).most_common(1)[0][0] final.append(most_common) return ''.join(final)

这两个技巧都不需要重新训练模型,纯后处理,代码量不到 50 行,但效果立竿见影。我自己的习惯是:任何字符识别项目,先加校验位修正,再加多帧投票,这两步做完再去调模型。因为模型调参的收益是线性的,而后处理的收益是指数级的——它利用的是问题本身的结构信息,不是数据里的统计规律。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询