一、背景需求
随着城市安防、智慧园区、智能零售以及工业监控等场景的快速发展,多路网络摄像头实时人脸检测已成为安防等系统中的核心需求。为了实现这些需求,我选择了虹软(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环境开发,等正式上线后再迁移到云服务器上。
三、项目架构

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