mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-12 20:58:51 +00:00
新增目标检测功能
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
package cn.smartjavaai.objectdetection;
|
||||
|
||||
/**
|
||||
* @author dwj
|
||||
* @date 2025/4/7
|
||||
*/
|
||||
public class DetectorConfig {
|
||||
|
||||
/**
|
||||
* 置信度阈值
|
||||
*/
|
||||
public static final float DEFAULT_THRESHOLD = 0.5F;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.smartjavaai.objectdetection;
|
||||
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 目标检测模型参数配置
|
||||
*
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
@Data
|
||||
public class DetectorModelConfig {
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private DetectorModelEnum modelEnum;
|
||||
|
||||
/**
|
||||
* 置信度阈值
|
||||
*/
|
||||
private float threshold = DetectorConfig.DEFAULT_THRESHOLD;
|
||||
|
||||
/**
|
||||
* 设备类型
|
||||
*/
|
||||
private DeviceEnum device;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.smartjavaai.objectdetection;
|
||||
|
||||
/**
|
||||
* 目标检测模型枚举
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public enum DetectorModelEnum {
|
||||
|
||||
// resnet50 系列
|
||||
SSD_300_RESNET50("ai.djl.pytorch/ssd/0.0.1/ssd_300_resnet50"),
|
||||
SSD_512_RESNET50_V1_VOC("ai.djl.mxnet/ssd/0.0.1/ssd_512_resnet50_v1_voc"),
|
||||
|
||||
// vgg16 系列
|
||||
SSD_512_VGG16_ATROUS_COCO("ai.djl.mxnet/ssd/0.0.1/ssd_512_vgg16_atrous_coco"),
|
||||
SSD_300_VGG16_ATROUS_VOC("ai.djl.mxnet/ssd/0.0.1/ssd_300_vgg16_atrous_voc"),
|
||||
|
||||
// mobilenet 系列
|
||||
SSD_512_MOBILENET1_VOC("ai.djl.mxnet/ssd/0.0.1/ssd_512_mobilenet1.0_voc"),
|
||||
|
||||
// YOLO 系列
|
||||
YOLOV8N("ai.djl.pytorch/yolov8n/0.0.1/yolov8n"),
|
||||
YOLO11N("ai.djl.pytorch/yolo11n/0.0.1/yolo11n"),
|
||||
YOLOV5S("ai.djl.pytorch/yolo5s/0.0.1/yolov5s"),
|
||||
YOLOV5S_ONNXRUNTIME("ai.djl.onnxruntime/yolo5s/0.0.1/yolo5s"),
|
||||
YOLO("ai.djl.mxnet/yolo/0.0.1/yolo"),
|
||||
|
||||
// YOLOv3 变体
|
||||
YOLO3_DARKNET_VOC_416("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_voc_416"),
|
||||
YOLO3_MOBILENET_VOC_320("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_voc_320"),
|
||||
YOLO3_MOBILENET_VOC_416("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_voc_41"),
|
||||
YOLO3_DARKNET_COCO_320("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_320"),
|
||||
YOLO3_DARKNET_COCO_416("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_416"),
|
||||
YOLO3_DARKNET_COCO_608("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_608"),
|
||||
YOLO3_MOBILENET_COCO_320("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_320"),
|
||||
YOLO3_MOBILENET_COCO_416("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_416"),
|
||||
YOLO3_MOBILENET_COCO_608("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_608");
|
||||
|
||||
/**
|
||||
* 根据名称获取枚举 (忽略大小写和下划线变体)
|
||||
*/
|
||||
public static DetectorModelEnum fromName(String name) {
|
||||
String formatted = name.trim().toUpperCase().replaceAll("[-_]", "");
|
||||
for (DetectorModelEnum model : values()) {
|
||||
if (model.name().replaceAll("_", "").equals(formatted)) {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未知模型名称: " + name);
|
||||
}
|
||||
|
||||
private final String modelUri;
|
||||
|
||||
DetectorModelEnum(String modelUri) {
|
||||
this.modelUri = modelUri;
|
||||
}
|
||||
|
||||
public String getModelUri() {
|
||||
return modelUri;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package cn.smartjavaai.objectdetection.exception;
|
||||
|
||||
/**
|
||||
* 目标检测异常
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
public class DetectionException extends RuntimeException{
|
||||
|
||||
public DetectionException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public DetectionException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
|
||||
public DetectionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public DetectionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DetectionException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package cn.smartjavaai.objectdetection.model;
|
||||
|
||||
import ai.djl.Application;
|
||||
import ai.djl.Device;
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.training.util.ProgressBar;
|
||||
import ai.djl.translate.TranslateException;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.enums.DeviceEnum;
|
||||
import cn.smartjavaai.common.pool.ModelPredictorPoolManager;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.objectdetection.DetectorConfig;
|
||||
import cn.smartjavaai.objectdetection.DetectorModelConfig;
|
||||
import cn.smartjavaai.objectdetection.exception.DetectionException;
|
||||
import cn.smartjavaai.objectdetection.utils.DetectorUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.pool2.ObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 目标检测模型
|
||||
* @author dwj
|
||||
* @date 2025/4/4
|
||||
*/
|
||||
@Slf4j
|
||||
public class DetectorModel implements AutoCloseable{
|
||||
|
||||
private ZooModel<Image, DetectedObjects> model;
|
||||
|
||||
//private Predictor<Image, DetectedObjects> predictor;
|
||||
|
||||
private static final String DJL_MODEL_PREFIX = "djl://";
|
||||
|
||||
private ObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
|
||||
|
||||
public void loadModel(DetectorModelConfig config){
|
||||
Device device = null;
|
||||
if(!Objects.isNull(config.getDevice())){
|
||||
device = config.getDevice() == DeviceEnum.CPU ? Device.cpu() : Device.gpu();
|
||||
}
|
||||
Criteria<Image, DetectedObjects> criteria = Criteria.builder()
|
||||
.optApplication(Application.CV.OBJECT_DETECTION)
|
||||
.setTypes(Image.class, DetectedObjects.class)
|
||||
.optArgument("threshold", config.getThreshold() > 0 ? config.getThreshold() : DetectorConfig.DEFAULT_THRESHOLD)
|
||||
.optModelUrls(DJL_MODEL_PREFIX + config.getModelEnum().getModelUri())
|
||||
.optDevice(device)
|
||||
.optProgress(new ProgressBar())
|
||||
.build();
|
||||
try {
|
||||
model = criteria.loadModel();
|
||||
// 创建池子:每个线程独享 Predictor
|
||||
this.predictorPool = new GenericObjectPool<>(new PredictorFactory<>(model));
|
||||
log.info("当前设备: " + model.getNDManager().getDevice());
|
||||
} catch (IOException | ModelNotFoundException | MalformedModelException e) {
|
||||
throw new DetectionException("模型加载失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标检测
|
||||
* @param imagePath
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
public DetectionResponse detect(String imagePath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new DetectionException("图像文件不存在");
|
||||
}
|
||||
Image image = null;
|
||||
try {
|
||||
image = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
} catch (IOException e) {
|
||||
throw new DetectionException("图片转换错误", e);
|
||||
}
|
||||
DetectedObjects detectedObjects = detect(image);
|
||||
return DetectorUtils.convertToDetectionResponse(detectedObjects, image);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 目标检测-将检测结果绘制到原图
|
||||
* @param imagePath
|
||||
* @return
|
||||
*/
|
||||
public void detectAndDraw(String imagePath, String outputPath){
|
||||
if(!FileUtils.isFileExists(imagePath)){
|
||||
throw new DetectionException("图像文件不存在");
|
||||
}
|
||||
try {
|
||||
Image img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectedObjects detectedObjects = detect(img);
|
||||
img.drawBoundingBoxes(detectedObjects);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
img.save(new FileOutputStream(Paths.get(outputPath).toAbsolutePath().toString()), "png");
|
||||
} catch (IOException e) {
|
||||
throw new DetectionException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标检测
|
||||
* @param imageData
|
||||
* @return
|
||||
*/
|
||||
public DetectionResponse detect(byte[] imageData){
|
||||
if(Objects.isNull(imageData)){
|
||||
throw new DetectionException("图像无效");
|
||||
}
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(new ByteArrayInputStream(imageData));
|
||||
return detect(image);
|
||||
} catch (IOException e) {
|
||||
throw new DetectionException("错误的图像", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 目标检测
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
public DetectionResponse detect(BufferedImage image){
|
||||
if(!ImageUtils.isImageValid(image)){
|
||||
throw new DetectionException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(image);
|
||||
DetectedObjects detectedObjects = detect(img);
|
||||
return DetectorUtils.convertToDetectionResponse(detectedObjects, img);
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标检测-将检测结果绘制到原图
|
||||
* @param sourceImage
|
||||
* @return
|
||||
*/
|
||||
public BufferedImage detectAndDraw(BufferedImage sourceImage){
|
||||
if(!ImageUtils.isImageValid(sourceImage)){
|
||||
throw new DetectionException("图像无效");
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(sourceImage);
|
||||
DetectedObjects detectedObjects = detect(img);
|
||||
img.drawBoundingBoxes(detectedObjects);
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
img.save(outputStream, "png");
|
||||
// 将字节流转换为 BufferedImage
|
||||
byte[] imageBytes = outputStream.toByteArray();
|
||||
return ImageIO.read(new ByteArrayInputStream(imageBytes));
|
||||
} catch (IOException e) {
|
||||
throw new DetectionException("导出图片失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 目标检测
|
||||
* @param image
|
||||
* @return
|
||||
*/
|
||||
private DetectedObjects detect(Image image){
|
||||
Predictor<Image, DetectedObjects> predictor = null;
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
return predictor.predict(image);
|
||||
} catch (Exception e) {
|
||||
throw new DetectionException("目标检测错误", e);
|
||||
}finally {
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
log.info("释放资源");
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显式释放资源(必须调用!)
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
if (predictorPool != null) {
|
||||
predictorPool.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package cn.smartjavaai.objectdetection.model;
|
||||
|
||||
import cn.smartjavaai.objectdetection.DetectorModelConfig;
|
||||
import cn.smartjavaai.objectdetection.DetectorModelEnum;
|
||||
import cn.smartjavaai.objectdetection.exception.DetectionException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 目标检测 模型工厂
|
||||
* @author dwj
|
||||
*/
|
||||
@Slf4j
|
||||
public class ObjectDetectionModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile ObjectDetectionModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<String, DetectorModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
// 私有构造函数,防止外部创建实例
|
||||
private ObjectDetectionModelFactory() {}
|
||||
|
||||
// 双重检查锁定的单例方法
|
||||
public static ObjectDetectionModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (ObjectDetectionModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new ObjectDetectionModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public DetectorModel getModel(DetectorModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new DetectionException("未配置模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum().name(), k -> {
|
||||
DetectorModel model = new DetectorModel();
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认模型
|
||||
* @return
|
||||
*/
|
||||
public DetectorModel getModel() {
|
||||
// 初始化默认配置
|
||||
DetectorModelConfig config = new DetectorModelConfig();
|
||||
config.setModelEnum(DetectorModelEnum.YOLO11N);
|
||||
return getModel(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有已加载的模型
|
||||
*/
|
||||
public void closeAll() {
|
||||
modelMap.values().forEach(DetectorModel::close);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package cn.smartjavaai.objectdetection.utils;
|
||||
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.output.BoundingBox;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import cn.smartjavaai.common.entity.DetectionRectangle;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 目标检测相关工具类
|
||||
* @author dwj
|
||||
* @date 2025/4/9
|
||||
*/
|
||||
public class DetectorUtils {
|
||||
|
||||
|
||||
/**
|
||||
* 转换为FaceDetectedResult
|
||||
* @param detection
|
||||
* @param img
|
||||
* @return
|
||||
*/
|
||||
public static DetectionResponse convertToDetectionResponse(DetectedObjects detection, Image img){
|
||||
if(Objects.isNull(detection) || Objects.isNull(detection.getProbabilities())
|
||||
|| detection.getProbabilities().isEmpty() || Objects.isNull(detection.items()) || detection.items().isEmpty()){
|
||||
return null;
|
||||
}
|
||||
DetectionResponse detectionResponse = new DetectionResponse();
|
||||
List<DetectionRectangle> rectangleList = new ArrayList<DetectionRectangle>();
|
||||
List<DetectedObjects.DetectedObject> detectedObjectList = detection.items();
|
||||
Iterator iterator = detectedObjectList.iterator();
|
||||
int index = 0;
|
||||
while(iterator.hasNext()) {
|
||||
DetectedObjects.DetectedObject result = (DetectedObjects.DetectedObject)iterator.next();
|
||||
String className = result.getClassName();
|
||||
BoundingBox box = result.getBoundingBox();
|
||||
int x = (int)(box.getBounds().getX() * (double)img.getWidth());
|
||||
int y = (int)(box.getBounds().getY() * (double)img.getHeight());
|
||||
int width = (int)(box.getBounds().getWidth() * (double)img.getWidth());
|
||||
int height = (int)(box.getBounds().getHeight() * (double)img.getHeight());
|
||||
DetectionRectangle rectangle = new DetectionRectangle(x, y, width, height, detection.getProbabilities().get(index).floatValue(),className);
|
||||
rectangleList.add(rectangle);
|
||||
index++;
|
||||
}
|
||||
detectionResponse.setRectangleList(rectangleList);
|
||||
return detectionResponse;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user