简介面向计算机视觉与深度学习方向的毕业设计、课程设计场景这份资源实现了基于YOLOv8的码头集装箱箱号自动识别完整覆盖数据集、模型训练、可视化界面与部署说明适合具备一定Python基础的学生直接运行或二次开发。压缩包共8个文件包含3个Python脚本负责模型训练、视频检测、可视化页面、3个模型权重文件及2个说明文档整体约15.91MB结构精简便于快速上手。项目代码均经过测试可成功运行能生成核心指标曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果与标签分布图可直观支撑答辩展示与效果论证。目前已有35人学习下载对于需要完整跑通目标检测流程、完成毕设或课设的读者该资源提供了可直接使用的训练代码、预训练权重、界面化演示方案压缩包内附说明文档按步骤操作即可复现结果。1. 智慧码头箱号识别为什么说 YOLOv8 只解决“框出来”不负责“读出来”拿到这套基于 YOLOv8 的智慧码头集装箱箱号自动识别系统大多数人的目标很直接把它跑通再在答辩现场让评委看到一串箱号被清晰地读出来。这里先泼一盆冷水YOLOv8 在这个项目里只负责“检测”——把集装箱门上的 11 位箱号区域或者箱号里的单个字符从复杂的码头背景中框出来真正把框里的内容读成字符串还要靠后面的分类、排序和校验逻辑。这套毕设/课程设计包自带源码、完整数据集、可视化界面和部署教程闭环了从数据到演示的整条链路这也是它适合拿来交作业的原因。但链路的每一个环节都有各自的坑环境、数据、训练、排查、界面部署我按实际落地顺序往下讲。2. 从压缩包到第一个推理结果环境搭建与最小运行路径2.1 拿到压缩包先别急着训练确认入口脚本与权重这类项目包一般会带源码目录、数据集目录、权重目录和一份 README 部署教程。我的建议是第一件事不是读代码而是先顺着 README 找到入口脚本把它跑起来。入口脚本通常是界面程序或者一个 demo 推理脚本先让它跑通你才能对项目的输入输出有体感后面改数据、改模型才有的放矢。常见做法是压缩包里会自带一个已经训练好的权重文件可能是.pt格式。先确认这个权重的存在如果它存在就跳过训练环节直接用它出一次推理结果。如果包里只有预训练模型yolov8s.pt而没有针对箱号数据训练过的权重那就得先把环境搭好然后用自己的数据集从头训练。另一个需要确认的是类别的数量和名称。箱号识别常见两种方案一种是把每个字符当作一个类别训练一个 36 类A-Z 加 0-9的检测器另一种是把整个箱号区域当作一个类框出区域后再交给 OCR 识别。这两种方案的权重输出维度不同后续处理逻辑也不同拿到手先看data.yaml里的nc值是多少就能判断它走的是哪条路。2.2 Ubuntu 20.04 搭建 CPU 版 YOLOv8 环境的最小命令如果你的机器没有独立显卡或者显卡驱动没配好最稳妥的做法是直接搭一个 CPU 版本的运行环境。Ubuntu 20.04 上用 conda 建环境最省事命令如下conda create -n yolo python3.10 -y conda activate yolo # 纯 CPU 机器直接用 CPU 版 torch不要自己去折腾 CUDA pip install torch2.0.1 torchvision0.15.2 \ --index-url https://download.pytorch.org/whl/cpu pip install ultralytics8.1.0 opencv-python pandas matplotlib pip install pyside6这里解释一下为什么先把 torch 单独装ultralytics 库依赖 torch但如果直接pip install ultralyticspip 会拉一个默认的 CUDA 版本 torch体积大且在你没有对应驱动的机器上根本起不来。先指定 CPU 版 torch再装 ultralytics就不会出现这种“装完跑不起来”的情况。PySide6 是后面跑可视化界面要用的提前装好免得推理跑通了界面又缺依赖。装完后用一个最小命令验证环境是否正常python -c from ultralytics import YOLO; mYOLO(weights/best.pt); rm.predict(test.jpg, saveTrue)如果项目包里有best.pt这条命令会直接读取权重并对test.jpg做推理结果会保存在runs/detect/predict/目录下。如果提示找不到权重文件先确认路径或者临时换用官方预训练权重python -c from ultralytics import YOLO; mYOLO(yolov8s.pt); m.predict(test.jpg, saveTrue)预训练权重会在第一次运行时自动下载离线环境提前手动下载放到项目根目录即可。这一步跑通说明环境、依赖、权重三件事都齐了可以进入下一步。2.3 模型结构不是黑匣子yolov8 各版本选型与推理输出YOLOv8 的网络结构图网上到处都是但真正影响你选型的不是那些卷积块的细节而是参数量级和推理速度的取舍。模型版本参数量级推理速度适用判断yolov8n约 320 万最快CPU 也能勉强跑适合实时预览yolov8s约 1100 万较快毕设推荐精度速度均衡yolov8m约 2600 万中等GPU 上有明显提升显存不足慎选yolov8l / x更大慢小数据集上容易过拟合不推荐箱号字符本身是典型的“小而清晰”目标特征不算复杂用 yolov8n 或 yolov8s 就足够。很多人在这一步迷信大模型结果显存爆了训练时间翻几倍精度却没涨多少。字符识别这种任务瓶颈往往在数据质量、后处理和相似字符区分不在模型容量。推理输出的理解也很关键。调用m.predict()后返回的结果里有boxes对象它包含xyxy框坐标、conf置信度、cls类别 id这些字段就是你后续做排序、拼接、去重的原始材料。比如字符级检测方案里识别“读出来”这一步本质就是按每个框的 x 坐标从左到右排序再把cls映射回字符串。这里也是后面最容易翻车的地方下一章讲数据时你会看到原因。3. 箱号数据集三条来源标注转换、清洗校验与合成增强3.1 箱号数据集为什么稀缺标注标准与“够用”配置如果你在公开数据集网站上搜集装箱箱号会发现结果非常零散。原因很简单箱号不像人脸、车辆那样有大规模公开标注它是港口运营数据的一部分版权和使用限制很多。所以做这个毕设最现实的路线是自己攒数据。一个“够用”的箱号数据集不需要几万张但要满足三个条件覆盖不同光照白天强光、夜间补光、阴天阴影箱号识别在夜间场景最容易让检测模型集体翻车。覆盖不同箱体和字体不同船公司的箱门布局差异很大字体也不统一尤其要注意“0”和“O”、“1”和“I”的打印形态。训练集和验证集按场景分开不要把所有同场景图片都随机打散否则验证集精度虚高换个摄像头就崩。常见的攒数据路线有三条第一条是网上搜集装箱实拍图用 labelme 或 labelImg 手动标注第二条是从港口监控视频里抽帧抽帧后先做初步筛选去掉没有箱号或箱号过小的帧再标注第三条是合成数据把真实箱号字符贴到不同集装箱背景上。毕设阶段建议三条路线并行以真实标注为主合成数据作为补充。3.2 把 labelme 多边形标注转成 YOLO 的 txt 格式labelme 标注出来的文件是 JSON里面存的是多边形点位。YOLO 训练需要的是归一化的中心坐标加宽高也就是每行class_id cx cy w h的 txt 文件。这里需要写一个转换脚本把 labelme 的 polygon 转成外接矩形。import json import os def labelme_to_yolo(json_path, img_w, img_h, class_id0): 把 labelme 标注文件转成 YOLO 格式的 txt。 json_path: labelme 输出的 json 文件路径 img_w, img_h: 原图宽高用于归一化 class_id: 当前标签对应的类别编号多类别时需要按映射表传入 out_path json_path.replace(.json, .txt) with open(json_path, r, encodingutf-8) as f: data json.load(f) lines [] for shape in data[shapes]: pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) w x_max - x_min h y_max - y_min if w 0 or h 0: continue cx (x_min w / 2) / img_w cy (y_min h / 2) / img_h nw w / img_w nh h / img_h lines.append(f{class_id} {cx:.6f} {cy:.6f} {nw:.6f} {nh:.6f}) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) if __name__ __main__: # 实际使用时原图宽高请通过 PIL 或 cv2 读取不要手写死 labelme_to_yolo(img_0001.json, 1920, 1080, class_id0)这里有几个值得注意的参数细节。第一w / img_w和h / img_h都是浮点除法Python 3 里没问题但如果你把代码移植到 Python 2 风格环境记得转成 float否则会丢失小数。第二class_id必须从 0 开始连续编号YOLO 不认字符串标签。第三标注时如果贴着字符边缘画框转出来的框会偏紧训练时容易把字符截断建议标注时比字符外扩 2-3 个像素再在转换脚本里统一 padding 一下。转换完成后一定要随机抽查几张图片和 txt 是否对齐。写个脚本把 YOLO 框画回图上肉眼确认框的位置和类别没有错位这一步能省下后面排查数据问题的时间。很多人跳过了这个检查训练到一半发现 loss 不降回头查才发现是标注文件错位纯属浪费算力。3.3 用 ISO 6346 校验码清洗数据和兜底识别结果集装箱箱号不是随意的一串字符它遵循 ISO 6346 标准前 4 位是箱主代码字母第 5 到第 10 位是注册码数字第 11 位是校验码。校验码可以通过前 10 位计算出来这个规则可以直接用在两个地方数据清洗和预测结果纠错。清洗数据时如果某张图片标注的箱号不满足校验规则说明标注员可能抄错了字符这张样本会污染训练集。预测时如果模型输出的 11 位字符串校验不过说明至少有一位字符识别错了结合混淆规则比如把 O 当 0、I 当 1去重识别能挽回不少精度。# ISO 6346 字符映射表字母值与数字直接取数字本身 CHARS { A: 10, B: 12, C: 13, D: 14, E: 15, F: 16, G: 17, H: 18, I: 19, J: 20, K: 21, L: 23, M: 24, N: 25, O: 26, P: 27, Q: 28, R: 29, S: 30, T: 31, U: 32, V: 34, W: 35, X: 36, Y: 37, Z: 38 } def iso6346_check_digit(payload10): 根据前 10 位箱号计算校验码。 payload10: 长度为 10 的字符串前 4 位字母 后 6 位数字 返回: 校验码数字 0-9余数为 10 时标准规定校验码记为 0 total 0 for i, ch in enumerate(payload10): if ch.isdigit(): v int(ch) else: v CHARS[ch.upper()] total v * (2 ** i) remainder total % 11 return 0 if remainder 10 else remainder # 示例模型识别出的 11 位字符串 raw MSCU1234567 expected iso6346_check_digit(raw[:10]) if str(expected) ! raw[10]: print(校验失败模型大概率在 0/O、1/I、2/Z 这类字符上认错了)这段代码的逻辑是把前 10 位每个字符映射成数值第 i 位从 0 开始乘以 2 的 i 次方全部累加后对 11 取余余数 10 则校验码记为 0。把计算结果和模型输出的第 11 位对比就能判断整串识别结果是否自洽。这个校验规则是毕设里的一个加分点答辩时可以直接说“系统内置了 ISO 6346 校验机制对模型输出做合法性验证”比单纯说“我用了 YOLOv8”要有说服力得多。把它写进后处理代码里也是后面避坑章节里相似字符混淆问题的一个重要解法。3.4 不够数据就合成copy-paste 增强的实操约束当真实标注数据只有一两百张时模型很容易过拟合。一个有效手段是 copy-paste 增强从已有的标注图中把箱号区域抠出来随机旋转、缩放、调亮度后贴到其他集装箱背景图上同时把标签里对应的坐标同步换算。操作上有几个约束。第一贴图的位置不要总放在图片正中央要模拟箱门不同区域第二背景图尽量不要和源图同一张否则模型只是记住了“这张图上有箱号”而不是学会了“箱号长什么样”第三旋转角度控制在正负 15 度以内避免出现现实中不会出现的夸张倾斜第四贴上后加一些高斯模糊和光照渐变让合成区域和背景融合得更自然。合成数据在训练时不要和真实数据混合后不打标记。我一般会用合成数据做预训练再用真实数据微调或者在训练配置里把合成样本的权重降一些。如果直接混合训练模型可能会学到合成图像的某些伪特征比如过于干净的边缘。4. 用自己的数据训练 YOLOv8目录、参数与损失曲线判读4.1 组织 data.yaml 与 train/val 目录YOLOv8 训练自己的数据集第一步是把数据目录整理成固定结构。最常见的格式是这样的dataset/ images/ train/ val/ labels/ train/ val/图片和标注文件的文件名必须一一对应比如img_001.jpg对应img_001.txt。val 集不要只从 train 里随机抽最好是按场景抽比如拍了 3 个不同码头区域用 2 个区域做训练1 个区域做验证这样能看出模型的泛化能力。在项目根目录创建data.yamlpath: /home/yourname/dataset train: images/train val: images/val nc: 36 names: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z ]注意path建议写绝对路径写相对路径时 YOLO 会相对于当前工作目录解析你在不同目录下执行训练命令结果可能不一样。nc是类别总数36 类对应大写字母加数字。如果你的方案是“只检测箱号区域识别交给 OCR”nc就是 1names 里写一个container_code即可。4.2 训练命令与参数在 GTX 1660Ti 上怎么调 batch数据组织好之后训练命令非常短。推荐直接用 Python 脚本方式启动便于传递参数和记录实验配置。from ultralytics import YOLO model YOLO(yolov8s.pt) # 加载官方预训练权重做迁移学习 results model.train( datadata.yaml, epochs100, imgsz640, batch8, device0, patience15, project./runs, namecontainer_code, pretrainedTrue, )这里每个参数都有明确的考量。imgsz640是默认推理尺寸对箱号字符这种小目标来说640 已经是下限再低会丢失细节但调成 1280 会让 6GB 显存直接爆掉。batch8是在 GTX 1660Ti 这种 6GB 显存显卡上的稳妥值我试过 batch16 大概率 OOMbatch4 又太浪费算力。patience15表示连续 15 个 epoch 验证指标没有提升就提前停止防止你挂机训练一晚上白白浪费电。如果你的显卡不是 1660Ti而是显存更大的 8GB 或 12GB可以先把 batch 设成 8训练开始后观察显存占用再逐步往上加。反过来如果显存不足优先减小 batch而不是减小图片尺寸。小 batch 会让损失曲线更抖但箱号数据集通常只有几百张图抖动在可接受范围内。还有一个容易忽略的参数是workers。默认的workers8在 Windows 上经常会因为多进程数据加载崩溃如果你在 Windows 下跑显式设置workers0能省掉很多烦恼。4.3 画损失函数曲线图用 results.csv 而不是截图训练完成之后ultralytics 会在runs/container_code/目录下生成results.csv和results.png。很多毕设直接截图results.png放到论文里但这张图有时候不够细致最好自己再画一版。results.csv里面每一列对应一个指标包括 train 的 box_loss、cls_loss、dfl_loss以及 val 的 mAP50、mAP50-95。画图代码如下import pandas as pd import matplotlib.pyplot as plt df pd.read_csv(runs/container_code/results.csv) # ultralytics 导出的列名有的带空格先打印出来确认 print(df.columns.tolist()) epochs df[ epoch] cls_loss df[ train/cls_loss] mAP50 df[ metrics/mAP50(B)] fig, ax1 plt.subplots(figsize(10, 5)) ax1.plot(epochs, cls_loss, labeltrain/cls_loss, colortab:blue) ax1.set_xlabel(epoch) ax1.set_ylabel(loss) ax2 ax1.twinx() ax2.plot(epochs, mAP50, labelmAP50, colortab:orange) ax2.set_ylabel(mAP50) plt.title(YOLOv8 train loss and mAP50) plt.tight_layout() plt.savefig(my_loss_curve.png, dpi200)这里有个高概率踩坑点results.csv 的列名是带前导空格的直接写df[train/cls_loss]会报 KeyError。正确做法是先print(df.columns.tolist())看一眼实际列名再选择对应的列。这个不是 bug是 ultralytics 为了对齐显示格式搞出来的小陷阱。判读曲线时重点关注三件事第一cls_loss 是否在前 20 个 epoch 内快速下降如果一直横盘甚至上升说明学习率可能过高或者数据标签有错第二train loss 和 val mAP 是否同时稳住了如果 train loss 还在降但 val mAP 开始波动说明过拟合patience提前停止能接住第三mAP50 如果卡在 0.8 左右上不去反复在区间震荡问题大概率不在训练参数而在数据本身比如相似字符样本太少。训练曲线本身存在玄学但数据问题永远是最常见的真相。5. 复现这套系统最容易翻车的 5 个具体排查点5.1 训练半天没有任何可见输出先定位数据路径现象执行训练命令后终端只打印了版本信息然后长时间无日志输出或者日志里显示No labels found in ...。原因绝大多数情况是 data.yaml 里的路径不对。path写成了相对路径而当前工作目录不在数据集上一级或者train和val字段写成了绝对路径但 Linux 下路径以~/开头没展开。解决在训练脚本开头加两行检查代码import os print(os.path.exists(data.yaml)) print(os.listdir(dataset/images/train)[:5])确认文件和目录都存在再启动训练。如果目录确实存在但仍报No labels found检查 labels 里的 txt 文件是否为空空的 txt 文件也会被判定为无效标注。5.2 6GB 显存跑不动OOM 排查现象训练脚本启动后终端报torch.cuda.OutOfMemoryError: CUDA out of memory或者训练界面直接闪退。原因batch 和 imgsz 的组合超过了显存容量。GTX 1660Ti 只有 6GB 显存用 640 分辨率时 batch16 必爆有些项目为了省事默认了较大的 batch刚拿到的压缩包直接跑就中招。解决先把 batch 降到 4跑通后再往上加。同时检查workers是不是设得过高数据加载线程也会占内存workers0或者workers2更稳妥。如果还不行把imgsz降到 480代价是字符检测精度会受一点影响。最后还有一个办法换 yolov8n 模型参数量是 yolov8s 的三分之一显存压力小很多。5.3 0 和 O、1 和 I 混淆导致序列错后处理兜底现象单字符识别精度看着有 95%但拼成的 11 位箱号整体正确率只有 70% 左右。细看错误样本全是 0 认成 O、1 认成 I、2 认成 Z 这类相似字符。原因集装箱打印字体本身就不区分这些字符的形态差异加上光照、反光、锈蚀模型在两个类别之间的置信度很接近argmax 取错是常态。解决利用 ISO 6346 校验码强行纠错。识别出多个候选结果比如 top-2 结果对每个候选字符串计算校验码校验通过的结果优先采纳。如果 11 位有效候选都不通过校验再对疑似混淆位做替换尝试比如把第 5 位的 O 换成 0 重新校验。这个后处理逻辑在毕设里能直接把箱号整体准确率拉高 10 个百分点以上。5.4 验证集 mAP 高、换场景就崩数据分布不一致现象训练时 mAP50 到了 0.93论文图很好看但换一批现场拍摄的照片或者换个摄像头的视频识别率惨不忍睹。原因train 和 val 的数据都来自同一批网图光照和拍摄角度高度相似模型学到的是“这些图的整体外观”而不是“箱号字符的普适特征”。解决按场景切分数据。比如你有 5 个来源的图片训练用 4 个来源验证用第 5 个来源。如果某个来源只有几十张可以让验证集小而全而不是强行凑比例。数据增强里把 HSV 扰动调大一点模拟不同光照环境能减少场景迁移的落差。5.5 界面点按钮就“未响应”推理线程阻塞现象可视化界面能打开但点击“开始识别”后界面马上白屏Windows 提示“未响应”过几秒才恢复。原因推理是耗时操作如果在界面主线程里直接调用模型推理事件循环被阻塞界面自然卡死。这是毕设可视化界面里最普遍的问题。解决把推理放到独立线程界面主线程只负责接收结果刷新控件。我一般用 PySide6 的 QThread或者更简单的threading.Thread加信号槽。具体接线在第 6 章给出这一步是界面能不能“像个产品”的关键分水岭。6. 界面接线、导出加速与边缘端部署把毕设变成能演示的系统6.1 PySide6 接线骨架把推理放到独立线程里可视化界面拿到手后不建议直接大刀阔斧重写只需要把推理调用从按钮回调里移出去放到一个 Worker 线程里。一个干净的最小骨架是这样import sys from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QLabel from PySide6.QtCore import QThread, Signal from ultralytics import YOLO class Worker(QThread): result_ready Signal(str) def __init__(self, model_path, img_path): super().__init__() self.model YOLO(model_path) self.img_path img_path def run(self): res self.model.predict(self.img_path, verboseFalse) # 这里把检测结果组装成一行字符串 info fdetected {len(res[0].boxes)} objects self.result_ready.emit(info) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.btn QPushButton(识别, self) self.label QLabel(结果, self) self.btn.clicked.connect(self.on_start) def on_start(self): self.worker Worker(weights/best.pt, test.jpg) self.worker.result_ready.connect(self.label.setText) self.worker.start() # 启动线程不阻塞界面按钮回调里只做了三件事创建 Worker、连接信号、启动线程。按钮点击后界面可以继续拖动、刷新推理完成后信号槽自动更新文本框。这个骨架不加任何额外的界面库直接跑就有效果。实际项目里在信号里再接一个参数传图片路径或者把识别结果写成表格思路完全一致。6.2 CPU 提速导出 OpenVINO 格式后换两行代码很多演示现场没有独显CPU 跑 PyTorch 模型慢得让人尴尬。一个性价比极高的操作是把 YOLOv8 导出为 OpenVINO 格式推理速度能有几倍提升。pip install openvino onnx yolo export modelweights/best.pt formatopenvino imgsz640导出后会在weights/下生成best_openvino_model/目录加载方式和原来几乎一样model YOLO(weights/best_openvino_model/)唯一要注意的是导出时的imgsz必须和训练时一致如果训练用了 640 而导出用 480模型会重新缩放输入精度会有损耗。量化到 int8 能进一步提速但对小字符检测来说精度掉点明显我一般选择 FP16 精度兼顾速度和准确率。6.3 再往前一步RK3588 板端部署与量化精度取舍如果项目想往实际产品方向靠瑞芯微 RK3588 这类带 NPU 的边缘板子是常见的部署目标。部署路径一般是PyTorch 权重导出为 ONNX再用 rknn-toolkit2 转换成 RKNN 格式最后上板推理。rk3588 的 NPU 对 int8 量化很友好但箱号字符属于小目标量化后经常出现小框丢失。我的建议是转换后先在板端跑一批验证集对比量化前后的 mAP50。如果掉点超过 5%优先用混合量化方案让大部分层走 int8敏感层保留 fp16。这一步在毕设里不做也没关系但论文里如果能写一句“预留了 RK3588 边缘部署的模型转换链路”多少能体现工程完成度。跑这类项目我养成了一个习惯固定留十几张最难的样本当回归集每次改后处理逻辑或调参都重跑一遍对比结果而不是肉眼扫几张图就下结论。比任何玄学调参都管用。希望帮到你。本文还有配套的精品资源点击获取