基于虹软Linux Pro SDK的多路RTSP流并发接入、解码与帧级处理实践

一、背景需求

随着城市安防、智慧园区、智能零售以及工业监控等场景的快速发展,多路网络摄像头实时人脸检测已成为安防等系统中的核心需求。为了实现这些需求,我选择了虹软(ArcSoft)服务器版人脸识别 SDK(以下简称"SDK"),利用其在人脸检测、跟踪、识别方面的高精度与高性能特性,实现多路网络摄像头人脸检测功能。 ​ 网络摄像头离不开ONVIF和RTSP,其中ONVIF(开放式网络视频接口论坛)是一个全球性开放标准,旨在实现不同品牌网络视频设备(如摄像头、录像机)之间的互联互通;RTSP是一种流媒体控制传输层协议,作为ONVIF中的一员,负责播放、暂停、快进、拉取实时视频流、暂停等操作;

本文不详细介绍ONVIF,主要针对RTSP流获取、人脸SDK集成、实时检测人脸和检测结果渲染这几点展开说明。

二、前期准备

2.1 ONVIF介绍

ONVIF通过制定统一的通信协议,解决早期监控系统厂商壁垒问题,让用户能用不同品牌设备构建统一的IP安防系统,支持设备发现、配置、流媒体传输、云台控制、事件管理等功能。ONVIF的技术实现主要基于Web Services架构,通过一系列标准协议实现不同设备间的互操作。技术架构包括服务层(SOAP & XML)、接口描述(WSDL)、传输层(HTTP)和流媒体层(RTSP)。整体技术架构图如下。

海康、大华等主流摄像头基本都支持ONVIF,不同厂商的产品可通过ONVIF实现互联。由于ONVIF体系过于庞大,本文就不做详细介绍了。

2.2 获取RTSP流

2.2.1 本地RTSP流

使用VLC media Player播放器,加载本地视频文件,通过设置流的方式提供本地RTSP播放地址,本方式适合本地临时测试。

2.2.2 远程RTSP流

接入远程硬件摄像头(如海康监控摄像头,基本都支持ONVIF协议),获取摄像头IP地址(带端口)、登录用户名和密码,在地址中输入rtsp://用户名:密码@IP地址:554/Streaming/Channels/通道号(主码流101,子码流102)即可获取视频流。

2.3 获取LINUX PRO SDK

登录虹软开放平台,按要求注册认证,然后选择SDK(注意要选择人脸识别LINUX PRO)。前期开发测试可以申请试用码,申请通过后直接下载SDK。

2.4 开发环境

Ubuntu虚拟机或WSL,Visual Studio Code,maven + spring boot,jdk 1.8。前期先使用本地机器WSL环境开发,等正式上线后再迁移到云服务器上。

三、项目架构

  1. 获取本地/远程RTSP流。
  2. UI界面/接口:使用VUE3实现UI界面,支持输入多路RTSP地址。
  3. 拉流解码/转码:使用 ffmpeg编解码,opencv转换格式,每路 RTSP流有独立的处理周期。
  4. 人脸检测/识别:使用线程池,每个线程持有独立的SDK引擎实例,保证线程安全。
  5. 结果显示/上报:将人脸检测结果实时绘制到原视频帧上,将绘制结果编码推流,实时渲染在UI上。

四、实现流程

4.1 项目创建

使用java spring boot + maven框架创建项目,导入SDK,填写SDK试用码。编写激活和初始化功能,首次运行项目,验证SDK可用。项目框架如下。

使用当前比较主流的技术,后端使用Java语言实现相关逻辑,前端使用VUE3。

4.2 实现流程

4.2.1 RTSP流输入

使用VUE3框架,简单编写了一个RTSP流实时输入和效果展示页面,目前支持4路RTSP流输入,页面如下图所示。如果只是实现后台检测服务,无需UI显示,可以省去此步骤。

通过前文介绍的方式获取到稳定的RTSP流地址,输入地址点击播放后,通过StreamController类接收相关参数,代码如下。

less 复制代码
@Controller
@Slf4j
public class StreamController {
    @Autowired
    private VideoPlayerService videoPlayerService;
    //解析RTSP流地址
    @RequestMapping(value = "/stream")
    public void test(HttpServletRequest request, HttpServletResponse response, @RequestParam(required = false) String address) throws Exception {
        response.addHeader("Content-Disposition", "attachment;filename="" + "127.0.0.1" + """);
        response.setContentType("video/x-flv");
        response.setHeader("Connection", "keep-alive");
        response.setHeader("accept_ranges", "bytes");
        response.setHeader("pragma", "no-cache");
        response.setHeader("cache_control", "no-cache");
        response.setHeader("transfer_encoding", "CHUNKED");
        response.setStatus(200);
        if(StringUtils.isEmpty(address)){
            throw new IllegalAccessException("地址不能为空");
        }
        try(FrameGrabber grabber=new FFmpegFrameGrabber(address)) {
            videoPlayerService.servletStreamPlayer(grabber, response.getOutputStream());
        }
    }
}

4.2.2 拉流解码和转码

因为SDK对图像数据格式有一定要求,需要稳定、高效地将视频流逐帧解码/转码成符合要求的图像数据,所以我选择了"org.bytedeco.javacv"这个第三方库。这个库集成了ffmpeg,支持对rtsp流进行逐帧解码;集成了opencv,支持不同颜色格式之间转换。javacv库需要在pom.xml文件中集成,javacv可在github上搜索,根据需求填写,示例如下图所示。

FFmpegFrameGrabber和OpenCVFrameConverter这两个类是核心,FFmpegFrameGrabber是对FFmpeg解码流程的完整封装,包含打开输入源、解析流信息、初始化解码器、分配AVMFrame/AVMPacket等主要功能,可以对视频进行抓帧、解码;OpenCVFrameConverter负责将解码出来的帧数据进行转码,FFmpeg解码出来的通常是YUV420格式,可以转换成人脸SDK要求的BGR格式。注意,要实现多路RTSP流同时解析,每路都需要创建converter实例。

java 复制代码
    public void servletStreamPlayer(FrameGrabber grabber, ServletOutputStream servletOutputStream) throws Exception {
        //要实现多路RTSP流同时解析,每路都需要创建一个converter实例
        OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
        // 开启ffmpeg相关log,建议前期开发测试阶段开启
        //avutil.av_log_set_level(avutil.AV_LOG_DEBUG);
        // 生产环境关闭warning信息
        //avutil.av_log_set_level(avutil.AV_LOG_ERROR);
        // 超时时间(10秒)
        grabber.setOption("stimeout", "10 * 1000 * 1000");  
        // 强制使用TCP,要不然无法解码成功
        grabber.setOption("rtsp_transport", "tcp");
        grabber.setOption("threads", "1");
        // 设置缓存大小,提高画质、减少卡顿花屏
        grabber.setOption("buffer_size", "1024000");
        // 读写超时,适用于所有协议的通用读写超时
        grabber.setOption("rw_timeout", "10 * 1000 * 1000");
        // 探测视频流信息,为空默认5000000微秒
        grabber.setOption("probesize", "10 * 1000 * 1000");
        // 解析视频流信息,为空默认5000000微秒
        grabber.setOption("analyzeduration", "10 * 1000 * 1000");
        grabber.start();
        //启动人脸处理引擎
        FacePreview faceProcessEngine = new FacePreview();
        for (; ; ) {
            Frame frame = grabber.grab();
            if (frame == null) {
                break;
            }
            //抓取一帧视频并将其转换为图像IplImage
            IplImage iplImage = converter.convert(frame);
        }
    }

4.2.3 人脸检测识别

4.2.3.1 人脸检测参数配置

根据官方解释,SDK单个引擎的同一功能模块中的算法功能函数不支持多线程调用,所以为了支持多路视频多线程并发检测,需要创建SDK人脸检测引擎池(以下简称"引擎池")。借助springboot框架和"org.apache.commons.pool2.GenericObjectPool"对象池类,我们可以很方便的实现引擎池。在application.properties文件中添加以下引擎池配置

ini 复制代码
config.arcface-sdk.detect-pool-size=5   //大小可根据实际需求配置
config.arcface-sdk.app-id=从虹软官网获取appId
config.arcface-sdk.sdk-key=从虹软官网获取sdkKey
config.arcface-sdk.active-key=从虹软官网获取activeKey

根据config文件实现通用人脸检测引擎池配置类,片段代码如下

kotlin 复制代码
@Configuration
@Slf4j
public class FaceEnginePoolConfig {
    @Value("${config.arcface-sdk.app-id}")
    public String appId;
    @Value("${config.arcface-sdk.sdk-key}")
    public String sdkKey;
    @Value("${config.arcface-sdk.active-key}")
    public String activeKey;
    @Value("${config.arcface-sdk.active-file}")
    public String activeFile;
    @Value("${config.arcface-sdk.detect-pool-size}")
    public Integer detectPoolSize;
    @Value("${config.arcface-sdk.compare-pool-size}")
    public Integer comparePoolSize;
    @Value("${config.arcface-sdk.faceModel}")
    public String faceModel;
    
    @Bean
    public FaceEnginePool faceEnginePool() {
        FaceModel faceModels = null;
        if ("large".equals(faceModel)) {
            faceModels = FaceModel.ASF_REC_LARGE;
        }else if ("middle".equals(faceModel)) {
            faceModels = FaceModel.ASF_REC_MIDDLE;
        }else {
            throw new BusinessException(ErrorCodeEnum.FAIL,"无效的人脸模型 config.arcface-sdk.faceModel");
        }
        return new FaceEnginePool(appId, sdkKey, activeKey, activeFile, detectPoolSize, comparePoolSize, faceModels);
    }
}
4.2.3.1 创建人脸检测引擎

创建通用人脸检测引擎池类FaceEnginePool,片段代码如下。

scss 复制代码
@Slf4j
public class FaceEnginePool {
    private GenericObjectPool<FaceEngine> ftPool;
​
    public FaceEnginePool(String appId, String sdkKey, String activeKey, String activeFile, Integer detectPoolSize, Integer comparePoolSize,FaceModel faceModel) {
        init();
    }
​
    private void init() {
        GenericObjectPoolConfig<FaceEngine> detectFtPoolConfig = new GenericObjectPoolConfig<>();
        detectFtPoolConfig.setMaxIdle(detectPoolSize);
        detectFtPoolConfig.setMaxTotal(detectPoolSize);
        detectFtPoolConfig.setMinIdle(detectPoolSize);
        detectFtPoolConfig.setLifo(false);
        EngineConfiguration detectFtCfg = new EngineConfiguration();
        FunctionConfiguration detectFtFunctionCfg = new FunctionConfiguration();
        detectFtFunctionCfg.setSupportFaceDetect(true);//开启人脸检测功能
        detectFtFunctionCfg.setSupportAge(true);//开启年龄检测功能
        detectFtFunctionCfg.setSupportGender(true);//开启性别检测功能
        detectFtFunctionCfg.setSupportLiveness(true);//开启活体检测功能
        detectFtCfg.setFunctionConfiguration(detectFtFunctionCfg);
        detectFtCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_VIDEO);//图片检测模式,如果是连续帧的视频流图片,那么改成VIDEO模式
        detectFtCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);//人脸旋转角度
        detectFtCfg.setFaceModel(faceModel);
        //底层库算法对象池
        ftPool = new GenericObjectPool<>(new FaceEngineFactory(appId, sdkKey, activeKey, activeFile, detectFtCfg), detectFtPoolConfig);
​
        //初始化特征比较线程池
        GenericObjectPoolConfig<FaceEngine> comparePoolConfig = new GenericObjectPoolConfig<>();
        comparePoolConfig.setMaxIdle(comparePoolSize);
        comparePoolConfig.setMaxTotal(comparePoolSize);
        comparePoolConfig.setMinIdle(comparePoolSize);
        comparePoolConfig.setLifo(false);
        EngineConfiguration compareCfg = new EngineConfiguration();
        FunctionConfiguration compareFunctionCfg = new FunctionConfiguration();
        compareFunctionCfg.setSupportFaceRecognition(true);//开启人脸识别功能
        compareFunctionCfg.setSupportMaskDetect(true);//开启口罩检测功能
        compareFunctionCfg.setSupportImageQuality(true);//开启图像质量检测功能
        compareCfg.setFunctionConfiguration(compareFunctionCfg);
        compareCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);//图片检测模式,如果是连续帧的视频流图片,那么改成VIDEO模式
        compareCfg.setDetectFaceOrientPriority(DetectOrient.ASF_OP_0_ONLY);//人脸旋转角度
        compareCfg.setFaceModel(faceModel);
        comparePool = new GenericObjectPool<>(new FaceEngineFactory(appId, sdkKey, activeKey, activeFile, compareCfg), comparePoolConfig);//底层库算法对象池
        try {
            comparePool.preparePool();
        } catch (Exception e) {
            log.error("", e);
        }
    }
​
    //通用人脸检测引擎池,适用于视频预览人脸检测
    public GenericObjectPool<FaceEngine> ftPool() {
        return ftPool;
    }
}
4.2.3.3 人脸检测识别

创建人脸检测、识别功能逻辑封装类FaceRecognize,支持人脸框检测、人脸图像质量检测和人脸比对识别等功能。片段代码如下。

java 复制代码
@Slf4j
public class FaceRecognize {
    //用于视频预览人脸检测的引擎池
    private final GenericObjectPool<FaceEngine> ftEnginePool = SpringUtil.getBean(FaceEnginePool.class).ftPool();
    //VIDEO模式人脸检测引擎,用于视频预览人脸检测
    private FaceEngine ftEngine = null;
​
    //视频预览图人脸框检测
    public List<FacePreviewInfo> detectFaces(ImageInfo imageInfo) {
        if (ftEngine != null) {
            List<FaceInfo> faceInfoList = new ArrayList<>();
            int code = ftEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),
                    imageInfo.getImageFormat(), faceInfoList);
            List<FacePreviewInfo> previewInfoList = new LinkedList<>();
            for (FaceInfo faceInfo : faceInfoList) {
                FacePreviewInfo facePreviewInfo = new FacePreviewInfo();
                facePreviewInfo.setFaceInfo(faceInfo);
                previewInfoList.add(facePreviewInfo);
            }
            clearFaceResultRegistry(faceInfoList);
            return previewInfoList;
        }
        return null;
    }
    
    //检测识别图像质量
    public boolean imageQuality(ImageInfo imageInfo, FaceInfo faceInfo) {}
    
    //人脸比对
    public FaceResult faceCompare(FaceInfo faceInfo, ImageInfo imageInfo) {
        faceResultRegistry.remove(faceInfo.getFaceId());
        FaceResult faceResult = new FaceResult();
        faceResultRegistry.put(faceInfo.getFaceId(), faceResult);
        frService.submit(new FaceInfoRunnable(faceInfo, imageInfo, faceResult));
        return faceResult;
    }
​
    //通过faceId获取人脸比对结果
    public FaceResult getFaceCompareResult(int faceId) {
        return faceResultRegistry.get(faceId);
    }

4.2.4 检测结果整合显示

4.2.4.1 检测结果处理

如果需要在UI上实时显示检测结果,比如绘制人脸框、显示人脸比对结果,需要缓存SDK检测到的人脸框数据,提取坐标Rect信息,然后使用opencv_imgproc.cvRectangle函数将Rect绘制到单帧图像上,使用opencv_imgproc.cvPutText函数将人脸比对结果绘制到单帧图形上,通过recoder编码成H264视频流,最后通过ServletOutputStream输出到浏览器上显示。人脸框检测和人脸比对逻辑片段代码如下。

ini 复制代码
public class FacePreview {
    private static final CvScalar color = cvScalar(0, 0, 255, 0);       // blue [green] [red]
    private static final CvFont cvFont = cvFont(opencv_imgproc.FONT_HERSHEY_DUPLEX);
    private final FaceRecognize faceRecognize;
    //模拟触发人脸对比事件,当needRecognizeCount计数满足条件时触发一次人脸比对事件,实际可按照项目需求修改
    private int needRecognizeCount;
    
    //使用SDK检测人脸框,并通过opencv绘制人脸识别结果至IplImage上
    public void preview(IplImage iplImage) {
        ImageInfo imageInfo = new ImageInfo();
        imageInfo.setWidth(iplImage.width());
        imageInfo.setHeight(iplImage.height());
        imageInfo.setImageFormat(ImageFormat.CP_PAF_RGB24);
        byte[] imageData = new byte[iplImage.imageSize()];
        iplImage.imageData().get(imageData);
        imageInfo.setImageData(imageData);
        List<FaceRecognize.FacePreviewInfo> previewInfoList = faceRecognize.detectFaces(imageInfo);
        for (FaceRecognize.FacePreviewInfo facePreviewInfo : previewInfoList) {
            //提取人脸框坐标Rect
            int x = facePreviewInfo.getFaceInfo().getRect().getLeft();
            int y = facePreviewInfo.getFaceInfo().getRect().getTop();
            int xMax = facePreviewInfo.getFaceInfo().getRect().getRight();
            int yMax = facePreviewInfo.getFaceInfo().getRect().getBottom();
            //将人脸框绘制到iplImage上,也就是视频预览帧数据上
            CvPoint pt1 = cvPoint(x, y);
            CvPoint pt2 = cvPoint(xMax, yMax);
            opencv_imgproc.cvRectangle(iplImage, pt1, pt2, color, 3, 4, 0);
        }
         FaceRecognize.FacePreviewInfo facePreviewInfo = previewInfoList.get(0);
            if (needRecognizeCount % 200 == 0) {
                FaceRecognize.FaceResult faceResult = faceRecognize.faceCompare(facePreviewInfo.getFaceInfo(), imageInfo);
                if (faceResult != null && faceResult.isFlag()) {
                    int x = facePreviewInfo.getFaceInfo().getRect().getLeft();
                    int y = facePreviewInfo.getFaceInfo().getRect().getTop();
                    CvPoint pt3 = cvPoint(x, y - 2);
                    opencv_imgproc.cvPutText(iplImage,  faceResult.getName(), pt3, cvFont, color);
                }
            } else {
                faceResult = faceRecognize.getFaceCompareResult(facePreviewInfo.getFaceInfo().getFaceId());
                if (faceResult != null && faceResult.isFlag()) {
                    int x = facePreviewInfo.getFaceInfo().getRect().getLeft();
                    int y = facePreviewInfo.getFaceInfo().getRect().getTop();
                    CvPoint pt3 = cvPoint(x, y - 2);
                    opencv_imgproc.cvPutText(iplImage, faceResult.getName(), pt3, cvFont, color);
                }
            }
    }
}
4.2.4.1 检测结果实时渲染

得到绘制结果帧数据(IplImage)后,将帧数据编码成h264文件,并推送到浏览器UI上,这里需要注意设置格式为"flv",其他配置可根据需求修改。代码片段如下。

ini 复制代码
private boolean needShowFacePreview = true;
public void servletStreamPlayer(FrameGrabber grabber, ServletOutputStream servletOutputStream) throws Exception {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        FFmpegFrameRecorder recorder = null;
        if (needShowFacePreview) {
            recorder = new FFmpegFrameRecorder(outputStream, grabber.getImageWidth(), grabber.getImageHeight(), 0);
            //UI渲染流格式/转码,当前支持flv格式
            recorder.setFormat("flv");
            recorder.setInterleaved(false);
            recorder.setVideoOption("tune", "zerolatency");
            recorder.setVideoOption("preset", "ultrafast");
            recorder.setVideoOption("crf", "26");
            recorder.setVideoOption("threads", "1");
            double frameRate = grabber.getFrameRate();
            recorder.setFrameRate(frameRate);// 设置帧率
            recorder.setGopSize(25);// 设置gop,关键帧
            int videoBitrate = grabber.getVideoBitrate();
            recorder.setVideoBitrate(videoBitrate);// 设置码率500kb/s,画质
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
            recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
            recorder.setTrellis(1);
            recorder.setMaxDelay(0);// 设置延迟
            recorder.setAudioChannels(grabber.getAudioChannels());
            recorder.start();
        }
        FacePreview faceProcessEngine = new FacePreview();
        long startTime = 0;
        long videoTS;
        for (; ; ) {
            Frame frame = grabber.grab();
            if (frame == null) {
                break;
            }
            IplImage iplImage = converter.convert(frame);//抓取一帧视频并将其转换为图像
            if (needShowFacePreview) {
                //需要将人脸检测框实时显示在UI上,首先将IplImage传入人脸处理引擎进行处理,
                //处理后IplImage会被修改,然后通过recoder编码成H264视频流,最后通过ServletOutputStream输出到浏览器
                if (iplImage != null) {
                    faceProcessEngine.preview(iplImage);
                    frame = converter.convert(iplImage);
                }
                if (startTime == 0) {
                    startTime = System.currentTimeMillis();
                }
                videoTS = 1000 * (System.currentTimeMillis() - startTime);
                if (recorder != null) {
                    if (videoTS > recorder.getTimestamp()) {
                        recorder.setTimestamp((videoTS));
                    }
                    recorder.record(frame);
                } 
                if (outputStream.size() > 0) {
                    //将检测结果实时渲染到UI上
                    byte[] bytes = outputStream.toByteArray();
                    servletOutputStream.write(bytes);
                    outputStream.reset();
                }
            } else {
                //不需要将人脸检测框实时显示在UI上,自定义实现保存逻辑,例如将结果聚合成JSON数据,在适当时机推送给其他端。
            }
        }
   }

如下图(保护隐私,做了脱敏处理),输入多个RTSP地址,通过人脸识别检测后,会实时渲染到UI上。

如果不需要实时显示人脸框,只保留后台服务,可以删除VUE3和HTML相关功能。也可以自定义实现人脸检测数据保存功能,对此本文不做详细展开。

六、总结

本文主要介绍了虹软LINUX PRO人脸识别SDK的集成流程,对RTSP流进行解码,并进行人脸检测和显示结果,使用了当前主流的前后端开发技术。文中使用的ffmpeg + opencv是开源框架,支持很多协议和格式,如果大家想拓展其他视频流检测功能,可以在本文基础上二次开发,本文就不做详细介绍了。

附上本文涉及到的工程源码路径。

主工程(java + springboot):github.com/WeiLiqiang/...

UI工程(vue3):github.com/WeiLiqiang/...

相关推荐
涵涵(互关)2 小时前
JavaScript 对大整数(超过 2^53 - 1)的精度丢失问题
java·javascript·vue.js
小北方城市网2 小时前
微服务架构设计实战指南:从拆分到落地,构建高可用分布式系统
java·运维·数据库·分布式·python·微服务
逛逛GitHub2 小时前
开源 3 天就 7000 Star!这个复刻 Manus 工作流的 GitHub 项目火了。
github
开开心心_Every2 小时前
离线黑白照片上色工具:操作简单效果逼真
java·服务器·前端·学习·edge·c#·powerpoint
予枫的编程笔记2 小时前
【Java进阶】深入浅出 Java 锁机制:从“单身公寓”到“交通管制”的并发艺术
java·人工智能·
while(1){yan}2 小时前
SpringAOP
java·开发语言·spring boot·spring·aop
专注于大数据技术栈2 小时前
java学习--Collection
java·开发语言·学习
heartbeat..2 小时前
Spring 全局上下文实现指南:单机→异步→分布式
java·分布式·spring·context
techdashen2 小时前
Go 1.18+ slice 扩容机制详解
开发语言·后端·golang