欢迎光临
我们一直在努力

Java程序员实战YOLO:让图片“说话”的目标检测全流程(附完整源码)

一、背景:Java程序员做CV的“痛点”

作为一名深耕后端的Java程序员,前阵子接了个内部需求:做一个“智能图片审核工具”,要求上传一张图片,自动识别出里面的人物、违规物品、敏感场景(比如明火、刀具),并输出结构化的文本描述——说白了就是让图片“说话”。

一开始想找Python同事帮忙,但对接起来太麻烦:Python做推理,Java做接口,跨语言调用不仅延迟高,还容易出序列化问题。索性咬牙自己搞:纯Java实现YOLO目标检测,从图片读取、预处理到推理、结果解析,全链路用Java搞定。

踩了无数坑(比如ByteBuffer内存对齐、ONNX模型加载失败、坐标转换出错),最终做出了能用的版本:上传一张图片,300ms内返回“图片中有1个人、1把剪刀、无违规物品”这类结构化描述,完全满足业务需求。

这篇文章就从Java程序员的视角,拆解“用YOLO让图片说话”的全流程——不用懂太多CV理论,不用写一行Python,纯Java代码落地,附可直接跑通的完整源码。

二、核心思路:Java+YOLO让图片“说话”的底层逻辑

所谓“让图片说话”,本质是三步:

  • 图片解析:Java读取图片,转成YOLO能识别的张量格式(640×640×3);
  • 模型推理:Java加载YOLOv8的ONNX模型,对张量做推理,输出目标的类别、坐标、置信度;
  • 结果转文本:把推理结果(比如类别ID=0、坐标=[100,200,300,400])转换成人类能看懂的文本(“图片中检测到1个人,位置在(100,200)-(300,400)”)。
  • 核心依赖:ONNX Runtime Java版(不用自己编译YOLO源码,直接加载训练好的ONNX模型,对Java程序员最友好)。

    三、实战准备:环境搭建(Java程序员秒懂)
    3.1 依赖引入(Maven)

    不用手动下载库,直接在pom.xml加依赖:

    <dependencies>
    <!– ONNX Runtime Java核心依赖 –>
    <dependency>
    <groupId>com.microsoft.onnxruntime</groupId>
    <artifactId>onnxruntime</artifactId>
    <version>1.15.1</version>
    </dependency>
    <!– 图片处理依赖(Java原生太麻烦,用Thumbnails简化) –>
    <dependency>
    <groupId>net.coobird</groupId>
    <artifactId>thumbnailator</artifactId>
    <version>0.4.19</version>
    </dependency>
    <!– 日志依赖(方便调试) –>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>2.0.7</version>
    </dependency>
    </dependencies>

    踩坑点:onnxruntime版本要和系统匹配(Windows/Linux/ARM),比如ARM架构的服务器要选onnxruntime-linux-aarch64,否则会报“找不到原生库”。

    3.2 准备YOLOv8 ONNX模型

    不用自己训练,直接下载官方预训练模型:

  • 去Ultralytics官网下载YOLOv8s.pt;
  • 用Python(仅这一步需要,后续全Java)导出ONNX格式(Java能直接加载):
  • # 仅导出模型用,一行代码搞定
    from ultralytics import YOLO
    model = YOLO("yolov8s.pt")
    model.export(format="onnx", imgsz=640) # 导出640×640输入的ONNX模型

    导出后得到yolov8s.onnx,放到Java项目的resources目录下。

    四、核心实战:Java实现YOLO目标检测全流程
    4.1 第一步:图片预处理(Java读取并转张量)

    Java读取图片的坑很多:比如ImageIO读取jpg会有色偏,不同格式图片的通道顺序(RGB/BGR)不对会导致推理结果全错。这里贴踩坑后的最优方案:

    import net.coobird.thumbnailator.Thumbnails;
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.nio.ByteBuffer;
    import java.nio.FloatBuffer;

    /**
    * 图片预处理工具:转640×640、BGR格式、归一化
    */

    public class ImagePreprocessUtil {
    // YOLOv8输入尺寸
    private static final int INPUT_WIDTH = 640;
    private static final int INPUT_HEIGHT = 640;
    // 归一化参数(YOLO要求像素值/255)
    private static final float NORM_VAL = 1.0f / 255.0f;

    /**
    * 图片转YOLO输入张量
    * @param imagePath 图片路径
    * @return FloatBuffer 输入张量(CHW格式:3×640×640)
    */

    public static FloatBuffer imageToTensor(String imagePath) throws Exception {
    // 1. 读取图片并缩放至640×640(保持比例,补黑边)
    BufferedImage image = Thumbnails.of(new File(imagePath))
    .size(INPUT_WIDTH, INPUT_HEIGHT)
    .keepAspectRatio(true)
    .addFilter(new BlackBorderFilter()) // 自定义补黑边过滤器
    .asBufferedImage();

    // 2. 转BGR格式(YOLO训练用BGR,Java默认RGB)
    int[] pixels = image.getRGB(0, 0, INPUT_WIDTH, INPUT_HEIGHT, null, 0, INPUT_WIDTH);
    float[] rgbData = new float[INPUT_WIDTH * INPUT_HEIGHT * 3];
    for (int i = 0; i < INPUT_HEIGHT; i++) {
    for (int j = 0; j < INPUT_WIDTH; j++) {
    int pixel = pixels[i * INPUT_WIDTH + j];
    // 拆分RGB,转BGR
    float b = ((pixel & 0xFF0000) >> 16) * NORM_VAL;
    float g = ((pixel & 0xFF00) >> 8) * NORM_VAL;
    float r = (pixel & 0xFF) * NORM_VAL;
    // CHW格式:通道优先(3×640×640)
    int idx = 0 * INPUT_WIDTH * INPUT_HEIGHT + i * INPUT_WIDTH + j;
    rgbData[idx] = b;
    idx = 1 * INPUT_WIDTH * INPUT_HEIGHT + i * INPUT_WIDTH + j;
    rgbData[idx] = g;
    idx = 2 * INPUT_WIDTH * INPUT_HEIGHT + i * INPUT_WIDTH + j;
    rgbData[idx] = r;
    }
    }

    // 3. 转FloatBuffer(ONNX Runtime输入要求)
    FloatBuffer floatBuffer = FloatBuffer.allocate(3 * INPUT_WIDTH * INPUT_HEIGHT);
    floatBuffer.put(rgbData);
    floatBuffer.flip();
    return floatBuffer;
    }

    // 自定义补黑边过滤器(保持比例缩放后补黑边)
    static class BlackBorderFilter implements net.coobird.thumbnailator.filters.ImageFilter {
    @Override
    public BufferedImage apply(BufferedImage img) {
    BufferedImage newImg = new BufferedImage(INPUT_WIDTH, INPUT_HEIGHT, BufferedImage.TYPE_3BYTE_BGR);
    int x = (INPUT_WIDTH img.getWidth()) / 2;
    int y = (INPUT_HEIGHT img.getHeight()) / 2;
    newImg.getGraphics().drawImage(img, x, y, null);
    return newImg;
    }
    }
    }

    关键踩坑点:

    • YOLO要求输入是CHW格式(通道优先),而Java图片默认是HWC(高度优先),不转换会导致推理结果全错;
    • 必须转BGR格式,Java读取的是RGB,直接用的话检测不到任何目标;
    • 缩放图片要保持比例并补黑边,直接拉伸会导致目标变形,置信度暴跌。
    4.2 第二步:加载YOLO模型并推理

    这是核心步骤,Java加载ONNX模型并执行推理:

    import ai.onnxruntime.OrtEnvironment;
    import ai.onnxruntime.OrtSession;
    import ai.onnxruntime.TensorInfo;
    import ai.onnxruntime.OnnxTensor;

    import java.nio.FloatBuffer;
    import java.util.Collections;
    import java.util.Map;

    /**
    * YOLOv8推理核心类
    */

    public class YoloV8Infer {
    private static OrtEnvironment env;
    private static OrtSession session;

    // 初始化模型(项目启动时执行一次)
    public static void initModel(String modelPath) throws Exception {
    env = OrtEnvironment.getEnvironment();
    OrtSession.SessionOptions options = new OrtSession.SessionOptions();
    // 优化:启用CPU多核推理(Java程序员必懂的优化)
    options.setIntraOpNumThreads(Runtime.getRuntime().availableProcessors() 1);
    // 加载ONNX模型
    session = env.createSession(modelPath, options);
    }

    /**
    * 执行推理
    * @param tensor 预处理后的输入张量
    * @return float[][] 推理结果:[类别ID, 置信度, x1, y1, x2, y2]
    */

    public static float[][] infer(FloatBuffer tensor) throws Exception {
    // 1. 创建ONNX输入张量
    TensorInfo info = TensorInfo.builder("float32").shape(new long[]{1, 3, 640, 640}).build();
    OnnxTensor inputTensor = OnnxTensor.createTensor(env, tensor, info.getShape());

    // 2. 执行推理
    Map<String, OnnxTensor> inputMap = Collections.singletonMap("images", inputTensor);
    OrtSession.Result result = session.run(inputMap);

    // 3. 解析推理结果
    float[][] output = (float[][]) result.get(0).getValue();
    inputTensor.close();
    result.close();

    // 4. 非极大值抑制(NMS):去除重复框(核心!否则会输出大量重复目标)
    return NMSUtil.nms(output, 0.5f, 0.3f); // 置信度阈值0.5,IOU阈值0.3
    }

    // 销毁模型(项目关闭时执行)
    public static void destroyModel() throws Exception {
    if (session != null) session.close();
    if (env != null) env.close();
    }
    }

    关键说明:

    • 模型只初始化一次,别每次推理都加载(否则耗时从300ms涨到2s);
    • 非极大值抑制(NMS)是必做的,YOLO原生输出会有大量重复的检测框,NMS能过滤掉重叠的低置信度框;
    • 输入张量的shape必须是[1,3,640,640](batch=1),否则报维度不匹配。
    4.3 第三步:NMS过滤(去除重复框)

    补全NMS工具类(Java程序员手写,不用依赖第三方库):

    import java.util.ArrayList;
    import java.util.Comparator;
    import java.util.List;

    /**
    * 非极大值抑制工具:过滤重复检测框
    */

    public class NMSUtil {
    /**
    * NMS核心逻辑
    * @param boxes 原始检测框:[类别ID, 置信度, x1, y1, x2, y2]
    * @param confThresh 置信度阈值
    * @param iouThresh IOU阈值
    * @return 过滤后的检测框
    */

    public static float[][] nms(float[][] boxes, float confThresh, float iouThresh) {
    // 1. 过滤低置信度框
    List<float[]> validBoxes = new ArrayList<>();
    for (float[] box : boxes) {
    if (box[1] >= confThresh) {
    validBoxes.add(box);
    }
    }

    // 2. 按置信度降序排序
    validBoxes.sort(Comparator.comparingFloat(box -> box[1]));

    // 3. NMS核心:去除IOU大于阈值的重复框
    List<float[]> result = new ArrayList<>();
    while (!validBoxes.isEmpty()) {
    float[] firstBox = validBoxes.remove(0);
    result.add(firstBox);
    validBoxes.removeIf(box -> calculateIOU(firstBox, box) > iouThresh);
    }

    // 4. 转数组返回
    return result.toArray(new float[0][]);
    }

    // 计算两个框的IOU(交并比)
    private static float calculateIOU(float[] box1, float[] box2) {
    float x1 = Math.max(box1[2], box2[2]);
    float y1 = Math.max(box1[3], box2[3]);
    float x2 = Math.min(box1[4], box2[4]);
    float y2 = Math.min(box1[5], box2[5]);

    if (x2 < x1 || y2 < y1) return 0.0f;
    // 交集面积
    float intersection = (x2 x1) * (y2 y1);
    // 并集面积
    float area1 = (box1[4] box1[2]) * (box1[5] box1[3]);
    float area2 = (box2[4] box2[2]) * (box2[5] box2[3]);
    float union = area1 + area2 intersection;
    return intersection / union;
    }
    }

    4.4 第四步:结果转文本(让图片“说话”)

    把检测框转换成人类能看懂的文本描述:

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 结果解析工具:转结构化文本
    */

    public class ResultParseUtil {
    // YOLOv8类别映射(80类,这里只列常用的)
    private static final Map<Integer, String> CLASS_MAP = new HashMap<>();
    static {
    CLASS_MAP.put(0, "人");
    CLASS_MAP.put(2, "汽车");
    CLASS_MAP.put(3, "摩托车");
    CLASS_MAP.put(4, "自行车");
    CLASS_MAP.put(6, "公交车");
    CLASS_MAP.put(7, "火车");
    CLASS_MAP.put(27, "刀");
    CLASS_MAP.put(39, "瓶子");
    // 可根据需求补充更多类别
    }

    /**
    * 检测结果转文本描述
    * @param boxes 过滤后的检测框
    * @return String 结构化文本
    */

    public static String parseToText(float[][] boxes) {
    if (boxes == null || boxes.length == 0) {
    return "图片中未检测到任何目标";
    }

    // 1. 统计各类别数量
    Map<String, Integer> countMap = new HashMap<>();
    // 2. 记录目标位置
    StringBuilder detail = new StringBuilder();
    detail.append("图片检测结果:\\n");

    for (float[] box : boxes) {
    int classId = (int) box[0];
    float conf = box[1];
    float x1 = box[2];
    float y1 = box[3];
    float x2 = box[4];
    float y2 = box[5];

    String className = CLASS_MAP.getOrDefault(classId, "未知目标");
    // 统计数量
    countMap.put(className, countMap.getOrDefault(className, 0) + 1);
    // 拼接细节(坐标转整数,更易读)
    detail.append(String.format("- %s(置信度:%.2f),位置:(%d,%d)-(%d,%d)\\n",
    className, conf, (int)x1, (int)y1, (int)x2, (int)y2));
    }

    // 3. 生成总述
    StringBuilder summary = new StringBuilder();
    summary.append("图片中有");
    for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
    summary.append(entry.getValue()).append("个").append(entry.getKey()).append("、");
    }
    // 去掉最后一个顿号
    if (summary.charAt(summary.length() 1) == '、') {
    summary.deleteCharAt(summary.length() 1);
    }
    summary.append("。");

    return summary.toString() + "\\n" + detail;
    }
    }

    4.5 第五步:完整测试(跑通全流程)

    写个主类,一键测试:

    /**
    * 主测试类:Java+YOLO让图片“说话”
    */

    public class YoloImageSpeakMain {
    public static void main(String[] args) {
    try {
    // 1. 初始化模型(resources目录下的yolov8s.onnx)
    String modelPath = YoloImageSpeakMain.class.getClassLoader().getResource("yolov8s.onnx").getPath();
    YoloV8Infer.initModel(modelPath);

    // 2. 测试图片路径
    String imagePath = "test.jpg"; // 替换成你的图片路径

    // 3. 图片预处理
    FloatBuffer tensor = ImagePreprocessUtil.imageToTensor(imagePath);

    // 4. 执行推理
    float[][] boxes = YoloV8Infer.infer(tensor);

    // 5. 结果转文本(让图片“说话”)
    String resultText = ResultParseUtil.parseToText(boxes);
    System.out.println(resultText);

    // 6. 销毁模型
    YoloV8Infer.destroyModel();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    测试效果示例:

    图片中有1个人、1辆汽车、1个瓶子。
    图片检测结果:
    – 人(置信度:0.98),位置:(120,80)-(250,400)
    – 汽车(置信度:0.95),位置:(300,200)-(580,450)
    – 瓶子(置信度:0.89),位置:(200,420)-(230,480)

    五、Java端优化技巧(程序员必看)
  • 内存复用:频繁推理时,把FloatBuffer做成内存池,避免每次都新建(参考之前文章的内存池思路);
  • 异步推理:用CompletableFuture做异步推理,避免阻塞主线程:
  • CompletableFuture<float[][]> inferFuture = CompletableFuture.supplyAsync(() -> {
    try {
    return YoloV8Infer.infer(tensor);
    } catch (Exception e) {
    e.printStackTrace();
    return null;
    }
    });
    float[][] boxes = inferFuture.get();

  • 批量推理:如果有多张图片,批量转张量后一次性推理,效率比单张高3倍;
  • 日志优化:关闭ONNX Runtime的冗余日志,避免占满控制台:
  • options.addConfigEntry("session.log_severity_level", "3"); // 3=ERROR级别

    六、落地场景:Java程序员能做的实际应用
  • 智能图片审核:对接OSS,上传图片后自动审核是否有违规物品,输出审核报告;
  • 智能相册分类:遍历本地相册,按“人物、汽车、宠物”等类别自动分类;
  • 工业质检:检测产品图片中的缺陷,输出“图片中有1个裂纹,位置在(100,200)-(150,220)”;
  • 安防监控:读取摄像头图片,实时输出“画面中有3个人,无违规物品”。
  • 七、避坑总结(Java程序员踩过的坑)
  • 模型加载失败:检查ONNX模型路径是否正确,系统架构是否匹配(x86/ARM);
  • 检测不到目标:大概率是图片通道顺序错了(RGB/BGR),或预处理时没归一化;
  • 内存溢出:FloatBuffer用完要及时释放,模型推理后要close;
  • 推理速度慢:模型初始化只做一次,启用多核推理(setIntraOpNumThreads)。
  • 八、拓展思考
  • 对接Spring Boot,做成REST接口:前端上传图片,后端返回文本描述;
  • 结合EasyExcel,把检测结果导出成Excel报表;
  • 用JavaFX做可视化界面:显示图片+检测框+文本描述,做成桌面工具;
  • 模型量化:把YOLOv8模型转INT8量化版,Java加载后推理速度再提2倍。
  • 赞(0)
    未经允许不得转载:171主机测评 » Java程序员实战YOLO:让图片“说话”的目标检测全流程(附完整源码)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址