ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java多模型推理实战:ONNX Runtime集成OCR、YOLOv8、人脸识别与以图搜图

Java多模型推理实战:ONNX Runtime集成OCR、YOLOv8、人脸识别与以图搜图 简介面向Java开发者的全能视觉智能识别项目基于SpringBoot框架构建。项目实现了PaddleOCR-V4文字识别、YoloV8物体识别、人脸识别与以图搜图等核心功能并预留语音识别、动物识别、安防检查等扩展方向适合需要在Java业务系统中快速集成视觉AI能力的开发者、架构师及相关技术学习者。压缩包共包含124个文件主体为92个Java源码文件完整呈现业务逻辑、API接口与多线程调用方式同时附带OCR推理所需的bin模型文件、param参数文件以及适配Windows、macOS、Linux的dll、dylib、so本地库另有yml、xml配置文件与md说明文档整体压缩包约69.98MB。目前已有167人学习下载。读者借助这套工程可以获得可编译运行的Java视觉识别项目参考其预置模型与跨平台依赖配置显著降低PaddleOCR、YoloV8等模型的接入成本项目提供简洁而强大的API接口支持二次开发也可灵活改造为安防检查、动物识别等应用同时结合多线程与算法优化设计能够高效处理复杂图像数据具备较高的工程参考价值。1. 一张图片进Java服务四种识别服务怎么编排如果业务方只给你一个Spring Boot工程不允许再起Python服务产品却要求同时支持OCR、目标检测、人脸比对和以图搜图那么这个标题对应的实战问题就是怎么用Java一个进程把这四种能力串起来。我的做法不是把算法用Java重写而是把PaddleOCR-V4、YoloV8和人脸特征模型统一导出成ONNX再交给ONNX Runtime JVM运行以图搜图部分则用向量索引完成。这条路线适合要接模型但团队是纯Java栈的人也适合在边缘设备上做视觉智能服务的人。2. Java跑PaddleOCR-V4和YoloV8先统一到ONNX再推理PaddleOCR-V4官方推理基于Paddle InferenceYoloV8基于PyTorch或Ultralytics想让两个模型在同一个Java进程里共存最省事的路线是导出成ONNX并用ONNX Runtime加载。导出后的静态图会被拆成算子序列OCR的文本检测、方向分类、文字识别三个子模型都能一次性转完YoloV8的C2f结构也会在导出时自动展开成多个卷积和concat节点Java端不需要关心原始网络内部细节只需要确认输入输出张量。2.1 PaddleOCR-V4与YoloV8的ONNX导出命令2.1.1 PaddleOCR-V4从Paddle格式导出ONNX先要有已经推理成功的PaddleOCR-V4推理目录目录里通常包含inference.pdmodel和inference.pdiparams。常见做法是使用paddle2onnx转换det、cls、rec三个子模型分开导出。# det文本检测模型 paddle2onnx --model_dir ./inference/ch_PP-OCRv4_det_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv4_det_infer.onnx \ --opset_version 12 \ --enable_onnx_checker True # cls方向分类模型 paddle2onnx --model_dir ./inference/ch_PP-OCRv4_cls_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv4_cls_infer.onnx \ --opset_version 12 # rec文字识别模型 paddle2onnx --model_dir ./inference/ch_PP-OCRv4_rec_infer \ --model_filename inference.pdmodel \ --params_filename inference.pdiparams \ --save_file ch_PP-OCRv4_rec_infer.onnx \ --opset_version 12命令里的opset_version我一般固定为12ONNX Runtime对opset 12的支持很稳定。enable_onnx_checker打开后会在转换结束时做一次算子检查如果某个算子不在ONNX算子集里尽早暴露出来。PaddleOCR-V4的文本检测模型输入是[1,3,H,W]动态尺寸导出后Java端按动态图处理即可rec模型输入通常是[1,3,48,320]如果训练时改过高度需要同步修改Java端的预处理参数否则运行时会报维度不匹配。2.1.2 YoloV8导出ONNXYoloV8导出建议直接用Ultralytics的CLI执行后会得到yolov8n.onnx默认输入尺寸是[1,3,640,640]输出是[1,84,8400]。yolo export modelyolov8n.pt formatonnx opset12 imgsz640 simplifyTruesimplifyTrue会调用onnx-simplifier合并冗余节点C2f展开后图会比较长简化后Java端加载更快。如果换成自己训练的数据集导出前先确认类别数量。[1,84,8400]里的84等于480也就是4个边框坐标加上80类得分Java端后处理要按这个布局解析。使用dynamicTrue可以得到动态batch但Java端同时处理多张图的需求不强我一般不加。2.2 ONNX Runtime的Java推理骨架两个模型导出后Java端的加载方式是统一的。用官方onnxruntime依赖创建一个OrtEnvironment每个模型一个OrtSession。OrtEnvironment env OrtEnvironment.getEnvironment(); OrtSession.SessionOptions opts new OrtSession.SessionOptions(); opts.setIntraOpNumThreads(4); OrtSession yoloSession env.createSession(yolov8n.onnx, opts); // 输入为 [1,3,640,640] 的 float 数组NCHW 格式 long[] inputShape {1, 3, 640, 640}; OnnxTensor inputTensor OnnxTensor.createTensor(env, FloatBuffer.wrap(rgbPixels), inputShape); OrtSession.Result result yoloSession.run(Collections.singletonMap(images, inputTensor)); // 输出名称不写死从模型信息里取第一个输出 String outputName yoloSession.getOutputInfo().keySet().iterator().next(); float[][][] out (float[][][]) result.get(outputName).getValue();setIntraOpNumThreads(4)控制单个模型内部的算子并行线程数。并发请求不多时可以设成4机器核多但模型多时反而要调小避免OCR、检测、人脸三个推理线程一起把CPU打满。run的入参是输入名称到OnnxTensor的Map模型导出后输入名不一定是images先通过session.getInputInfo()确认名称和shape。YoloV8后处理第一步是transpose因为ONNX导出的输出是通道优先布局需要先转成[1,8400,84]再按行处理否则候选框坐标是乱的。2.3 OCR全流程det、cls、rec三个模型串联PaddleOCR-V4不是单个模型而是文本检测、方向分类、文字识别三个子模型的组合。Java端最稳妥的编排顺序是先跑det得到文本框再按文本区域裁出小图用cls判断是否需要旋转最后交给rec识别。// 封装后的简化伪代码实际要补上各自的预处理和后处理 ListTextBox boxes detSession.run(image); // 检测文本行 for (TextBox box : boxes) { Mat crop perspectiveCrop(image, box); // 按四点坐标做透视变换 float angle clsSession.run(crop); // 0 或 180 度 String text recSession.run(rotate(crop, angle)); // CTC 解码后得到文本 result.add(new OCRLine(box, text)); }perspectiveCrop要用OpenCV的透视变换实现PaddleOCR检测出来的框是四点坐标直接submat会带进大量背景影响识别准确率。cls方向分类器输出两个类别的概率通常取最高概率即可。rec的输出需要做CTC解码把重复相邻字符和blank字符去掉模型词汇表要和训练时的字典一致Java端需要维护一份字符索引表。这个串联逻辑在Python版里由PaddleOCR类封装Java端没有等价SDK所以整条链路要在自己的代码里串起来。2.4 两种模型的后处理对比与常见异常PaddleOCR-V4和YoloV8有一个共同坑点输入输出shape是静态还是动态直接影响Java端代码。PaddleOCR的det可以导出动态shaperec尽量固定成[1,3,48,320]YoloV8导出后输入维度固定为640如果原图不是正方形必须用letterbox缩放到640x640不能直接resize拉伸否则检测框坐标和宽高比会产生偏移。异常通常出现在三个地方输入名称对不上、预处理通道顺序不对、NMS阈值不合理。输入名称最好在导出后先用netron看一眼不要盲写images通道顺序要和训练时一致YoloV8训练时用RGB还是BGR要看导出时的配置我一般统一在Java端转RGB并除以255。检测后处理时conf_thres从0.25起步iou_thres固定0.45目标严重遮挡时再调低IOU不然重叠框会被砍掉。模型输入Shape输出Shape主要后处理YoloV8检测[1,3,640,640][1,84,8400]transpose、置信度过滤、NMSPaddleOCR-V4 det[1,3,H,W]动态[1,1,H,W]DB二值化、连通域PaddleOCR-V4 rec[1,3,48,320][1,词表长度]CTC解码3. Java人脸识别ArcFace特征提取比对与门禁阈值调参人脸识别在Java里不要走“自己训练模型”的路线直接用开源免费商用的ArcFace类预训练模型更现实。InsightFace仓库里有现成ONNX权重选一个输出512维特征的模型用ONNX Runtime加载Java端只做对齐、归一化和余弦相似度计算。门禁机场景里人脸底库可能来自不同年代的照片包含侧脸、逆光和低分辨率阈值必须根据真实环境调整。3.1 识别流程检测、关键点、对齐、特征提取完整链路比目标检测多两步。先用检测模型定位人脸框可以自己训练YoloV8的人脸类别也可以直接用OpenCV DNN的人脸检测器。关键点用5点人脸关键点模型检测出左眼、右眼、鼻尖、左嘴角、右嘴角再按标准位置做仿射对齐。这里最容易被忽略的是对齐。ArcFace输入是112x112训练时所有人脸都做了相似变换如果Java端直接把检测框resize到112x112关键点位置会产生偏差特征质量明显下降。常见做法是先从关键点估计仿射矩阵再用warpAffine对齐到112x112。3.2 ArcFace特征提取与归一化以InsightFace的w600k_r50导出的ONNX为例Java端推理代码OrtSession faceSession env.createSession(w600k_r50.onnx, opts); long[] shape {1, 3, 112, 112}; OnnxTensor tensor OnnxTensor.createTensor(env, FloatBuffer.wrap(rgbAligned), shape); String featureName faceSession.getOutputInfo().keySet().iterator().next(); OrtSession.Result result faceSession.run(Collections.singletonMap(input, tensor)); float[] embedding (float[]) result.get(featureName).getValue(); float l2 0; for (float v : embedding) { l2 v * v; } l2 (float) Math.sqrt(l2); for (int i 0; i embedding.length; i) { embedding[i] / l2; }输入名称同样要用getInputInfo()确认不同导出版本的输入名可能不同。ArcFace输出是512维浮点特征很多模型的输出没有做L2归一化所以特征提取后要手动归一化。归一化后的向量可以直接作为以图搜图的向量也可以作为人脸比对的目标特征。3.3 余弦相似度与阈值设定比对两个人脸时直接计算两个归一化向量的点积double similarity 0; for (int i 0; i embeddingA.length; i) { similarity embeddingA[i] * embeddingB[i]; }两个归一化向量的点积就是余弦相似度。阈值没有固定值需要结合误识率标定。我一般会从门禁现场抽三百张同一个人的照片再抽三百张不同人的照片分别计算同类和跨类的cosine分布选择能使两类分布重叠区最小的点作为阈值。如果重叠区太大先检查对齐和图片质量。采集时段要跨越白天和夜间因为红外补光的照片特征分布会和自然光照片明显偏离。应用场景推荐阈值说明刷脸门禁、闸机0.38 ~ 0.45偏向严格误识率低但光线差时拒识率高考勤打卡0.45 ~ 0.55平衡识别率和易用性以图搜图辅助检索0.5 ~ 0.6召回优先后面还有人工确认3.4 大量底库的1:N比对思路门禁平台上的大量数据很可能跨到百万级把百万人的512维向量逐条点积Java单机内存和时间都撑不住。常见做法是先按项目、部门、时间等业务属性缩小范围再用向量索引召回Top-N最后用上面的相似度阈值精确过滤。向量索引的部分直接引出第4章的以图搜图实现。4. Java以图搜图向量索引与HNSW召回参数以图搜图有两种理解。第一种是搜人脸特征模型用第三章的ArcFace第二种是搜任意物体那要用通用图像特征模型比如CLIP或SwinTransformer导出的ONNX。两条路在Java端最后都收敛成同一个问题把图片变成向量然后从向量库里找出最接近的Top-K。4.1 选型Lucene KNN还是自研HNSW万级图像直接在内存里逐条算余弦也能跑但Java堆内存会随底库线性增长。到十万级以上需要倒排索引或图索引。HNSW的Java实现不一定要自己写Lucene从9.0开始内置KNN向量检索底层就是HNSW图向量和图片元信息可以存在同一个文档里对Spring Boot项目来说删旧索引、更新字段都方便。如果索引量到了百万级并且要跨节点扩容再考虑部署Milvus之类的独立向量数据库。如果不想引入外部服务且底库在百万级以下我一般直接在项目里引入Lucene的KNN比自研HashMap方案可靠得多。4.2 用Lucene的KnnFloatVectorField建索引Lucene 9里建立以图搜图索引的最小流程Directory directory new ByteBuffersDirectory(); IndexWriterConfig config new IndexWriterConfig(new KeywordAnalyzer()); IndexWriter writer new IndexWriter(directory, config); for (ImageItem item : imageList) { Document doc new Document(); doc.add(new StringField(imageId, item.getId(), Field.Store.YES)); doc.add(new KnnFloatVectorField(embedding, item.getVector(), VectorSimilarityFunction.COSINE)); writer.addDocument(doc); } writer.commit();KnnFloatVectorField需要固定向量维度不同特征模型必须用固定维度。VectorSimilarityFunction.COSINE表示Lucene在构建HNSW时按余弦相似度计算图上的边。KeywordAnalyzer在这里为imageId字段建索引向量字段不依赖分词器。ByteBuffersDirectory适合单机测试生产环境建议换用MMapDirectory指向本地磁盘目录否则重启后索引会丢失。检索时用KnnFloatVectorQueryIndexReader reader DirectoryReader.open(directory); IndexSearcher searcher new IndexSearcher(reader); TopDocs hits searcher.search( new KnnFloatVectorQuery(embedding, queryVector, topK), topK); for (ScoreDoc scoreDoc : hits.scoreDocs) { Document doc searcher.doc(scoreDoc.doc); String imageId doc.get(imageId); }4.3 HNSW召回参数与二次精排HNSW是近似检索不一定能拿到全局最相似的Top-K。所以以图搜图接口要保留二次精排先用HNSW召回3倍或5倍候选集再在Java里对这少量向量做精确余弦相似度排序。召回数量直接影响精度和延迟我一般设置topK * 3数据量极大时降到topK * 2。参数推荐值影响HNSW MaxConn16节点连接数越大召回高内存越大HNSW BeamWidth100建索引搜索宽度越大索引耗时越长检索召回倍数3倍给二次精排留空间最终相似度阈值0.5 ~ 0.6过滤无关图片Lucene对MaxConn和BeamWidth有默认值我很少改。索引量到千万级才需要换独立向量库。二次精排代码不长但它能修正HNSW带来的偶发漏召回。4.4 特征模型不同以图搜图语义不同ArcFace只能表示人脸用它做以图搜图只有在图片全是人像时才有意义。通用图片搜索要换成CLIP这类模型导出成ONNX后加载方式与ArcFace一致只是输入尺寸、归一化参数和输出维度不一样。所以这个项目的代码结构可以抽出一个FeatureExtractor接口具体的ArcFace、CLIP实现负责输出float数组向量索引部分完全复用。5. Spring Boot里四个视觉模型的线程池编排与索引版本管理同时加载OCR、YoloV8、人脸和特征模型后Java服务最大的风险是线程和内存被推理撑爆。ONNX Runtime默认按CPU核数创建线程四个模型同时推理时线程数可能膨胀到几十个接口RT会直线上涨。常见解法是给每个模型开独立线程池同时限制模型内部的算子线程数。ExecutorService ocrExecutor Executors.newFixedThreadPool(2, r - new Thread(r, ocr)); ExecutorService detectExecutor Executors.newFixedThreadPool(2, r - new Thread(r, detect)); // 上传图后并行跑OCR和检测而不是串行等待 CompletableFutureOCRResult ocr CompletableFuture.supplyAsync( () - ocrService.run(image), ocrExecutor); CompletableFutureListBox detect CompletableFuture.supplyAsync( () - detectService.run(image), detectExecutor);当一张图片同时要OCR和检测时并行编排比串行能省一半时间。人脸对齐和特征提取不要放在检测线程池里否则检测请求会被拖慢。OrtEnvironment和OrtSession可以跨线程使用不需要每次请求重新创建用单例持有即可。5.1 推理对象复用与线程隔离输入tensor每次新建无法避免但FloatBuffer可以放在线程局部变量里复用减少GC压力。Java服务里用ByteBuffer.allocateDirect还是堆内Buffer实测结论是后者更稳DirectBuffer虽然减少一次拷贝但申请和释放成本高不适合高并发的小图片输入。5.2 识别接口上线前的验证手段四个模型串起来后单独测每个模型通过不代表全链路正确。我通常准备一组标准图片OCR至少20张包含不同字体和旋转角的图人脸准备同一人多角度照片以图搜图准备50张以上互有重复物体的图集跑一次回归脚本。OCR验证看检测框是否贴住文字行人脸验证看同人cosine分布和不同人分布是否有明显分界以图搜图验证直接刷一遍索引随机抽一张query图看Top1是否在预期集合里。验证项通过标准失败排查点OCR文本行检测框与文本无大面积偏移det的DB阈值、rec的字典人脸1:1同人相似度高于跨人0.2以上对齐、归一化、输入通道以图搜图Top10同语义图片比例≥80%HNSW召回数、特征模型选择以图搜图的索引最好保留version字段。模型微调后特征分布会变旧向量和新向量混在一起会导致相似度不可比上线时给索引加version查询只搜当前版本旧数据用异步任务重新抽取特征。本文还有配套的精品资源点击获取
返回列表