JavaCV的摄像头实战之十二:性别检测

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 本文是《JavaCV的摄像头实战》系列的第十二篇,咱们来开发一个实用功能:识别性别并显示在预览页面,如下图:
    JavaCV的摄像头实战之十二:性别检测

  • 今天的代码,主要功能如下图所示:
    JavaCV的摄像头实战之十二:性别检测

  • 如果您看过《JavaCV的摄像头实战》系列的其他文章,就会发现上图中只有蓝色部分是新增内容,其余的步骤都是固定套路,《JavaCV的摄像头实战》系列的每一个应用玩的都是相同套路:别看步骤挺多,其实都是同一个流程

关于性别和年龄检测

源码下载

名称 链接 备注
项目主页 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的摄像头实战之十二:性别检测
  • javacv-tutorials里面有多个子工程,《JavaCV的摄像头实战》系列的代码在simple-grab-push工程下:
    JavaCV的摄像头实战之十二:性别检测

准备:文件下载

  • 本次实战需要三个文件:
  1. 人脸检测的模型文件:https://raw.github.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt.xml
  2. 性别识别的配置文件:https://raw.githubusercontent.com/GilLevi/AgeGenderDeepLearning/master/gender_net_definitions/deploy.prototxt
  3. 性别识别的模型文件:https://raw.githubusercontent.com/GilLevi/AgeGenderDeepLearning/master/models/gender_net.caffemodel

准备:代码接口简介

  • 编码前,先把涉及到的所有java文件说明一下:
  1. AbstractCameraApplication.java:主程序的抽象类,这里面定义了打开摄像头、抓取每一帧、处理每一帧的基本框架,避免每个应用都把这些事情重复做一遍
  2. PreviewCameraWithGenderAge.java:主程序,是AbstractCameraApplication的实现类,本次实战的核心功能人脸检测和性别检测,都委托给它的成员变量detectService去完成
  3. DetectService.java:检测服务的接口,里面定义了几个重要的api,例如初始化、处理每一帧、释放资源等
  4. GenderDetectService.java:是DetectService接口的实现类,本次实战的核心功能都写在这个类中
  • 介绍完毕,可以开始编码了,先从最简单的主程序开始

编码:主程序

  • 《JavaCV的摄像头实战之一:基础》创建的simple-grab-push工程中已经准备好了父类AbstractCameraApplication,所以本篇继续使用该工程,创建子类实现那些抽象方法即可
  • 编码前先回顾父类的基础结构,如下图,粗体是父类定义的各个方法,红色块都是需要子类来实现抽象方法,所以接下来,咱们以本地窗口预览为目标实现这三个红色方法即可:
    JavaCV的摄像头实战之十二:性别检测
  • 新建文件PreviewCameraWithGenderAge.java,这是AbstractCameraApplication的子类,其代码很简单,接下来按上图顺序依次说明
  • 先定义CanvasFrame类型的成员变量previewCanvas,这是展示视频帧的本地窗口:
protected CanvasFrame previewCanvas 
  • 把前面创建的DetectService作为成员变量,后面检测的时候会用到:
    /**      * 检测工具接口      */     private DetectService detectService; 
  • PreviewCameraWithGenderAge的构造方法,接受DetectService的实例:
    /**      * 不同的检测工具,可以通过构造方法传入      * @param detectService      */     public PreviewCameraWithGenderAge(DetectService detectService) {         this.detectService = detectService;     } 
  • 然后是初始化操作,可见是previewCanvas的实例化和参数设置,还有检测、识别的初始化操作:
    @Override     protected void initOutput() throws Exception {         previewCanvas = new CanvasFrame("摄像头预览", CanvasFrame.getDefaultGamma() / grabber.getGamma());         previewCanvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);         previewCanvas.setAlwaysOnTop(true);          // 检测服务的初始化操作         detectService.init();     } 
  • 接下来是output方法,定义了拿到每一帧视频数据后做什么事情,这里调用了detectService.convert检测人脸并识别性别,然后在本地窗口显示:
    @Override     protected void output(Frame frame) {         // 原始帧先交给检测服务处理,这个处理包括物体检测,再将检测结果标注在原始图片上,         // 然后转换为帧返回         Frame detectedFrame = detectService.convert(frame);         // 预览窗口上显示的帧是标注了检测结果的帧         previewCanvas.showImage(detectedFrame);     } 
  • 最后是处理视频的循环结束后,程序退出前要做的事情,先关闭本地窗口,再释放检测服务的资源:
    @Override     protected void releaseOutputResource() {         if (null!= previewCanvas) {             previewCanvas.dispose();         }          // 检测工具也要释放资源         detectService.releaseOutputResource();     } 
  • 由于检测有些耗时,所以两帧之间的间隔时间要低于普通预览:
    @Override     protected int getInterval() {         return super.getInterval()/8;     } 
  • 至此,功能已开发完成,再写上main方法,代码如下,请注意AgeDetectService构造方法的三个入参,分别是前面下载的三个文件在本机的位置:
    public static void main(String[] args) {         String base = "E:\temp\202112\25\opencv\";            DetectService detectService = new GenderDetectService(                 base + "haarcascade_frontalface_alt.xml",                 base + "gender\deploy.prototxt",                 base + "gender\gender_net.caffemodel");                          new PreviewCameraWithGenderAge(detectService).action(1000);     } 
  • 主程序已经写完,接下来是核心功能

编码:服务接口回顾

  • 本篇的核心功能是检测性别,相关代码被封装在DetectService接口的实现类GenderDetectService中,这个DetectService接口是咱们的老朋友了,之前识别相关的实战都有它的身影,再来回顾一下,如下,定义了初始化、处理原始帧、释放资源等关键行为的接口:
package com.bolingcavalry.grabpush.extend;  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接口的实现类,也就是今天实战的核心:GenderDetectService.java

编码:检测服务实现

  • 今天的核心功能都集中在GenderDetectService.java中,直接贴出全部源码吧,有几处要注意的地方稍后会提到:
package com.bolingcavalry.grabpush.extend;  import com.bolingcavalry.grabpush.Constants; import lombok.extern.slf4j.Slf4j; import org.bytedeco.javacpp.indexer.Indexer; import org.bytedeco.javacv.Frame; import org.bytedeco.javacv.OpenCVFrameConverter; import org.bytedeco.opencv.opencv_core.*; import org.bytedeco.opencv.opencv_dnn.Net; import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;  import static org.bytedeco.opencv.global.opencv_core.NORM_MINMAX; import static org.bytedeco.opencv.global.opencv_core.normalize; import static org.bytedeco.opencv.global.opencv_dnn.blobFromImage; import static org.bytedeco.opencv.global.opencv_dnn.readNetFromCaffe; import static org.bytedeco.opencv.global.opencv_imgproc.*;  /**  * @author willzhao  * @version 1.0  * @description 音频相关的服务  * @date 2021/12/3 8:09  */ @Slf4j public class GenderDetectService implements DetectService {      /**      * 每一帧原始图片的对象      */     private Mat grabbedImage = null;      /**      * 原始图片对应的灰度图片对象      */     private Mat grayImage = null;      /**      * 分类器      */     private CascadeClassifier classifier;      /**      * 转换器      */     private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();      /**      * 人脸检测模型文件的下载地址      */     private String classifierModelFilePath;      /**      * 性别识别proto文件的下载地址      */     private String genderProtoFilePath;      /**      * 性别识别模型文件的下载地址      */     private String genderModelFilePath;      /**      * 推理性别的神经网络对象      */     private Net cnnNet;      /**      * 构造方法,在此指定proto和模型文件的下载地址      * @param classifierModelFilePath      * @param cnnProtoFilePath      * @param cnnModelFilePath      */     public GenderDetectService(String classifierModelFilePath,                                String cnnProtoFilePath,                                String cnnModelFilePath) {         this.classifierModelFilePath = classifierModelFilePath;         this.genderProtoFilePath = cnnProtoFilePath;         this.genderModelFilePath = cnnModelFilePath;     }      /**      * 初始化操作,主要是创建推理用的神经网络      * @throws Exception      */     @Override     public void init() throws Exception {         // 根据模型文件实例化分类器         classifier = new CascadeClassifier(classifierModelFilePath);         // 实例化推理性别的神经网络         cnnNet = readNetFromCaffe(genderProtoFilePath, genderModelFilePath);     }      @Override     public Frame convert(Frame frame) {         // 由帧转为Mat         grabbedImage = converter.convert(frame);          // 灰度Mat,用于检测         if (null==grayImage) {             grayImage = DetectService.buildGrayImage(grabbedImage);         }          // 当前图片转为灰度图片         cvtColor(grabbedImage, grayImage, CV_BGR2GRAY);          // 存放检测结果的容器         RectVector objects = new RectVector();          // 开始检测         classifier.detectMultiScale(grayImage, objects);          // 检测结果总数         long total = objects.size();          // 如果没有检测到结果,就用原始帧返回         if (total<1) {             return frame;         }          int pos_x;         int pos_y;          Mat faceMat;          //推理时的入参         Mat inputBlob;          // 推理结果         Mat prob;          // 如果有检测结果,就根据结果的数据构造矩形框,画在原图上         for (long i = 0; i < total; i++) {             Rect r = objects.get(i);              // 人脸对应的Mat实例(注意:要用彩图,不能用灰度图!!!)             faceMat = new Mat(grabbedImage, r);             // 缩放到神经网络所需的尺寸             resize(faceMat, faceMat, new Size(Constants.CNN_PREIDICT_IMG_WIDTH, Constants.CNN_PREIDICT_IMG_HEIGHT));             // 归一化             normalize(faceMat, faceMat, 0, Math.pow(2, frame.imageDepth), NORM_MINMAX, -1, null);             // 转为推理时所需的的blob类型             inputBlob = blobFromImage(faceMat);             // 为神经网络设置入参             cnnNet.setInput(inputBlob, "data", 1.0, null);      //set the network input             // 推理             prob = cnnNet.forward("prob");              // 根据推理结果得到在人脸上标注的内容             String lable = getDescriptionFromPredictResult(prob);              // 人脸标注的横坐标             pos_x = Math.max(r.tl().x()-10, 0);             // 人脸标注的纵坐标             pos_y = Math.max(r.tl().y()-10, 0);              // 给人脸做标注,标注性别             putText(grabbedImage, lable, new Point(pos_x, pos_y), FONT_HERSHEY_PLAIN, 1.5, new Scalar(0,255,0,2.0));              // 给人脸加边框时的边框位置             int x = r.x(), y = r.y(), w = r.width(), h = r.height();             // 给人脸加边框             rectangle(grabbedImage, new Point(x, y), new Point(x + w, y + h), Scalar.RED, 1, CV_AA, 0);         }          // 释放检测结果资源         objects.close();          // 将标注过的图片转为帧,返回         return converter.convert(grabbedImage);     }      /**      * 程序结束前,释放人脸识别的资源      */     @Override     public void releaseOutputResource() {         if (null!=grabbedImage) {             grabbedImage.release();         }          if (null!=grayImage) {             grayImage.release();         }          if (null!=classifier) {             classifier.close();         }          if (null!= cnnNet) {             cnnNet.close();         }     }      /**      * 根据推理结果得到在头像上要标注的内容      * @param prob      * @return      */     protected String getDescriptionFromPredictResult(Mat prob) {         Indexer indexer = prob.createIndexer();          // 比较两种性别的概率,概率大的作为当前头像的性别         return indexer.getDouble(0,0) > indexer.getDouble(0,1)                ? "male"                : "female";     } } 
  • 上述代码,有以下几处需要注意的:
  1. 构造方法的三个入参:classifierModelFilePath、cnnProtoFilePath、cnnModelFilePath分别是人脸检测模型、性别检测配置、性别检测模型三个文件的本地存放地址
  2. 检测性别靠的是卷积神经网络的推理,初始化的时候通过readNetFromCaffe方法新建神经网络对象
  3. convert方法被调用时,会收到摄像头捕捉的每一帧,在这里面先检测出每个人脸,再拿每个人脸去神经网络进行推理
  4. 用神经网络的推理结果生成人脸的标注内容,这段逻辑被放入getDescriptionFromPredictResult,下一篇《年龄检测》的实战同样是使用神经网络推理头像的年龄,咱们只要写一个GenderDetectService,并重写getDescriptionFromPredictResult方法,里面的逻辑改成根据推理结果得到年龄,即可轻松完成任务,其他类都可以维持不变
  • 至此,编码完成,接下来开始验证

验证

  • 确保摄像头工作正常,运行PreviewCameraWithGenderAge类的main方法
  • 请群众演员登场,让他站在摄像头前,如下图,性别识别成功,且实时展示:
    JavaCV的摄像头实战之十二:性别检测
  • 至此,本地窗口预览集成人脸检测和性别检测的功能就完成了,得益于JavaCV的强大,整个过程是如此的轻松愉快,接下来请继续关注欣宸原创,《JavaCV的摄像头实战》系列还会呈现更多丰富的应用;
  • 得益于本篇所做的扩展准备,下一篇《年龄检测》会更加简单,一起来期待下一段轻松愉快的旅程吧;

欢迎关注博客园:程序员欣宸

学习路上,你不孤单,欣宸原创一路相伴...

发表评论

评论已关闭。

相关文章