愛在花開的季節 发表于 2023-6-30 08:41:46

JavaCV人脸识别三部曲之三:识别和预览

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
《JavaCV人脸识别三部曲》链接


[*]《视频中的人脸保存为图片》
[*]《训练》
[*]《识别和预览》
本篇概览


[*]作为《JavaCV人脸识别三部曲》的终篇,今天咱们要开发一个实用的功能:有人出现在摄像头中时,应用程序在预览窗口标注出此人的身份,效果如下图所示:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195058833-506323078.png
[*]简单来说,本篇要做的事情如下:

[*]理解重点概念:confidence
[*]理解重点概念:threshold
[*]编码
[*]验证


[*]今天编写的代码,主要功能如下图所示:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305420-1005910732.png
理解重点概念:confidence


[*]confidence和threshold是OpenCV的人脸识别中非常重要的两个概念,咱们先把这两个概念搞清楚,再去编码就非常容易了
[*]假设,咱们用下面六张照片训练出包含两个类别的模型:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305382-127803281.png
[*]用一张新的照片去训练好的模型中做识别,如下图,识别结果有两部分内容:label和confidence
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305544-660212398.png
[*]先说lable,这个好理解,与训练时的lable一致(回顾上一篇的代码,lable如下图红框所示),前面图中lable等于2,表示被判定为郭富城:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305467-1964780066.png
[*]按照上面的说法,lable等于2就能确定照片中的人像是郭富城吗?
[*]当然不能!!!此时confidence字段就非常重要了,先看JavaCV源码中对confidence的解释,如下图红框所示,我的理解是:与lable值相关联的置信度,或者说这张脸是郭富城的可能性:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305153-334901597.png
[*]如果理解为可能性,那么问题来了,这是个double型的值,这个值越大,表示可能性越大还是越小?
[*]上图并没有明说,但是那一句e.g. distance,让我想起了机器学习中的K-means,此时我脑海中的画面如下:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305418-1707394825.png
-若真如上图所示,那么显然confidence越小,是郭富城的可能性就越大了,接下来再去找一些权威的说法:
[*]OpenCV的官方论坛有个帖子的说法如下图:代码中的confidence变量属于命名不当,其含义不是可信度,而是与模型中的类别的距离:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305513-1276339562.png
[*]再看第二个解释,如下图红框,说得很清楚了,值越小,与模型中类别的相似度越高,0表示完全匹配:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305204-1614240081.png
[*]再看一个Stack Overflow的解释:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305420-1419814391.png
[*]至此,相信您对confidence已经足够理解了,lable等于2,confidence=30.01,意思是:被识别照片与郭富城最相似,距离为30.01,距离越小,是郭富城的可能性越大
理解重点概念:threshold


[*]在聊threshold之前,咱们先看一个场景,还是刘德华郭富城的模型,这次咱们拿喜洋洋的照片给模型识别,识别结果如下:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305419-1129935156.png
[*]显然,模型不会告诉你照片里是谁,只会告诉你:和郭富城的距离是3000.01
[*]看到这里,聪明的您可能会这么想:那我就写一段代码吧,识别结果的confidence如果太大(例如超过100),就判定用于识别的人不属于训练模型的任何一个类别
[*]上述功能,OpenCV已经帮咱们想到了,那就是:threshold,翻译过来即门限,如果咱们设置了threshold等于100,那么,一旦距离超过100,OpenCV的lable返回值就是-1
[*]理解了confidence和threshold,接下来可以写人脸识别的代码了,感谢咱们的充分准备,接下来是丝般顺滑的编码过程...
源码下载


[*]《JavaCV人脸识别三部曲》的完整源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称链接备注项目主页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文件夹下,如下图红框所示:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195304966-2146429069.png
[*]javacv-tutorials里面有多个子工程,《JavaCV人脸识别三部曲》系列的代码在simple-grab-push工程下:
https://img2023.cnblogs.com/blog/485422/202306/485422-20230629195305252-783181138.png
编码:人脸识别服务


[*]开始正式编码,今天咱们不会新建工程,而是继续使用《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;
      pconfidence = new double;
      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);
      predictRlt.setConfidence(pconfidence);

      return predictRlt;
    }
}

[*]上述代码有以下几处需要注意:

[*]构造方法中,通过faceRecognizer.setThreshold设置门限,我在实际使用中发现50比较合适,您可以根据自己的情况不断调整
[*]predict方法中,用于识别的图片要用resize方法调整大小,尺寸要和训练时的尺寸一致
[*]实测发现,在一张照片中出现多个人脸时,faceRecognizer.predict可能抛出RuntimeException异常,因此这里要捕获异常,避免程序崩溃退出
编码:检测和识别


[*]检测有关的接口DetectService.java,如下,和《JavaCV人脸识别三部曲之一:视频中的人脸保存为图片》中的完全一致:
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做识别,完整代码如下,有几处要注意的地方稍后提到:
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
页: [1]
查看完整版本: JavaCV人脸识别三部曲之三:识别和预览