代码改变世界

虹软人脸识别-Java实现人脸查找及跟踪

2020-09-30 15:44  码仔很忙  阅读(884)  评论(0编辑  收藏  举报

一、前言

​ 人工智能时代的到来,相信大家已耳濡目染,虹软免费离线开放的人脸识别 SDK,正推动着全行业进入刷脸时代,为了方便开发者接入,虹软提供了多种语言,多种平台的人脸识别SDK的支持,使用场景广泛。产品主要功能有:人脸检测、追踪、特征提取、特征比对、属性检测,活体检测,图像质量检测等,此外,虹软提供的是基于本地算法特征的离线识别SDK,提供全平台的离线支持。

​ 现如今,人脸查找及跟踪这例Demo非常火,之前我的大学室友也曾用python调opencv库函数来实现过类似的功能,包括很多比赛,也会在此基础上构造赛题,而虹软也正是提供了这方面的技术支持。因此作为初学者的我,也想尝试基于虹软的SDK来写个人脸查找及跟踪的样例,并写此文章进行记录,向广大初学开发者作分享。

​ 此Demo采用Maven作为项目管理工具,并基于Windows x64,Java 8,SDK是基于虹软人脸识别SDK3.0

二、项目结构

SDK依赖Jar包 可从虹软官网获取 点击“免费获取” ”登录“后 选择 “具体平台/版本/语言”进行获取

在这里插入图片描述

三、项目依赖

pom.xml 依赖包括

  • Javacv,Lombok,Guava,Apache Common系列工具类,Logback日志相关依赖
  • 虹软人脸识别SDK依赖Jar包
    <dependencies>
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-access</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

        <dependency>
            <groupId>com.arcsoft.face</groupId>
            <artifactId>arcsoft-sdk-face</artifactId>
            <version>1.0.0</version>
            <scope>system</scope>
            <systemPath>${basedir}/libs/arcsoft-sdk-face-3.0.0.0.jar</systemPath>
        </dependency>

        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>26.0-jre</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.8.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.0</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>
    </dependencies>

四、项目流程

在这里插入图片描述

五、效果展示

在这里插入图片描述

六、核心代码说明

1.FaceEngineFactory类 源码说明

此类继承BasePooledObjectFactory抽象类,为FaceEngine对象池

1)成员变量说明

//人脸识别引擎库路径
private String libPath;
//SDK的APP_ID
private String appId;
//SDK的SDK_KEY
private String sdkKey;
//引擎配置类
private EngineConfiguration engineConfiguration;

其中 人脸识别引擎库,APP_ID,SDK_KEY 可通过虹软官网”开发者中心“ 进行 “登录”后 在“我的应用“中进行获取

2)create()方法

public FaceEngine create() throws Exception {
    FaceEngine faceEngine = new FaceEngine(libPath);
    int activeCode = faceEngine.activeOnline(appId, sdkKey);
    log.info("faceEngineActiveCode:" + activeCode);
    int initCode = faceEngine.init(engineConfiguration);
    log.info("faceEngineInitCode:" + initCode);
    return faceEngine;
}
  1. 参数说明:无

  2. 返回结果:FaceEngine人脸识别引擎

  3. 代码流程解读:

    此方法,通过libPath(SDK引擎库的路径)实例化FaceEngine对象,再根据APP_ID,SDK_KEY调用activeOnline()方法激活引擎(联网状态下)

    成功激活引擎后,根据EngineConfiguration引擎配置类 调用init()方法初始化引擎

3)wrap()方法

public PooledObject<FaceEngine> wrap(FaceEngine faceEngine) {
    return new DefaultPooledObject<>(faceEngine);
}
  1. 参数说明:FaceEngine人脸识别引擎

  2. 返回结果:PooledObject包装类

  3. 代码流程解读:

    此方法,通过PooledObject包装器对象 将faceEngine进行包装,便于维护引擎的状态

4)destroyObject()方法

public void destroyObject(PooledObject<FaceEngine> p) throws Exception {
    FaceEngine faceEngine = p.getObject();
    int result = faceEngine.unInit();
    super.destroyObject(p);
}
  1. 参数说明:PooledObject包装类

  2. 返回结果:无

  3. 代码流程解读:

    此方法,从PooledObject包装器对象中获取faceEngine引擎,随后卸载引擎

2.FaceRecognize类 源码说明

1)成员变量说明

//VIDEO模式人脸检测引擎,用于预览帧人脸追踪
private FaceEngine ftEngine;
//人脸注册引擎
private FaceEngine regEngine;
//用于人脸识别的引擎池
private GenericObjectPool<FaceEngine> frEnginePool;
//存放视频中识别到的人脸信息(faceId为key,FaceResult为Value) (FaceId用来标记一张人脸,从进入画面到离开画面这个值不变,可以使用FaceId判断用户)
//ConcurrentHashMap:在多线程运行情况下,增/删faceResultRegistry中的键值对时,保证其线程安全
//volatile关键字:在多线程运行情况下,增/删faceResultRegistry中的键值对后,保证其线程之间的可见性
private volatile ConcurrentHashMap<Integer, FaceResult> faceResultRegistry = new ConcurrentHashMap<>();
//线程池
private ExecutorService frService = Executors.newFixedThreadPool(20);
//存放 注册照与注册照人脸特征值 的映射
public ConcurrentHashMap<String, byte[]> faceFeatureRegistry = new ConcurrentHashMap<>();
//记录上次清理过时人脸时间
private long lastClearTime = System.currentTimeMillis();
//封装 视频中检测到的人脸与注册照人脸 比对结果
@Data
    public class FaceResult {
        private boolean flag = false;
        private String name;
        private float score;
    }
//封装 视频中检测到的人脸信息
    @Data
    public class FacePreviewInfo {
        private FaceInfo faceInfo;
        private int age;
        private boolean liveness;
    }

2)init()方法

public void initEngine(String libPath,String appId,String sdkKey) {

    //引擎配置
    ftEngine = new FaceEngine(libPath);
    int activeCode = ftEngine.activeOnline(appId, sdkKey);
    EngineConfiguration ftEngineCfg = new EngineConfiguration();
    ftEngineCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_VIDEO);
    ftEngineCfg.setFunctionConfiguration(FunctionConfiguration.builder().supportFaceDetect(true).build());
    int ftInitCode = ftEngine.init(ftEngineCfg);

    //引擎配置
    regEngine = new FaceEngine(libPath);
    EngineConfiguration regEngineCfg = new EngineConfiguration();
    regEngineCfg.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
    regEngineCfg.setFunctionConfiguration(FunctionConfiguration.builder().supportFaceDetect(true).supportFaceRecognition(true).build());
    int regInitCode = regEngine.init(regEngineCfg);


    GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
    poolConfig.setMaxIdle(5);
    poolConfig.setMaxTotal(5);
    poolConfig.setMinIdle(5);
    poolConfig.setLifo(false);
    EngineConfiguration frEngineCfg = new EngineConfiguration();
    frEngineCfg.setFunctionConfiguration(FunctionConfiguration.builder().supportFaceRecognition(true).build());
    frEnginePool = new GenericObjectPool(new FaceEngineFactory(libPath, appId, sdkKey, frEngineCfg), poolConfig);//底层库算法对象池


    if (!(activeCode == ErrorInfo.MOK.getValue() || activeCode == ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue())) {
        log.error("activeCode: " + activeCode);
        throw new RuntimeException("activeCode: " + activeCode);
    }
    if (ftInitCode != ErrorInfo.MOK.getValue()) {
        log.error("ftInitEngine: " + ftInitCode);
        throw new RuntimeException("ftInitEngine: " + ftInitCode);
    }

    if (regInitCode != ErrorInfo.MOK.getValue()) {
        log.error("regInitEngine: " + regInitCode);
        throw new RuntimeException("regInitEngine: " + regInitCode);
    }

}
  1. 参数说明:人脸识别引擎库路径,APP_ID,SDK_KEY

  2. 返回结果:无

  3. 代码流程解读:

    此方法,根据传入的libPath,APP_ID,SDK_KEY去初始化用于人脸检测跟踪引擎(VIDEO模式,开启人脸检测功能) 以及用于人脸注册的引擎(IMAGE模式,开启人脸识别功能),然后再去实例化人脸识别引擎池,设置引擎池对应属性后,实例化EngineConfiguration对象(开启人脸识别功能),最后通过FaceEngineFactory的构造方法去初始化引擎并获取对象池。

3)registerFace()方法 注册人脸

public void registerFace(String imagePath) {
    log.info("正在注册人脸");
    int count = 0;
    if (regEngine != null) {
        File file = new File(imagePath);
        File[] files = file.listFiles();
        for (File file1 : files) {
            ImageInfo imageInfo = ImageFactory.getRGBData(file1);
            if (imageInfo != null) {
                List<FaceInfo> faceInfoList = new ArrayList<>();
                int code = regEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),
                        imageInfo.getImageFormat(), faceInfoList);

                if (code == 0 && faceInfoList.size() > 0) {
                    FaceFeature faceFeature = new FaceFeature();
                    int resCode = regEngine.extractFaceFeature(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),imageInfo.getImageFormat(), faceInfoList.get(0), faceFeature);
                    if (resCode == 0) {
                        int lastIndexOf = file1.getName().lastIndexOf(".");
                        String name = file1.getName().substring(0, lastIndexOf);
                        faceFeatureRegistry.put(name, faceFeature.getFeatureData());
                        log.info("成功注册人脸:" + name);
                        count++;
                    }
                }
            }
        }
        log.info("人脸注册完成,共注册:" + count + "张人脸");
    } else {
        throw new RuntimeException("注册失败,引擎未初始化或初始化失败");
    }
}
  1. 参数说明:注册人脸照的目录路径

  2. 返回结果:无

  3. 代码流程解读:

    此方法,将参数目录下的每个文件解析为ImageInfo类型的RGB图像信息数据,再调用FaceEngineService对象的detectFaces()方法检测并获取人脸信息数据(其所需参数有 图像数据 ,图像宽度(4的倍数),图片高度,图像的颜色格式,存放检测到的人脸信息List)。成功检测到人脸后,再通过extractFaceFeature()方法提取人脸特征值(其所需参数有图像数据,图像宽度(4的倍数),图像高度,图像的颜色格式,人脸信息,存放提取到的人脸特征信息)。成功获取到人脸特征值后,将图片文件名和人脸特征值以key-value的形式存放于ConcurrentHashMap中。

4)detectFaces()方法 人脸检测

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;
}
  1. 参数说明:每一帧的ImageInfo图像信息

  2. 返回结果:FacePreviewInfo列表信息

  3. 代码流程解读:

    此方法,根据传入的ImageInfo类型的RGB图像信息数据,调用ftEngine引擎的detectFaces()方法 获取人脸信息,遍历获取到的人脸信息列表设置于FacePreviewInfo类型对象中,随后将faceInfoList列表 传入clearFaceResultRegistry()方法,清理过时的人脸,并返回FacePreviewInfo列表。

5)clearFaceResultRegistry()方法 清理过时人脸

private void clearFaceResultRegistry(List<FaceInfo> faceInfoList) {
    if (System.currentTimeMillis() - lastClearTime > 5000) {
        Iterator<Integer> iterator = faceResultRegistry.keySet().iterator();
        for (; iterator.hasNext(); ) {
            Integer next = iterator.next();
            boolean flag = false;
            for (FaceInfo faceInfo : faceInfoList) {
                if (next.equals(faceInfo.getFaceId())) {
                    flag = true;
                }
            }
            if (!flag) {
                iterator.remove();
            }

        }
    }
}
  1. 参数说明:在视频中新识别到的人脸信息列表

  2. 返回结果:FacePreviewInfo列表信息

  3. 代码流程解读:

    此方法,若当前时间距离上次清理过时人脸已有5s(用户可根据需要自行设置),则遍历faceResultRegistry的key,判断faceResultRegistryfaceInfoList(即之前识别到的与新识别到的人脸)是否存在相同FaceId的人脸,是,则删除faceResultRegistry中此过时的人脸信息。

6)getFaceResult() 方法

public FaceResult getFaceResult(FaceInfo faceInfo, ImageInfo imageInfo) {
    FaceResult faceResult = faceResultRegistry.get(faceInfo.getFaceId());
    if (faceResult == null) {
        faceResult = new FaceResult();
        faceResultRegistry.put(faceInfo.getFaceId(), faceResult);
        frService.submit(new FaceInfoRunnable(faceInfo, imageInfo, faceResult));
    } else if (faceResult.isFlag()) {
        return faceResult;
    }
    return null;
}
  1. 参数说明: 在视频中识别到的人脸信息,ImageInfo图像信息

  2. 返回结果:FaceResult结果

  3. 代码流程解读:

    此方法,先尝试根据faceIdfaceResultRegistry中获取 FaceResult(即之前是否比对过相同人脸),若不存在则实例化一个faceResult并将其以faceId为Key,faceResult为value 存放到faceResultRegistry中,同时新建一个FaceInfoRunnable线程并将faceInfo, imageInfo, faceResult三者传入线程中 运行;若存在,则判断 faceResult的flag是否为true(即是否可从注册照找到相似人脸), 若为true 直接返回即可 。

7)FaceInfoRunnable

此类为一个实现 Runnable 接口的线程实现类

成员变量说明

//传入 视频中识别到的人脸信息
private FaceInfo faceInfo;
//传入 ImageInfo图像信息
private ImageInfo imageInfo;
//人脸比对结果封装
private FaceResult faceResult;

run()方法

@Override
public void run() {
    FaceEngine frEngine = null;
    try {
        frEngine = frEnginePool.borrowObject();
        if (frEngine != null) {
            FaceFeature faceFeature = new FaceFeature();
            int resCode = frEngine.extractFaceFeature(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(),
                    imageInfo.getImageFormat(), faceInfo, faceFeature);
            if (resCode == 0) {
                float score = 0.0F;
                Iterator<Map.Entry<String, byte[]>> iterator = faceFeatureRegistry.entrySet().iterator();
                for (; iterator.hasNext(); ) {
                    Map.Entry<String, byte[]> next = iterator.next();
                    FaceFeature faceFeatureTarget = new FaceFeature();
                    faceFeatureTarget.setFeatureData(next.getValue());

                    FaceSimilar faceSimilar = new FaceSimilar();
                    frEngine.compareFaceFeature(faceFeatureTarget, faceFeature, faceSimilar);
                    if (faceSimilar.getScore() > score) {
                        score = faceSimilar.getScore();
                        faceResult.setName(next.getKey());
                    }
                }

                log.info("相似度:" + score);
                if (score >= 0.8f) {
                    faceResult.setScore(score);
                    faceResult.setFlag(true);
                    faceResultRegistry.put(faceInfo.getFaceId(), faceResult);
                } else {
                    faceResultRegistry.remove(faceInfo.getFaceId());
                }
            }
        }
    } catch (Exception e) {
    } finally {
        if (frEngine != null) {
            frEnginePool.returnObject(frEngine);
        }
    }
}
  1. 参数说明:无

  2. 返回结果:无

  3. 代码流程解读:

    run()方法,根据成员变量FaceInfoImageInfo 调用frEngine的extractFaceFeature()方法获取人脸特征值。成功获取特征值后,遍历faceFeatureRegistry(注册照人脸)中的特征值,结合刚获取到的特征值通过compareFaceFeature()方法比对 俩人脸相似度(其所需参数有人脸特征值1,人脸特征值2,比对模型,存放比对相似值结果),并以相似度最高的注册照命名faceResult的Name,最终,若相似度大于等于0.8(用户可根据需要自行设置) 则将相似度值设置于faceResult对象并将其flag设为true(即注册照中找到相似人脸),并以faceId为key 再次put到faceResultRegistry中,否则remove此faceIdfaceResult,最后释放引擎。

3.VideoPlayer类 源码说明

此类是视频播放类

1)成员变量说明

//视频帧抓取器
private FFmpegFrameGrabber fFmpegFrameGrabber;
//视频播放监听器
private VideoListener videoListener;
//管理定时任务(true表示其关联的线程设为守护线程)
private Timer timer = new Timer(true);

2)start()方法

public void start() {
        try {
            fFmpegFrameGrabber.setPixelFormat(AV_PIX_FMT_BGR24 );
            fFmpegFrameGrabber.start();
            videoListener.onStart();
        } catch (FrameGrabber.Exception e) {
            videoListener.onError(e);
        }
        final int[] lengthInVideoFrames = {fFmpegFrameGrabber.getLengthInVideoFrames()};
        OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();//转换器
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                try {
                    Frame grab = fFmpegFrameGrabber.grabImage();
                    lengthInVideoFrames[0]--;
                    if (grab != null) {
                        IplImage iplImage = converter.convert(grab);
                        if (iplImage != null) {
                            videoListener.onPreview(iplImage);
                        }
                    }
                    if (lengthInVideoFrames[0] <= 0) {
                        stop();
                    }
                } catch (Exception e) {
                    videoListener.onError(e);
                }
            }
        };
        timer.schedule(task, 0, 40);
    }
  1. 参数说明:无

  2. 返回结果:无

  3. 代码流程解读:

    此方法,用于开始播放视频,首先为帧抓取器设置 要转换成的图像数据格式,随后启动帧抓取器和视频播放监听器。lengthInVideoFrames数组中存放视频的帧数,而converter变量为帧与图片之间的转换器。通过TimerTask生成一个线程,在线程run()方法中去抓取视频中的每一帧并将其转换为图像,获取到的图像交给videoListeneronPreview()回调方法进行处理,若帧数已处理完则停止运行。另外,TimerTask线程将由timer进行管理,每40毫秒执行一次。

4.MainApplication类 源码说明

此类为包含main()方法的主类,右击 执行 Run ‘MainApplication.main()’ 即可运行此Demo

main() 主方法

//虹软引擎库存放路径
String libPath = "D:\\arcsoft_lib";
//sdk的APP_ID
String appId = "9iSfMeAhjA52N**************iW1aKes2TpSrd";
//sdk的SDK_KEY
String sdkKey = "BuRTH3hGs91m**************dxEgyP9xu6fiFG7G";
//视频文件路径(从视频中查找并跟踪人脸)
String videoPath="D:\icon-man.mp4";
//需要识别人的注册照目录路径
String imagePath="D:\\photo";
Loader.load(opencv_imgproc.class);
Loader.load(CvPoint.class);
Loader.load(CvFont.class);
CanvasFrame canvas = new CanvasFrame("预览");
canvas.setDefaultCloseOperation(EXIT_ON_CLOSE);
VideoPlayer videoPlayer = new VideoPlayer(videoPath);

首先,加载opencv_imgprocCvPointCvFont等程序所需类,并实例化预览窗口(设置程序退出即窗口关闭)

同时向VideoPlayer的构造方法传入视频路径(为FFmpegFrameGrabber成员变量 指定具体视频) 实例化VideoPlayer

FaceRecognize faceRecognize = new FaceRecognize();
faceRecognize.initEngine(libPath,appId,sdkKey);
faceRecognize.registerFace(imagePath);
OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();

之后,实例化FaceRecognize 并根据LibPath,APP_ID,SDK_KEY调用initEngine()方法初始化FaceRecognize 各引擎,同时实例化转换器

videoPlayer.setListener(new VideoListener() {

    CvScalar color = cvScalar(0, 0, 255, 0);       // blue [green] [red]
    CvFont cvFont = cvFont(opencv_imgproc.FONT_HERSHEY_DUPLEX);

    @Override
    public void onStart() {
    }

    @Override
    public void onPreview(IplImage iplImage) {
        ImageInfo imageInfo = new ImageInfo();
        imageInfo.setWidth(iplImage.width());
        imageInfo.setHeight(iplImage.height());
        imageInfo.setImageFormat(ImageFormat.CP_PAF_BGR24);
        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) {
            int x = facePreviewInfo.getFaceInfo().getRect().getLeft();
            int y = facePreviewInfo.getFaceInfo().getRect().getTop();
            int xMax = facePreviewInfo.getFaceInfo().getRect().getRight();
            int yMax = facePreviewInfo.getFaceInfo().getRect().getBottom();

            CvPoint pt1 = cvPoint(x, y);
            CvPoint pt2 = cvPoint(xMax, yMax);
            opencv_imgproc.cvRectangle(iplImage, pt1, pt2, color, 1, 4, 0);

            FaceRecognize.FaceResult faceResult = faceRecognize.getFaceResult(facePreviewInfo.getFaceInfo(), imageInfo);
            if (faceResult != null) {
                try {
                    CvPoint pt3 = cvPoint(x, y - 2);
                    opencv_imgproc.cvPutText(iplImage, faceResult.getName(), pt3, cvFont, color);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        Frame frame = converter.convert(iplImage);
        canvas.showImage(frame);
    }
    @Override
    public void onCancel() {
    }
    @Override
    public void onError(Exception e) {
    }
});

videoPlayer.start();

​ 上述代码 ,首先为VideoPlayer设置监听器,再启动VideoPlayer。而监听器VideoListeneronPreview()方法中,先将传入的IplImage类型图像信息数据(即由帧抓取器获取到的图片)设置到ImageInfo类型对象中,之后调用faceRecognizedetectFaces()方法获取人脸信息。成功获取到人脸后,根据人脸信息确定人脸方位坐标,调用opencv_imgproc绘制方形矩阵。再根据识别到的人脸信息和图像信息数据 调用faceRecognizegetFaceResult()方法获取FaceResult(即注册照中是否拥有与视频中相似的人脸)。若FaceResult不为空,则调用 opencv_imgproc将对应图片文件名写于人脸框上方,最后将图片转化为视频帧作用于canvas上进行展示。

六、源码下载

若有想一起学习虹软SDK,感受人脸识别奥秘的同学,可通过点击此链接获取Demo源码
了解更多人脸识别产品相关内容请到虹软视觉开放平台