返回项目列表
2025.01 - 至今
工业视觉识别
港口 / 铁路股道

智能龙门吊集装箱识别系统

这个项目我最想讲的不是“调用 OCR 识别箱号”,而是我怎么把现场设备信号、 多路相机、识别算法、业务记录和外部平台上报串成一条可追踪的生产链路。 代码里真正有价值的地方,是那些为了现场稳定性做的状态治理、证据留存和可复盘设计。

1 个
作业周期模型

用 seqNo 把锁/解锁、箱号、车皮号、称重、GPS 和图片证据串起来。

多策略
识别能力编排

预识别、空中识别、车皮号识别独立成策略,按 PLC 数据驱动。

可复盘
候选级排障

车皮号误识别能输出候选过滤日志和回放结果,不只看最终值。

Java
Spring Boot
PLC
Hikvision SDK
CompletableFuture
MySQL
OCR
设备接入
动态配置
Why It Is Hard

现场难点不是识别,而是让识别稳定地发生

龙门吊现场的数据来源多、动作快、设备状态会抖,识别结果还要能给调度和外部平台使用。下面这些问题,才是这个项目最值得讲的部分。

串吊和旧 seqNo 复用

LockUnlockMonitorService
原问题

预识别、空中识别、车皮号识别都可能在不同时间点读写同一个周期号。如果上一吊没有正常清掉,下一吊就可能复用旧 seqNo,结果表现为图片和识别结果串台。

我的处理

LockUnlockMonitorService 统一管理 ensure / set / consume / reset / cleanup。解锁后根据车道类型和车皮号开关决定是否立即清理,股道场景则让车皮号策略消费后再清。

相机缺图但识别结果有效

TrainCarStageRecognitionService
原问题

现场常见情况是 5 台或 6 台相机里有一台瞬时失败,但其他相机已经识别出有效车皮号。如果只给一个 success/fail,就说不清到底是抓拍失败还是识别失败。

我的处理

车皮号结果拆成 capture_status 和 recognition_status。抓拍不完整可以是 FAIL,但只要已有图片识别出有效车号,recognition_status 仍然可以是 SUCCESS。

同图多个车皮号候选互相干扰

TrainCarSingleImageFilterService
原问题

近端场景里,一张图可能同时识别到主目标和后车小目标。小目标如果多张图重复出现,后面按频次汇总时反而可能压过真正主目标。

我的处理

在单图阶段先合并同车号候选,再按面积比例过滤明显小的次目标,并把 raw / merged / filtered / selected 全部写进专用调试日志。

现场调参不能每次都改代码

RecognitionConfigManager
原问题

空中识别高度、抓拍间隔、候选过滤阈值、调试车道、长短箱相机组都和现场环境强相关,写死会让联调很慢。

我的处理

这些参数进入 DynamicConfigManager / RecognitionConfigManager,运行时监听配置变化,现场能小步调整并观察效果。

Architecture

系统架构图

这张图表达的是代码分层:设备信号先被翻译成作业周期,再进入识别策略,最后沉淀证据并异步上报。

设备信号层

PLC 高度/锁状态/坐标、相机、称重、GPS 持续进入系统。

周期语义层

PlcDataProcessor + LockUnlockMonitorService 把设备数据转成一次作业周期。

识别策略层

预识别、空中箱号、车皮号、锁/解锁抓拍按策略独立运行。

证据沉淀层

保存结果、图片、候选、抓拍状态、最高高度、失败相机。

外部集成层

通过事件和平台适配器异步上报铁科、ECS、聚合平台。

Flow

业务流程图

这张图更贴近运行时:PLC 先把状态推过来,系统再根据动作趋势、锁边沿、车道类型和相机状态推进不同识别链路。

01
PLC 数据进入

高度、锁状态、坐标持续上报,先过滤无效锁状态并补齐车道。

02
判断动作趋势

结合高度变化、下降/上升趋势、锁边沿,决定是否启动或停止识别。

03
分发识别策略

按配置开关和优先级调用预识别、空中识别、车皮号识别。

04
选择相机抓拍

按车道、箱型、远近股道和在线状态选择相机,异步抓拍识别。

05
沉淀周期证据

保存图片、OCR 结果、候选过滤日志、抓拍状态和失败原因。

06
异步上报平台

构建 WorkCycleData,通过事件分发给不同平台适配器。

普通车道 / 空中箱号

上锁后高度达到阈值,按箱型和车道取相机配置,设置预置位,并发识别箱号。

铁路股道 / 车皮号

锁/解锁阶段触发车皮号识别,近端走长短箱相机组,远股道走专用相机和预置位分支。

Sequence

一次吊装的时序图

从吊具下降到解锁上报,主要动作并不是同步排队执行,而是由 PLC 边沿事件和策略状态共同推进。

时间
PLC
PlcDataProcessor
周期服务
识别策略
证据 / 平台
T0
高度、锁状态、坐标进入

补车道、推 WebSocket、过滤空锁状态。

T1
识别周期准备

维护 seqNo,监听上锁/解锁边沿。

T2
下降未上锁,启动预识别

高度进入阈值后异步抓拍,等待上锁通知收口。

T3
上锁后进入空中 / 车皮号策略

按车道、箱型、股道类型选择不同相机链路。

T4
结果融合与证据保存

保存图片、候选、状态、失败相机和最终识别值。

T5
周期结束后异步上报

WorkCycleEvent 触发平台适配器,失败不阻塞识别主链路。

时间
模块
触发
T0
PLC
吊具下降,持续上报高度、锁状态、坐标
PlcDataProcessor 补车道并推送前端
T1
PreRecognition
高度进入预识别范围且未上锁
生成 seqNo,开始 RTSP/SDK 抓拍识别
T2
LockUnlockMonitor
检测到上锁边沿
结束预识别,记录锁事件,启动称重周期
T3
AirRecognition
上锁后高度达到空中识别阈值
按车道和箱型选择相机,设置预置位,并发识别箱号
T4
TrainCarRecognition
股道场景触发车皮号锁/解锁阶段
近端走相机组,远股道走专用相机分支
T5
Dispatcher
解锁后构建 WorkCycleData
异步分发外部平台,上报失败不阻塞主链路
State Machine

识别策略状态流转

我更愿意把它理解成一个由 PLC 驱动的状态机,而不是“定时拍几张图然后识别”。

IDLE
01

等待有效 PLC 数据。锁状态为空、车道未知、异常高度不会进入识别主链路。

PRE
02

吊具下降且未上锁时开始预识别。上升趋势或超时会废弃本轮 seqNo。

LOCK
03

上锁边沿生成/沿用 seqNo,锁位抓拍、称重周期、周期聚合同时启动。

AIR / TRAIN
04

空中箱号按高度阈值运行;股道场景触发车皮号阶段识别。

UNLOCK
05

解锁边沿记录解锁抓拍,结束称重,消费或清理 seqNo。

REPORT
06

构建周期快照,持久化证据,并异步上报外部平台。

Project Highlights

面试或代码讲解时,我会重点讲这些

用 seqNo 把一次吊装变成完整业务周期

我没有把识别结果当成孤立数据保存,而是围绕一次上锁到解锁的动作生成 seqNo。预识别、空中箱号、车皮号、锁/解锁图片、称重、GPS、最高高度都会挂到同一个周期上,后面查问题时能按一吊还原现场。

PLC 只进一个入口,识别能力用策略扩展

PLC 数据先进入 PlcDataProcessor,再由 RecognitionStrategyManager 分发给预识别、空中识别、车皮号等策略。新增能力时接入 IRecognitionStrategy,不需要把主流程改成一长串 if else。

相机选择按现场场景走,不写死几路相机

空中箱号按箱型和车道取相机配置;近端车皮号按长箱/短箱选择相机组;远股道走专用单相机加预置位分支;车皮号抓拍还会只使用当前在线相机。

稳定性重点放在边界状态和失败留证

PLC 锁状态为空会被过滤,预识别遇到上升趋势或超时会废弃 seqNo,车皮号抓拍每台相机独立重试并记录失败原因,结果里也区分抓拍完整性和识别是否成功。

车皮号误识别能复盘到候选框级别

现场出现后车、次目标误识别时,我补了单图候选过滤、风险标记、JSON Lines 调试日志和手动回放导出。不是只看最终车号,而是能看原始候选、过滤后候选和最终选择。

识别主链路和外部平台上报解耦

WorkCycleDataAggregator 在内存里聚合周期快照,WorkCycleEvent 发布后由 PlatformDispatcher 异步分发到平台适配器。某个平台上报失败,不应该拖垮识别主流程。

Train Car OCR

车皮号链路是最能体现工程细节的一块

这部分不是普通 OCR 调用。它处理的是近端/远股道、长箱/短箱、在线相机、候选误选、调试日志和离线回放。

相机选择

近端根据箱型选择长箱 2 台或短箱 4 台相机组;配置缺失时回退全部在线相机。远股道走专用相机和预置位。

抓拍策略

每台相机独立重试,成功即停,超时记录失败原因。最终结果同时保留成功相机、失败相机、期望数量和实际数量。

候选排障

单图多候选先合并同车号,再按面积过滤疑似次目标;调试日志记录 raw、merged、filtered、selected 四个阶段。

Code Map

代码阅读路线

如果别人点进来看代码,我会建议按这条路线看。它比直接从 controller 找接口更容易理解系统。

1

PLC 主入口

module/device/plc/PlcDataProcessor.java

补车道、推 WebSocket、过滤空锁状态、远股道相机预占用、开闭锁监听、策略分发都从这里进入。它是设备数据到业务语义的第一层翻译。

2

一吊周期管理

module/recognition/manager/lockunlock/LockUnlockMonitorService.java

监听 0/1 锁状态边沿,生成 seqNo,启动称重周期,触发锁/解锁抓拍,并处理解锁后的 seqNo 消费和清理。

3

策略编排

module/recognition/strategy/RecognitionStrategyManager.java

自动注册所有识别策略,按优先级排序。功能开关关闭时会强制停止正在运行的策略,避免关闭后仍有旧任务继续写结果。

4

预识别策略

module/recognition/strategy/PreRecognitionStrategy.java

在吊具下降、未上锁、高度进入阈值范围时启动。上锁由 LockUnlockMonitorService 通知结束,超时或明显上升会废弃本轮 seqNo。

5

空中箱号识别

module/recognition/manager/aircontainer/AirContainerRecognitionService.java

按箱型和车道读取相机配置,先打预置位,再并发抓拍识别。单帧会先检测吊具,过滤不在吊具下方的箱号区域。

6

车皮号阶段识别

module/recognition/manager/traincar/TrainCarStageRecognitionService.java

上锁和解锁阶段共用同一套识别流程。它负责近端/远股道分支、相机选择、图片抓拍、候选过滤、最终选择和持久化结果组装。

7

单图候选过滤

module/recognition/manager/traincar/TrainCarSingleImageFilterService.java

先合并同车号多框,再按面积比例标记小目标、疑似对侧目标,最后把保留下来的候选交给原有选择器,保持改动可控。

8

外部平台分发

module/integration/adapter/dispatcher/PlatformDispatcher.java

监听 WorkCycleEvent,遍历 PlatformAdapter 异步上报。平台是否处理某个事件由 supportsEventType 决定。

Key Snippets

几个能说明设计意图的代码片段

这些不是为了展示代码量,而是为了说明我怎么处理异步、状态、候选过滤和现场异常。

PLC 分发给多个识别策略

RecognitionStrategyManager.java

for (IRecognitionStrategy strategy : sortedStrategies) {
    if (!shouldHandleStrategy(strategy)) {
        forceStopIfDisabled(strategy);
        continue;
    }
    strategy.handlePlcData(currentPlc, lane);
}
lastPlc = currentPlc;

解锁后避免 seqNo 串吊

LockUnlockMonitorService.java

boolean trainTrackLane = lane != null && LaneTypeEnum.isTrainTrack(lane.getLaneType());
boolean trainCarRecognitionEnabled = recognitionConfigManager.isTrainCarRecognitionEnabled();

if (!trainTrackLane || !trainCarRecognitionEnabled) {
    synchronized (seqNoLock) {
        if (Objects.equals(currentCycleSeqNo, seqNo)) {
            currentCycleSeqNo = null;
        }
    }
}

每台车皮号相机独立异步抓拍

TrainCarImageCaptureService.java

for (RecognizeConfigVO camera : cameras) {
    Future<CaptureResult> future = CompletableFuture.supplyAsync(
        () -> captureSingleCameraImage(camera, seqNo, stage));
    pendingTasks.add(new PendingCaptureTask(camera, future));
}

CaptureResult captureResult =
    future.get(stage.getSnapshotTimeoutSeconds() + 1L, TimeUnit.SECONDS);

单图候选先清洗,再进入最终选择

TrainCarSingleImageFilterService.java

List<ImageRecognitionCandidate> mergedCandidates =
    mergeSameTrainNoCandidates(normalizedRawCandidates, mergeSuppressedCandidates);

float areaRatioToMax = (float) candidate.getArea() / (float) maxArea;
if (areaRatioToMax < minAreaRatio) {
    reasonCodes.add(TrainCarCandidateFilterReason.SMALL_AREA_RATIO);
}

我对这个项目的理解

这套系统最核心的价值,是把一个容易被现场状态打断的识别任务,整理成可追踪、 可配置、可复盘的工程链路。算法识别只是其中一环;真正决定系统能不能上线跑稳的, 是 seqNo 周期治理、PLC 边沿判断、相机失败兜底、候选过滤、图片证据留存和外部系统解耦。