串吊和旧 seqNo 复用
预识别、空中识别、车皮号识别都可能在不同时间点读写同一个周期号。如果上一吊没有正常清掉,下一吊就可能复用旧 seqNo,结果表现为图片和识别结果串台。
LockUnlockMonitorService 统一管理 ensure / set / consume / reset / cleanup。解锁后根据车道类型和车皮号开关决定是否立即清理,股道场景则让车皮号策略消费后再清。
这个项目我最想讲的不是“调用 OCR 识别箱号”,而是我怎么把现场设备信号、 多路相机、识别算法、业务记录和外部平台上报串成一条可追踪的生产链路。 代码里真正有价值的地方,是那些为了现场稳定性做的状态治理、证据留存和可复盘设计。
用 seqNo 把锁/解锁、箱号、车皮号、称重、GPS 和图片证据串起来。
预识别、空中识别、车皮号识别独立成策略,按 PLC 数据驱动。
车皮号误识别能输出候选过滤日志和回放结果,不只看最终值。
龙门吊现场的数据来源多、动作快、设备状态会抖,识别结果还要能给调度和外部平台使用。下面这些问题,才是这个项目最值得讲的部分。
预识别、空中识别、车皮号识别都可能在不同时间点读写同一个周期号。如果上一吊没有正常清掉,下一吊就可能复用旧 seqNo,结果表现为图片和识别结果串台。
LockUnlockMonitorService 统一管理 ensure / set / consume / reset / cleanup。解锁后根据车道类型和车皮号开关决定是否立即清理,股道场景则让车皮号策略消费后再清。
现场常见情况是 5 台或 6 台相机里有一台瞬时失败,但其他相机已经识别出有效车皮号。如果只给一个 success/fail,就说不清到底是抓拍失败还是识别失败。
车皮号结果拆成 capture_status 和 recognition_status。抓拍不完整可以是 FAIL,但只要已有图片识别出有效车号,recognition_status 仍然可以是 SUCCESS。
近端场景里,一张图可能同时识别到主目标和后车小目标。小目标如果多张图重复出现,后面按频次汇总时反而可能压过真正主目标。
在单图阶段先合并同车号候选,再按面积比例过滤明显小的次目标,并把 raw / merged / filtered / selected 全部写进专用调试日志。
空中识别高度、抓拍间隔、候选过滤阈值、调试车道、长短箱相机组都和现场环境强相关,写死会让联调很慢。
这些参数进入 DynamicConfigManager / RecognitionConfigManager,运行时监听配置变化,现场能小步调整并观察效果。
这张图表达的是代码分层:设备信号先被翻译成作业周期,再进入识别策略,最后沉淀证据并异步上报。
PLC 高度/锁状态/坐标、相机、称重、GPS 持续进入系统。
PlcDataProcessor + LockUnlockMonitorService 把设备数据转成一次作业周期。
预识别、空中箱号、车皮号、锁/解锁抓拍按策略独立运行。
保存结果、图片、候选、抓拍状态、最高高度、失败相机。
通过事件和平台适配器异步上报铁科、ECS、聚合平台。
这张图更贴近运行时:PLC 先把状态推过来,系统再根据动作趋势、锁边沿、车道类型和相机状态推进不同识别链路。
高度、锁状态、坐标持续上报,先过滤无效锁状态并补齐车道。
结合高度变化、下降/上升趋势、锁边沿,决定是否启动或停止识别。
按配置开关和优先级调用预识别、空中识别、车皮号识别。
按车道、箱型、远近股道和在线状态选择相机,异步抓拍识别。
保存图片、OCR 结果、候选过滤日志、抓拍状态和失败原因。
构建 WorkCycleData,通过事件分发给不同平台适配器。
上锁后高度达到阈值,按箱型和车道取相机配置,设置预置位,并发识别箱号。
锁/解锁阶段触发车皮号识别,近端走长短箱相机组,远股道走专用相机和预置位分支。
从吊具下降到解锁上报,主要动作并不是同步排队执行,而是由 PLC 边沿事件和策略状态共同推进。
补车道、推 WebSocket、过滤空锁状态。
维护 seqNo,监听上锁/解锁边沿。
高度进入阈值后异步抓拍,等待上锁通知收口。
按车道、箱型、股道类型选择不同相机链路。
保存图片、候选、状态、失败相机和最终识别值。
WorkCycleEvent 触发平台适配器,失败不阻塞识别主链路。
我更愿意把它理解成一个由 PLC 驱动的状态机,而不是“定时拍几张图然后识别”。
等待有效 PLC 数据。锁状态为空、车道未知、异常高度不会进入识别主链路。
吊具下降且未上锁时开始预识别。上升趋势或超时会废弃本轮 seqNo。
上锁边沿生成/沿用 seqNo,锁位抓拍、称重周期、周期聚合同时启动。
空中箱号按高度阈值运行;股道场景触发车皮号阶段识别。
解锁边沿记录解锁抓拍,结束称重,消费或清理 seqNo。
构建周期快照,持久化证据,并异步上报外部平台。
我没有把识别结果当成孤立数据保存,而是围绕一次上锁到解锁的动作生成 seqNo。预识别、空中箱号、车皮号、锁/解锁图片、称重、GPS、最高高度都会挂到同一个周期上,后面查问题时能按一吊还原现场。
PLC 数据先进入 PlcDataProcessor,再由 RecognitionStrategyManager 分发给预识别、空中识别、车皮号等策略。新增能力时接入 IRecognitionStrategy,不需要把主流程改成一长串 if else。
空中箱号按箱型和车道取相机配置;近端车皮号按长箱/短箱选择相机组;远股道走专用单相机加预置位分支;车皮号抓拍还会只使用当前在线相机。
PLC 锁状态为空会被过滤,预识别遇到上升趋势或超时会废弃 seqNo,车皮号抓拍每台相机独立重试并记录失败原因,结果里也区分抓拍完整性和识别是否成功。
现场出现后车、次目标误识别时,我补了单图候选过滤、风险标记、JSON Lines 调试日志和手动回放导出。不是只看最终车号,而是能看原始候选、过滤后候选和最终选择。
WorkCycleDataAggregator 在内存里聚合周期快照,WorkCycleEvent 发布后由 PlatformDispatcher 异步分发到平台适配器。某个平台上报失败,不应该拖垮识别主流程。
这部分不是普通 OCR 调用。它处理的是近端/远股道、长箱/短箱、在线相机、候选误选、调试日志和离线回放。
近端根据箱型选择长箱 2 台或短箱 4 台相机组;配置缺失时回退全部在线相机。远股道走专用相机和预置位。
每台相机独立重试,成功即停,超时记录失败原因。最终结果同时保留成功相机、失败相机、期望数量和实际数量。
单图多候选先合并同车号,再按面积过滤疑似次目标;调试日志记录 raw、merged、filtered、selected 四个阶段。
如果别人点进来看代码,我会建议按这条路线看。它比直接从 controller 找接口更容易理解系统。
module/device/plc/PlcDataProcessor.java
补车道、推 WebSocket、过滤空锁状态、远股道相机预占用、开闭锁监听、策略分发都从这里进入。它是设备数据到业务语义的第一层翻译。
module/recognition/manager/lockunlock/LockUnlockMonitorService.java
监听 0/1 锁状态边沿,生成 seqNo,启动称重周期,触发锁/解锁抓拍,并处理解锁后的 seqNo 消费和清理。
module/recognition/strategy/RecognitionStrategyManager.java
自动注册所有识别策略,按优先级排序。功能开关关闭时会强制停止正在运行的策略,避免关闭后仍有旧任务继续写结果。
module/recognition/strategy/PreRecognitionStrategy.java
在吊具下降、未上锁、高度进入阈值范围时启动。上锁由 LockUnlockMonitorService 通知结束,超时或明显上升会废弃本轮 seqNo。
module/recognition/manager/aircontainer/AirContainerRecognitionService.java
按箱型和车道读取相机配置,先打预置位,再并发抓拍识别。单帧会先检测吊具,过滤不在吊具下方的箱号区域。
module/recognition/manager/traincar/TrainCarStageRecognitionService.java
上锁和解锁阶段共用同一套识别流程。它负责近端/远股道分支、相机选择、图片抓拍、候选过滤、最终选择和持久化结果组装。
module/recognition/manager/traincar/TrainCarSingleImageFilterService.java
先合并同车号多框,再按面积比例标记小目标、疑似对侧目标,最后把保留下来的候选交给原有选择器,保持改动可控。
module/integration/adapter/dispatcher/PlatformDispatcher.java
监听 WorkCycleEvent,遍历 PlatformAdapter 异步上报。平台是否处理某个事件由 supportsEventType 决定。
这些不是为了展示代码量,而是为了说明我怎么处理异步、状态、候选过滤和现场异常。
RecognitionStrategyManager.java
for (IRecognitionStrategy strategy : sortedStrategies) {
if (!shouldHandleStrategy(strategy)) {
forceStopIfDisabled(strategy);
continue;
}
strategy.handlePlcData(currentPlc, lane);
}
lastPlc = currentPlc;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 边沿判断、相机失败兜底、候选过滤、图片证据留存和外部系统解耦。