1. 先把链路想清楚车牌识别相机到底在跑什么业务我这两年接手过几个车牌识别相机的活儿客户描述都特别简单一台相机对着车道车过来读出车牌号抬杆。听起来像是一个模型就能搞定的事真做起来才发现从能识别到能连续跑三个月不出事中间隔着一整套工程化的工作量。用 VideoPipe 这类视频分析框架来做最大的好处不是它自带多少模型而是它把取流—解码—检测—跟踪—二次识别—绘制—输出这条链路拆成了可以独立调试的节点你能精确知道问题出在哪一环。这篇内容我想聊的就是怎么用 VideoPipe 把一个车牌识别相机从零搭起来包括节点怎么选、参数怎么算、坑在哪、怎么排。适合已经会写 C、懂一点深度学习推理、但还没做过完整视频结构化流水线的开发者如果你只是想跑个 demo 看效果前面两节看完就够了后面几节是真正上现场才用得着的部分。车牌识别相机这个品类本质上是边缘视频结构化设备的一个细分。它和普通监控相机的区别在于普通相机只负责把画面传回去车牌相机要在本地把画面里的车辆、车牌位置、车牌字符三样东西抽出来变成一个字符串和一张小图再送给业务系统。所以它的核心技术点不是单一的车牌检测模型而是三层级联先在大画面里找车再在车上找牌最后在牌上读字。少一层误检率就会飙上去——我见过直接全图跑车牌检测的方案白天能用晚上路灯光斑、广告牌反光、货车尾部贴纸全都会被当成车牌误识别率高到没法交付。1.1 一台车牌识别相机的真实工作链路把业务链路摊开来看其实是这样一条流水相机出流RTSP 或 USB/GigE→ 解码成帧 → 车辆检测找出画面里所有车拿到 bbox 和 track_id→ 目标跟踪给同一辆车分配稳定的 ID避免每帧重复识别→ 车牌检测在车辆 bbox 内做二次检测定位车牌四角或矩形→ 车牌矫正与裁剪透视变换把倾斜车牌拉正→ 字符识别输出车牌字符串和置信度→ 结果融合与去重同一 track_id 只保留最优结果→ OSD 绘制与推流/落盘/上报。这条链路上每一步都有独立的失败模式这也是我坚持用流水线框架而不是写一个大函数的原因失败要能定位。举个实际遇到的例子。某个园区入口的项目白天识别率 97%晚上掉到 70%。如果是一坨代码你只能猜是模型不行还是图像不行拆成节点之后我把每个节点的输出帧单独存下来看一眼就发现是车辆检测节点在夜间把车和地上的水渍反光混在一起导致送进车牌检测的 ROI 压根不包含车牌。问题定位在十分钟内完成解决方案也很简单把送检 ROI 往上收缩 10%并且给车辆检测加一个夜间专用的阈值。这种排查效率是流水线架构最实在的回报。1.2 单模型一把梭的问题在哪里新手最容易犯的错是拿一个车牌检测模型直接怼全图检测到车牌就直接送 OCR。这条路在小图上看着很美实际落地会碰到三个硬问题。第一个是召回和精度的矛盾为了让远处的小车牌也能被检出你得把输入分辨率拉高、置信度阈值调低结果近处的干扰物全进来了反过来调高阈值远车就漏了。第二个是没有跟踪就没有去重同一辆车在画面里停留 3 秒、25fps就是 75 次识别业务系统会收到 75 条重复的过车记录还得在上层做去重纯属给自己找麻烦。第三个是算力浪费全图跑车牌检测的计算量远大于先找车、只在车的框里找牌尤其当你一台设备要接 4 路、8 路相机的时候这个差距直接决定你能不能用一张消费级显卡扛下来。所以我在方案选型上有一个固定原则能用层级过滤的绝不靠单模型硬扛。车辆检测负责把搜索空间从整幅画面压缩到若干个小矩形这一步的召回可以做得比较高因为车辆目标大、特征明显车牌检测只在车辆框内跑输入尺寸小、干扰少可以做得很严格。两级过滤下来误检率的下降是指数级的不是线性的。1.3 我为什么把 VideoPipe 作为骨架选 VideoPipe 的理由很朴素它是 C 写的节点化设计管道可以灵活拼装自带了一批常用的推理节点封装检测、分类、跟踪、车牌识别、OSD 绘制、多种输出而且节点的 attach/detach 机制让一个源分发给多个下游这种事变得很自然。对于车牌识别相机这种一条主链 若干旁路存图、推流、上报的场景这个模型非常贴合。相比之下用 Python 脚本串 OpenCV ONNX Runtime 也能跑但一旦要做多路并发、要做优雅停止、要做节点级的帧缓存和背压控制就得自己造一遍轮子而这些东西恰恰是现场稳定性的关键。需要说明的是下面涉及的具体节点类名、构造参数签名我会按 VideoPipe 的通用设计来写但不同版本的接口可能有差异你落地时以自己拉下来的那份代码里的头文件为准思路是一致的。这也是我一直建议的做法框架是骨架参数和业务逻辑是你自己的肉别指望照抄某一份示例就能直接上生产。2. VideoPipe 的节点模型拆解与选型思路2.1 节点、管道与数据流把业务拆成快递分拣线VideoPipe 的核心抽象就两个节点node和管道pipeline。节点是处理单元有输入队列和输出队列管道负责把节点串起来、启动线程、管理生命周期。数据在节点之间流动的单位是帧frame每一帧上挂着一个 meta 仓库用来在节点之间传递结构化信息——比如车辆检测节点往 meta 里写detections跟踪节点往里写track_ids车牌识别节点往里写plate_text和plate_score。这个设计的关键价值在于解耦下游节点不需要知道上游是怎么实现的只需要约定好 meta 里的字段。你可以把它想成快递分拣包裹帧从传送带进来第一站贴面单检测第二站编组跟踪第三站称重识别最后装车输出。每一站只管自己那一段坏了就换那一站不影响别的。我在实际项目中特别依赖这个特性做灰度替换比如车牌检测模型从 A 换成 B只需要换掉一个节点的构造参数其他链路一行不动。管道构建的写法大致是这样用attach_to和detach_to描述拓扑#include vp_pipeline.h #include nodes/vp_video_src_node.h #include nodes/vp_vehicle_detector.h #include nodes/vp_tracker_node.h #include nodes/vp_plate_detector.h #include nodes/vp_lprnet_node.h #include nodes/vp_osd_node.h #include nodes/vp_screen_des_node.h int main() { auto src std::make_sharedvp_nodes::vp_video_src_node( src, rtsp://192.168.1.64:554/stream1, 1.0 /* 播放倍速 */, false /* 循环 */); auto veh_det std::make_sharedvp_nodes::vp_vehicle_detector( veh_det, models/vehicle_det.onnx, 1 /* batch */, 0.5f /* score */, 0.5f /* nms */); auto tracker std::make_sharedvp_nodes::vp_tracker_node( tracker, vp_nodes::vp_track_algo::SORT, 30 /* max_age */); auto plt_det std::make_sharedvp_nodes::vp_plate_detector( plt_det, models/plate_det.onnx, 1, 0.55f, 0.5f, /* 额外的裁剪扩展比例 */ 0.1f); auto lpr std::make_sharedvp_nodes::vp_lprnet_node( lpr, models/lprnet.onnx, 1, 0.8f /* 输出置信度下限 */); auto osd std::make_sharedvp_nodes::vp_osd_node(osd); auto desk std::make_sharedvp_nodes::vp_screen_des_node(desk, 0); src-attach_to({veh_det}); veh_det-detach_to({tracker}); tracker-detach_to({plt_det}); plt_det-detach_to({lpr}); lpr-detach_to({osd}); osd-detach_to({desk}); vp_pipeline pipe; pipe.set_print_interval(10); pipe.start(); if (!pipe.wait()) { return -1; } pipe.stop(); return 0; }注意上面的构造参数顺序是按常见设计写的部分版本里 ROI 裁剪比例、跳过帧数、最大目标数这些可能是通过单独的 setter 或 config 结构体传的。编译报错就去看头文件别硬猜。2.2 视频源与解码节点的选型要点视频源节点看起来最简单其实是现场最容易出问题的一环。它要处理的事情包括连接建立、断线重连、解码硬件解码还是软件解码、时间戳对齐、以及推流端卡住不吐数据的检测。我的经验是能走硬件解码就不要走软件解码。1080P 25fps 的 H.264 流软件解码单路就能吃掉一两个 CPU 核你接 8 路的时候 CPU 直接满载后面的推理节点反倒没时间片了。另一个常被忽略的点是取流分辨率与推理分辨率的关系。很多相机的子码流是 720P、主码流是 400 万或 800 万像素我的做法是如果做车牌识别主码流直接用别贪省带宽去用子码流。原因在下一节的像素计算里会说清楚——车牌字符在画面里就那么几十个像素你每降一档分辨率就是直接砍掉识别裕度。断线重连这块我自己的做法是在源节点前面加一层守护逻辑定期检查帧时间戳如果超过 N 秒我一般设 3 到 5 秒没有新帧进来就主动 stop 再 start 一次管道。VideoPipe 的管道本身支持优雅停止但什么时候该停需要你自己判断框架不知道你的业务容忍度。2.3 检测、跟踪、识别三类节点的组合方式这三个节点的组合方式是整个方案的核心我把它拆成三句话检测给初值跟踪保连续识别做收敛。车辆检测节点的职责是粗筛。我通常把它配置成高召回、容忍一定误检的模式置信度阈值压到 0.4 到 0.5NMS 阈值 0.5 左右输入尺寸用模型的默认值比如 640别为了小目标去拉到 1280因为车辆在画面里通常够大拉高输入只会拖慢速度。检测出来的框还要做一个业务过滤宽高比异常的比如特别扁的横条直接扔掉太小的宽或高小于某个阈值也扔掉这些框大概率是误检。跟踪节点解决的是同一辆车别重复报。SORT 类的跟踪器靠 IOU 匹配实现简单、速度快对车牌场景完全够用。这里的max_age参数很关键它决定了目标消失多少帧后销毁轨迹。车牌识别场景里车辆被前车遮挡几帧是常态max_age设太小比如 5会导致 ID 频繁切换同一辆车被当成两辆车识别两次设太大比如 60又会让死掉的轨迹占着 ID 不释放。我一般在 25fps 的场景下设 25 到 30也就是容忍一秒左右的遮挡。车牌检测节点是二段检测只在车辆框内跑。这里有个细节必须处理车辆框往往贴得很紧车牌可能在框边缘被切掉一半。所以我在把 vehicle bbox 传给车牌检测之前会按一定比例向外扩展比如宽高各扩 10%并且裁剪时做边界截断防止越界访问。这个扩展比例不能太大否则又把隔壁车道的车牌框进来了10% 到 15% 是比较稳的区间。字符识别节点LPRNet 这类轻量序列识别模型负责把裁剪出来的车牌小图变成字符串。它的输入尺寸通常是固定的比如 94x24 或 128x32所以进入之前一定要做 resize而 resize 的方式直接决定识别率。这一点下一节展开讲。2.4 输出与 OSD 节点别小看这一层OSD 节点负责把框、ID、车牌字符串画到帧上看起来是锦上添花实际上调试期它是你唯一能直观看到每个节点干了什么的窗口。我在联调阶段会特意在 OSD 上画三类信息车辆框和 track_id不同 ID 用不同颜色、车牌框、识别出的字符串和置信度。这样一眼就能看出是车没检到还是牌没检到还是字读错了。输出节点按业务需求选。屏显输出vp_screen_des_node用于本地调试文件输出vp_file_des_node用于存证我一般只存识别成功且置信度达标的帧和对应的车牌小图不然硬盘一周就满了推流输出RTMP 之类用于把带 OSD 的画面回传给管理端。这里有个工程上的小技巧输出节点一定要做限速或异步否则下游写盘慢的时候会反压整条管道导致取流节点丢帧看起来像是识别卡顿实际是 IO 拖累的。3. 核心参数怎么定把凭感觉调变成算出来3.1 分辨率与车牌像素高度一个必须算的账我见过太多项目在相机选型阶段拍脑袋最后识别率上不去回头怪模型。其实车牌识别能不能成在你选相机和定安装位置的那一刻就决定了七成。这笔账很好算我把它写成一个固定的估算流程。普通蓝牌的外廓尺寸是 440mm × 140mm字符高度大约 90mm。字符识别模型要稳定工作车牌区域在送入网络前的字符高度至少要有 16 到 24 像素低于这个值笔画粘连、字符糊成一团什么模型都救不回来。假设相机水平视场覆盖的车道宽度是 W 米分辨率水平像素是 P那么车牌在画面中的横向像素数为车牌横向像素 P × (0.44 / W)举两个实际配置对比场景分辨率 P覆盖宽度 W车牌横向像素字符高度约结论配置 A19204 m21143 px裕度充足夜间也稳配置 B19208 m10521 px勉强够用夜间易掉配置 C192016 m5210 px基本不可用配置 D384016 m10521 px与配置 B 等效覆盖翻倍看清楚这张表很多问题就不用争论了。配置 C 那种一台相机看两个车道还想识车牌的方案无论换什么模型都白搭唯一解是换长焦镜头或者提高分辨率。我给客户的建议通常是先按字符高度 25 像素以上的目标反推相机焦距和安装距离再谈算法。这条建议帮我省掉的返工比任何模型优化都多。还有一个容易被忽略的因素是俯仰角。车牌是垂直安装的相机如果装得太高、俯角太大车牌在画面里会被强烈压缩垂直方向的像素数骤减等效于字符高度打折。我一般把俯角控制在 15 到 25 度之间既能覆盖一定纵深又不会把车牌压扁。3.2 裁剪与缩放的细节形变是识别率的隐形杀手车牌检测框裁出来之后下一步是 resize 到识别模型的输入尺寸。这里有个很多人不注意的坑车牌的真实长宽比是 440:140 ≈ 3.14:1而多数识别网络的输入是 94:24 ≈ 3.92:1。如果你直接把 3.14 的图硬拉到 3.92字符在水平方向被拉宽了 25%笔画的粗细比例全变了识别率会明显下降。我的处理方式是裁剪时不按检测框原样裁而是以框中心为基准按目标长宽比3.9:1 左右重新计算裁剪区域。具体做法是先取检测框的中心点 (cx, cy) 和宽度 w然后令裁剪高度 h w / 3.9再按这个 h 上下扩展。这样裁出来的图比例和网络输入一致resize 的时候几乎不产生形变。同时要留一点 padding。车牌检测框有时会贴得太紧把边框的白色字符切掉一角识别就会出错。我在裁剪时会在四周各留 5% 到 8% 的余量宁可多带一点车身背景也别切断字符。提示如果你的车牌检测模型输出的是四个角点而不是水平矩形那更好办——直接做透视变换把车牌拉成正视的矩形再 resize。这个操作对倾斜车牌的识别率提升非常明显尤其是斜向驶入的车辆。3.3 检测阈值与跟踪阈值的取舍阈值的设定没有万能值但有一套判断逻辑。检测阈值我分两类看车辆检测阈值我倾向压得低一些0.4 到 0.5。因为漏掉一辆车的代价是整个车牌都丢了而多检一个框的代价只是多跑一次车牌检测几十毫秒。这个买卖明显划算。当然低的代价是误检变多所以要在后面用车牌检测的严格阈值兜住。车牌检测阈值我倾向设得高一些0.55 到 0.65。因为送进车牌检测的区域已经是车了误检的空间小收紧阈值能有效过滤掉车身上的装饰条、反光贴、后备胎这类长得像车牌的东西。字符识别置信度我一般设 0.8 作为接受下限低于这个值的结果标记为待定而不是直接丢弃。实际跑起来你会发现很多低置信度结果是部分正确比如把 8 认成 B保留下来给上层做模糊匹配比直接扔掉更有价值。跟踪器的 IOU 匹配阈值我通常用 0.3。设太高比如 0.5会导致车速快的时候框位移大、匹配不上ID 频繁切换设太低比如 0.1又会让相邻两辆车串 ID。0.3 是个比较中庸的起点实际按帧率和车速微调。3.4 跳帧与算力预算一张卡能接几路这笔账要这么算单帧的总耗时 解码 车辆检测 车牌检测 字符识别 绘制。以一张中端显卡为例1080P 下单帧大致是车辆检测 8ms、车牌检测 2ms因为只在小 ROI 上跑、字符识别 1ms、再加上解码和绘制的 5ms 左右合计约 16ms理论上一路相机 25fps 只要 400ms/秒也就是 40% 的单卡占用。听起来能接两路但实际要打折扣因为管道有排队、有序列化开销还有突发流量。所以我的经验值是一张中端卡稳定接 4 路 1080P 车牌识别前提是开了跳帧。跳帧的思路是——车不是每秒都在过没必要每帧都跑完整链路。我的做法是用跟踪节点的结果做触发只有当某个 track_id 连续出现 3 帧以上说明是个真实目标不是一闪而过的误检才把它送进车牌检测和识别链路且同一个 track_id 最多识别 3 次取置信度最高的那次作为最终结果。这样绝大多数帧只跑了车辆检测这一步算力省下来一大半而识别结果的准确度反而更高因为我们是从多次识别里挑的最优解。这个多次识别投票的策略在车牌轻微模糊或者反光的时候特别好用。我见过一个场景车牌被阳光照得局部过曝单帧识别出来是京A·12B45但前后几帧分别识别出京A·12845和京A·12845投票之后取多数结果是对的。4. 实操从零跑通一条车牌识别流水线4.1 环境准备与编译VideoPipe 是 CMake 工程依赖 OpenCV 和推理后端ONNX Runtime 或 TensorRT 之类。我在 Linux 上的一般流程是# 依赖装齐OpenCV带 videoio、imgproc、CMake 3.15、g 支持 C17 sudo apt install -y build-essential cmake libopencv-dev # 拉代码注意第三方依赖opencv、onnxruntime 等按仓库 README 的脚本准备好 git clone repo VideoPipe cd VideoPipe mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc)这里有两个坑我要提前说。第一个是OpenCV 版本不同版本之间 videoio 模块的 RTSP 行为有差异我遇到过换版本之后取流延迟从 200ms 涨到 1.5 秒的情况建议固定用你自己验证过的那一版。第二个是推理后端的 GPU 支持如果你打算上 TensorRT要确认 CUDA、cuDNN、TensorRT 三者版本互相兼容这个组合的坑最深装完先跑一个最小的推理示例验证别直接上管道。模型方面你需要准备三个车辆检测模型、车牌检测模型、车牌字符识别模型。前两个用常见的单阶段检测器导出成 ONNX 即可第三个用 LPRNet 这类轻量序列模型。字符集一定要和你的业务区域对齐国内车牌包含省份简称汉字、字母、数字还有新能源的 8 位牌如果你的训练集只覆盖了 7 位蓝牌遇到绿牌就会出错。这一点我在第五节还会展开。4.2 流水线的代码骨架与关键改动上一节给了一个基础骨架实际落地时我一般会做三处改造。第一处在车辆检测和车牌检测之间加一层 ROI 管理。不是所有车辆框都值得送检太小的宽度小于画面宽度的 6%直接跳过因为它们大概率在远处、车牌像素不够位于画面边缘的也要谨慎边缘框常常是被裁掉一半的车。// 伪代码车辆框过滤逻辑 for (auto det : frame_meta-detections) { int w det.box.width; int h det.box.height; if (w frame_w * 0.06) continue; // 太远的车不检 if (det.box.x 2 || det.box.y 2) continue; // 贴边的不检 if (w * 1.0 / h 5.0) continue; // 极端长条大概率误检 candidates.push_back(det); }第二处外扩裁剪框并做边界截断。这是防止车牌被切的关键int pad_w static_castint(det.box.width * 0.12f); int pad_h static_castint(det.box.height * 0.12f); cv::Rect roi(det.box.x - pad_w, det.box.y - pad_h, det.box.width 2 * pad_w, det.box.height 2 * pad_h); roi cv::Rect(0, 0, frame.cols, frame.rows); // 截断越界部分 if (roi.width 16 || roi.height 16) continue;第三处结果缓存与去重。用一个mapint, PlateResult按 track_id 存最优结果只有当前帧的识别置信度高于缓存值时才更新。这样输出节点吐出去的数据天然就是一车一条上层业务不用再自己去重。4.3 字符识别的输入处理与后处理字符识别这一环输入处理和输出处理各占一半的重要性。输入处理前面说过核心是按目标长宽比裁剪 不变形 resize。这里补一个细节resize 的插值方式。缩小图像时用INTER_AREA比INTER_LINEAR干净得多能减少摩尔纹放大时用INTER_LINEAR或INTER_CUBIC。车牌在画面里通常比网络输入大属于缩小所以走INTER_AREA。输出后处理有两件事必须做。第一件是字符集映射校验LPRNet 输出的是 CTC 解码后的序列偶尔会吐出训练集里不存在的组合比如第一位出现数字。国内的普通车牌第一位必然是省份简称汉字第二位是字母后面五位是字母数字混合有具体规则。我会加一层轻量的规则校验明显不合规的结果降权处理。第二件是置信度计算不要用 CTC 最后一层的最大值当置信度要用整个序列路径的概率乘积或者平均值否则短字符串会天然占便宜。// 伪代码CTC 解码 简单规则校验 std::string text ctc_decode(output); float score sequence_confidence(output, text); if (text.size() 7 || text.size() 8) score * 0.5f; // 长度异常降权 if (!is_valid_first_char(text[0])) score * 0.6f; // 首位不是省份简称 if (score 0.5f) return; // 低于底线直接丢 plate_cache[track_id] keep_better(plate_cache[track_id], {text, score});4.4 联调验证的做法联调阶段我不用实时流而是先录一段视频文件跑离线。原因是实时流不可控你没法复现同一辆车、同一帧的问题。用vp_video_src_node读本地 MP4把每个节点的中间结果都落盘车辆检测结果存成 JSON、车牌裁剪图存成 jpg、识别结果存成 CSV。跑完之后统计三个指标——车辆检出率、车牌检出率、字符准确率一层一层往上排查。这套流程跑一遍你就知道瓶颈在哪一层了比在现场盯着屏幕猜高效得多。现场部署的时候我还会做一件事留一个回放模式的开关。把现场录的流存下来出问题时直接回放能复现的问题都好解决不能复现的问题才是噩梦。这个习惯救过我好几次。5. 常见问题排查实录5.1 漏检与误检速查表现象最可能的原因排查方法处理方式白天正常夜间车牌全漏车灯过曝导致车牌区域饱和存夜间帧看车牌区域像素值相机开宽动态或降低曝光补偿远处车辆识别不到车牌像素高度不足用 3.1 的公式反算换长焦或提高分辨率别调模型车身上贴纸被识别成车牌车牌检测阈值过低看误检框的位置分布阈值提到 0.6 以上加长宽比过滤同一辆车报了多次跟踪 ID 频繁切换打印 track_id 变化日志调大 max_age降低 IOU 阈值车牌被切掉一半车辆框贴太紧看裁剪出的车牌小图外扩裁剪比例提到 12% 到 15%斜向车辆识别错字车牌透视变形看原始裁剪图加透视变换矫正或改用四点检测这张表是我这几年遇到问题的一个浓缩其中远处车辆识别不到和车贴误检这两条出现频率最高。前者是硬件问题后者是阈值问题都不该在模型层面折腾。5.2 字符识别错字的典型原因字符错了先别急着换模型八成是输入的问题。我总结了三类高频原因。第一类是形近字。8和B、0和D、1和I、2和Z这几组在低分辨率下几乎无法区分。出现这种情况先看字符高度够不够如果只有十几像素那就是像素不够不是模型不行。如果像素够还错才考虑模型的字符集或者训练数据问题。第二类是字符集缺项。国内车牌有省份简称汉字新能源车牌是 8 位还有一些特殊用途的车牌格式。如果你的模型训练集里没有 8 位绿牌那它遇到绿牌时会强行按 7 位输出结果必然是错的。这个问题只能靠扩充训练数据解决没有捷径。第三类是裁剪偏移。车牌检测框稍微偏一点边缘字符就被切掉了识别出来就成了京A·1234少一位。这类错误有个特征识别结果的长度经常偏短置信度反而不低。排查方法是把裁剪图存下来人工看一眼就能确认。心得我在项目里固定了一个错例归档流程——凡是置信度在 0.5 到 0.8 之间、或者长度异常的结果自动把原始帧和裁剪图存到一个目录。每周扫一遍能发现大量系统性问题比盲目调参有用得多。5.3 性能、内存与稳定性跑长稳测试是必须的最少 72 小时。我遇到过的问题里有一半只在长时间运行后才暴露。常见的有三个。内存缓慢增长。多数是节点里的缓存没清理比如结果缓存 map 按 track_id 存结果但轨迹销毁时没删对应的缓存条目跑几天就吃掉几个 G。解决办法是给缓存加过期机制比如每条记录带上时间戳定期清理超过 5 分钟的条目。帧率抖动。表现为平时 25fps隔几十秒卡一下。原因通常是下游 IO 阻塞导致反压或者解码器遇到关键帧时耗时突增。我的做法是给输出节点加独立线程和队列上限队列满了就丢帧而不是阻塞——车牌识别场景里丢几帧不影响结果阻塞整条链路才是灾难。断流不重连。RTSP 流在网络抖动后会静默断开管道还在跑但帧不再更新。这个必须靠前面说的时间戳看门狗解决框架本身不负责这件事。6. 工程化落地的一些经验补充6.1 相机选型与安装位置选相机这件事我的建议顺序是先定识别距离和车道宽度按字符高度 25 像素反算需要的水平像素数再选分辨率和焦距。别反过来——先买个便宜相机再想办法优化算法这条路基本走不通。镜头的焦距选择上宁可长一点、视场窄一点把像素集中在车道上也别为了看得广牺牲车牌像素密度。补光也是绕不开的一环。夜间车牌识别最怕两件事车灯直射导致过曝以及环境光不足。我一般建议用相机自带的补光或者外置的频闪补光灯频闪的好处是能和相机曝光同步减少对驾驶员的影响同时提升车牌区域的信噪比。安装位置上补光灯和镜头尽量同轴或者夹角很小避免车牌反光直接打进镜头。6.2 触发策略与数据闭环纯视频检测触发靠车辆检测框出现来判断有车实现简单但会有无效计算——比如画面里停着一辆车不动它就一直在跑。如果有条件我建议加一路地感或者雷达做硬触发车到了才开始跑识别这样算力能省一大半误报也能压下去。数据闭环这件事我现在的做法是在设备上留一个疑难样本上传的通道置信度落在中间区间、或者规则校验失败的结果连同原始帧一起上传到服务器。这些数据攒够了就是下一版模型最好的训练集。这套机制跑上半年识别率通常能再往上提几个点而且提的都是你现场真实遇到的难例比公开数据集有用得多。最后说个我踩过最深的坑。早期做这类项目我总想着把参数调到最优检测阈值精确到小数点后两位裁剪比例反复试。后来发现现场环境是会变的——早上顺光、下午逆光、雨天车牌沾泥、冬天车牌结霜你在实验室调出来的那组参数到了现场能覆盖七成场景就不错了。真正让系统稳下来的不是某一组完美参数而是分层兜底的策略检测阈值放宽一点、多识别几次投票、置信度分档处理、难例自动归档。这套东西看起来笨但它在真实的、脏的、千变万化的场景里比任何精调都靠谱。