智慧安防实战:Java+YOLO 解析 RTSP 流实现监控画面实时异常检测

45 阅读14分钟

一、前言

智慧安防是边缘计算与计算机视觉结合的典型场景,传统安防监控多以人工回看为主,效率低且易遗漏;而基于 AI 的实时异常检测可在事件发生时立即告警,大幅提升安防响应效率。RTSP(实时流传输协议)作为安防摄像头的主流流媒体协议,是实现监控画面实时分析的核心入口;YOLO 系列模型凭借 “端到端” 的检测特性和高帧率优势,成为实时异常检测的首选算法。

Java 作为企业级开发的主流语言,在安防系统的后端服务、业务逻辑层应用广泛,但在实时视频流解析和 AI 推理层面的实践较少。本文将从底层原理到工程落地,完整讲解如何基于 Java+YOLO 构建监控画面实时异常检测系统,覆盖 RTSP 流解析、YOLO 模型推理、异常规则引擎、告警联动等核心环节,解决智慧安防场景中 “流解析延迟高、推理效率低、异常规则难配置” 三大痛点。

二、技术体系与核心原理

2.1 核心技术栈选型

技术组件选型依据与核心作用
Java 17 (LTS)兼顾性能与稳定性,提供成熟的多线程、网络编程能力,适配企业级安防系统集成需求
FFmpeg + JavaCVJavaCV 封装 FFmpeg 原生接口,实现跨平台 RTSP 流解码,解决纯 Java 解析 RTSP 效率低的问题
YOLOv8 (nano 版)平衡检测精度与推理速度,nano 版本适配边缘 / 服务器端部署,支持 ONNX 格式便于 Java 调用
ONNX Runtime跨平台推理引擎,Java 端通过 JNI 调用,支持 GPU 加速,适配 YOLO 模型的推理需求
Redis轻量级缓存,用于存储实时检测结果、告警阈值配置,支持快速规则匹配
Netty高性能网络框架,用于告警信息的实时推送(如 WebSocket 推送到前端、TCP 推送到告警服务器)

2.2 核心流程原理

智慧安防异常检测的核心逻辑是 “流解析→帧预处理→AI 推理→规则匹配→告警联动”,具体流程如下:

image.png

  • RTSP 流解码:通过 JavaCV 调用 FFmpeg 的 avformat/avcodec 模块,将 RTSP 流解码为 Mat 格式的视频帧,解决纯 Java 解析 RTSP 流延迟高、兼容性差的问题;
  • YOLO 推理:将预处理后的视频帧输入 YOLO 模型,识别画面中的目标(如人员、车辆、危险物品),输出目标类别、置信度和边界框;
  • 异常规则引擎:基于业务场景定义异常规则(如 “禁区出现人员”“停车场异常停车超过 5 分钟”“画面出现明火”),匹配推理结果触发异常;
  • 告警联动:异常触发后,通过 WebSocket 推送至前端监控大屏、调用短信接口通知安保人员、联动声光告警设备,实现多维度响应。

三、环境搭建与依赖配置

3.1 基础环境准备

3.1.1 系统依赖(以 Linux 为例)

bash

运行

# 安装FFmpeg(JavaCV依赖)
sudo apt update && sudo apt install -y ffmpeg libavcodec-dev libavformat-dev libswscale-dev

# 安装Redis(规则配置/结果存储)
sudo apt install -y redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server

# 安装显卡驱动(可选,GPU加速推理)
# NVIDIA显卡需安装CUDA 11.8 + cuDNN 8.9

3.1.2 Java 项目依赖(Maven)

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>

    <groupId>com.security.ai</groupId>
    <artifactId>rtsp-yolo-detection</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <javacv.version>1.5.9</javacv.version>
        <onnxruntime.version>1.16.0</onnxruntime.version>
        <netty.version>4.1.100.Final</netty.version>
        <jedis.version>4.4.6</jedis.version>
    </properties>

    <dependencies>
        <!-- JavaCV:RTSP流解码 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>${javacv.version}</version>
        </dependency>

        <!-- ONNX Runtime:YOLO推理 -->
        <dependency>
            <groupId>com.microsoft.onnxruntime</groupId>
            <artifactId>onnxruntime</artifactId>
            <version>${onnxruntime.version}</version>
            <!-- 区分CPU/GPU版本,GPU版本需添加classifier -->
            <!-- <classifier>linux-x64-gpu</classifier> -->
        </dependency>

        <!-- Netty:WebSocket告警推送 -->
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>${netty.version}</version>
        </dependency>

        <!-- Redis:规则配置/结果存储 -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

        <!-- 日志 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>2.0.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.security.ai.Main</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

3.2 YOLO 模型准备

3.2.1 YOLOv8 模型转换为 ONNX 格式

YOLO 官方提供的.pt 模型无法直接被 Java 调用,需转换为 ONNX 格式:

python

运行

# 安装ultralytics库
pip install ultralytics

# 模型转换脚本
from ultralytics import YOLO

# 加载YOLOv8n模型(nano版,兼顾速度)
model = YOLO("yolov8n.pt")

# 导出ONNX格式(简化模型,适配ONNX Runtime)
model.export(
    format="onnx",
    imgsz=640,  # 输入尺寸
    simplify=True,  # 简化模型节点
    opset=12,       # 兼容ONNX Runtime版本
    device=0        # GPU加速(无GPU则设为cpu)
)

转换完成后得到yolov8n.onnx文件,放入项目resources目录。

四、核心功能实现

4.1 RTSP 流解码工具类

java

运行

package com.security.ai.utils;

import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.*;
import org.opencv.core.Mat;

/**
 * RTSP流解码工具类
 * 基于JavaCV封装FFmpeg,实现RTSP流的高效解码
 */
public class RtspDecoder {
    private final String rtspUrl;
    private FFmpegFrameGrabber grabber;
    private final int frameRate;  // 解码帧率(控制推理频率)
    private volatile boolean isRunning = false;

    /**
     * 构造函数
     * @param rtspUrl RTSP流地址(如rtsp://admin:123456@192.168.1.100:554/stream1)
     * @param frameRate 解码帧率(如10帧/秒)
     */
    public RtspDecoder(String rtspUrl, int frameRate) {
        this.rtspUrl = rtspUrl;
        this.frameRate = frameRate;
    }

    /**
     * 初始化解码器
     */
    public void init() throws Exception {
        grabber = new FFmpegFrameGrabber(rtspUrl);
        // FFmpeg配置优化(降低延迟)
        grabber.setOption("rtsp_transport", "tcp");  // TCP传输,避免UDP丢包
        grabber.setOption("stimeout", "5000000");    // 超时时间5秒
        grabber.setOption("max_delay", "100000");    // 最大延迟100ms
        grabber.setVideoCodec(avcodec.AV_CODEC_ID_H264);  // 指定H264解码
        grabber.setImageWidth(1280);  // 摄像头原始分辨率
        grabber.setImageHeight(720);
        grabber.start();
        isRunning = true;
    }

    /**
     * 读取视频帧(阻塞式)
     * @return Mat格式视频帧
     */
    public Mat readFrame() throws Exception {
        if (!isRunning || grabber == null) {
            throw new IllegalStateException("解码器未启动");
        }

        // 控制解码帧率
        long interval = 1000 / frameRate;
        long lastReadTime = System.currentTimeMillis();

        while (isRunning) {
            Frame frame = grabber.grabImage();
            if (frame == null) {
                Thread.sleep(10);
                continue;
            }

            // 转换为OpenCV的Mat格式
            OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
            Mat mat = converter.convert(frame);

            // 控制帧率,避免推理过载
            long currentTime = System.currentTimeMillis();
            if (currentTime - lastReadTime >= interval) {
                lastReadTime = currentTime;
                return mat;
            }
        }
        return null;
    }

    /**
     * 停止解码器
     */
    public void stop() {
        isRunning = false;
        if (grabber != null) {
            try {
                grabber.stop();
                grabber.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
            grabber = null;
        }
    }

    public boolean isRunning() {
        return isRunning;
    }
}

4.2 YOLO 推理核心类

java

运行

package com.security.ai.yolo;

import ai.onnxruntime.*;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;

import java.nio.FloatBuffer;
import java.util.*;

/**
 * YOLOv8推理核心类
 * 实现视频帧预处理、模型推理、结果解析
 */
public class YOLOv8Detector {
    // COCO 80类标签(安防场景重点关注:person, car, knife, fire等)
    private static final List<String> CLASS_NAMES = Arrays.asList(
            "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"
    );

    // 模型参数
    private static final int INPUT_WIDTH = 640;
    private static final int INPUT_HEIGHT = 640;
    private static final float CONF_THRESHOLD = 0.5f;  // 置信度阈值
    private static final float NMS_THRESHOLD = 0.4f;   // 非极大值抑制阈值

    private OrtEnvironment env;
    private OrtSession session;

    /**
     * 初始化YOLO模型
     * @param modelPath ONNX模型路径
     * @param useGpu 是否使用GPU加速
     */
    public YOLOv8Detector(String modelPath, boolean useGpu) throws OrtException {
        env = OrtEnvironment.getEnvironment();
        OrtSession.SessionOptions options = new OrtSession.SessionOptions();
        
        // 性能优化配置
        options.setOptimizationLevel(OrtSession.SessionOptions.OptLevel.ALL);
        options.setInterOpNumThreads(Runtime.getRuntime().availableProcessors());
        options.setIntraOpNumThreads(Runtime.getRuntime().availableProcessors());
        
        // GPU加速配置
        if (useGpu) {
            options.addCUDA(0);  // 使用第0块GPU
        }

        // 加载模型
        session = env.createSession(modelPath, options);
    }

    /**
     * 视频帧预处理
     * @param src 原始帧
     * @return 预处理后的输入张量数据
     */
    private float[] preprocess(Mat src) {
        // 1. 等比例缩放+黑边填充
        Mat resized = new Mat();
        double scaleX = (double) INPUT_WIDTH / src.cols();
        double scaleY = (double) INPUT_HEIGHT / src.rows();
        double scale = Math.min(scaleX, scaleY);
        int newWidth = (int) (src.cols() * scale);
        int newHeight = (int) (src.rows() * scale);
        
        Imgproc.resize(src, resized, new Size(newWidth, newHeight), 0, 0, Imgproc.INTER_LINEAR);
        
        // 2. 创建黑边画布
        Mat inputMat = Mat.zeros(new Size(INPUT_WIDTH, INPUT_HEIGHT), CvType.CV_32FC3);
        int xOffset = (INPUT_WIDTH - newWidth) / 2;
        int yOffset = (INPUT_HEIGHT - newHeight) / 2;
        Mat roi = new Mat(inputMat, new Rect(xOffset, yOffset, newWidth, newHeight));
        resized.convertTo(roi, CvType.CV_32FC3, 1.0 / 255.0);  // 归一化到0-1
        
        // 3. 转换为NCHW格式(YOLO输入要求)
        float[] inputData = new float[3 * INPUT_WIDTH * INPUT_HEIGHT];
        List<Mat> channels = new ArrayList<>();
        Core.split(inputMat, channels);
        
        int idx = 0;
        for (Mat channel : channels) {
            float[] channelData = new float[INPUT_WIDTH * INPUT_HEIGHT];
            channel.get(0, 0, channelData);
            System.arraycopy(channelData, 0, inputData, idx, channelData.length);
            idx += channelData.length;
        }
        
        // 释放资源
        resized.release();
        inputMat.release();
        roi.release();
        return inputData;
    }

    /**
     * 模型推理
     * @param src 原始视频帧
     * @return 检测结果列表
     */
    public List<DetectionResult> detect(Mat src) throws OrtException {
        // 预处理
        float[] inputData = preprocess(src);
        long[] inputShape = new long[]{1, 3, INPUT_HEIGHT, INPUT_WIDTH};  // NCHW
        
        // 构建输入张量
        OrtTensor inputTensor = OrtTensor.createTensor(env, FloatBuffer.wrap(inputData), inputShape);
        Map<String, OrtTensor> inputs = new HashMap<>();
        inputs.put("images", inputTensor);  // YOLOv8 ONNX输入名称为images
        
        // 执行推理
        OrtSession.Result result = session.run(inputs);
        
        // 解析输出
        float[][] output = (float[][]) result.get(0).getValue();
        List<DetectionResult> results = postprocess(output, src.cols(), src.rows());
        
        // 释放资源
        inputTensor.close();
        result.close();
        
        return results;
    }

    /**
     * 后处理:解析结果、NMS过滤、还原坐标
     */
    private List<DetectionResult> postprocess(float[][] output, int originalWidth, int originalHeight) {
        List<DetectionResult> results = new ArrayList<>();
        
        // YOLOv8输出格式:[x, y, w, h, conf, cls1, cls2, ..., cls80]
        int numClasses = CLASS_NAMES.size();
        float[] outputData = output[0];
        int numBoxes = outputData.length / (4 + 1 + numClasses);
        
        // 1. 解析所有候选框
        List<Rect2d> boxes = new ArrayList<>();
        List<Float> confidences = new ArrayList<>();
        List<Integer> classIds = new ArrayList<>();
        
        for (int i = 0; i < numBoxes; i++) {
            int baseIdx = i * (4 + 1 + numClasses);
            float conf = outputData[baseIdx + 4];
            
            // 过滤低置信度
            if (conf < CONF_THRESHOLD) continue;
            
            // 找到最高置信度类别
            int clsIdx = 0;
            float maxClsConf = 0;
            for (int j = 0; j < numClasses; j++) {
                float clsConf = outputData[baseIdx + 5 + j];
                if (clsConf > maxClsConf) {
                    maxClsConf = clsConf;
                    clsIdx = j;
                }
            }
            
            // 还原坐标到原始图像
            float x = outputData[baseIdx];
            float y = outputData[baseIdx + 1];
            float w = outputData[baseIdx + 2];
            float h = outputData[baseIdx + 3];
            
            // 计算缩放比例(还原黑边填充的影响)
            double scale = Math.min((double) INPUT_WIDTH / originalWidth, (double) INPUT_HEIGHT / originalHeight);
            double padW = (INPUT_WIDTH - originalWidth * scale) / 2;
            double padH = (INPUT_HEIGHT - originalHeight * scale) / 2;
            
            // 转换为左上角坐标
            double x1 = (x - padW) / scale;
            double y1 = (y - padH) / scale;
            double x2 = (x + w - padW) / scale;
            double y2 = (y + h - padH) / scale;
            
            // 边界检查
            x1 = Math.max(0, x1);
            y1 = Math.max(0, y1);
            x2 = Math.min(originalWidth, x2);
            y2 = Math.min(originalHeight, y2);
            
            boxes.add(new Rect2d(x1, y1, x2 - x1, y2 - y1));
            confidences.add(conf * maxClsConf);
            classIds.add(clsIdx);
        }
        
        // 2. 非极大值抑制(NMS)
        MatOfFloat confMat = new MatOfFloat(confidences.stream().mapToFloat(f -> f).toArray());
        MatOfInt indices = new MatOfInt();
        Imgproc.dnn.NMSBoxes(boxes, confMat, CONF_THRESHOLD, NMS_THRESHOLD, indices);
        
        // 3. 整理结果
        int[] indicesArr = indices.toArray();
        for (int idx : indicesArr) {
            results.add(new DetectionResult(
                    CLASS_NAMES.get(classIds.get(idx)),
                    confidences.get(idx),
                    boxes.get(idx)
            ));
        }
        
        return results;
    }

    /**
     * 释放资源
     */
    public void close() {
        if (session != null) session.close();
        if (env != null) env.close();
    }

    /**
     * 检测结果实体类
     */
    public static class DetectionResult {
        private String className;    // 目标类别
        private float confidence;    // 置信度
        private Rect2d boundingBox;  // 边界框

        public DetectionResult(String className, float confidence, Rect2d boundingBox) {
            this.className = className;
            this.confidence = confidence;
            this.boundingBox = boundingBox;
        }

        // Getter/Setter
        public String getClassName() { return className; }
        public float getConfidence() { return confidence; }
        public Rect2d getBoundingBox() { return boundingBox; }
    }
}

4.3 异常规则引擎

java

运行

package com.security.ai.rule;

import com.security.ai.yolo.YOLOv8Detector;
import redis.clients.jedis.Jedis;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 异常规则引擎
 * 支持动态配置规则,匹配检测结果触发异常
 */
public class AnomalyRuleEngine {
    // 规则存储(key:摄像头ID,value:规则配置)
    private final Map<String, RuleConfig> ruleMap = new ConcurrentHashMap<>();
    private final Jedis jedis;

    // 异常缓存(用于处理持续异常,避免频繁告警)
    private final Map<String, Long> anomalyCache = new ConcurrentHashMap<>();
    private static final long ALARM_INTERVAL = 5000;  // 告警间隔5秒

    public AnomalyRuleEngine(String redisHost, int redisPort) {
        this.jedis = new Jedis(redisHost, redisPort);
        // 从Redis加载规则(示例:禁区人员检测规则)
        loadRulesFromRedis();
    }

    /**
     * 从Redis加载规则
     * Redis中规则格式:key=security:rule:{cameraId}, value=JSON字符串
     */
    private void loadRulesFromRedis() {
        // 示例:加载摄像头camera_001的规则
        String ruleJson = jedis.get("security:rule:camera_001");
        if (ruleJson != null) {
            // 实际项目中使用JSON解析器(如Gson)解析
            RuleConfig rule = new RuleConfig();
            rule.setCameraId("camera_001");
            rule.setForbiddenClasses(List.of("person"));  // 禁区禁止人员进入
            rule.setForbiddenArea(new double[]{0, 0, 640, 480});  // 禁区坐标(x1,y1,x2,y2)
            ruleMap.put("camera_001", rule);
        }
    }

    /**
     * 匹配规则,检测异常
     * @param cameraId 摄像头ID
     * @param detectionResults YOLO检测结果
     * @return 异常信息(无异常返回null)
     */
    public AnomalyResult matchRule(String cameraId, List<YOLOv8Detector.DetectionResult> detectionResults) {
        RuleConfig rule = ruleMap.get(cameraId);
        if (rule == null) {
            return null;  // 无规则配置,不检测
        }

        // 1. 检查禁区是否有禁止类别出现
        double[] forbiddenArea = rule.getForbiddenArea();
        for (YOLOv8Detector.DetectionResult result : detectionResults) {
            String className = result.getClassName();
            if (!rule.getForbiddenClasses().contains(className)) {
                continue;
            }

            // 2. 检查目标是否在禁区内
            Rect2d box = result.getBoundingBox();
            double centerX = box.x + box.width / 2;
            double centerY = box.y + box.height / 2;
            if (centerX >= forbiddenArea[0] && centerX <= forbiddenArea[2] &&
                centerY >= forbiddenArea[1] && centerY <= forbiddenArea[3]) {

                // 3. 控制告警频率
                String cacheKey = cameraId + "_" + className;
                long now = System.currentTimeMillis();
                if (!anomalyCache.containsKey(cacheKey) || now - anomalyCache.get(cacheKey) > ALARM_INTERVAL) {
                    anomalyCache.put(cacheKey, now);
                    return new AnomalyResult(
                            cameraId,
                            "禁区出现" + className,
                            result.getConfidence(),
                            box
                    );
                }
            }
        }

        return null;
    }

    /**
     * 动态更新规则
     * @param cameraId 摄像头ID
     * @param ruleConfig 新规则
     */
    public void updateRule(String cameraId, RuleConfig ruleConfig) {
        ruleMap.put(cameraId, ruleConfig);
        // 同步到Redis
        jedis.set("security:rule:" + cameraId, ruleConfig.toJson());
    }

    /**
     * 规则配置类
     */
    public static class RuleConfig {
        private String cameraId;          // 摄像头ID
        private List<String> forbiddenClasses;  // 禁止出现的类别
        private double[] forbiddenArea;   // 禁区坐标(x1,y1,x2,y2)

        // Getter/Setter
        public String getCameraId() { return cameraId; }
        public void setCameraId(String cameraId) { this.cameraId = cameraId; }
        public List<String> getForbiddenClasses() { return forbiddenClasses; }
        public void setForbiddenClasses(List<String> forbiddenClasses) { this.forbiddenClasses = forbiddenClasses; }
        public double[] getForbiddenArea() { return forbiddenArea; }
        public void setForbiddenArea(double[] forbiddenArea) { this.forbiddenArea = forbiddenArea; }

        // 转换为JSON(示例)
        public String toJson() {
            return String.format("{"cameraId":"%s","forbiddenClasses":%s,"forbiddenArea":%s}",
                    cameraId, forbiddenClasses, Arrays.toString(forbiddenArea));
        }
    }

    /**
     * 异常结果类
     */
    public static class AnomalyResult {
        private String cameraId;
        private String anomalyMsg;
        private float confidence;
        private Rect2d boundingBox;

        public AnomalyResult(String cameraId, String anomalyMsg, float confidence, Rect2d boundingBox) {
            this.cameraId = cameraId;
            this.anomalyMsg = anomalyMsg;
            this.confidence = confidence;
            this.boundingBox = boundingBox;
        }

        // Getter
        public String getCameraId() { return cameraId; }
        public String getAnomalyMsg() { return anomalyMsg; }
        public float getConfidence() { return confidence; }
        public Rect2d getBoundingBox() { return boundingBox; }
    }
}

4.4 告警联动与主程序

java

运行

package com.security.ai;

import com.security.ai.rule.AnomalyRuleEngine;
import com.security.ai.rule.AnomalyRuleEngine.AnomalyResult;
import com.security.ai.utils.RtspDecoder;
import com.security.ai.yolo.YOLOv8Detector;
import com.security.ai.yolo.YOLOv8Detector.DetectionResult;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;

import java.util.List;

/**
 * 主程序:整合流解码、推理、规则匹配、告警
 */
public class Main {
    // 静态加载OpenCV库
    static {
        System.loadLibrary(org.opencv.core.Core.NATIVE_LIBRARY_NAME);
    }

    public static void main(String[] args) {
        // 1. 配置参数
        String rtspUrl = "rtsp://admin:123456@192.168.1.100:554/stream1";  // 摄像头RTSP地址
        String cameraId = "camera_001";
        String yoloModelPath = "src/main/resources/yolov8n.onnx";
        boolean useGpu = true;

        // 2. 初始化组件
        RtspDecoder decoder = new RtspDecoder(rtspUrl, 10);  // 10帧/秒解码
        YOLOv8Detector detector = null;
        AnomalyRuleEngine ruleEngine = new AnomalyRuleEngine("127.0.0.1", 6379);

        try {
            // 初始化解码器和检测器
            decoder.init();
            detector = new YOLOv8Detector(yoloModelPath, useGpu);

            // 3. 实时检测循环
            while (decoder.isRunning()) {
                // 读取视频帧
                Mat frame = decoder.readFrame();
                if (frame.empty()) {
                    continue;
                }

                // YOLO推理
                List<DetectionResult> detectionResults = detector.detect(frame);

                // 规则匹配,检测异常
                AnomalyResult anomalyResult = ruleEngine.matchRule(cameraId, detectionResults);

                // 绘制检测结果
                drawDetectionResult(frame, detectionResults);

                // 异常告警
                if (anomalyResult != null) {
                    System.out.println("异常告警:" + anomalyResult.getAnomalyMsg() + " 置信度:" + anomalyResult.getConfidence());
                    // 1. WebSocket推送告警(集成Netty实现)
                    // 2. 短信告警(调用短信API)
                    // 3. 声光告警(串口/网络调用告警设备)
                    drawAnomalyFrame(frame, anomalyResult);
                }

                // 显示画面(调试用)
                Imgproc.resize(frame, frame, new org.opencv.core.Size(1280, 720));
                org.opencv.highgui.HighGui.imshow("Security Detection", frame);
                if (org.opencv.highgui.HighGui.waitKey(1) == 27) {  // ESC退出
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 释放资源
            decoder.stop();
            if (detector != null) {
                detector.close();
            }
            org.opencv.highgui.HighGui.destroyAllWindows();
        }
    }

    /**
     * 绘制检测结果
     */
    private static void drawDetectionResult(Mat frame, List<DetectionResult> results) {
        for (DetectionResult result : results) {
            // 绘制边界框
            Rect2d box = result.getBoundingBox();
            Imgproc.rectangle(frame, box, new Scalar(0, 255, 0), 2);

            // 绘制类别和置信度
            String label = String.format("%s: %.2f", result.getClassName(), result.getConfidence());
            Imgproc.putText(frame, label,
                    new org.opencv.core.Point(box.x, box.y - 10),
                    Imgproc.FONT_HERSHEY_SIMPLEX,
                    0.5,
                    new Scalar(0, 255, 0),
                    2);
        }
    }

    /**
     * 绘制异常标记
     */
    private static void drawAnomalyFrame(Mat frame, AnomalyResult anomalyResult) {
        // 绘制红色告警框
        Rect2d box = anomalyResult.getBoundingBox();
        Imgproc.rectangle(frame, box, new Scalar(0, 0, 255), 3);

        // 绘制告警信息
        String alarmMsg = "ALARM: " + anomalyResult.getAnomalyMsg();
        Imgproc.putText(frame, alarmMsg,
                new org.opencv.core.Point(10, 50),
                Imgproc.FONT_HERSHEY_SIMPLEX,
                1.2,
                new Scalar(0, 0, 255),
                3);
    }
}

五、性能优化与工程化实践

5.1 性能优化策略

5.1.1 流解码优化

  1. 帧率控制:无需对每帧进行推理,根据场景设置解码帧率(如 5-10 帧 / 秒),降低推理压力;

  2. 硬解码启用:通过 JavaCV 启用 FFmpeg 的硬件解码(如 NVIDIA NVDEC),CPU 占用率降低 40%:

    java

    运行

    grabber.setOption("hwaccel", "cuda");
    grabber.setOption("hwaccel_device", "0");
    
  3. 缓冲区优化:限制 FFmpeg 的缓存大小,降低延迟:

    java

    运行

    grabber.setOption("fflags", "nobuffer");
    grabber.setOption("flags", "low_delay");
    

5.1.2 推理优化

  1. 模型轻量化:使用 YOLOv8n/YOLOv8s 模型,输入尺寸降至 480x480,推理耗时减少 50%;

  2. GPU 加速:启用 ONNX Runtime 的 CUDA 加速,推理速度提升 3-5 倍;

  3. 批处理推理:对连续帧进行批处理推理(如一次推理 4 帧),提升吞吐量;

  4. JVM 调优:针对推理场景调整 JVM 参数,减少 GC 停顿:

    bash

    运行

    java -Xms2g -Xmx4g -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -jar xxx.jar
    

5.2 工程化实践

  1. 规则动态配置:通过 Redis 实现规则的热更新,无需重启服务即可修改禁区、禁止类别等配置;
  2. 多摄像头管理:基于线程池实现多摄像头并发解析,每个摄像头对应一个独立线程,避免单摄像头阻塞;
  3. 异常分级告警:根据置信度和异常类型分级(一级告警:明火 / 持刀人员;二级告警:禁区人员),不同级别对应不同的响应策略;
  4. 日志与监控:集成 Prometheus+Grafana 监控系统性能(帧率、推理耗时、告警次数),记录异常告警日志便于溯源;
  5. 容错机制:摄像头断流自动重连、模型推理失败降级为纯帧分析、GPU 异常自动切换为 CPU 推理。

六、常见问题与解决方案

问题现象根因分析解决方案
RTSP 流解码延迟高(>3 秒)FFmpeg 缓存过大、UDP 传输丢包启用 TCP 传输、配置 nobuffer、限制缓存大小
推理结果坐标偏移预处理时未等比例缩放,黑边填充未还原预处理使用等比例缩放 + 黑边填充,后处理还原坐标时扣除填充偏移量
GPU 推理报 CUDA 错误CUDA 版本与 ONNX Runtime 不兼容升级 CUDA 至 11.8,ONNX Runtime 使用 1.16.0 + 版本
多摄像头解析 CPU 占用率高未启用硬件解码、单线程处理启用硬件解码、基于线程池实现多摄像头并发处理
告警频繁触发无告警频率限制、置信度阈值过低设置告警间隔、提高置信度阈值、增加异常持续时间判断(如连续 3 帧检测到才告警)

七、总结与展望

本文基于 Java+YOLO+RTSP 构建了智慧安防实时异常检测系统,解决了 Java 在视频流解析、AI 推理层面的工程化问题,实现了从 “被动监控” 到 “主动预警” 的安防升级。该方案可直接落地于园区、工厂、小区等安防场景,核心优势在于:

  1. 基于 Java 开发,易于与企业级安防平台集成;
  2. 轻量化模型 + 硬件加速,满足实时检测需求;
  3. 动态规则配置,适配不同场景的异常检测需求。

后续可拓展方向:

  1. 行为分析:基于 YOLO 的跟踪能力,实现人员徘徊、翻越、奔跑等行为检测;
  2. 多模型融合:集成火焰 / 烟雾检测模型、人脸识别模型,提升异常检测精度;
  3. 边缘 - 云端协同:边缘端轻量化推理,云端进行规则聚合、全局分析;
  4. 私有化部署:基于 Docker+K8s 实现多节点部署,适配大规模安防场景。

智慧安防的核心是 “AI + 业务” 的深度融合,Java 作为企业级开发语言,结合 YOLO 的实时检测能力,可让 AI 安防系统更贴近实际业务需求,真正实现安防的智能化、自动化。