☰
全卷积神经网络实现船舶检测与船牌识别:原理、数据与部署实践
2026/10/5 9:17:37 网站建设 项目流程

简介:《基于全卷积神经网络的船舶检测和船牌识别系统》是一篇发表于《计算机与现代化》2019年第12期的技术论文,面向港口智能监控、船舶管理与交通管理等领域的研究人员和工程师。论文针对船舶轮廓复杂、船牌位置不固定、文字类型多样等难题,提出基于全卷积神经网络(FCN)的SDR-FCN系统:利用SDNet实现船舶定位,通过PDNet检测船牌文本,并借助结合AIS信息的在线自适应分类器OA-Classifier完成船牌识别,兼顾了高精度与可靠性。文档共1个PDF文件,大小约4.12MB,属于深度学习、计算机视觉方向的专业参考资料。全文包含研究背景、算法原理、网络结构、实验评估等完整内容,非常适合希望在船舶检测、目标识别或港口智能化方向进行算法设计或论文复现的读者。目前已有221人学习下载,可作为相关课题的入门与进阶参考。

1. 全卷积神经网络做船舶检测与船牌识别:这套方案到底解决什么问题

在港口监控、内河航道管理和海事执法里,最重复又最烧人力的工作不是看雷达,而是盯视频:一条船驶过,值班员要在一两秒内判断它是什么船型、挂没挂船牌、牌号是多少。人工看视频的方式,漏看率和疲劳度都很高。把这件事自动化的常见做法是部署两套独立模型——一套检测船舶,一套识别船牌,中间还要写一堆衔接逻辑。而基于全卷积神经网络的船舶检测和船牌识别系统,核心思路是让一个网络同时干完这两件事:共享一份卷积特征,一个分支输出船舶位置和轮廓,另一个分支直接从特征图上读船牌文本,全程不经过全连接层的分类头。

这个方案最直接的价值是省掉两套模型之间的重复计算和工程对接,尤其在边缘设备上,推理时间可以从几百毫秒降到一个可接受的范围。它适合的读者是有一定深度学习基础、正在做海事视觉项目的工程师,也包括刚接手船舶识别任务的算法同学。这里有一个反直觉的结论先放在前面:船牌识别比车牌识别难在它没有一个统一的安装位置和字符规范,船牌经常贴着船舷斜挂、被水花溅到、被缆绳挡住一半,这些问题靠单纯的检测模型解决不了,必须在数据、损失函数和后处理上同时下功夫。下面把原理、数据和落地坑一条条拆开讲清楚。

2. 为什么是全卷积而不是两阶段检测:原理与选型逻辑

2.1 全卷积到底全在哪里:去掉全连接层后的空间一致性

常规分类网络最后接的是全连接层,把二维特征图压成一维向量,这个过程会丢失目标的位置信息。全卷积神经网络的“全”字,指的是网络里的所有层都是卷积层、池化层、上采样层这些空间算子,没有全连接层。最简单的替代做法是把全连接层替换成1×1卷积加全局平均池化,但这样的后果是只输出类别概率、失去了定位能力,所以做检测时通常保留整张特征图,通过卷积预测每个像素类别,或者通过卷积预测每个位置上的边界框偏移。换句话说,全卷积是“在哪看”和“看什么”同时完成的前提。

在船舶检测这个任务里,全卷积网络有很实际的意义:船舶不是居中正拍的物体,它可能在画面边缘,也可能被桥墩遮挡一半。如果先经过全连接层把特征压平,边缘船舶的空间结构会被严重破坏。全卷积的方式允许输入任意尺寸,输出一个与输入空间对应的密集预测图,每个位置都是一个候选。这也是为什么后来大量检测器都声称自己是全卷积——YOLO、FCOS、CenterNet这些模型都没有全连接层,它们本质上都是全卷积思想在目标检测上的变体。

2.2 三种技术路线的对比:两阶段、单阶段、全卷积分割

我一般会在做这类系统前先把技术路线定死,避免后来频繁返工。最常用的三条路线分别是:

路线代表结构输出形式船牌识别方式推理速度工程复杂度
两阶段检测Faster R-CNN边界框ROI 区域送 OCR慢高,需要两套模型联动
单阶段检测 + 额外OCRYOLO 检测船牌区域,再送 CRNN边界框 + 文本序列串接两个模型中中,需要保存中间结果
全卷积端到端FCN 分割船舶 + 检测头预测船牌位置 + 识别头输出文本掩码 + 框 + 文本共享骨干的多任务头快中高,但部署更紧凑

全卷积路线的优势在于船舶检测和船牌识别可以共享同一个骨干网络。一艘船的特征——船体外型、颜色、甲板结构——和船牌位置有着强相关,船牌总是挂在船体某个固定区域内,共享卷积自然把这种相关性利用起来。两阶段方案最大的问题是船牌识别依赖检测框的质量,如果检测框偏了,识别率会断崖式下跌。全卷积方案里识别分支读到的是特征图而不是裁剪图,对框的绝对精度要求反而低一些,容错性更好。

2.3 从概念到可编译的结构定义:一个最小全卷积检测识别头

选定路线后,第一步是搭出网络骨架。这里给出一个可运行的最小结构定义(PyTorch 风格),它用 ResNet 的前几层做骨干,再用一个全卷积分割头输出船舶掩码,同时用一个全卷积检测头预测船牌框和一个识别头输出船牌字符序列。

import torch import torch.nn as nn import torchvision.models as models class ShipFCN(nn.Module): def __init__(self, num_char_classes=37, max_char_len=8): super().__init__() # 骨干:使用 ResNet18 前四层,去掉最后的池化和全连接 backbone = models.resnet18(pretrained=True) self.features = nn.Sequential( backbone.conv1, # 64通道,/2 backbone.bn1, backbone.relu, backbone.maxpool, # /2 backbone.layer1, # /2 backbone.layer2, # /2 backbone.layer3, # /2,到此特征图是原图的1/16 ) # 船舶分割头:把1/16特征图上采样回1/4 self.seg_head = nn.Sequential( nn.Conv2d(256, 64, 3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.ConvTranspose2d(64, 1, kernel_size=4, stride=4), ) # 船牌检测头:预测中心点偏移和宽高,输出通道5 self.det_head = nn.Conv2d(256, 5, 1) # 船牌识别头:把特征图压成序列,再用LSTM+CTC输出字符 self.rec_conv = nn.Conv2d(256, 64, 3, padding=1) self.lstm = nn.LSTM(64 * 56, 128, bidirectional=True, batch_first=True) self.rec_fc = nn.Linear(256, num_char_classes) def forward(self, x): feat = self.features(x) # (B, 256, H/16, W/16) seg_out = self.seg_head(feat) # (B, 1, H/4, W/4) det_out = self.det_head(feat) # (B, 5, H/16, W/16) # 识别分支:将特征图按宽度方向展开成时间步 B, C, H, W = feat.shape rec_feat = self.rec_conv(feat).permute(0, 3, 2, 1) # (B, W, H, 64) rec_feat = rec_feat.reshape(B, W, -1) rec_out, _ = self.lstm(rec_feat) rec_out = self.rec_fc(rec_out) # (B, W, num_char_classes) return seg_out, det_out, rec_out

这个结构的核心是全卷积的一致空间对应:seg_out 每个像素对应原图4×4区域,det_out 每个位置对应原图16×16区域,rec_out 按宽度方向把特征图切成时间步,对应船牌字符从左到右的序列。注意这里为了凑代码长度,LSTM 的输入维度写的是固定值,实际使用时需要根据输入尺寸动态计算,或者干脆把识别分支换成全卷积的 CTC 头。识别分支的输入高度如果太小,字符特征会被压没,这是后面避坑章要展开的问题。检测头和分割头共享骨干但各自输出,损失函数要分开加权,训练时不能直接用一个 loss 一把梭。

3. 数据准备与标注:船舶检测和船牌识别的数据怎么造才不白忙

3.1 数据来源与标注规范:多边形框、船牌框、字符三级标注

船舶检测和车牌的公开数据集不少,但船牌识别几乎没有公共标准数据集,原因很简单:船牌涉及具体船舶身份,数据获取和公开都有法律边界,而且各地船牌格式差异很大。所以这个项目最常见的做法是自建私有数据集,采集渠道包括港区高点摄像头、海事执法船随船拍摄、模拟器渲染图片。采集时要注意正样本分布,很多船长时间停泊不动,模型容易把码头上的船全识别成同一艘,那就失去识别意义了。

标注规范我建议分三级:第一级是船舶轮廓,用多边形标注,因为船体往往不是标准矩形,尤其俯拍视角下船头是尖的;第二级是船牌所在区域,用矩形框标注,框尽量只包含字符部分,不要包含船牌的金属底板边缘;第三级是船牌文本本身,把每个字符对齐到船牌框内。三级标注最终合成一张标注文件,格式类似 JSON。不要把船牌框和船舶轮廓混在同一层里标注,否则训练时两个任务相互干扰的概率会变大。

3.2 把标注转成训练标签:分割掩码与中心点热力图

拿到 JSON 标注后,要把标注转换成网络能吃的标签。这里给出一段转换脚本的核心逻辑:船舶轮廓转成分割掩码(mask),船牌框转成检测头用的中心点热力图和偏移量。热力图的做法参考 CenterNet,把船牌框中心点作为一个二维高斯峰,检测头输出类别为1,位置偏移作为回归目标。

import json import numpy as np import cv2 def json_to_labels(annotation_path, seg_size=(256, 256), det_size=(64, 64)): with open(annotation_path, 'r') as f: ann = json.load(f) # 分割掩码:0为背景,1为船体 mask = np.zeros(seg_size, dtype=np.uint8) for poly in ann['ship_polygons']: pts = np.array(poly, dtype=np.int32).reshape(-1, 2) cv2.fillPoly(mask, [pts], 1) # 检测热力图与偏移:det_size是1/16下采样后的网格 heatmap = np.zeros((det_size[1], det_size[0]), dtype=np.float32) offset = np.zeros((2, det_size[1], det_size[0]), dtype=np.float32) for box in ann['plate_boxes']: cx, cy = (box[0] + box[2]) / 2, (box[1] + box[3]) / 2 cx_s = cx * det_size[0] / ann['image_width'] cy_s = cy * det_size[1] / ann['image_height'] gx, gy = int(cx_s), int(cy_s) sigma = 3 for i in range(gx - sigma, gx + sigma + 1): for j in range(gy - sigma, gy + sigma + 1): if 0 <= i < det_size[0] and 0 <= j < det_size[1]: d = (i - gx) ** 2 + (j - gy) ** 2 heatmap[j, i] = max(heatmap[j, i], np.exp(-d / (2 * sigma ** 2))) offset[0, gy, gx] = cx_s - gx offset[1, gy, gx] = cy_s - gy return mask, heatmap, offset, ann['plate_chars']

这段脚本里最关键的是分割掩码的生成:cv2.fillPoly 会把多边形内部填为1,但如果船体之间存在遮挡,需要按顶层顺序覆盖,否则后面的船体区域会被前面船的 mask 盖掉,损失函数会越来越糊涂。偏移量只在热力图的峰值点有监督,其他位置不参与回归损失,这是 CenterNet 设计的精髓,全卷积检测头靠这个避免“一个船牌框附近所有像素都学同一个偏移”的情况。

3.3 针对船牌的专门增强:合成字符与视角扰动

船牌数据不足是这个系统最常见的卡点。我常用的补数据方法是合成字符:用字体文件把真实船牌字符写到背景图像上,再用透视变换和光照扰动让它接近真实拍摄效果。合成时要注意字符宽度比例,船牌的字符间距比车牌大,且不同地区的船牌允许的字符集不同,合成脚本里要有一个地区字符集配置表。另一个有效增强是“混合增强”,把处理好的一小块船牌照贴到不同场景的船体上,先粗对齐,再做随机平移、旋转、加高斯噪声,这样能把船舶背景和船牌之间的解耦关系强化。

增强参数的设置要看训练效果调整。我最开始用旋转±30度,结果把码头钢架结构也当成了船舶,因为俯拍视角下很多场景本身就有结构性纹理。后来把旋转范围压到±10度,补上水平翻转,训练稳定了很多。视角扰动对船牌识别特别重要,船牌的真实安装角度五花八门,测试集中有一批船牌的字符长宽比被严重压缩,就是因为在增强里只做了旋转、没做非等比例缩放。加上 x 方向 0.8~1.2、y 方向 0.9~1.1 的随机缩放之后,识别率提升了近5个点。

4. 模型实现与训练:多任务损失怎么配、权重怎么调

4.1 损失函数拼接与权重搜索的工程经验

一个全卷积多任务模型的训练难点不是网络结构,而是三个损失函数如何平衡:船舶分割用 Dice 损失,船牌检测用中心点损失加 L1 偏移损失,船牌识别用 CTC 损失。CTC 损失对序列长度有要求,识别分支的时间步数必须大于字符数量,否则它会强行把多余时间步对齐成空白符,导致识别结果出现大量重复字符。

我一般先单独预训练三个分支,再联合微调。预训练阶段分割头的 Dice 损失大概在0.02左右就能看到船体轮廓,检测热力图的损失要降到0.01以下再开始联合训练。联合训练时的损失权重,我的经验是分割:检测:识别 = 1 : 0.5 : 2。识别任务最脆弱,权重给高一点;检测任务相对简单,给低了不容易让检测头在后期拖慢其他分支。这个权重不是玄学,它跟三个分支收敛速度有关,识别分支收敛慢,如果不拉高权重,等分割和检测都收敛了,识别分支还在原地。

import torch import torch.nn.functional as F def dice_loss(pred, target): pred = torch.sigmoid(pred) smooth = 1.0 intersection = (pred * target).sum() return 1 - (2 * intersection + smooth) / (pred.sum() + target.sum() + smooth) def det_loss(heat, offset_pred, offset_target, mask_positive): heat_loss = F.binary_cross_entropy_with_logits(heat, mask_positive) offset_loss = F.l1_loss(offset_pred, offset_target, reduction='none') offset_loss = (offset_loss * mask_positive.unsqueeze(1)).mean() return heat_loss + offset_loss total_loss = dice_loss(seg_pred, seg_mask) \ + 0.5 * det_loss(det_pred, det_mask, det_offset, heat_target) \ + 2.0 * ctc_loss(rec_pred, rec_target, input_lengths, target_lengths)

这里的一个细节是 offset_loss 计算时要用 mask_positive 把非中心点区域的 L1 损失置零,否则背景区域的大量零偏移会成为主要损失来源,让网络学会“全预测零偏移”,中心点附近的细微位置误差反而不被惩罚。

4.2 训练参数与学习率策略

输入尺寸的选择对全卷积网络影响很大。船舶检测需要看全貌,输入太小时小目标船直接灭失;输入太大又会让识别分支的计算量爆炸。我通常选 768×768,分割头上采样到 192×192,检测头在 48×48 网格上做,识别分支的时间步是 48。这个配置在 1080Ti 上显存占用约 9GB,batch size 8 可以跑。如果显存不够,优先降输入尺寸而不是降 batch size,因为 BN 层在 batch size 很小时统计量会抖动,损失曲线会呈锯齿状。

学习率我用 OneCycle 策略,最大学习率 1e-3,权重衰减 5e-4,总 epoch 120。第一个 epoch 用 warmup,从 1e-5 线性升到 1e-3,这是为了避免多任务头在初始阶段互相踩踏。联合训练每 10 个 epoch 在验证集上算一次整体指标——船舶检测的 mAP@0.5 和船牌字符识别率(不考虑顺序只算字符命中率)。如果发现 mAP 在涨但识别率不涨,多半是识别分支的输入特征高度被压缩太狠,需要把骨干里 layer4 的步长调成 1 或者给识别分支单独接一个上采样层,给字符多留几行像素。

4.3 推理时怎么把三个输出合并成可用结果

训练完的模型在推理阶段需要把分割掩码、检测热力图和识别序列合起来。我的做法是:先对分割掩码做连通域分析,剔除面积过小的区域,得出船舶候选位置;再对检测热力图取局部极大值点作为船牌中心,从分割结果中截取船牌中心点附近的 ROI;最后把识别分支输出的序列拿 CTC decode 还原成文本。这里有个先后顺序问题:必须先用分割掩码确认船牌中心点落在某个船体内部,如果热力图上的中心点落在水面上或者甲板边缘,大概率是误检,直接丢弃。这样做的原因是船牌不会漂浮在水面上,利用两个任务的几何关系做交叉验证,可以大幅降低船牌的假阳性,这个技巧在夜间场景里尤其有效。

5. 避坑指南:船舶检测加船牌识别的五个典型翻车现场

5.1 船牌字符识别率低:先查数据高度,再查字体

现象:训练能收敛,但船牌字符正确率只有 40%,尤其是字符“8”和“0”混得一塌糊涂。原因有两类:一是识别分支的输入特征图高度太小,全卷积网络的 rec_feat 在高度方向上只剩 1~2 个像素,字符的上下结构信息全部丢失;二是船牌字体中字符之间的间距和宽度比例跟车牌差别很大,模型把车牌的先验带过来了。解决方法是把识别分支的特征图高度从 56 提到 96,并给识别头单独加一个步长为 2 的转置卷积上采样,同时从合成数据阶段就使用与真实船牌字体一致的字体文件,不要把“互联驾驶”那种通用字体当船牌字体。

5.2 检测框在视频里来回抖动:缺少时间后处理

现象:单帧检测很准,但视频里船牌框每帧抖动几个像素,字符识别结果也跟着闪。原因不是模型不稳定,而是检测热力图的峰值点在亚像素层面会漂移。解决方法是做时间维度上的平滑:维护一个最近 10 帧的船牌中心点队列,用加权平均更新当前帧位置,权重按检测置信度排序。帧与帧之间船牌通常只有几个像素的位移,用指数移动平均足够,不要用卡尔曼滤波,卡尔曼对这个场景反而容易把轻微晃动积累成大偏移。

5.3 夜间和逆光场景下的漏检:输入域太单一

现象:白天测试 mAP 都有 90% 以上,晚上只有 60%,逆光时更低。原因主要是模型在 RGB 图像上训练,夜间的红外监控图像和白天可见光图像在像素分布上差异巨大。解决方法是做“域适应增强”:训练时按一定概率把图像转成灰度、加模拟低照度噪声、降低对比度。如果目标是夜间的红外摄像头,更直接的做法是把输入转成单通道灰度再进网络,避免网络去学三种通道的伪彩色组合,降低过拟合风险。

5.4 小目标船舶在远处完全检测不到:多尺度特征没利用

现象:近处大船检测很稳,远处 100 米外的小船常常被漏掉。原因是骨干网络经过 5 次下采样后,小目标的特征已经被压缩到 1~2 个像素,分割头即使上采样也补不回信息。解决方法是给分割头接上 FPN 式的多尺度融合:把 layer2、layer3 的输出各自上采样到同一分辨率,拼接后再送分割头。这个改动会带来 5%~10% 的显存开销,但小目标的召回率通常能提升 8 个点以上。注意只对分割头做多尺度融合就够了,检测头和识别头保持原有尺度,否则多任务之间的感受野差异会变得很难调。

5.5 船牌背景特征泄漏让识别“作弊”

现象:训练后期识别率很高,但换一批新场景后识别率崩了。这通常是数据泄漏的经典问题:合成数据时,贴图背景和船牌字符来自同一批渲染参数,网络学会了通过背景纹理预测字符,而没学会读字符本身。解决方法是把船牌区域直接做色彩扰动和模糊处理,强制网络只依赖字符像素;同时做交叉验证时,要确保同一艘船的图像不会同时出现在训练集和验证集中,否则模型把船体“记住”了,识别的是船不是牌。

6. 验证与部署技巧:TTA 与置信度校准让系统真正敢上线

多任务模型上线前的验证,不能只看单帧指标,我会额外跑一个“连续视频稳定性测试”:取一段 10 分钟监控视频,统计船牌识别文本连续帧的切换次数。正常情况下一艘船在画面里停留 5 秒以上,输出的船牌文本应该保持不变,如果中间有一帧识别出不同字符,就要检查那一帧是不是有水花溅到船牌上。对这种偶然帧,我习惯做一个“两帧确认”逻辑:只有当两帧输出相同的船牌文本时才真正更新结果,这个简单技巧能让系统在风浪天里不炸。

测试时增强(TTA)对这个小模型效果明显,尤其是水平翻转。全卷积网络的检测头不会有方向性偏好,但船牌识别头对字符方向敏感,原始图像水平翻转后,字符顺序也翻转,识别头需要把序列反转回来再和原图结果比对。比对时不是取 argmax,而是取原图和翻转图各自 top-3 序列的交集,如果交集只有一个候选,就采用它。这个方法能把字符准确率再拉高 1~2 个点,代价只是推理时间翻倍。部署时如果算力吃紧,我会把 TTA 关掉,但保留“两帧确认”,因为时间维度的稳定性比单帧极端精度更重要。

说起来这套系统最让我长记性的一件事,就是最开始我完全信赖单帧指标,结果去航道现场测试时,一个上午就看到系统把桥墩阴影识别成了船。后来每次迭代,我都逼自己录一段真实监控视频回放,而不是只跑验证集图片。视频里那些一晃而过的小船、反光的水面、挡住船牌的缆绳,才是真正检验模型的照妖镜。这些坑每一个都有对应的参数和策略,没有一个是玄学。希望这篇笔记能帮你少踩几个我已经踩过的坑,把全卷积船舶检测和船牌识别真正跑通、跑稳。

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

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

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

立即咨询