修改
This commit is contained in:
@@ -91,7 +91,6 @@ public class FlvHandler extends SimpleChannelInboundHandler<Object> {
|
||||
}
|
||||
|
||||
CameraDto cameraDto = buildCamera(req.uri());
|
||||
System.out.println(cameraDto);
|
||||
if (StrUtil.isBlank(cameraDto.getUrl())) {
|
||||
log.info("url有误");
|
||||
sendError(ctx, HttpResponseStatus.BAD_REQUEST);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.ruoyi.video.service;
|
||||
|
||||
import org.bytedeco.opencv.opencv_core.Mat;
|
||||
|
||||
/**
|
||||
* 截取“叠好框的最新一帧”并存证(文件系统 / 数据库BLOB)
|
||||
*/
|
||||
public interface ImageStoreService {
|
||||
|
||||
/**
|
||||
* 从指定 device 的推流实例中,读取“叠好框”的最新一帧并保存。
|
||||
* @param deviceId 设备ID
|
||||
* @return 文件路径,或 "db://image/{id}"
|
||||
*/
|
||||
String saveLastAnnotatedFrame(Long deviceId);
|
||||
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package com.ruoyi.video.service.impl;
|
||||
|
||||
import com.ruoyi.common.config.RuoYiConfig;
|
||||
import com.ruoyi.common.utils.file.FileUploadUtils;
|
||||
import com.ruoyi.video.domain.Device;
|
||||
import com.ruoyi.video.service.IDeviceService;
|
||||
import com.ruoyi.video.service.ImageStoreService;
|
||||
import com.ruoyi.video.service.MediaService;
|
||||
import com.ruoyi.video.thread.MediaTransferFlvByJavacv;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.bytedeco.javacpp.BytePointer;
|
||||
import org.bytedeco.opencv.opencv_core.Mat;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import static org.bytedeco.opencv.global.opencv_imgcodecs.imencode;
|
||||
|
||||
/**
|
||||
* 后端从视频流抓帧 -> JPEG -> 包成 MultipartFile -> 交给 FileUploadUtils.upload 存储。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FileImageStoreServiceImpl implements ImageStoreService {
|
||||
|
||||
private final IDeviceService deviceService;
|
||||
private final MediaService mediaService;
|
||||
|
||||
@Override
|
||||
public String saveLastAnnotatedFrame(Long deviceId) {
|
||||
// 1) 定位设备 & JavaCV 实例
|
||||
Device device = deviceService.selectDeviceByDeviceId(deviceId);
|
||||
if (device == null) throw new IllegalArgumentException("device not found: " + deviceId);
|
||||
|
||||
MediaTransferFlvByJavacv mt = mediaService.getJavacv(device.getMediaKey());
|
||||
if (mt == null) {
|
||||
throw new IllegalStateException("media (javacv) not running for mediaKey=" + device.getMediaKey());
|
||||
}
|
||||
|
||||
// 2) 取“叠好框”的最近一帧
|
||||
Mat mat = mt.getLatestAnnotatedFrameCopy();
|
||||
if (mat == null || mat.empty()) {
|
||||
throw new IllegalStateException("no annotated frame available currently.");
|
||||
}
|
||||
|
||||
try {
|
||||
// 3) 编码为 JPEG 字节
|
||||
BytePointer buf = new BytePointer();
|
||||
if (!imencode(".jpg", mat, buf) || buf.isNull() || buf.limit() <= 0) {
|
||||
throw new IllegalStateException("encode jpeg failed.");
|
||||
}
|
||||
byte[] bytes = new byte[(int) buf.limit()];
|
||||
buf.get(bytes);
|
||||
|
||||
// 4) 计算保存目录:{profile}/snapshots/device-{deviceId}/
|
||||
String profile = RuoYiConfig.getProfile();
|
||||
String uploadBaseDir = Paths.get(profile, "snapshots", "device-" + deviceId).toString();
|
||||
|
||||
// 5) 生成文件名(给 MultipartFile 用;最终 FileUploadUtils 会做统一命名与日期分桶)
|
||||
String fileName = buildFileName(deviceId);
|
||||
|
||||
// 6) 把字节包成 MultipartFile,走若依工具存储
|
||||
MultipartFile multipart = new InMemoryMultipartFile(
|
||||
"file",
|
||||
fileName,
|
||||
"image/jpeg",
|
||||
bytes
|
||||
);
|
||||
|
||||
String stored = FileUploadUtils.upload(uploadBaseDir, multipart);
|
||||
log.info("snapshot saved by FileUploadUtils: {}", stored);
|
||||
return stored; // 形如 /profile/snapshots/device-x/20250927/xxx.jpg
|
||||
} catch (Exception e) {
|
||||
log.error("saveLastAnnotatedFrame failed: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("save snapshot failed", e);
|
||||
} finally {
|
||||
try { mat.release(); } catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildFileName(Long deviceId) {
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault());
|
||||
String ts = now.format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS"));
|
||||
return "cam" + deviceId + "_" + ts + ".jpg";
|
||||
}
|
||||
|
||||
/**
|
||||
* 轻量内存 MultipartFile,不依赖 spring-test。
|
||||
*/
|
||||
static class InMemoryMultipartFile implements MultipartFile {
|
||||
private final String name;
|
||||
private final String originalFilename;
|
||||
private final String contentType;
|
||||
private final byte[] content;
|
||||
|
||||
InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
|
||||
this.name = name;
|
||||
this.originalFilename = originalFilename;
|
||||
this.contentType = contentType;
|
||||
this.content = content != null ? content : new byte[0];
|
||||
}
|
||||
|
||||
@Override public String getName() { return name; }
|
||||
@Override public String getOriginalFilename() { return originalFilename; }
|
||||
@Override public String getContentType() { return contentType; }
|
||||
@Override public boolean isEmpty() { return content.length == 0; }
|
||||
@Override public long getSize() { return content.length; }
|
||||
@Override public byte[] getBytes() { return content; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(content); }
|
||||
@Override public void transferTo(java.io.File dest) throws IOException {
|
||||
try (var in = getInputStream(); var out = new java.io.FileOutputStream(dest)) {
|
||||
in.transferTo(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,26 +151,8 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
||||
minioObject.setCreateTime(new Date());
|
||||
Long minioId = vMinioObjectService.insertVMinioObject(minioObject);
|
||||
|
||||
// 更新巡检任务的原始视频ID
|
||||
if (currentTaskId != null) {
|
||||
InspectionTask task = new InspectionTask();
|
||||
task.setId(currentTaskId);
|
||||
task.setVideoOssId(minioId);
|
||||
task.setVideoStatus(1); // 1: 已上传原始视频
|
||||
taskMapper.updateInspectionTask(task);
|
||||
|
||||
log.info("巡检任务视频保存成功: 任务ID={}, MinIO对象ID={}", currentTaskId, minioId);
|
||||
|
||||
// 触发异步视频分析任务
|
||||
try {
|
||||
// 这里可以通过Spring事件、消息队列或直接调用服务来触发
|
||||
log.info("触发异步视频分析任务: 任务ID={}", currentTaskId);
|
||||
// 例如: eventPublisher.publishEvent(new VideoAnalysisEvent(currentTaskId, minioId));
|
||||
} catch (Exception e) {
|
||||
log.error("触发异步分析任务失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("视频上传失败: {}", e.getMessage());
|
||||
} finally {
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
package com.ruoyi.video.thread.detector;
|
||||
|
||||
import com.ruoyi.video.domain.Detection;
|
||||
import org.bytedeco.javacpp.indexer.FloatRawIndexer;
|
||||
import org.bytedeco.opencv.opencv_core.*;
|
||||
import org.bytedeco.opencv.opencv_dnn.Net;
|
||||
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
|
||||
import static org.bytedeco.opencv.global.opencv_dnn.*; // DNN API(readNetFromModelOptimizer / blobFromImage 等)
|
||||
import static org.bytedeco.opencv.global.opencv_core.*; // Mat/Size/Scalar/transpose 等
|
||||
import static org.bytedeco.opencv.global.opencv_imgproc.*; // cvtColor 等
|
||||
|
||||
public final class OpenVinoYoloDetector implements YoloDetector {
|
||||
private final String modelName;
|
||||
private final Net net;
|
||||
private final Size input;
|
||||
private final float confTh = 0.25f, nmsTh = 0.45f;
|
||||
private final String[] classes;
|
||||
private final int colorBGR;
|
||||
|
||||
public OpenVinoYoloDetector(String name, Path dir, int inW, int inH, String backend, int colorBGR) throws Exception {
|
||||
this.modelName = name;
|
||||
this.input = new Size(inW, inH);
|
||||
this.colorBGR = colorBGR;
|
||||
|
||||
String xml = dir.resolve("model.xml").toString();
|
||||
String bin = dir.resolve("model.bin").toString();
|
||||
|
||||
Path clsPath = dir.resolve("classes.txt");
|
||||
if (Files.exists(clsPath)) {
|
||||
this.classes = Files.readAllLines(clsPath).stream().map(String::trim)
|
||||
.filter(s -> !s.isEmpty()).toArray(String[]::new);
|
||||
} else {
|
||||
this.classes = new String[0];
|
||||
}
|
||||
|
||||
this.net = readNetFromModelOptimizer(xml, bin);
|
||||
|
||||
boolean set = false;
|
||||
if ("openvino".equalsIgnoreCase(backend)) {
|
||||
try {
|
||||
net.setPreferableBackend(DNN_BACKEND_INFERENCE_ENGINE);
|
||||
net.setPreferableTarget(DNN_TARGET_CPU);
|
||||
set = true;
|
||||
} catch (Throwable ignore) { /* 回退 */ }
|
||||
}
|
||||
if (!set) {
|
||||
net.setPreferableBackend(DNN_BACKEND_OPENCV);
|
||||
net.setPreferableTarget(DNN_TARGET_CPU);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public String name() { return modelName; }
|
||||
|
||||
@Override
|
||||
public List<Detection> detect(Mat bgr) {
|
||||
if (bgr == null || bgr.empty()) return Collections.emptyList();
|
||||
|
||||
// 统一成 BGR 3 通道,避免 blobFromImage 断言失败
|
||||
if (bgr.channels() != 3) {
|
||||
Mat tmp = new Mat();
|
||||
if (bgr.channels() == 1) cvtColor(bgr, tmp, COLOR_GRAY2BGR);
|
||||
else if (bgr.channels() == 4) cvtColor(bgr, tmp, COLOR_BGRA2BGR);
|
||||
else bgr.copyTo(tmp);
|
||||
bgr = tmp;
|
||||
}
|
||||
|
||||
try (Mat blob = blobFromImage(bgr, 1.0/255.0, input, new Scalar(0.0), true, false, CV_32F)) {
|
||||
net.setInput(blob);
|
||||
// ===== 多输出兼容(Bytedeco 正确写法)=====
|
||||
org.bytedeco.opencv.opencv_core.StringVector outNames = net.getUnconnectedOutLayersNames();
|
||||
List<Mat> outs = new ArrayList<>();
|
||||
|
||||
if (outNames == null || outNames.size() == 0) {
|
||||
// 只有一个默认输出
|
||||
Mat out = net.forward(); // ← 直接返回 Mat
|
||||
outs.add(out);
|
||||
} else {
|
||||
// 多输出:用 MatVector 承接
|
||||
org.bytedeco.opencv.opencv_core.MatVector outBlobs =
|
||||
new org.bytedeco.opencv.opencv_core.MatVector(outNames.size());
|
||||
net.forward(outBlobs, outNames); // ← 正确的重载
|
||||
|
||||
for (long i = 0; i < outBlobs.size(); i++) {
|
||||
outs.add(outBlobs.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
int fw = bgr.cols(), fh = bgr.rows();
|
||||
List<Rect2d> boxes = new ArrayList<>();
|
||||
List<Float> scores = new ArrayList<>();
|
||||
List<Integer> classIds = new ArrayList<>();
|
||||
|
||||
for (Mat out : outs) {
|
||||
parseYoloOutput(out, fw, fh, boxes, scores, classIds);
|
||||
}
|
||||
if (boxes.isEmpty()) return Collections.emptyList();
|
||||
|
||||
// 纯 Java NMS,避免 MatOf* / Vector API 兼容问题
|
||||
List<Integer> keep = nmsIndices(boxes, scores, nmsTh);
|
||||
|
||||
List<Detection> result = new ArrayList<>(keep.size());
|
||||
for (int k : keep) {
|
||||
Rect2d r = boxes.get(k);
|
||||
Rect rect = new Rect((int)r.x(), (int)r.y(), (int)r.width(), (int)r.height());
|
||||
int cid = classIds.get(k);
|
||||
String cname = (cid >= 0 && cid < classes.length) ? classes[cid] : ("cls"+cid);
|
||||
result.add(new Detection("["+modelName+"] "+cname, scores.get(k), rect, colorBGR));
|
||||
}
|
||||
return result;
|
||||
} catch (Throwable e) {
|
||||
// 单帧失败不影响整体
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 YOLO-IR 输出为 N×C(C>=6),并填充 boxes/scores/classIds。 */
|
||||
private void parseYoloOutput(Mat out, int fw, int fh,
|
||||
List<Rect2d> boxes, List<Float> scores, List<Integer> classIds) {
|
||||
int dims = out.dims();
|
||||
Mat m;
|
||||
|
||||
if (dims == 2) {
|
||||
// NxC 或 CxN
|
||||
if (out.cols() >= 6) {
|
||||
m = out;
|
||||
} else {
|
||||
Mat tmp = new Mat();
|
||||
transpose(out, tmp); // CxN -> NxC
|
||||
m = tmp;
|
||||
}
|
||||
} else if (dims == 3) {
|
||||
// [1,N,C] 或 [1,C,N]
|
||||
if (out.size(2) >= 6) {
|
||||
m = out.reshape(1, out.size(1)); // -> N×C
|
||||
} else {
|
||||
Mat squeezed = out.reshape(1, out.size(1)); // C×N
|
||||
Mat tmp = new Mat();
|
||||
transpose(squeezed, tmp); // -> N×C
|
||||
m = tmp;
|
||||
}
|
||||
} else if (dims == 4) {
|
||||
// [1,1,N,C] 或 [1,1,C,N]
|
||||
int a = out.size(2), b = out.size(3);
|
||||
if (b >= 6) {
|
||||
m = out.reshape(1, a).clone(); // -> N×C
|
||||
} else {
|
||||
Mat cxn = out.reshape(1, b); // C×N
|
||||
Mat tmp = new Mat();
|
||||
transpose(cxn, tmp); // -> N×C
|
||||
m = tmp.clone();
|
||||
}
|
||||
} else {
|
||||
return; // 不支持的形状
|
||||
}
|
||||
|
||||
int N = m.rows(), C = m.cols();
|
||||
if (C < 6 || N <= 0) return;
|
||||
|
||||
FloatRawIndexer idx = m.createIndexer();
|
||||
for (int i = 0; i < N; i++) {
|
||||
float cx = idx.get(i,0), cy = idx.get(i,1), w = idx.get(i,2), h = idx.get(i,3);
|
||||
float obj = idx.get(i,4);
|
||||
|
||||
int bestCls = -1; float bestScore = 0f;
|
||||
for (int c = 5; c < C; c++) {
|
||||
float p = idx.get(i,c);
|
||||
if (p > bestScore) { bestScore = p; bestCls = c - 5; }
|
||||
}
|
||||
float conf = obj * bestScore;
|
||||
if (conf < confTh) continue;
|
||||
|
||||
// 默认假设归一化中心点格式 (cx,cy,w,h);若你的 IR 是 x1,y1,x2,y2,请把这里换算改掉
|
||||
int bx = Math.max(0, Math.round(cx * fw - (w * fw) / 2f));
|
||||
int by = Math.max(0, Math.round(cy * fh - (h * fh) / 2f));
|
||||
int bw = Math.min(fw - bx, Math.round(w * fw));
|
||||
int bh = Math.min(fh - by, Math.round(h * fh));
|
||||
if (bw <= 0 || bh <= 0) continue;
|
||||
|
||||
boxes.add(new Rect2d(bx, by, bw, bh));
|
||||
scores.add(conf);
|
||||
classIds.add(bestCls);
|
||||
}
|
||||
}
|
||||
|
||||
/** 纯 Java NMS(IoU 抑制),返回保留的下标列表。 */
|
||||
private List<Integer> nmsIndices(List<Rect2d> boxes, List<Float> scores, float nmsThreshold) {
|
||||
List<Integer> order = new ArrayList<>(boxes.size());
|
||||
for (int i = 0; i < boxes.size(); i++) order.add(i);
|
||||
// 按分数降序
|
||||
order.sort((i, j) -> Float.compare(scores.get(j), scores.get(i)));
|
||||
|
||||
List<Integer> keep = new ArrayList<>();
|
||||
boolean[] removed = new boolean[boxes.size()];
|
||||
|
||||
for (int a = 0; a < order.size(); a++) {
|
||||
int i = order.get(a);
|
||||
if (removed[i]) continue;
|
||||
keep.add(i);
|
||||
|
||||
Rect2d bi = boxes.get(i);
|
||||
double areaI = bi.width() * bi.height();
|
||||
|
||||
for (int b = a + 1; b < order.size(); b++) {
|
||||
int j = order.get(b);
|
||||
if (removed[j]) continue;
|
||||
|
||||
Rect2d bj = boxes.get(j);
|
||||
double areaJ = bj.width() * bj.height();
|
||||
|
||||
double xx1 = Math.max(bi.x(), bj.x());
|
||||
double yy1 = Math.max(bi.y(), bj.y());
|
||||
double xx2 = Math.min(bi.x() + bi.width(), bj.x() + bj.width());
|
||||
double yy2 = Math.min(bi.y() + bi.height(), bj.y() + bj.height());
|
||||
|
||||
double w = Math.max(0, xx2 - xx1);
|
||||
double h = Math.max(0, yy2 - yy1);
|
||||
double inter = w * h;
|
||||
double iou = inter / (areaI + areaJ - inter + 1e-9);
|
||||
|
||||
if (iou > nmsThreshold) removed[j] = true;
|
||||
}
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
|
||||
@Override public void close(){ net.close(); }
|
||||
}
|
||||
Reference in New Issue
Block a user