欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
《JavaCV人脸识别三部曲》链接
本篇概览
- 作为《JavaCV人脸识别三部曲》的终篇,今天咱们要开发一个实用的功能:有人出现在摄像头中时,应用程序在预览窗口标注出此人的身份,效果如下图所示:

- 简单来说,本篇要做的事情如下:
- 理解重点概念:confidence
- 理解重点概念:threshold
- 编码
- 验证
- 今天编写的代码,主要功能如下图所示:

理解重点概念:confidence
- confidence和threshold是OpenCV的人脸识别中非常重要的两个概念,咱们先把这两个概念搞清楚,再去编码就非常容易了
- 假设,咱们用下面六张照片训练出包含两个类别的模型:

- 用一张新的照片去训练好的模型中做识别,如下图,识别结果有两部分内容:label和confidence

- 先说lable,这个好理解,与训练时的lable一致(回顾上一篇的代码,lable如下图红框所示),前面图中lable等于2,表示被判定为郭富城:

- 按照上面的说法,lable等于2就能确定照片中的人像是郭富城吗?
- 当然不能!!!此时confidence字段就非常重要了,先看JavaCV源码中对confidence的解释,如下图红框所示,我的理解是:与lable值相关联的置信度,或者说这张脸是郭富城的可能性:

- 如果理解为可能性,那么问题来了,这是个double型的值,这个值越大,表示可能性越大还是越小?
- 上图并没有明说,但是那一句e.g. distance,让我想起了机器学习中的K-means,此时我脑海中的画面如下:

-若真如上图所示,那么显然confidence越小,是郭富城的可能性就越大了,接下来再去找一些权威的说法:
- OpenCV的官方论坛有个帖子的说法如下图:代码中的confidence变量属于命名不当,其含义不是可信度,而是与模型中的类别的距离:

- 再看第二个解释,如下图红框,说得很清楚了,值越小,与模型中类别的相似度越高,0表示完全匹配:

- 再看一个Stack Overflow的解释:

- 至此,相信您对confidence已经足够理解了,lable等于2,confidence=30.01,意思是:被识别照片与郭富城最相似,距离为30.01,距离越小,是郭富城的可能性越大
理解重点概念:threshold
- 在聊threshold之前,咱们先看一个场景,还是刘德华郭富城的模型,这次咱们拿喜洋洋的照片给模型识别,识别结果如下:

- 显然,模型不会告诉你照片里是谁,只会告诉你:和郭富城的距离是3000.01
- 看到这里,聪明的您可能会这么想:那我就写一段代码吧,识别结果的confidence如果太大(例如超过100),就判定用于识别的人不属于训练模型的任何一个类别
- 上述功能,OpenCV已经帮咱们想到了,那就是:threshold,翻译过来即门限,如果咱们设置了threshold等于100,那么,一旦距离超过100,OpenCV的lable返回值就是-1
- 理解了confidence和threshold,接下来可以写人脸识别的代码了,感谢咱们的充分准备,接下来是丝般顺滑的编码过程...
源码下载
名称链接备注项目主页https://github.com/zq2599/blog_demos该项目在GitHub上的主页git仓库地址(https)https://github.com/zq2599/blog_demos.git该项目源码的仓库地址,https协议git仓库地址(ssh)git@github.com:zq2599/blog_demos.git该项目源码的仓库地址,ssh协议
- 这个git项目中有多个文件夹,本篇的源码在javacv-tutorials文件夹下,如下图红框所示:

- javacv-tutorials里面有多个子工程,《JavaCV人脸识别三部曲》系列的代码在simple-grab-push工程下:

编码:人脸识别服务
- 开始正式编码,今天咱们不会新建工程,而是继续使用《JavaCV的摄像头实战之一:基础》中创建的simple-grab-push工程
- 先定义一个Bean类PredictRlt.java,用来保存识别结果(lable和confidence字段):
- package com.bolingcavalry.grabpush.extend;
- import lombok.Data;
- @Data
- public class PredictRlt {
- private int lable;
- private double confidence;
- }
复制代码
- 然后把人脸识别有关的服务集中在RecognizeService.java中,方便主程序使用,代码如下,有几处要注意的地方稍后提到:
- package com.bolingcavalry.grabpush.extend;
- import com.bolingcavalry.grabpush.Constants;
- import org.bytedeco.opencv.global.opencv_imgcodecs;
- import org.bytedeco.opencv.opencv_core.Mat;
- import org.bytedeco.opencv.opencv_core.Size;
- import org.bytedeco.opencv.opencv_face.FaceRecognizer;
- import org.bytedeco.opencv.opencv_face.FisherFaceRecognizer;
- import static org.bytedeco.opencv.global.opencv_imgcodecs.IMREAD_GRAYSCALE;
- import static org.bytedeco.opencv.global.opencv_imgproc.resize;
- /**
- * @author willzhao
- * @version 1.0
- * @description 把人脸识别的服务集中在这里
- * @date 2021/12/12 21:32
- */
- public class RecognizeService {
- private FaceRecognizer faceRecognizer;
- // 推理结果的标签
- private int[] plabel;
- // 推理结果的置信度
- private double[] pconfidence;
- // 推理结果
- private PredictRlt predictRlt;
- // 用于推理的图片尺寸,要和训练时的尺寸保持一致
- private Size size= new Size(Constants.RESIZE_WIDTH, Constants.RESIZE_HEIGHT);
- public RecognizeService(String modelPath) {
- plabel = new int[1];
- pconfidence = new double[1];
- predictRlt = new PredictRlt();
-
- // 识别类的实例化,与训练时相同
- faceRecognizer = FisherFaceRecognizer.create();
- // 加载的是训练时生成的模型
- faceRecognizer.read(modelPath);
- // 设置门限,这个可以根据您自身的情况不断调整
- faceRecognizer.setThreshold(Constants.MAX_CONFIDENCE);
- }
- /**
- * 将Mat实例给模型去推理
- * @param mat
- * @return
- */
- public PredictRlt predict(Mat mat) {
- // 调整到和训练一致的尺寸
- resize(mat, mat, size);
- boolean isFinish = false;
- try {
- // 推理(这一行可能抛出RuntimeException异常,因此要补货,否则会导致程序退出)
- faceRecognizer.predict(mat, plabel, pconfidence);
- isFinish = true;
- } catch (RuntimeException runtimeException) {
- runtimeException.printStackTrace();
- }
- // 如果发生过异常,就提前返回
- if (!isFinish) {
- return null;
- }
- // 将推理结果写入返回对象中
- predictRlt.setLable(plabel[0]);
- predictRlt.setConfidence(pconfidence[0]);
- return predictRlt;
- }
- }
复制代码
- 构造方法中,通过faceRecognizer.setThreshold设置门限,我在实际使用中发现50比较合适,您可以根据自己的情况不断调整
- predict方法中,用于识别的图片要用resize方法调整大小,尺寸要和训练时的尺寸一致
- 实测发现,在一张照片中出现多个人脸时,faceRecognizer.predict可能抛出RuntimeException异常,因此这里要捕获异常,避免程序崩溃退出
编码:检测和识别
- package com.bolingcavalry.grabpush.extend;
- import com.bolingcavalry.grabpush.Constants;
- import org.bytedeco.javacv.Frame;
- import org.bytedeco.javacv.OpenCVFrameConverter;
- import org.bytedeco.opencv.opencv_core.*;
- import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
- import static org.bytedeco.opencv.global.opencv_core.CV_8UC1;
- import static org.bytedeco.opencv.global.opencv_imgcodecs.imwrite;
- import static org.bytedeco.opencv.global.opencv_imgproc.*;
- /**
- * @author willzhao
- * @version 1.0
- * @description 检测工具的通用接口
- * @date 2021/12/5 10:57
- */
- public interface DetectService {
- /**
- * 根据传入的MAT构造相同尺寸的MAT,存放灰度图片用于以后的检测
- * @param src 原始图片的MAT对象
- * @return 相同尺寸的灰度图片的MAT对象
- */
- static Mat buildGrayImage(Mat src) {
- return new Mat(src.rows(), src.cols(), CV_8UC1);
- }
-
- /**
- * 初始化操作,例如模型下载
- * @throws Exception
- */
- void init() throws Exception;
- /**
- * 得到原始帧,做识别,添加框选
- * @param frame
- * @return
- */
- Frame convert(Frame frame);
- /**
- * 释放资源
- */
- void releaseOutputResource();
- }
复制代码
- 然后就是DetectService的实现类DetectAndRecognizeService .java,功能是用摄像头的一帧图片检测人脸,再拿检测到的人脸给RecognizeService做识别,完整代码如下,有几处要注意的地方稍后提到:
[code]package com.bolingcavalry.grabpush.extend;import lombok.extern.slf4j.Slf4j;import org.bytedeco.javacpp.Loader;import org.bytedeco.javacv.Frame;import org.bytedeco.javacv.OpenCVFrameConverter;import org.bytedeco.opencv.opencv_core.*;import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;import java.io.File;import java.net.URL;import java.util.Map;import static org.bytedeco.opencv.global.opencv_imgproc.*;/** * @author willzhao * @version 1.0 * @description 音频相关的服务 * @date 2021/12/3 8:09 */@Slf4jpublic class DetectAndRecognizeService implements DetectService { /** * 每一帧原始图片的对象 */ private Mat grabbedImage = null; /** * 原始图片对应的灰度图片对象 */ private Mat grayImage = null; /** * 分类器 */ private CascadeClassifier classifier; /** * 转换器 */ private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat(); /** * 检测模型文件的下载地址 */ private String detectModelFileUrl; /** * 处理每一帧的服务 */ private RecognizeService recognizeService; /** * 为了显示的时候更加友好,给每个分类对应一个名称 */ private Map kindNameMap; /** * 构造方法 * @param detectModelFileUrl * @param recognizeModelFilePath * @param kindNameMap */ public DetectAndRecognizeService(String detectModelFileUrl, String recognizeModelFilePath, Map kindNameMap) { this.detectModelFileUrl = detectModelFileUrl; this.recognizeService = new RecognizeService(recognizeModelFilePath); this.kindNameMap = kindNameMap; } /** * 音频采样对象的初始化 * @throws Exception */ @Override public void init() throws Exception { // 下载模型文件 URL url = new URL(detectModelFileUrl); File file = Loader.cacheResource(url); // 模型文件下载后的完整地址 String classifierName = file.getAbsolutePath(); // 根据模型文件实例化分类器 classifier = new CascadeClassifier(classifierName); if (classifier == null) { log.error("Error loading classifier file [{}]", classifierName); System.exit(1); } } @Override public Frame convert(Frame frame) { // 由帧转为Mat grabbedImage = converter.convert(frame); // 灰度Mat,用于检测 if (null==grayImage) { grayImage = DetectService.buildGrayImage(grabbedImage); } // 进行人脸识别,根据结果做处理得到预览窗口显示的帧 return detectAndRecoginze(classifier, converter, frame, grabbedImage, grayImage, recognizeService, kindNameMap); } /** * 程序结束前,释放人脸识别的资源 */ @Override public void releaseOutputResource() { if (null!=grabbedImage) { grabbedImage.release(); } if (null!=grayImage) { grayImage.release(); } if (null==classifier) { classifier.close(); } } /** * 检测图片,将检测结果用矩形标注在原始图片上 * @param classifier 分类器 * @param converter Frame和mat的转换器 * @param rawFrame 原始视频帧 * @param grabbedImage 原始视频帧对应的mat * @param grayImage 存放灰度图片的mat * @param kindNameMap 每个分类编号对应的名称 * @return 标注了识别结果的视频帧 */ static Frame detectAndRecoginze(CascadeClassifier classifier, OpenCVFrameConverter.ToMat converter, Frame rawFrame, Mat grabbedImage, Mat grayImage, RecognizeService recognizeService, Map kindNameMap) { // 当前图片转为灰度图片 cvtColor(grabbedImage, grayImage, CV_BGR2GRAY); // 存放检测结果的容器 RectVector objects = new RectVector(); // 开始检测 classifier.detectMultiScale(grayImage, objects); // 检测结果总数 long total = objects.size(); // 如果没有检测到结果,就用原始帧返回 if (total |