diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/server/FlvHandler.java b/ruoyi-video/src/main/java/com/ruoyi/video/server/FlvHandler.java index 44b0d30..4da0cbd 100644 --- a/ruoyi-video/src/main/java/com/ruoyi/video/server/FlvHandler.java +++ b/ruoyi-video/src/main/java/com/ruoyi/video/server/FlvHandler.java @@ -91,7 +91,6 @@ public class FlvHandler extends SimpleChannelInboundHandler { } CameraDto cameraDto = buildCamera(req.uri()); - System.out.println(cameraDto); if (StrUtil.isBlank(cameraDto.getUrl())) { log.info("url有误"); sendError(ctx, HttpResponseStatus.BAD_REQUEST); diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/service/ImageStoreService.java b/ruoyi-video/src/main/java/com/ruoyi/video/service/ImageStoreService.java deleted file mode 100644 index 815ed0e..0000000 --- a/ruoyi-video/src/main/java/com/ruoyi/video/service/ImageStoreService.java +++ /dev/null @@ -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); - -} diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/service/impl/FileImageStoreServiceImpl.java b/ruoyi-video/src/main/java/com/ruoyi/video/service/impl/FileImageStoreServiceImpl.java deleted file mode 100644 index dc8a73d..0000000 --- a/ruoyi-video/src/main/java/com/ruoyi/video/service/impl/FileImageStoreServiceImpl.java +++ /dev/null @@ -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); - } - } - } -} diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java index cc98212..8160119 100644 --- a/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java +++ b/ruoyi-video/src/main/java/com/ruoyi/video/thread/MediaTransferFlvByJavacv.java @@ -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 { diff --git a/ruoyi-video/src/main/java/com/ruoyi/video/thread/detector/OpenVinoYoloDetector.java b/ruoyi-video/src/main/java/com/ruoyi/video/thread/detector/OpenVinoYoloDetector.java deleted file mode 100644 index c7ea03e..0000000 --- a/ruoyi-video/src/main/java/com/ruoyi/video/thread/detector/OpenVinoYoloDetector.java +++ /dev/null @@ -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 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 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 boxes = new ArrayList<>(); - List scores = new ArrayList<>(); - List 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 keep = nmsIndices(boxes, scores, nmsTh); - - List 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 boxes, List scores, List 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 nmsIndices(List boxes, List scores, float nmsThreshold) { - List 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 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(); } -}