1. 为什么选择YOLO+ONNX Runtime做车牌识别?
车牌识别是计算机视觉的经典应用场景,传统方案依赖手工特征提取和模板匹配,但遇到角度倾斜、光照变化时效果直线下降。我在实际项目中测试过多种方案,最终发现YOLOv5+ONNX Runtime的组合最能兼顾精度和效率。
YOLO的检测速度有天然优势,实测在RTX 3060显卡上能跑到150FPS。而ONNX Runtime作为跨平台推理引擎,比直接加载PyTorch模型体积小40%,在Java环境下内存占用减少35%。更关键的是,这个方案可以轻松部署到树莓派等边缘设备。
2. 环境搭建与依赖配置
2.1 开发环境准备
推荐使用JDK 17+和Maven 3.8+的组合,我在Windows和Linux平台都验证过。pom.xml关键依赖如下:
<dependencies> <!-- ONNX Runtime --> <dependency> <groupId>com.microsoft.onnxruntime</groupId> <artifactId>onnxruntime</artifactId> <version>1.16.1</version> </dependency> <!-- OpenCV --> <dependency> <groupId>org.openpnp</groupId> <artifactId>opencv</artifactId> <version>4.7.0-0</version> </dependency> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.2.4</version> </dependency> </dependencies>踩坑提醒:OpenCV的Java版有多个实现,实测openpnp的封装最稳定。如果遇到动态库加载失败,可以尝试显式指定库路径:
static { nu.pattern.OpenCV.loadShared(); System.loadLibrary(Core.NATIVE_LIBRARY_NAME); }2.2 模型准备与转换
需要准备两个ONNX模型:
- 车牌检测模型(推荐YOLOv5s改造版)
- 车牌识别模型(CRNN+CTC结构)
我用Python转换的示例代码:
import torch model = torch.load('yolov5s_plate.pt') model.eval() dummy_input = torch.randn(1, 3, 640, 640) torch.onnx.export(model, dummy_input, "plate_detect.onnx", input_names=["images"], output_names=["output"], dynamic_axes={'images': {0: 'batch'}, 'output': {0: 'batch'}})转换时务必注意:
- 添加dynamic_axes支持多batch推理
- 验证输出节点名称与Java代码匹配
- 用onnxruntime-tools检查模型有效性
3. 核心实现逻辑剖析
3.1 图像预处理流水线
车牌识别效果80%取决于预处理质量。我的标准处理流程:
- 颜色空间转换:BGR→RGB(OpenCV默认读取为BGR格式)
Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2RGB);- 尺寸归一化:采用letterbox保持宽高比
Letterbox letterbox = new Letterbox(640, 640); Mat processed = letterbox.letterbox(src);- 数值归一化:像素值缩放到0-1范围
processed.convertTo(processed, CvType.CV_32FC3, 1.0/255);- 通道顺序调整:HWC→CHW格式转换
float[] chw = new float[3*640*640]; for(int c=0; c<3; c++){ for(int h=0; h<640; h++){ System.arraycopy(rgbData, h*640*3+c, chw, c*640*640+h*640, 640); } }3.2 双模型协同工作流程
- 检测阶段:
// 创建输入tensor OnnxTensor tensor = OnnxTensor.createTensor(env, FloatBuffer.wrap(chw), shape); // 执行推理 try(OrtSession.Result results = session.run(Collections.singletonMap("images", tensor))) { float[][] outputs = ((float[][][])results.get(0).getValue())[0]; }- NMS后处理:
List<float[]> filtered = bboxes.stream() .filter(b -> b[4] > confThreshold) .sorted(Comparator.comparing(b -> -b[4])) .collect(Collectors.toList());- 识别阶段:
// 裁剪车牌区域 Mat plateROI = new Mat(src, new Rect(x1,y1,x2-x1,y2-y1)); // 执行字符识别 OrtSession.Result recResult = recSession.run(recInputs); int[] charIndexes = argmax((float[][][])recResult.get(0).getValue());3.3 结果可视化技巧
在原始图像上标注结果时,我推荐使用自适应绘制策略:
// 根据图像尺寸动态计算线宽和字体大小 int baseSize = Math.min(img.width(), img.height()); int thickness = baseSize / 300; int fontSize = baseSize / 40; // 绘制识别结果 Imgproc.putText(img, plateText, new Point(rect.x, rect.y-5), Imgproc.FONT_HERSHEY_SIMPLEX, fontSize, new Scalar(0,255,0), thickness);4. Spring Boot服务化实战
4.1 高效文件上传处理
@PostMapping("/upload") public List<PlateResult> handleUpload(@RequestParam MultipartFile file) { // 临时文件转换 Mat img = Imgcodecs.imdecode( new MatOfByte(file.getBytes()), Imgcodecs.IMREAD_COLOR); // 执行识别流程 return plateService.detect(img); }性能优化点:
- 使用内存映射避免磁盘IO
- 预加载模型到内存
- 采用线程池处理并发请求
4.2 接口响应设计
推荐结构化的返回格式:
public class PlateResult { private String plateNumber; private String plateColor; private float confidence; private Rect position; private long processTime; }4.3 性能监控方案
通过Spring Actuator添加指标:
@Bean MeterRegistryCustomizer<MeterRegistry> metrics() { return registry -> { registry.gauge("plate.detect.time", Tags.of("model","yolov5"), detectTimer); }; }我在压力测试中发现的主要瓶颈:
- 图像解码耗时占比30%
- ONNX初始会话创建耗时2-3秒
- 大尺寸图像resize消耗CPU
对应的优化手段:
- 启用OpenCV的IPP加速
- 实现会话池化管理
- 添加图片尺寸限制
5. 常见问题解决方案
5.1 车牌颜色识别不准
根本原因是颜色分类模型训练数据不均衡。我的改进方案:
- 在HSV空间进行颜色增强
- 添加难例样本(反光、污损车牌)
- 改用HSV直方图特征辅助判断
5.2 小尺寸车牌漏检
通过修改YOLO锚点参数提升小目标检测能力:
# yolov5s.yaml anchors: - [5,6, 8,14, 15,11] # P3/8 - [10,13, 16,30, 33,23] # P4/16 - [30,61, 62,45, 59,119] # P5/325.3 复杂背景干扰
采用两阶段检测策略:
- 先用YOLO检测车辆
- 在车辆ROI内做二次车牌检测
实测准确率提升27%,但推理时间增加15ms。可以根据场景需求灵活选择方案。
6. 效果优化与部署建议
6.1 模型量化加速
将FP32模型转为INT8格式:
python -m onnxruntime.tools.quantize \ --input plate_detect.onnx \ --output plate_detect_int8.onnx \ --quantize_type QInt8实测效果:
| 精度类型 | 推理速度(ms) | 内存占用(MB) |
|---|---|---|
| FP32 | 45 | 320 |
| INT8 | 28 | 190 |
6.2 多线程流水线设计
ExecutorService pool = Executors.newFixedThreadPool(3); CompletableFuture<Mat> decodeFuture = CompletableFuture.supplyAsync(() -> { return Imgcodecs.imdecode(buf, IMREAD_COLOR); }, pool); CompletableFuture<DetectionResult> detectFuture = decodeFuture.thenApplyAsync(img -> { return detector.detect(img); }, pool);6.3 边缘设备部署
树莓派4B上的优化技巧:
- 使用ARM64版本的ONNX Runtime
- 启用OpenVINO后端
- 限制识别区域减少计算量
- 降低检测模型输入分辨率到480x480
实测在树莓派上能达到800ms的单帧处理速度,满足大部分实时场景需求。