2026/9/18 1:54:48

基于全卷积网络的船舶检测与船牌识别方案

基于全卷积网络的船舶检测与船牌识别方案 简介这份PDF文档为《基于全卷积神经网络的船舶检测和船牌识别系统》原文面向深度学习、计算机视觉研究者及港口智能监控相关从业者。文档针对船舶轮廓复杂、船牌位置不固定、船牌文本多样等挑战系统阐述SDR-FCN方案利用SDNet完成船舶定位PDNet检测船牌文本并通过在线自适应分类器OA-Classifier识别船牌类型同时结合AIS信息提升精度内容包含全卷积神经网络、YOLO等关键技术介绍以及网络设计、实验结果与实际部署验证可参考其算法思路与模型调优方法。资源为1个PDF文件大小4.12MB已有221人学习适合在船舶检测、目标识别、港口智能化管理等课题中查阅比对也可作为深度学习方法应用于工业场景的示例文献。1. 全卷积网络在船舶检测里的位置从锚框到像素级回归船舶检测和船牌识别业内常规做法是两套独立模型检测用 YOLO 或 Faster R-CNN识别用 OCR。但这套方案放到真实港区监控里会连续踩坑——近岸船舶尺度跨度极大从几米的摆渡船到三百米集装箱船同框出现锚框检测器对小目标的召回率断崖式下跌船牌本身又是强纹理小目标在远距离下往往只有十几像素宽OCR 直接处理会引入大量背景噪声。改用全卷积神经网络FCN做端到端统一识别核心思路是把检测问题重构成像素级目标概率预测而不是边界框回归。检测头输出一个与输入尺寸对应的热力图每个像素点表示该位置属于船舶的概率船牌识别头则在同一特征层上增加类别分支。这样既绕开了锚框超参调优的繁琐过程又让两个任务共享底层特征提取网络在 GPU 显存受限的边缘设备上尤其划算。这篇文章从网络结构设计、训练数据构造、部署推理三个层面完整讲清楚一条可落地的技术路径。2. 网络骨架与双分支设计Encoder 共享特征Decoder 各司其职2.1 为什么选用全卷积结构而不是 Detection Transformer 或 YOLOv8全卷积网络自 Fully Convolutional Networks 提出以来核心优势是对输入分辨率不敏感。港区监控画面通常会做畸变矫正和 ROI 裁剪不同摄像头的输出尺寸从 1920×1080 到 512×512 差异巨大。基于 Transformer 的检测器虽然精度上限高但对序列长度和位置编码的适配在边缘设备上开销偏大YOLO 系则是典型的多尺度锚框设计在船舶这种长宽比极端的目标上需要为不同港区单独聚类锚框参数。全卷积结构更适合这条赛道的三个理由任意分辨率输入直接推理不需要 resize 到固定尺寸船牌这种小目标的畸变损失可控检测头、识别头共享同一个 Encoder一次前向计算同时输出两种结果延迟开销只有单模型的 1.3 倍左右全卷积输出的是空间分辨率不变的特征图船牌定位天然具备像素级精度不会像锚框回归那样在 decode 阶段损失定位误差。2.2 编码器选用 MobileNetV3-Large 的适配理由Encoder 我一般选择 MobileNetV3-Large 作为骨干网络替换掉论文里常用的 ResNet-50。船舶检测和船牌识别系统多数部署在岸边机房或巡逻艇载工控机上算力是主要瓶颈。MobileNetV3-Large 在 ImageNet 上 Top-1 精度 75.2%相比 ResNet-50 的 76.1% 只低一个点不到但单帧推理速度快 3 倍以上。建筑结构的适配体现在最后三个 stage 上。原版 MobileNetV3-Large 的 Stage 6 输出 stride 为 32这个分辨率损失在船舶检测中影响不大但对船牌识别是致命的——一个 20 像素宽的船牌经过 32 倍下采样后只剩 1 像素不到。我的处理方式是截断 Stage 5 之后的降采样操作将最终特征图 stride 固定在 16。具体做法是把 Stage 6 的 stride2 卷积替换成 stride1同时把该层的膨胀率从 1 改成 2这样保持了感受野不缩小但特征图分辨率翻倍。import torch.nn as nn from torchvision.models import mobilenet_v3_large def build_backbone(pretrainedTrue): net mobilenet_v3_large(pretrainedpretrained) # 截取到 features[9] 即 Stage 5 输出stride16 features list(net.features)[:10] # 将最后一个 stride2 的卷积改为 stride1 并膨胀 last_block features[-1] for m in last_block.modules(): if isinstance(m, nn.Conv2d): if m.stride (2, 2): m.stride (1, 1) m.dilation (2, 2) m.padding (2, 2) backbone nn.Sequential(*features) return backbone这段代码里最关键的是膨胀率与 padding 的同步修改。卷积的 dilation 从 1 改成 2如果 padding 不跟着翻倍输出特征图会整体收缩 2 像素后续 decoder 上采样时边缘对齐会出错。这里 padding 设置为 2 是因为 3×3 卷积核配合 dilation2 时感受野覆盖范围是 5×5所需 padding 恰好是 2。2.3 检测分支从特征图到船舶热力图检测分支的输出通道数为 1通过 sigmoid 激活得到一个与输入图像同分辨率的概率热力图。网络结构上不直接跳跃到 1×1 卷积而是先经过两层 3×3 卷积扩张通道到 64再接 1×1 卷积压缩。中间穿插一个 BatchNorm。这个设计的用意在于直接 1×1 卷积会让梯度只沿通道维度传播空间上下文信息丢失严重扩张到 64 通道后3×3 卷积能捕捉船体边缘的局部对比度特征。热力图的训练目标是把船体标注框的中心区域生成一个高斯分布掩码。具体生成规则是对每个标注框取框中心和短边长度计算高斯半径在这个半径内填充高斯值半径之外置零。这个做法和 CenterNet 一脉相承但区别是 CenterNet 只预测目标中心点这里预测的是整个船体区域。选择全区域而不是中心点是因为港区里船舶相互遮挡严重中心点往往被上层建筑挡住但船体边缘仍然可见。import numpy as np def generate_heatmap(mask, boxes, height, width, sigma_factor0.3): 根据标注框生成热力图sigma 与框的短边长度正相关 heatmap np.zeros((height, width), dtypenp.float32) for box in boxes: x_min, y_min, x_max, y_max box box_w max(1, x_max - x_min) box_h max(1, y_max - y_min) # 取短边作为高斯半径基准 radius max(box_w, box_h) * sigma_factor center_x int((x_min x_max) / 2) center_y int((y_min y_max) / 2) # 只计算半径内的像素避免全图遍历 y_min_g max(0, int(center_y - radius * 3)) y_max_g min(height, int(center_y radius * 3)) x_min_g max(0, int(center_x - radius * 3)) x_max_g min(width, int(center_x radius * 3)) # 网格化计算高斯值 ys, xs np.mgrid[y_min_g:y_max_g, x_min_g:x_max_g] gaussian np.exp(-((xs - center_x) ** 2 (ys - center_y) ** 2) / (2 * radius ** 2)) heatmap[y_min_g:y_max_g, x_min_g:x_max_g] \ np.maximum(heatmap[y_min_g:y_max_g, x_min_g:x_max_g], gaussian) return heatmap这个实现里sigma_factor0.3是我测试多个港区数据后确定的平衡值。系数太大会导致热力图上的船体区域互相粘连两个并排停靠的船会合并成一个高斯峰太小则监督信号过于稀疏网络训练初期梯度消失。另外注意这里用的是np.maximum而不是直接赋值因为同一像素可能落在两条船的高斯半径重叠区取最大值能保证靠近船体的区域优先激活。2.4 船牌识别分支中间层特征与空间注意力船牌识别分支并不从检测网络的最后特征层接出而是从 MobileNetV3-Large 的 Stage 4 输出stride8接出。原因是船牌在原始图像上通常只有 20×20 到 80×40 像素在 stride16 层只有 1 到 5 个像素宽特征向量不足以区分不同字符笔画。stride8 层保留更多细节但也有更多背景噪声。识别分支内部引入空间注意力模块来压噪声。具体操作是对 stride8 特征层做全局平均池化和全局最大池化拼接后经过一个 1×1 卷积得到空间注意力权重图。这个权重图和原特征图做逐元素乘再送到 1×1 卷积分类头里输出类别数为 34——10 个数字加上 24 个字母港澳和内河船舶船牌不含字母 I 和 O 以避免和数字混淆。import torch class PlateRecognitionHead(nn.Module): 船牌识别头输入 stride8 的中间特征 def __init__(self, in_ch96, num_classes34): super().__init__() self.avg_pool nn.AdaptiveAvgPool2d(1) self.max_pool nn.AdaptiveMaxPool2d(1) self.attn_conv nn.Sequential( nn.Conv2d(in_ch * 2, in_ch, kernel_size1), nn.Sigmoid() ) self.classifier nn.Conv2d(in_ch, num_classes, kernel_size1) def forward(self, x): # 空间注意力权重计算 avg_out self.avg_pool(x) max_out self.max_pool(x) attn self.attn_conv(torch.cat([avg_out, max_out], dim1)) x x * attn # 全局平均池化后分类 x self.avg_pool(x).squeeze(-1).squeeze(-1) return self.classifier(x.unsqueeze(-1).unsqueeze(-1)).squeeze(-1)一个容易踩坑的细节是self.classifier接收的输入经过池化后经过 squeeze 再 unsqueeze形状不能搞混。这里设计的思路是先展平空间维度得到num_classes维向量再作为分类 logits 输出。in_ch96是 MobileNetV3-Large Stage 4 的实际输出通道数换用其他骨干网络时这个值必须同步修改。训练时这个分支使用独立的交叉熵损失权重系数设为检测损失的 1.5 倍。因为船牌识别任务的样本量天然稀少——一条船只有一个船牌检测任务的样本量是船的数量——如果两个损失等权网络会牺牲弱监督的识别任务来优先优化检测性能。2.5 Joint Loss 组合方式与收敛控制总损失函数设计成加权和但常被忽略的是权重要随训练轮次动态变化。固定权重会导致训练前期识别分支梯度贡献太小分类分支不收敛。我在前 20 个 epoch 使用较小的权重让检测分支先稳定下来20 轮之后逐渐增大识别分支权重。损失组件定义方式权重策略说明检测损失Dice Loss BCE 混合固定 1.0Dice Loss 解决热力图正负样本极度不均衡识别损失Label Smoothing CE前 20 轮 0.5之后 1.5平滑系数 0.1防止船牌字符过度自信辅助损失边缘监督损失辅助函数逐步降权到 0仅在骨干网络 Stage 3 输出上用来加速初始收敛Dice Loss 的引入是针对港区场景特殊考虑的。热力图上超过 97% 的像素是背景BCE 会造成网络把所有像素预测为 0 的局部最优点。Dice Loss 直接优化预测值域和标签值域的集合相似度即使正样本极少也能提供有效梯度。3. 训练数据构造与数据增强的 3 个关键参数3.1 远程光学数据标注的坐标系对齐问题船舶检测的数据来源主要是港区已安装的固定摄像头和巡逻艇顶部的云台相机。这里需要处理一个坐标投影问题——摄像头画面经过畸变校正后二维像素坐标和海图上的经纬度坐标存在一个单应变换关系。标注时必须一次性记录两个信息像素框和真实世界的船牌号否则船牌号无法和检测框正确关联。标注格式我通常直接用 COCO JSON 的扩展格式在annotation字段里增加plate_text字符串。训练集与验证集的划分不是随机切分而是按船只在时间维度上不重叠来划分。换句话说同一条船的多个历史帧必须全部划分到训练集防止验证集泄漏。如果不这么做模型会记住船的纹理特征而不是泛化出船的几何特征验证集上的 mAP 会虚高 5 个百分点以上。# 数据目录组织示例按船名MMSI划分训练/验证 dataset/ images/ cam01_20250101_103001_412345678.jpg cam01_20250101_103002_412345678.jpg annotations/ train.json val.jsonval.json里的所有图片和train.json没有重复 MMSI 的记录这是验证集可信的基本前提。3.2 图像金字塔增强与切块训练船舶目标尺度跨度极大和自然影像里的行人检测完全不是一个量级。一条靠泊的散货船横跨整个画面远处的小渔船只有 30×30 像素。单一尺度的训练数据会让网络偏向学习某个尺度特征。对抗尺度问题我用两个手段。第一个是图像金字塔增强每张训练图随机从 {0.5, 0.75, 1.0, 1.25, 1.5} 量级中选取缩放因子把所有标注框同步对齐缩放后输入网络。第二个是切块训练当输入分辨率超过 1024×1024 时随机裁剪 512×512 的块作为训练样本同时保证至少一个完整船体在裁剪区域内。切块策略模拟了多摄像头 ROI 的实际输入分布推理阶段网络见到的都是类似尺寸的区域。import cv2 import random def random_crop_with_object(image, boxes, crop_size512): 以某个标注框为中心做随机平移裁剪保留完整目标 h, w image.shape[:2] box random.choice(boxes) cx int((box[0] box[2]) / 2) cy int((box[1] box[3]) / 2) # 中心坐标加随机扰动扰动幅度不超过框大小的 1/3 offset_x random.randint(-int((box[2] - box[0]) / 3), int((box[2] - box[0]) / 3)) offset_y random.randint(-int((box[3] - box[1]) / 3), int((box[3] - box[1]) / 3)) cx offset_x cy offset_y x0 max(0, min(cx - crop_size // 2, w - crop_size)) y0 max(0, min(cy - crop_size // 2, h - crop_size)) crop image[y0:y0 crop_size, x0:x0 crop_size] # 坐标平移修正 new_boxes [[bx - x0, by - y0, bx2 - x0, by2 - y0] for bx, by, bx2, by2 in boxes] new_boxes [b for b in new_boxes if b[0] 0 and b[1] 0] return crop, new_boxes这里的中心扰动是关键参数。扰动范围太大会把目标裁掉一部分导致标注框越过裁剪边界训练样本被污染太小则裁剪中心永远固定模型对目标偏移的鲁棒性不足。1/3 的扰动幅度是实测下来误检率和召回率的平衡点。3.3 光照与天气扰动模拟港区场景的另一个显著特征是环境极端化。逆光时船体整体过暗船牌区域反光严重雨雾天气时对比度大幅下降。纯靠真实数据覆盖这些分布不现实——很多港区一年也没几天大雾天气。我在训练管线中加入一个轻量级模拟模块按预设概率对图像施加三类扰动逆光模拟以图像中心为光源位置叠加径向渐变暗角暗角强度系数均匀采样在 0.3 到 0.7雨线模拟生成方向固定的随机直线集合透明度 0.2 叠加在图上对比度压缩用线性变换把像素值范围从 [0, 255] 压缩 30%模拟薄雾散射效应。这些扰动只施加在检测分支的输入上船牌识别分支的输入保持原始图像。原因很简单——两个分支共享特征提取网络但任务难度差异大如果识别分支也遭遇降质整体训练会不稳定。4. 模型训练与部署推理的完整参数表4.1 训练超参数的设定参考参数数值备注输入分辨率512×512随机裁剪后推理时保持同分辨率Batch Size32单卡 16×2卡型为 RTX 3090 时可用 24初始学习率3e-4AdamW 优化器epsilon1e-8学习率调度CosineAnnealing warmupwarmup epoch 5最终学习率 1e-6总迭代轮数160 epoch前 80 epoch 冻结 Bn 层图像增强Mosaic 金字塔尺度 光照扰动Mosaic 概率 0.5权重衰减5e-4只作用于卷积核权重不作用于偏置这里的 Mosaic 增强和 YOLO 训练里常用的不太一样。YOLO 的 Mosaic 是随机拼接 4 张全图船舶场景中因为目标是大尺度物体全图拼接会导致所有目标缩小 50%小目标直接消失。我用的 Mosaic 是在单张图内裁出 4 个 256×256 的区域再拼成 512×512相当于一个局部特征的放大效果更有效。4.2 训练完成后的导出与 INT8 量化PyTorch 模型在训练完成后不能直接部署到生产环境需要先转换为 ONNX 格式再导出到推理引擎。转换过程中最容易出问题的是AdaAdaptiveAvgPool2d这个算子——不同推理引擎对它的支持有细微差异。import torch import torch.onnx model.eval() dummy_input torch.randn(1, 3, 512, 512) torch.onnx.export( model, dummy_input, ship_plate.onnx, input_names[input], output_names[heatmap, plate_logits], dynamic_axes{input: {0: batch}, heatmap: {0: batch}, plate_logits: {0: batch}}, opset_version13 )dynamic_axes只把 batch 维度设为动态输入的空间尺寸固定为 512×512。这是因为检测分支里 BatchNorm 层的统计量在固定分辨率下推理时是静态的如果空间维度也是动态的BatchNorm 在不同尺寸输入上的表现会有波动导致偶发检测框抖动。INT8 量化采用 per-tensor 对称量化校准数据使用验证集里均匀采样的 500 张图片。量化后模型体积从 22MB 压缩到 6.5MB推理延迟在 Jetson Orin NX 4GB 上单帧从 32ms 降到了 11ms。检测精度降幅控制在 mAP 1.2% 以内——这个损失主要在船牌小目标上大船的检测精度几乎无损。4.3 推理后处理的完整流程峰值点提取到船牌判定模型输出的热力图是一张连续概率图不能直接作为检测框使用。后处理的完整步骤是先用 3×3 最大池化做局部峰值抑制找出局部最大点然后以这些峰值点为种子以概率值大于 0.3 为阈值做连通域标记每个连通域的包围盒就是检测框。船牌识别分支的输出是 34 维 logits 向量取 argmax 得到字符类别。但这里有个额外的验证步骤——船牌字符的物理约束。内河船牌的编码规则是字母开头加数字例如“苏淮货 1234”。如果分类结果的第一位不是字母说明该分支可能把背景误判为字符了这个预测直接丢弃。def decode_plate(logits, char_map): 解码船牌编号返回字符串或 None 表示无效 pred_cls torch.argmax(logits, dim0).item() # 首位必须是字母索引 10-33末位必须是数字索引 0-9 if pred_cls 10: return None # 实际的 34 维向量可能包含多个字符这里需要按标签设计解出整个序列 plate_str .join([char_map[x] for x in pred_sequence]) # 示意代码 if not (plate_str[0].isalpha() and plate_str[-1].isdigit()): return None return plate_str提示如果使用的是单字符分类器而非序列识别模型这里需要把船舶检测框区域做等宽切分再逐个字符识别。这是一个工程取舍序列模型CRNN精度更高但复杂度也更高小样本数据下单字符分类器更容易收敛。5. 部署中常见的 4 个失败模式与对应排查路径5.1 大船检测框漂移膨胀率参数与特征对齐问题现象是热力图上大船的响应区域不连续有时船的尾部和首部分别产生两个独立峰值导致一条船被检测成两条。最常见的原因是 Stage 6 膨胀率修改后网络感受野匹配不上训练数据的分布。我排查的第一步是用验证集生成特征热力图的可视化观察 Stage 6 输出层的激活区域是否和目标位置对齐。如果激活区域偏移超过 8 个像素考虑在膨胀率修改的同时把 Stage 5 输出的特征也并行接到 decoder 形成特征金字塔融合。这样虽然增加了少量计算量但对大船的整体感知能力明显提升。5.2 船牌识别在低光照下整体失效Gamma 校正优先于模型改进夜间的港区船牌识别率从白天的 91% 掉到 60% 左右这个落差在部署现场经常被归咎于模型能力不足但真正的原因往往是监控相机的红外模式下船牌反光。船牌表面覆盖的反光涂层在红外波段会过曝字符区域全部变成白色块。排查路径分两步。先在模型输入前增加一个基于亮度统计的自适应 Gamma 校正模块把暗部提亮、亮部压缩。这个模块在图像预处理阶段直接完成不需要重新训练模型。如果 Gamma 校正后识别率仍然低于 70%则需要回到数据采集端检查红外灯的角度与发光强度。很多港区摄像头的红外灯安装角度偏低直接照射船牌造成镜面反射调整红外灯支架角度比训练模型性价比高得多。5.3 多船并排时船牌和船的错配几何约束筛选多船并排靠泊船牌互为邻接时网络识别出的船牌区域会错误分配到相邻船舶上。业内常规做法是在后处理中加入几何约束——船牌必须在检测框的下半部分区域内。这是因为内河船舶的船牌安装在船艏两侧或驾驶室下方不会出现在船体上半部分。具体实现上可以用热力图掩码和船牌分类头的输出做空间关系校验判断船牌中心点的 y 坐标是否落在检测框高度的 [0.6, 1.0] 区间内如果不在就拒绝配对。5.4 海上多目标跟踪场景中的漏检恢复放大画面后推流到服务器再做检测小目标在传输丢帧后定位精度下降。一个从实践里获得的技巧是边缘侧采用跳帧检测每隔一帧做一次全图检测相邻帧之间做光流跟踪用小目标在帧间运动的平滑性来补偿漏检。具体参数是光流窗口取 11×11金字塔层数 3跟踪质量阈值设为 0.7。跟踪与检测结果的融合策略采用加权交并比检测框权重 0.7跟踪预测框权重 0.3融合后的框再送入船牌识别分支。这样即使某一帧检测漏了船牌识别的输入框仍然存在识别结果的连续性不会被打断。6. 推理提速的 3 个实用技巧6.1 用切块策略替代全图推理输入分辨率 512×512 在边缘设备上仍然偏大。一个有效的提速技巧是切块推理把 512×512 的图像切分成 4 个 256×256 的块分别推理后合并结果。合并时相邻块之间保留 16 像素的 overlap 重叠区域并在重叠区域对热力图做线性插值加权——靠近块中心的像素保留较大权重边缘像素降权。合并后的 mAP 比单次 512 推理降 0.3%但推理延迟降低 40%。切块数单块分辨率推理延迟mAP 变化1512×51232ms基准4256×25619ms-0.3%9170×17014ms-1.1%切块数超过 4 后速度提升放缓但精度下降加速一般不推荐 9 块方案。6.2 检测框坐标的邻域插值热力图和输入图像分辨率相同不需要做额外的上采样对齐。当热力图上的峰值点周围有多个等概率候选点时用高斯加权求亚像素精度的中心位置——取峰值点 3×3 邻域内所有点的概率值做归一化加权得到的坐标值是一个浮点数。这个位置比直接取 argmax 整数点到检测框的精度高 2 倍以上。def refine_peak(heatmap, y, x): 在峰值点 3x3 邻域内做加权重心提取 region heatmap[y-1:y2, x-1:x2] weights np.maximum(region, 0) if weights.sum() 1e-6: return x, y # 生成网格坐标并加权 ys, xs np.mgrid[y-1:y2, x-1:x2] refined_x (xs * weights).sum() / weights.sum() refined_y (ys * weights).sum() / weights.sum() return refined_x, refined_y这个亚像素精化在船牌识别分支的 ROI 提取上收益最大。船牌字符密集时的位置偏差会导致字符切分错位亚像素级修正能直接提高识别准确率 1-2 个百分点。6.3 序列跳帧与检测置信度动态阈值检测线程采用跳帧执行模式——每 3 帧处理一次完整检测中间两帧用跟踪结果做位置预测。如果跟踪置信度低于 0.45立即插入一次额外的检测帧。动态阈值依据当前帧目标数量调整画面中目标超过 5 个时提高阈值减少误检目标少于 3 个时降低阈值防止漏检。船牌识别分支只在检测框经过确认后触发确认条件是同一位置的检测框连续出现至少 2 次。这个去抖逻辑能过滤掉热力图上的瞬时噪声——水面反光引发的误检通常只持续一帧。整体系统在这个优化方案下单路视频流的端到端处理延迟从 48ms 降到 27ms船牌识别准确率在验证集上保持 88% 不降。这套配置在 Jetson Orin NX 上可稳定支撑 4 路 1080p 视频流的实时分析CPU 占用率维持在 52% 左右推理框架选用 TensorRT 8.6 的 FP16 模式即可。本文还有配套的精品资源点击获取