mirror of
https://github.com/geekwenjie/SmartJavaAI.git
synced 2026-09-18 16:39:21 +00:00
- 【人脸检测】新增6个模型(MTCNN、YOLOV5、RetinaFace小尺寸版),大幅提升性能
- 【人脸识别】新增Seetaface6轻量模型 - 【目标检测】支持视频流目标检测(rtsp、视频文件等) - 【目标检测】支持tensorflow2目标检测模型 - 【目标检测】新增行人检测模型(yolo-person) - 【通用视觉】新增4个动作识别模型 - 【通用视觉】新增语义分割模型 - 【通用视觉】新增5个实例分割模型(含yolov8-seg、yolov11-seg) - 【通用视觉】新增yolo-obb11旋转框检测(含yolov11-obb) - 【通用视觉】新增5个姿态估计模型(含yolov8-pose、yolov11-pose)
This commit is contained in:
@@ -34,12 +34,6 @@ public class PersonDetModelConfig extends ModelConfig {
|
||||
*/
|
||||
private String modelPath;
|
||||
|
||||
|
||||
/**
|
||||
* 允许的分类列表
|
||||
*/
|
||||
private List<String> allowedClasses;
|
||||
|
||||
/**
|
||||
* 按置信度分数排序后,最多保留的检测框数量
|
||||
*/
|
||||
|
||||
@@ -17,23 +17,23 @@ public class CriteriaBuilderFactory {
|
||||
|
||||
public static Criteria<Image, DetectedObjects> createCriteria(DetectorModelConfig config) {
|
||||
//以下模型modelPath不允许为空
|
||||
if(config.getModelEnum() == DetectorModelEnum.YOLOV8_OFFICIAL ||
|
||||
config.getModelEnum() == DetectorModelEnum.YOLOV12_OFFICIAL ||
|
||||
config.getModelEnum() == DetectorModelEnum.YOLOV8_CUSTOM ||
|
||||
config.getModelEnum() == DetectorModelEnum.YOLOV12_CUSTOM ||
|
||||
if(config.getModelEnum() == DetectorModelEnum.YOLOV8_OFFICIAL_ONNX ||
|
||||
config.getModelEnum() == DetectorModelEnum.YOLOV12_OFFICIAL_ONNX ||
|
||||
config.getModelEnum() == DetectorModelEnum.YOLOV8_CUSTOM_ONNX ||
|
||||
config.getModelEnum() == DetectorModelEnum.YOLOV12_CUSTOM_ONNX ||
|
||||
config.getModelEnum() == DetectorModelEnum.TENSORFLOW2_OFFICIAL){
|
||||
if(StringUtils.isBlank(config.getModelPath())){
|
||||
throw new DetectionException("modelPath is null");
|
||||
}
|
||||
}
|
||||
switch (config.getModelEnum()) {
|
||||
case YOLOV8_OFFICIAL:
|
||||
case YOLOV8_OFFICIAL_ONNX:
|
||||
return new YoloCriteriaBuilder().buildCriteria(config);
|
||||
case YOLOV12_OFFICIAL:
|
||||
case YOLOV12_OFFICIAL_ONNX:
|
||||
return new YoloCriteriaBuilder().buildCriteria(config);
|
||||
case YOLOV8_CUSTOM:
|
||||
case YOLOV8_CUSTOM_ONNX:
|
||||
return new YoloCriteriaBuilder().buildCriteria(config);
|
||||
case YOLOV12_CUSTOM:
|
||||
case YOLOV12_CUSTOM_ONNX:
|
||||
return new YoloCriteriaBuilder().buildCriteria(config);
|
||||
case TENSORFLOW2_OFFICIAL:
|
||||
return new Tensorflow2CriteriaBuilder().buildCriteria(config);
|
||||
|
||||
@@ -8,40 +8,43 @@ package cn.smartjavaai.objectdetection.enums;
|
||||
public enum DetectorModelEnum {
|
||||
|
||||
// resnet50 系列
|
||||
SSD_300_RESNET50("ai.djl.pytorch/ssd/0.0.1/ssd_300_resnet50"),
|
||||
SSD_512_RESNET50_V1_VOC("ai.djl./ssd/0.0.1/ssd_512_resnet50_v1_voc"),
|
||||
SSD_300_RESNET50_DJL("ai.djl.pytorch/ssd/0.0.1/ssd_300_resnet50"),
|
||||
SSD_512_RESNET50_V1_VOC_DJL("ai.djl./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"),
|
||||
SSD_512_VGG16_ATROUS_COCO_DJL("ai.djl.mxnet/ssd/0.0.1/ssd_512_vgg16_atrous_coco"),
|
||||
SSD_300_VGG16_ATROUS_VOC_DJL("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"),
|
||||
SSD_512_MOBILENET1_VOC_DJL("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"),
|
||||
// YOLOV8N("ai.djl.pytorch/yolov8n/0.0.1/yolov8n"),
|
||||
// YOLO11N("ai.djl.pytorch/yolo11n/0.0.1/yolo11n"),
|
||||
YOLOV5S_DJL("ai.djl.pytorch/yolo5s/0.0.1/yolov5s"),
|
||||
YOLOV5S_ONNX_DJL("ai.djl.onnxruntime/yolo5s/0.0.1/yolo5s"),
|
||||
YOLO_DJL("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"),
|
||||
YOLO3_DARKNET_VOC_416_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_voc_416"),
|
||||
YOLO3_MOBILENET_VOC_320_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_voc_320"),
|
||||
YOLO3_MOBILENET_VOC_416_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_voc_41"),
|
||||
YOLO3_DARKNET_COCO_320_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_320"),
|
||||
YOLO3_DARKNET_COCO_416_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_416"),
|
||||
YOLO3_DARKNET_COCO_608_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_darknet_coco_608"),
|
||||
YOLO3_MOBILENET_COCO_320_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_320"),
|
||||
YOLO3_MOBILENET_COCO_416_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_416"),
|
||||
YOLO3_MOBILENET_COCO_608_DJL("ai.djl.mxnet/yolo/0.0.1/yolo3_mobilenet_coco_608"),
|
||||
|
||||
YOLOV12_OFFICIAL(""),
|
||||
YOLOV8_OFFICIAL(""),
|
||||
|
||||
YOLOV8_CUSTOM(""),
|
||||
YOLOV8_OFFICIAL_ONNX(""),
|
||||
YOLOV11_OFFICIAL_ONNX(""),
|
||||
YOLOV12_OFFICIAL_ONNX(""),
|
||||
|
||||
YOLOV12_CUSTOM(""),
|
||||
|
||||
YOLOV8_CUSTOM_ONNX(""),
|
||||
YOLOV11_CUSTOM_ONNX(""),
|
||||
YOLOV12_CUSTOM_ONNX(""),
|
||||
|
||||
// TensorFlow 2.x 官方模型
|
||||
TENSORFLOW2_OFFICIAL("");
|
||||
|
||||
@@ -11,9 +11,9 @@ import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.FileUtils;
|
||||
import cn.smartjavaai.common.utils.FrameConverterUtil;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||
import cn.smartjavaai.objectdetection.config.DetectorModelConfig;
|
||||
@@ -119,6 +119,9 @@ public class DetectorModel implements AutoCloseable{
|
||||
try {
|
||||
img = ImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectedObjects detectedObjects = detect(img);
|
||||
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
|
||||
throw new DetectionException("未检测到图片中的物体");
|
||||
}
|
||||
img.drawBoundingBoxes(detectedObjects);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
@@ -186,6 +189,9 @@ public class DetectorModel implements AutoCloseable{
|
||||
}
|
||||
Image img = ImageFactory.getInstance().fromImage(OpenCVUtils.image2Mat(sourceImage));
|
||||
DetectedObjects detectedObjects = detect(img);
|
||||
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
|
||||
throw new DetectionException("未检测到图片中的物体");
|
||||
}
|
||||
img.drawBoundingBoxes(detectedObjects);
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
@@ -225,7 +231,6 @@ public class DetectorModel implements AutoCloseable{
|
||||
if (predictor != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
log.debug("释放资源");
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
|
||||
@@ -61,12 +61,12 @@ public class ObjectDetectionModelFactory {
|
||||
* 获取默认模型
|
||||
* @return
|
||||
*/
|
||||
public DetectorModel getModel() {
|
||||
// 初始化默认配置
|
||||
DetectorModelConfig config = new DetectorModelConfig();
|
||||
config.setModelEnum(DetectorModelEnum.YOLO11N);
|
||||
return getModel(config);
|
||||
}
|
||||
// public DetectorModel getModel() {
|
||||
// // 初始化默认配置
|
||||
// DetectorModelConfig config = new DetectorModelConfig();
|
||||
// config.setModelEnum(DetectorModelEnum.YOLO11N);
|
||||
// return getModel(config);
|
||||
// }
|
||||
|
||||
/**
|
||||
* 关闭所有已加载的模型
|
||||
|
||||
@@ -8,10 +8,12 @@ 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 cn.hutool.core.img.ImgUtil;
|
||||
import cn.smartjavaai.common.cv.SmartImageFactory;
|
||||
import cn.smartjavaai.common.entity.DetectionResponse;
|
||||
import cn.smartjavaai.common.entity.R;
|
||||
import cn.smartjavaai.common.pool.PredictorFactory;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.obb.config.ObbDetModelConfig;
|
||||
import cn.smartjavaai.obb.criteria.ObbDetCriteriaFactory;
|
||||
import cn.smartjavaai.obb.entity.ObbResult;
|
||||
@@ -27,6 +29,7 @@ import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.opencv.core.Mat;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -92,7 +95,7 @@ public class CommonPersonDetModel implements PersonDetModel {
|
||||
DetectedObjects detectedObjects = predictor.predict(image);
|
||||
//过滤
|
||||
if(Objects.nonNull(detectedObjects) && detectedObjects.getNumberOfObjects() > 0){
|
||||
DetectedObjectsFilter detectedObjectsFilter = new DetectedObjectsFilter(config.getAllowedClasses(), config.getTopK());
|
||||
DetectedObjectsFilter detectedObjectsFilter = new DetectedObjectsFilter(null, config.getTopK());
|
||||
detectedObjects = detectedObjectsFilter.filter(detectedObjects);
|
||||
}
|
||||
return detectedObjects;
|
||||
@@ -118,9 +121,13 @@ public class CommonPersonDetModel implements PersonDetModel {
|
||||
@Override
|
||||
public R<DetectionResponse> detectAndDraw(Image image) {
|
||||
DetectedObjects detectedObjects = detectCore(image);
|
||||
image.drawBoundingBoxes(detectedObjects);
|
||||
DetectionResponse detectionResponse = DetectorUtils.convertToDetectionResponse(detectedObjects, image);
|
||||
detectionResponse.setDrawnImage(image);
|
||||
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
|
||||
return R.fail(R.Status.NO_OBJECT_DETECTED);
|
||||
}
|
||||
Image drawnImage = ImageUtils.copy(image);
|
||||
drawnImage.drawBoundingBoxes(detectedObjects);
|
||||
DetectionResponse detectionResponse = DetectorUtils.convertToDetectionResponse(detectedObjects, drawnImage);
|
||||
detectionResponse.setDrawnImage(drawnImage);
|
||||
return R.ok(detectionResponse);
|
||||
}
|
||||
|
||||
@@ -129,6 +136,9 @@ public class CommonPersonDetModel implements PersonDetModel {
|
||||
try {
|
||||
Image img = SmartImageFactory.getInstance().fromFile(Paths.get(imagePath));
|
||||
DetectedObjects detectedObjects = detectCore(img);
|
||||
if(Objects.isNull(detectedObjects) || detectedObjects.getNumberOfObjects() == 0){
|
||||
return R.fail(R.Status.NO_OBJECT_DETECTED);
|
||||
}
|
||||
img.drawBoundingBoxes(detectedObjects);
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
// 调用 save 方法将 Image 写入字节流
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package cn.smartjavaai.objectdetection.model.person;
|
||||
|
||||
import cn.smartjavaai.common.config.Config;
|
||||
import cn.smartjavaai.objectdetection.config.PersonDetModelConfig;
|
||||
import cn.smartjavaai.objectdetection.config.PersonDetModelConfig;
|
||||
import cn.smartjavaai.objectdetection.enums.PersonDetectorModelEnum;
|
||||
import cn.smartjavaai.objectdetection.enums.PersonDetectorModelEnum;
|
||||
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 PersonDetModelFactory {
|
||||
|
||||
// 使用 volatile 和双重检查锁定来确保线程安全的单例模式
|
||||
private static volatile PersonDetModelFactory instance;
|
||||
|
||||
private static final ConcurrentHashMap<PersonDetectorModelEnum, PersonDetModel> modelMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 模型注册表
|
||||
*/
|
||||
private static final Map<PersonDetectorModelEnum, Class<? extends PersonDetModel>> registry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
// 私有构造函数,防止外部创建实例
|
||||
private PersonDetModelFactory() {}
|
||||
|
||||
// 双重检查锁定的单例方法
|
||||
public static PersonDetModelFactory getInstance() {
|
||||
if (instance == null) {
|
||||
synchronized (PersonDetModelFactory.class) {
|
||||
if (instance == null) {
|
||||
instance = new PersonDetModelFactory();
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型(通过配置)
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
public PersonDetModel getModel(PersonDetModelConfig config) {
|
||||
if(Objects.isNull(config) || Objects.isNull(config.getModelEnum())){
|
||||
throw new DetectionException("未配置模型");
|
||||
}
|
||||
return modelMap.computeIfAbsent(config.getModelEnum(), k -> {
|
||||
return createFaceDetModel(config);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用ModelConfig创建模型
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
private PersonDetModel createFaceDetModel(PersonDetModelConfig config) {
|
||||
Class<?> clazz = registry.get(config.getModelEnum());
|
||||
if(clazz == null){
|
||||
throw new DetectionException("Unsupported model");
|
||||
}
|
||||
PersonDetModel model = null;
|
||||
try {
|
||||
model = (PersonDetModel) clazz.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException e) {
|
||||
throw new DetectionException(e);
|
||||
}
|
||||
model.loadModel(config);
|
||||
return model;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册模型
|
||||
* @param modelEnum
|
||||
* @param clazz
|
||||
*/
|
||||
private static void registerAlgorithm(PersonDetectorModelEnum modelEnum, Class<? extends PersonDetModel> clazz) {
|
||||
registry.put(modelEnum, clazz);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 移除缓存的模型
|
||||
* @param modelEnum
|
||||
*/
|
||||
public static void removeFromCache(PersonDetectorModelEnum modelEnum) {
|
||||
modelMap.remove(modelEnum);
|
||||
}
|
||||
|
||||
|
||||
// 初始化默认算法
|
||||
static {
|
||||
registerAlgorithm(PersonDetectorModelEnum.YOLOV8_PERSON, CommonPersonDetModel.class);
|
||||
log.debug("缓存目录:{}", Config.getCachePath());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,21 @@ import java.util.List;
|
||||
public interface StreamDetectionListener {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 当检测到目标时回调
|
||||
* @param detectionInfoList 目标信息列表
|
||||
* @param image 检测到的图片
|
||||
*/
|
||||
void onObjectDetected(List<DetectionInfo> detectionInfoList, Image image);
|
||||
|
||||
/**
|
||||
* 当视频文件读取完毕时回调
|
||||
*/
|
||||
void onStreamEnded();
|
||||
|
||||
/**
|
||||
* 当视频流断开连接时回调
|
||||
*/
|
||||
void onStreamDisconnected();
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package cn.smartjavaai.objectdetection.stream;
|
||||
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.modality.cv.output.BoundingBox;
|
||||
import ai.djl.modality.cv.output.DetectedObjects;
|
||||
import ai.djl.modality.cv.output.Mask;
|
||||
import ai.djl.modality.cv.output.Rectangle;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.smartjavaai.common.entity.*;
|
||||
import cn.smartjavaai.common.enums.VideoSourceType;
|
||||
import cn.smartjavaai.common.utils.FrameConverterUtil;
|
||||
import cn.smartjavaai.common.utils.ImageUtils;
|
||||
import cn.smartjavaai.common.utils.OpenCVUtils;
|
||||
import cn.smartjavaai.objectdetection.exception.DetectionException;
|
||||
@@ -18,11 +19,10 @@ import cn.smartjavaai.vision.utils.DetectorUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import nu.pattern.OpenCV;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.pool2.impl.GenericObjectPool;
|
||||
import org.bytedeco.ffmpeg.global.avutil;
|
||||
import org.bytedeco.javacv.*;
|
||||
import org.bytedeco.opencv.global.opencv_imgcodecs;
|
||||
import org.opencv.core.Core;
|
||||
import org.opencv.core.Mat;
|
||||
import org.opencv.imgcodecs.Imgcodecs;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
@@ -41,10 +41,14 @@ public class StreamDetector implements AutoCloseable{
|
||||
|
||||
private DetectorModel detectorModel;
|
||||
private String streamUrl;
|
||||
private ExecutorService grabberExecutor; // 专门抓帧的线程
|
||||
private ExecutorService processorExecutor; // 专门处理帧的线程
|
||||
//专门抓帧的线程
|
||||
private ExecutorService grabberExecutor;
|
||||
//专门处理帧的线程
|
||||
private ExecutorService processorExecutor;
|
||||
//回调线程池
|
||||
ExecutorService callbackExecutor;
|
||||
private int frameDetectionInterval = 1;
|
||||
private int repeatGap = 5; // 秒
|
||||
private long repeatGap = 5; // 秒
|
||||
private volatile boolean isRunning;
|
||||
private FrameGrabber grabber;
|
||||
private StreamDetectionListener listener;
|
||||
@@ -55,6 +59,18 @@ public class StreamDetector implements AutoCloseable{
|
||||
private Map<String, Long> lastDetectTime = new ConcurrentHashMap<>();
|
||||
private BlockingQueue<Frame> frameQueue = new LinkedBlockingQueue<>(100);
|
||||
|
||||
private GenericObjectPool<Predictor<Image, DetectedObjects>> predictorPool;
|
||||
|
||||
private Predictor<Image, DetectedObjects> predictor;
|
||||
|
||||
boolean grabberFinished = false; // 标记结束
|
||||
|
||||
//空帧数量
|
||||
private int nullFrameCount = 0;
|
||||
|
||||
// 连续多少次空帧认为断联
|
||||
private static final int MAX_NULL_FRAMES = 5;
|
||||
|
||||
public static Builder builder() { return new Builder(); }
|
||||
|
||||
private StreamDetector(Builder builder) {
|
||||
@@ -64,9 +80,8 @@ public class StreamDetector implements AutoCloseable{
|
||||
this.listener = builder.listener;
|
||||
this.sourceType = builder.sourceType;
|
||||
this.cameraIndex = builder.cameraIndex;
|
||||
this.repeatGap = builder.repeatGap;
|
||||
this.converterToMat = new OpenCVFrameConverter.ToOrgOpenCvCoreMat();
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void initializeGrabber() throws FrameGrabber.Exception {
|
||||
@@ -78,34 +93,61 @@ public class StreamDetector implements AutoCloseable{
|
||||
if (sourceType == VideoSourceType.STREAM) {
|
||||
grabber.setOption("rtsp_transport", "tcp");
|
||||
grabber.setOption("buffer_size", "1024000");
|
||||
grabber.setOption("stimeout", "20000000");
|
||||
grabber.setOption("max_delay", "500000");
|
||||
grabber.setOption("stimeout", "2000000"); // 超时:单位微秒,这里是2秒
|
||||
grabber.setOption("rw_timeout", "2000000"); // 读超时
|
||||
grabber.setOption("max_delay", "5000000");
|
||||
grabber.setOption("timeout", "2000000"); // 总超时
|
||||
}
|
||||
}
|
||||
//日志级别
|
||||
avutil.av_log_set_level(avutil.AV_LOG_ERROR);
|
||||
grabber.start();
|
||||
if(sourceType == VideoSourceType.FILE){
|
||||
// 总帧数
|
||||
int totalFrames = grabber.getLengthInFrames();
|
||||
log.info("视频帧数:{}", totalFrames);
|
||||
}
|
||||
}
|
||||
|
||||
public void startDetection() {
|
||||
if (isRunning) return;
|
||||
isRunning = true;
|
||||
if (isRunning){
|
||||
throw new RuntimeException("当前正在运行中");
|
||||
}
|
||||
grabberFinished = false;
|
||||
//获取模型Predictor
|
||||
predictorPool = detectorModel.getPool();
|
||||
try {
|
||||
predictor = predictorPool.borrowObject();
|
||||
} catch (Exception e) {
|
||||
throw new DetectionException(e);
|
||||
}
|
||||
|
||||
// 初始化抓帧线程池
|
||||
if (grabberExecutor == null) grabberExecutor = Executors.newSingleThreadExecutor();
|
||||
if (grabberExecutor == null || grabberExecutor.isShutdown())
|
||||
grabberExecutor = Executors.newFixedThreadPool(2);
|
||||
// 初始化帧处理线程池
|
||||
if (processorExecutor == null) processorExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
if (processorExecutor == null || processorExecutor.isShutdown())
|
||||
processorExecutor = Executors.newFixedThreadPool(2);
|
||||
if (callbackExecutor == null || callbackExecutor.isShutdown())
|
||||
callbackExecutor = Executors.newFixedThreadPool(4);
|
||||
try {
|
||||
initializeGrabber();
|
||||
} catch (FrameGrabber.Exception e) {
|
||||
throw new DetectionException("视频流检测启动失败", e);
|
||||
}
|
||||
isRunning = true;
|
||||
log.debug("视频流处理已启动");
|
||||
// 初始化抓帧线程
|
||||
grabberExecutor.submit(() -> {
|
||||
try {
|
||||
initializeGrabber();
|
||||
processFrames();
|
||||
} catch (Exception e) {
|
||||
log.error("视频流处理异常", e);
|
||||
} finally {
|
||||
release();
|
||||
//标识抓取已结束
|
||||
grabberFinished = true;
|
||||
}
|
||||
});
|
||||
log.info("视频流处理已启动");
|
||||
// 初始化队列处理线程:解决回调比较耗时,导致线程池爆满
|
||||
startFrameProcessor();
|
||||
}
|
||||
@@ -114,22 +156,42 @@ public class StreamDetector implements AutoCloseable{
|
||||
* 负责抓取视频帧到队列
|
||||
*/
|
||||
private void processFrames() {
|
||||
int frameCount = 0;
|
||||
while (isRunning) {
|
||||
long frameCount = 0;
|
||||
while (!grabberFinished && isRunning) {
|
||||
try {
|
||||
Frame frame = grabber.grab();
|
||||
if (frame == null || frame.image == null) continue;
|
||||
|
||||
Frame frame = grabber.grabFrame();
|
||||
if (frame == null || frame.image == null) {
|
||||
if(sourceType == VideoSourceType.FILE){
|
||||
log.debug("视频检测结束");
|
||||
grabberFinished = true;
|
||||
break;
|
||||
}else{
|
||||
log.debug("未检测到视频帧");
|
||||
nullFrameCount++;
|
||||
if (nullFrameCount > MAX_NULL_FRAMES) {
|
||||
log.warn("检测到视频断开,已超过最大空帧次数");
|
||||
if(isRunning){
|
||||
stopDetection();
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onStreamDisconnected();
|
||||
}
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
nullFrameCount = 0; // 只要拿到正常帧就清零
|
||||
frameCount++;
|
||||
if (frameCount % frameDetectionInterval != 0) continue;
|
||||
|
||||
Frame currentFrame = frame.clone();
|
||||
frameQueue.offer(currentFrame); // 队列满则丢弃,可改为 put 阻塞
|
||||
frameQueue.offer(currentFrame);
|
||||
// log.debug("正在抓取第{}帧,当前帧数:{}", frameCount, frameQueue.size());
|
||||
} catch (Exception e) {
|
||||
log.error("抓取视频帧异常", e);
|
||||
if (e instanceof FFmpegFrameGrabber.Exception) reconnect();
|
||||
}
|
||||
}
|
||||
log.debug("抓取帧线程退出");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,18 +199,29 @@ public class StreamDetector implements AutoCloseable{
|
||||
*/
|
||||
private void startFrameProcessor() {
|
||||
processorExecutor.submit(() -> {
|
||||
log.info("帧处理线程已启动");
|
||||
while (isRunning || !frameQueue.isEmpty()) {
|
||||
log.debug("帧处理线程已启动");
|
||||
while ((!grabberFinished || !frameQueue.isEmpty()) && isRunning) {
|
||||
try {
|
||||
Frame frame = frameQueue.poll(100, TimeUnit.MILLISECONDS);
|
||||
if (frame != null) processFrame(frame);
|
||||
if (frame != null) {
|
||||
processFrame(frame);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
log.debug("帧处理线程被中断,准备退出");
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
log.error("帧处理异常", e);
|
||||
}
|
||||
}
|
||||
if(isRunning){
|
||||
stopDetection();
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onStreamEnded();
|
||||
}
|
||||
log.debug("帧处理线程退出");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void processFrame(Frame frame) {
|
||||
@@ -158,12 +231,16 @@ public class StreamDetector implements AutoCloseable{
|
||||
if (mat == null) return;
|
||||
|
||||
Image image = ImageFactory.getInstance().fromImage(mat);
|
||||
DetectedObjects detectedObjects = detectorModel.detect(image);
|
||||
// log.debug("检测结果:{}", detectedObjects.toString());
|
||||
DetectedObjects detectedObjects = predictor.predict(image);
|
||||
// log.info("内部检测结果:{}", detectedObjects.toString());
|
||||
DetectionResponse detectionResponse = DetectorUtils.convertToDetectionResponse(detectedObjects, image);
|
||||
if(Objects.isNull(detectionResponse)){
|
||||
return;
|
||||
}
|
||||
List<DetectionInfo> filtered = filterRepeatedObjects(detectionResponse);
|
||||
if (!filtered.isEmpty() && listener != null) {
|
||||
listener.onObjectDetected(filtered, image); // 同帧多物体一次回调
|
||||
Image copyImage = image.duplicate();
|
||||
callbackExecutor.submit(() -> listener.onObjectDetected(filtered, copyImage));
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
@@ -187,47 +264,86 @@ public class StreamDetector implements AutoCloseable{
|
||||
return result;
|
||||
}
|
||||
|
||||
private void reconnect() {
|
||||
log.info("尝试重新连接视频流");
|
||||
try {
|
||||
release();
|
||||
Thread.sleep(5000);
|
||||
initializeGrabber();
|
||||
} catch (Exception e) {
|
||||
log.error("重新连接RTSP流失败", e);
|
||||
|
||||
/**
|
||||
* 开始检测下一个视频文件
|
||||
*/
|
||||
public void startNextVideo(String videoPath) {
|
||||
if(!grabberFinished){
|
||||
throw new DetectionException("当前视频未检测结束,请先关闭当前检测,再切换下一个视频");
|
||||
}
|
||||
if(sourceType != VideoSourceType.FILE){
|
||||
throw new DetectionException("sourceType不是文件");
|
||||
}
|
||||
this.streamUrl = videoPath;
|
||||
this.grabberFinished = false;
|
||||
startDetection();
|
||||
}
|
||||
|
||||
public void stopDetection() { isRunning = false; }
|
||||
|
||||
private void release() {
|
||||
public void stopDetection() {
|
||||
log.debug("停止检测中...");
|
||||
isRunning = false;
|
||||
grabberFinished = true;
|
||||
if (predictor != null && predictorPool != null) {
|
||||
try {
|
||||
predictorPool.returnObject(predictor); //归还
|
||||
predictor = null;
|
||||
predictorPool = null;
|
||||
} catch (Exception e) {
|
||||
log.warn("归还Predictor失败", e);
|
||||
try {
|
||||
predictor.close(); // 归还失败才销毁
|
||||
} catch (Exception ex) {
|
||||
log.error("关闭Predictor失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (grabber != null) {
|
||||
try { grabber.stop(); grabber.release(); }
|
||||
catch (FrameGrabber.Exception e) { log.error("释放Grabber失败", e); }
|
||||
try {
|
||||
grabber.stop(); grabber.release();
|
||||
grabber = null;
|
||||
}catch (FrameGrabber.Exception e) {
|
||||
log.error("释放Grabber失败", e);
|
||||
}
|
||||
}
|
||||
log.debug("停止检测完毕");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
stopDetection();
|
||||
if (grabberExecutor != null) grabberExecutor.shutdownNow();
|
||||
if (processorExecutor != null) processorExecutor.shutdownNow();
|
||||
release();
|
||||
// if(isRunning){
|
||||
// System.out.println("--isRunning:" + isRunning);
|
||||
// stopDetection();
|
||||
// }
|
||||
if (grabberExecutor != null){
|
||||
grabberExecutor.shutdownNow();
|
||||
}
|
||||
if (processorExecutor != null) {
|
||||
processorExecutor.shutdownNow();
|
||||
}
|
||||
if (callbackExecutor != null) {
|
||||
callbackExecutor.shutdownNow();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private DetectorModel detectorModel;
|
||||
private String streamUrl;
|
||||
private ExecutorService executorService;
|
||||
private int frameDetectionInterval = 1;
|
||||
private StreamDetectionListener listener;
|
||||
private VideoSourceType sourceType = VideoSourceType.STREAM; // 默认流
|
||||
private int cameraIndex = 0; // 默认第一个摄像头
|
||||
|
||||
private long repeatGap = 5;//同物体重复检测间隔
|
||||
|
||||
public Builder detectorModel(DetectorModel m) { this.detectorModel = m; return this; }
|
||||
public Builder streamUrl(String url) { this.streamUrl = url; return this; }
|
||||
public Builder executorService(ExecutorService es) { this.executorService = es; return this; }
|
||||
public Builder listener(StreamDetectionListener listener) { this.listener = listener; return this; }
|
||||
public Builder repeatGap(long repeatGap) {
|
||||
this.repeatGap = repeatGap;
|
||||
return this;
|
||||
}
|
||||
public Builder sourceType(VideoSourceType sourceType) {
|
||||
this.sourceType = sourceType;
|
||||
return this;
|
||||
@@ -268,10 +384,6 @@ public class StreamDetector implements AutoCloseable{
|
||||
throw new DetectionException("不支持的视频源类型: " + sourceType);
|
||||
}
|
||||
|
||||
if (executorService == null) {
|
||||
executorService = Executors.newFixedThreadPool(2); // 至少2个线程
|
||||
}
|
||||
|
||||
return new StreamDetector(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ public class YoloV8PersonDetTranslator implements Translator<Image, DetectedObje
|
||||
int numberRows = Math.toIntExact(shape.get(0));
|
||||
int nClasses = Math.toIntExact(shape.get(1));
|
||||
int padding = nClasses - classes.size();
|
||||
System.out.println(Arrays.toString(reshapedResult.get(0).toFloatArray()));
|
||||
if (padding != 0 && padding != 4) {
|
||||
throw new IllegalStateException(
|
||||
"Expected classes: " + (nClasses - 4) + ", got " + classes.size());
|
||||
@@ -208,17 +207,7 @@ public class YoloV8PersonDetTranslator implements Translator<Image, DetectedObje
|
||||
retProbs.add(scores.get(pos).doubleValue());
|
||||
// Rectangle rect = boxes.get(pos);
|
||||
Rectangle rect = boxes.get(pos);
|
||||
if (removePadding) {
|
||||
rect =
|
||||
LetterBoxUtils.restoreBox(rect, scale, origImageWidth, origImageHeight, width, height);
|
||||
} else if (applyRatio) {
|
||||
rect =
|
||||
new Rectangle(
|
||||
rect.getX() / width,
|
||||
rect.getY() / height,
|
||||
rect.getWidth() / width,
|
||||
rect.getHeight() / height);
|
||||
}
|
||||
rect = LetterBoxUtils.restoreBox(rect, scale, origImageWidth, origImageHeight, width, height);
|
||||
retBB.add(rect);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user