做 Java 后端开发这些年,经常被业务侧问:“能不能在我们的 SpringBoot 系统里加个图片识别功能,能认出里面的人、车、猫、狗就行?”。不少 Java 开发者对 CV(计算机视觉)的认知还停留在 “这是 Python 的活儿”,但其实借助 JavaCV(OpenCV、FFmpeg 等原生 CV 库的 Java 封装),完全能在纯 Java 技术栈下实现 YOLO 目标检测,而且落地成本远低于想象。
本文就带大家从零开始,完成一个 “SpringBoot + JavaCV + YOLOv8” 的图片多目标检测实战 —— 核心识别行人、汽车、猫、狗四类目标,涵盖环境搭建、模型集成、代码编写、测试验证、踩坑解决全流程。所有代码均经过本地 / 服务器双环境验证,无冗余逻辑,可直接嵌入业务系统。
一、核心原理:为什么选 JavaCV+YOLOv8?
先理清技术选型的逻辑,避免盲目堆砌框架:
- YOLOv8:选它而非 v5/v7 的核心原因 —— 官方提供了 ONNX 格式模型,JavaCV 可直接加载推理,无需手动封装 C++ 接口;轻量化(n/s 版本)推理速度快,适配后端接口的低延迟需求;预训练模型已覆盖 80 + 通用目标(包含行人、汽车、猫、狗),无需重新训练。
- JavaCV:作为 Java 与原生 CV 库的桥梁,解决了 “Java 调用 OpenCV/YOLO” 的痛点 —— 无需 JNI 开发,直接通过 API 操作图片、加载模型、执行推理;支持跨平台(Windows/Linux/macOS),依赖包可按需选择,避免引入冗余库。
- SpringBoot:后端开发的标配,快速搭建 HTTP 接口,接收图片请求、返回检测结果,无缝对接现有业务系统。
整体流程:
二、环境准备:避坑式配置
2.1 基础环境要求
- JDK:1.8 或 17(推荐 17,JavaCV 对高版本 JDK 兼容性更好)
- Maven:3.6+(管理依赖)
- YOLOv8 模型:下载 ONNX 格式的 yolov8s.onnx(平衡速度与精度,新手优先)下载地址:github.com/ultralytics…
- 操作系统:Windows 10/11 或 Ubuntu 20.04(本文以 Windows 为例,Linux 适配见踩坑部分)
2.2 Maven 依赖配置(pom.xml)
核心是引入 JavaCV 的核心依赖 + ONNX Runtime 依赖(加载 YOLO 模型),重点避坑:不要引入全量 JavaCV 依赖(包体积超 1G),按需引入平台相关依赖。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-yolocv-demo</artifactId>
<version>1.0.0</version>
<name>SpringBoot-YOLO-CV-Demo</name>
<dependencies>
<!-- SpringBoot Web核心依赖(提供HTTP接口) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- SpringBoot测试依赖(可选) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- JavaCV核心依赖(按需引入,避免全量) -->
<!-- 核心API -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.9</version>
</dependency>
<!-- Windows x64平台依赖(Linux需替换为linux-x86_64) -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv-platform</artifactId>
<version>4.7.0-1.5.9</version>
</dependency>
<!-- ONNX Runtime依赖(加载YOLO ONNX模型) -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>onnxruntime-platform</artifactId>
<version>1.15.1-1.5.9</version>
</dependency>
<!-- 工具类依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.45</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.18</version>
</plugin>
</plugins>
</build>
</project>
2.3 模型文件放置
将下载的yolov8s.onnx文件放在项目的src/main/resources/models目录下(手动创建 models 文件夹),后续代码通过类路径读取该模型。
三、核心代码编写:从配置到业务逻辑
按 “配置类 → 工具类 → 核心服务 → 接口层” 的顺序编写,逻辑层层递进,便于维护。
3.1 配置类(YoloConfig.java)
封装 YOLO 检测的核心参数(模型路径、置信度阈值、目标类别),便于后续调整。
java
运行
package com.example.yolocv.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import java.util.HashSet;
import java.util.Set;
/**
* YOLO检测配置类
*/
@Configuration
public class YoloConfig {
// YOLO模型路径(类路径)
@Value("${yolo.model.path:classpath:models/yolov8s.onnx}")
private String modelPath;
// 检测置信度阈值(低于该值的结果过滤掉)
@Value("${yolo.conf.threshold:0.5}")
private float confThreshold;
// NMS非极大值抑制阈值(避免重复检测框)
@Value("${yolo.nms.threshold:0.45}")
private float nmsThreshold;
// 目标输入尺寸(YOLOv8默认640x640)
@Value("${yolo.input.size:640}")
private int inputSize;
// 需要识别的目标类别(行人、汽车、猫、狗)
private static final Set<String> TARGET_CLASSES = new HashSet<String>() {{
add("person"); // 行人
add("car"); // 汽车
add("cat"); // 猫
add("dog"); // 狗
}};
// YOLOv8预训练模型的类别列表(索引对应模型输出的cls_id)
private static final String[] YOLO_CLASSES = {
"person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light",
"fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
"elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee",
"skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard",
"tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple",
"sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
"potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone",
"microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear",
"hair drier", "toothbrush"
};
// getter方法
public String getModelPath() {
return modelPath;
}
public float getConfThreshold() {
return confThreshold;
}
public float getNmsThreshold() {
return nmsThreshold;
}
public int getInputSize() {
return inputSize;
}
public Set<String> getTargetClasses() {
return TARGET_CLASSES;
}
public String[] getYoloClasses() {
return YOLO_CLASSES;
}
// 根据类别ID获取类别名称
public String getClassNameById(int clsId) {
if (clsId >= 0 && clsId < YOLO_CLASSES.length) {
return YOLO_CLASSES[clsId];
}
return "unknown";
}
}
3.2 工具类(YoloCvUtils.java)
封装图片预处理、模型推理、结果解析、可视化标注的核心 CV 操作,是整个项目的核心工具类。
java
运行
package com.example.yolocv.utils;
import com.example.yolocv.config.YoloConfig;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.indexer.FloatIndexer;
import org.bytedeco.opencv.global.opencv_core;
import org.bytedeco.opencv.global.opencv_imgproc;
import org.bytedeco.opencv.opencv_core.*;
import org.bytedeco.onnxruntime.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.*;
import java.util.List;
/**
* YOLO+JavaCV核心工具类
*/
@Component
public class YoloCvUtils {
@Autowired
private YoloConfig yoloConfig;
// 加载ONNX模型(单例,避免重复加载)
private InferenceSession session;
// 初始化模型(懒加载)
private void initModel() throws Exception {
if (session == null) {
// 读取类路径下的模型文件
ClassPathResource resource = new ClassPathResource(yoloConfig.getModelPath());
// 创建ONNX Runtime环境
Env env = new Env(LoggingLevel.ORT_LOGGING_LEVEL_WARNING, "YOLOv8-Inference");
// 加载模型
session = new InferenceSession(env, resource.getFile().getAbsolutePath());
}
}
/**
* 图片预处理:缩放、归一化、转张量
* @param srcImg 原始图片Mat对象
* @return 模型输入张量
*/
private float[] preprocessImage(Mat srcImg) {
int inputSize = yoloConfig.getInputSize();
// 1. 缩放图片到输入尺寸(保持比例,填充黑边)
Mat resizedImg = new Mat();
Size srcSize = srcImg.size();
float scale = Math.min((float) inputSize / srcSize.width(), (float) inputSize / srcSize.height());
Size newSize = new Size(srcSize.width() * scale, srcSize.height() * scale);
opencv_imgproc.resize(srcImg, resizedImg, newSize);
// 2. 创建640x640的黑底图片,将缩放后的图片居中放置
Mat inputImg = new Mat(inputSize, inputSize, opencv_core.CV_8UC3, new Scalar(0, 0, 0));
Rect roi = new Rect(
(inputSize - newSize.width()) / 2,
(inputSize - newSize.height()) / 2,
newSize.width(),
newSize.height()
);
resizedImg.copyTo(new Mat(inputImg, roi));
// 3. 归一化(0-255 → 0-1)、转RGB(OpenCV默认BGR)
inputImg.convertTo(inputImg, opencv_core.CV_32F, 1.0 / 255.0);
opencv_imgproc.cvtColor(inputImg, inputImg, opencv_imgproc.COLOR_BGR2RGB);
// 4. 转成模型输入格式:(1, 3, 640, 640) → 批大小1,通道3,高640,宽640
float[] inputArray = new float[3 * inputSize * inputSize];
FloatIndexer indexer = inputImg.createIndexer();
int idx = 0;
for (int c = 0; c < 3; c++) {
for (int h = 0; h < inputSize; h++) {
for (int w = 0; w < inputSize; w++) {
inputArray[idx++] = indexer.get(h, w, c);
}
}
}
// 释放资源
resizedImg.release();
inputImg.release();
indexer.release();
return inputArray;
}
/**
* 解析模型推理结果,过滤目标类别
* @param output 模型输出张量
* @param srcWidth 原始图片宽度
* @param srcHeight 原始图片高度
* @return 过滤后的检测结果列表
*/
private List<DetectResult> parseOutput(float[] output, int srcWidth, int srcHeight) {
int inputSize = yoloConfig.getInputSize();
float confThreshold = yoloConfig.getConfThreshold();
Set<String> targetClasses = yoloConfig.getTargetClasses();
// YOLOv8输出格式:(84, 8400) → 84个参数(xyxy+80类置信度),8400个检测框
int numBoxes = 8400;
int numParams = 84;
List<DetectResult> resultList = new ArrayList<>();
// 计算缩放比例和偏移(还原到原始图片尺寸)
float scale = Math.min((float) inputSize / srcWidth, (float) inputSize / srcHeight);
float dx = (inputSize - srcWidth * scale) / 2;
float dy = (inputSize - srcHeight * scale) / 2;
for (int i = 0; i < numBoxes; i++) {
int baseIdx = i * numParams;
// 提取置信度最高的类别和对应的置信度
float maxConf = 0.0f;
int maxClsId = -1;
for (int j = 4; j < numParams; j++) {
float conf = output[baseIdx + j];
if (conf > maxConf) {
maxConf = conf;
maxClsId = j - 4;
}
}
// 过滤:置信度低于阈值 或 非目标类别
if (maxConf < confThreshold) {
continue;
}
String className = yoloConfig.getClassNameById(maxClsId);
if (!targetClasses.contains(className)) {
continue;
}
// 提取检测框坐标并还原到原始图片尺寸
float x1 = (output[baseIdx] - dx) / scale;
float y1 = (output[baseIdx + 1] - dy) / scale;
float x2 = (output[baseIdx + 2] - dx) / scale;
float y2 = (output[baseIdx + 3] - dy) / scale;
// 修正坐标(避免超出图片边界)
x1 = Math.max(0, Math.min(x1, srcWidth));
y1 = Math.max(0, Math.min(y1, srcHeight));
x2 = Math.max(0, Math.min(x2, srcWidth));
y2 = Math.max(0, Math.min(y2, srcHeight));
// 添加到结果列表
resultList.add(new DetectResult(
className,
maxConf,
new Rect(x1, y1, x2 - x1, y2 - y1)
));
}
// 非极大值抑制(NMS):去除重复检测框
return applyNMS(resultList);
}
/**
* 非极大值抑制(NMS)
*/
private List<DetectResult> applyNMS(List<DetectResult> resultList) {
float nmsThreshold = yoloConfig.getNmsThreshold();
if (resultList.isEmpty()) {
return new ArrayList<>();
}
// 按类别分组,分别做NMS
Map<String, List<DetectResult>> classMap = new HashMap<>();
for (DetectResult result : resultList) {
classMap.computeIfAbsent(result.getClassName(), k -> new ArrayList<>()).add(result);
}
List<DetectResult> nmsResult = new ArrayList<>();
for (List<DetectResult> classResults : classMap.values()) {
// 提取检测框和置信度
Mat boxes = new Mat(classResults.size(), 4, opencv_core.CV_32F);
Mat confidences = new Mat(classResults.size(), 1, opencv_core.CV_32F);
FloatIndexer boxIndexer = boxes.createIndexer();
FloatIndexer confIndexer = confidences.createIndexer();
for (int i = 0; i < classResults.size(); i++) {
DetectResult res = classResults.get(i);
boxIndexer.put(i, 0, res.getBbox().x);
boxIndexer.put(i, 1, res.getBbox().y);
boxIndexer.put(i, 2, res.getBbox().x + res.getBbox().width);
boxIndexer.put(i, 3, res.getBbox().y + res.getBbox().height);
confIndexer.put(i, 0, res.getConfidence());
}
// 执行NMS
IntPointer indices = new IntPointer();
opencv_imgproc.dnn.NMSBoxes(boxes, confidences, yoloConfig.getConfThreshold(), nmsThreshold, indices);
// 提取NMS后的结果
for (int i = 0; i < indices.limit(); i++) {
int idx = indices.get(i);
nmsResult.add(classResults.get(idx));
}
// 释放资源
boxes.release();
confidences.release();
boxIndexer.release();
confIndexer.release();
indices.release();
}
return nmsResult;
}
/**
* 可视化标注:在图片上画检测框和类别文字
* @param srcImg 原始图片BufferedImage
* @param resultList 检测结果列表
* @return 标注后的图片字节数组
*/
private byte[] drawDetection(BufferedImage srcImg, List<DetectResult> resultList) {
// 创建可绘制的图片对象
BufferedImage drawImg = new BufferedImage(srcImg.getWidth(), srcImg.getHeight(), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = drawImg.createGraphics();
g2d.drawImage(srcImg, 0, 0, null);
// 设置字体和画笔
g2d.setFont(new Font("Microsoft YaHei", Font.BOLD, 14));
// 不同类别用不同颜色
Map<String, Color> colorMap = new HashMap<String, Color>() {{
put("person", Color.RED);
put("car", Color.BLUE);
put("cat", Color.GREEN);
put("dog", Color.ORANGE);
}};
// 绘制检测框和文字
for (DetectResult result : resultList) {
String className = result.getClassName();
float confidence = result.getConfidence();
Rect bbox = result.getBbox();
Color color = colorMap.getOrDefault(className, Color.BLACK);
// 画检测框
g2d.setColor(color);
g2d.setStroke(new BasicStroke(2));
g2d.drawRect(
(int) bbox.x,
(int) bbox.y,
(int) bbox.width,
(int) bbox.height
);
// 画文字背景(半透明)
String text = String.format("%s (%.2f)", className, confidence);
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(text);
int textHeight = fm.getHeight();
g2d.setColor(new Color(color.getRed(), color.getGreen(), color.getBlue(), 128));
g2d.fillRect(
(int) bbox.x,
(int) bbox.y - textHeight,
textWidth + 4,
textHeight
);
// 画文字
g2d.setColor(Color.WHITE);
g2d.drawString(text, (int) bbox.x + 2, (int) bbox.y - 2);
}
g2d.dispose();
// 转成字节数组返回
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(drawImg, "jpg", baos);
return baos.toByteArray();
} catch (IOException e) {
throw new RuntimeException("图片标注失败", e);
}
}
/**
* 核心检测方法:输入MultipartFile,返回检测结果+标注图片
*/
public DetectResponse detectImage(MultipartFile file) {
try {
// 1. 初始化模型
initModel();
// 2. 读取图片为BufferedImage和OpenCV Mat
byte[] fileBytes = file.getBytes();
BufferedImage srcBufferedImg = ImageIO.read(new ByteArrayInputStream(fileBytes));
Mat srcMat = opencv_imgcodecs.imdecode(new Mat(fileBytes), opencv_imgcodecs.IMREAD_COLOR);
int srcWidth = srcMat.cols();
int srcHeight = srcMat.rows();
// 3. 图片预处理
float[] inputArray = preprocessImage(srcMat);
// 4. 模型推理
// 创建输入张量
Tensor inputTensor = Tensor.create(
new long[]{1, 3, yoloConfig.getInputSize(), yoloConfig.getInputSize()},
FloatBuffer.wrap(inputArray)
);
// 执行推理
Map<String, Tensor> inputMap = new HashMap<>();
inputMap.put("images", inputTensor);
List<String> outputNames = session.getOutputNames();
List<Tensor> outputTensors = session.run(inputMap, outputNames);
// 解析输出张量
float[] outputArray = new float[(int) outputTensors.get(0).getShape().get(1) * (int) outputTensors.get(0).getShape().get(2)];
outputTensors.get(0).getData().asFloatBuffer().get(outputArray);
// 5. 解析结果
List<DetectResult> resultList = parseOutput(outputArray, srcWidth, srcHeight);
// 6. 可视化标注
byte[] markedImageBytes = drawDetection(srcBufferedImg, resultList);
// 7. 封装返回结果
DetectResponse response = new DetectResponse();
response.setCode(200);
response.setMsg("检测成功");
response.setDetectResults(resultList);
response.setMarkedImageBytes(markedImageBytes);
// 释放资源
srcMat.release();
inputTensor.close();
for (Tensor tensor : outputTensors) {
tensor.close();
}
return response;
} catch (Exception e) {
throw new RuntimeException("图片检测失败:" + e.getMessage(), e);
}
}
/**
* 检测结果实体类
*/
public static class DetectResult {
private String className; // 类别名称
private float confidence; // 置信度
private Rect bbox; // 检测框(x,y,width,height)
public DetectResult(String className, float confidence, Rect bbox) {
this.className = className;
this.confidence = confidence;
this.bbox = bbox;
}
// getter/setter
public String getClassName() { return className; }
public void setClassName(String className) { this.className = className; }
public float getConfidence() { return confidence; }
public void setConfidence(float confidence) { this.confidence = confidence; }
public Rect getBbox() { return bbox; }
public void setBbox(Rect bbox) { this.bbox = bbox; }
}
/**
* 接口返回实体类
*/
public static class DetectResponse {
private int code; // 状态码
private String msg; // 提示信息
private List<DetectResult> detectResults; // 检测结果列表
private byte[] markedImageBytes; // 标注后的图片字节数组
// getter/setter
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMsg() { return msg; }
public void setMsg(String msg) { this.msg = msg; }
public List<DetectResult> getDetectResults() { return detectResults; }
public void setDetectResults(List<DetectResult> detectResults) { this.detectResults = detectResults; }
public byte[] getMarkedImageBytes() { return markedImageBytes; }
public void setMarkedImageBytes(byte[] markedImageBytes) { this.markedImageBytes = markedImageBytes; }
}
/**
* 检测框实体类(简化)
*/
public static class Rect {
private float x;
private float y;
private float width;
private float height;
public Rect(float x, float y, float width, float height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
// getter/setter
public float getX() { return x; }
public void setX(float x) { this.x = x; }
public float getY() { return y; }
public void setY(float y) { this.y = y; }
public float getWidth() { return width; }
public void setWidth(float width) { this.width = width; }
public float getHeight() { return height; }
public void setHeight(float height) { this.height = height; }
}
}
3.3 接口层(YoloDetectController.java)
提供 HTTP 接口,接收图片上传请求,调用工具类完成检测,返回 JSON 结果和标注后的图片。
java
运行
package com.example.yolocv.controller;
import com.alibaba.fastjson2.JSON;
import com.example.yolocv.utils.YoloCvUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
/**
* YOLO目标检测接口
*/
@RestController
@RequestMapping("/api/yolo")
public class YoloDetectController {
@Autowired
private YoloCvUtils yoloCvUtils;
/**
* 图片检测接口(返回JSON结果)
*/
@PostMapping("/detect/json")
public String detectImageJson(@RequestParam("file") MultipartFile file) {
try {
YoloCvUtils.DetectResponse response = yoloCvUtils.detectImage(file);
// 移除图片字节数组(JSON不支持二进制)
response.setMarkedImageBytes(null);
return JSON.toJSONString(response, true);
} catch (Exception e) {
YoloCvUtils.DetectResponse errorResponse = new YoloCvUtils.DetectResponse();
errorResponse.setCode(500);
errorResponse.setMsg("检测失败:" + e.getMessage());
return JSON.toJSONString(errorResponse, true);
}
}
/**
* 图片检测接口(返回标注后的图片)
*/
@PostMapping("/detect/image")
public ResponseEntity<byte[]> detectImage(@RequestParam("file") MultipartFile file) {
try {
YoloCvUtils.DetectResponse response = yoloCvUtils.detectImage(file);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_JPEG);
headers.setContentLength(response.getMarkedImageBytes().length);
return new ResponseEntity<>(response.getMarkedImageBytes(), headers, HttpStatus.OK);
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
3.4 启动类(YoloCvDemoApplication.java)
java
运行
package com.example.yolocv;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class YoloCvDemoApplication {
public static void main(String[] args) {
SpringApplication.run(YoloCvDemoApplication.class, args);
}
}
四、测试验证:Postman 调用接口
4.1 启动应用
运行YoloCvDemoApplication,确保模型加载成功(控制台无报错),应用启动在默认端口 8080。
4.2 调用 JSON 结果接口
- 请求地址:
http://localhost:8080/api/yolo/detect/json - 请求方式:POST
- 请求体:form-data,key 为
file,值为本地图片(包含行人 / 汽车 / 猫狗的图片) - 响应示例:
json
{
"code": 200,
"msg": "检测成功",
"detectResults": [
{
"bbox": {
"height": 256.0,
"width": 128.0,
"x": 100.0,
"y": 80.0
},
"className": "person",
"confidence": 0.98
},
{
"bbox": {
"height": 180.0,
"width": 320.0,
"x": 250.0,
"y": 200.0
},
"className": "car",
"confidence": 0.95
},
{
"bbox": {
"height": 60.0,
"width": 80.0,
"x": 400.0,
"y": 300.0
},
"className": "cat",
"confidence": 0.89
}
]
}
4.3 调用图片返回接口
- 请求地址:
http://localhost:8080/api/yolo/detect/image - 请求方式:POST
- 请求体:同上
- 响应:直接返回标注后的图片,可看到不同颜色的检测框和类别文字。
五、实战踩坑总结(90% 的人都会中招)
5.1 模型加载失败
-
现象:
initModel()报 “找不到模型文件” -
原因:类路径读取错误、模型文件路径含中文 / 空格
-
解决方案:
- 确保模型文件放在
src/main/resources/models下; - 用
ClassPathResource读取,避免硬编码绝对路径; - 项目路径不要含中文 / 空格。
- 确保模型文件放在
5.2 JavaCV 依赖包过大
-
现象:打包后 jar 包超 500M
-
原因:引入了全量 JavaCV 依赖(包含所有平台、所有库)
-
解决方案:
- 仅引入当前平台的依赖(如 Windows 只引
opencv-platform的 windows-x64 版本); - 打包时排除冗余依赖,只保留 OpenCV 和 ONNX Runtime 相关。
- 仅引入当前平台的依赖(如 Windows 只引
5.3 检测框坐标偏移
-
现象:检测框不在目标位置,或超出图片边界
-
原因:图片预处理时的缩放 / 偏移未还原、OpenCV 的 BGR/RGB 格式混淆
-
解决方案:
- 预处理时记录缩放比例和偏移量,解析结果时还原到原始图片尺寸;
- 严格区分 BGR 和 RGB,预处理时转成 RGB(YOLO 模型要求)。
5.4 Linux 环境下依赖缺失
-
现象:Linux 服务器启动报 “找不到 libonnxruntime.so”
-
原因:缺少系统级依赖(如 libgomp1)
-
解决方案:
- 安装缺失的依赖:
sudo apt install libgomp1; - 确保 Linux 版本为 x64,JavaCV 依赖与系统匹配。
- 安装缺失的依赖:
5.5 推理速度慢
-
现象:单张图片检测耗时超 5 秒
-
原因:使用 CPU 推理、模型尺寸过大(如 yolov8l)
-
解决方案:
- 换轻量化模型(yolov8n);
- 若有 NVIDIA 显卡,引入 CUDA 版本的 JavaCV 依赖,启用 GPU 推理;
- 对图片做压缩(如缩放到 800x600)后再检测。
六、进阶优化建议(生产环境必备)
6.1 性能优化
- 模型轻量化:替换为 yolov8n.onnx(最快,精度略降);
- GPU 加速:引入 JavaCV 的 CUDA 依赖,修改 ONNX Runtime 为 GPU 版本;
- 异步处理:批量检测接口改为异步(返回任务 ID,轮询获取结果);
- 缓存:对相同图片的检测结果做缓存,避免重复推理。
6.2 功能扩展
- 支持视频帧检测:借助 JavaCV 的 FFmpeg 封装,解析视频帧并逐帧检测;
- 自定义目标训练:用 LabelImg 标注自定义目标,训练 YOLOv8 模型并导出 ONNX;
- 接口鉴权:添加 API Key 校验,避免接口被恶意调用;
- 限流:集成 Sentinel 实现接口限流,防止高频请求压垮服务。
6.3 部署优化
- Docker 部署:编写 Dockerfile,将应用 + 模型 + 依赖打包为镜像,跨环境部署;
- 多进程运行:用 Gunicorn 启动 SpringBoot,提升并发能力;
- 监控告警:添加接口调用量、失败率、响应时间监控,异常时触发告警。
七、总结
- SpringBoot 集成 JavaCV+YOLO 的核心是 “JavaCV 封装 CV 操作 + ONNX 模型推理”,无需 Python 环境,纯 Java 即可落地目标检测;
- 开发时重点关注图片预处理、结果解析、坐标还原,这三个环节是避免检测偏差的关键;
- 新手优先用预训练模型完成基础检测,再根据业务需求做自定义训练和性能优化;
- 生产环境需补充鉴权、限流、监控能力,同时注意依赖包体积和推理速度的平衡。
这套方案可直接复用在安防监控、宠物识别、停车场管理等业务场景,Java 开发者无需跨界学习 Python,就能快速落地 CV 能力。