Java版人脸跟踪三部曲之三:编码实战

Posted 程序员欣宸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java版人脸跟踪三部曲之三:编码实战相关的知识,希望对你有一定的参考价值。

欢迎访问我的GitHub

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

《Java版人脸跟踪三部曲》全文链接

  1. 《极速体验》
  2. 《开发设计》
  3. 《编码实战》

本篇概览

  • 作为《Java版人脸跟踪三部曲》系列的终篇,本文会与大家一起写出完整的人脸跟踪应用代码
  • 前文《开发设计》中,已经对人脸跟踪的核心技术、应用主流程、异常处理等方方面面做了详细设计,建议您简单回顾一下
  • 接下来,自顶向下,先整体设计好主框架和关键类

程序主框架和关键类

  • 听欣宸唠叨了两篇文章,终于要看具体代码了,整体上看,最关键的三个类如下图:
  • 可见把功能、流程、知识点梳理清楚后,代码其实并不多,而且各司其职,分工明确,接下来开始编码,ObejctTracker负责实现跟踪功能,就从它开始

ObejctTracker.java:跟踪能力的提供者

  • 从前面的图中可知,与跟踪有关的服务都是ObejctTracker类提供的,此类涉及知识点略多,在编写代码前,先做一下简单的设计
  • 从功能看,ObejctTracker会对外提供如下两个方法:
方法名作用入参返回内部实现
createTrackedObject主程序如果从视频帧中首次次检测到人脸,就会调用createTrackedObject方法,表示开始跟踪了mRgba:出现人脸的图片
region:人脸在图片中的位置
提取人脸的hue,生成直方图
objectTracking开始跟踪后,主程序从摄像头取到的每一帧图片后,都会调用此方法,用于得到人脸在这一帧中的位置mRgba:图片人脸在输入图片中位置用人脸hue直方图对输入图片进行计算,得到反向投影图,在反向投影图上做CamShift计算得到人脸位置
  • 除了上述两个对外方法,ObejctTracker内部还要准备如下两个辅助方法:
方法名作用入参返回内部实现
rgba2Hue将RGB颜色空间的图片转为HSV,再提取出hue通道,生成直方图rgba:人脸图片List<Mat>:直方图
lostTrace对比objectTracking方法返回的结果与上次出现的位置,确定人有没有跟丢lastRect:上次出现的位置
currentRect:objectTracking方法检测到的当前帧上的位置
true表示跟丢了,false表示没有跟丢对比两个矩形的差距是否超过一个门限,正常情况下连续两帧中的人脸差别不会太大,所以一旦差别大了就表示跟丢了,currentRect的位置上不是人脸
  • 还有几个成员变量也很重要:
    // 每一帧图像的反向投影图都用这个成员变量来保存
    private Mat prob;

    // 保存最近一次确认的头像的位置,每当新的一帧到来时,都从这个位置开始追踪(也就是反向投影图做CamShift计算的起始位置)
    private Rect trackRect;

    // 直方图,在跟丢之前,每一帧图像都要用到这个直方图来生成反向投影
    private Mat hist;
  • 设计完成,现在可以给出完整的ObejctTracker.java源码了:
package com.bolingcavalry.grabpush.extend;

import lombok.extern.slf4j.Slf4j;
import org.opencv.core.*;
import org.opencv.imgproc.Imgproc;
import org.opencv.video.Video;
import java.util.Collections;
import java.util.List;
import java.util.Vector;

/**
 * @author willzhao
 * @version 1.0
 * @description TODO
 * @date 2022/1/8 21:21
 */
@Slf4j
public class ObjectTracker 

    /**
     * 上一个矩形和当前矩形的差距达到多少的时候,才算跟丢,您可以自行调整
     */
    private static final double LOST_GATE = 0.8d;

    // [0.0, 256.0]表示直方图能表示像素值从0.0到256的像素
    private static final MatOfFloat RANGES = new MatOfFloat(0f, 256f);

    private Mat mask;

    // 保存用来追踪的每一帧的反向投影图
    private Mat prob;

    // 保存最近一次确认的头像的位置,每当新的一帧到来时,都从这个位置开始追踪(也就是反向投影图做CamShift计算的起始位置)
    private Rect trackRect;

    // 直方图
    private Mat hist;


    public ObjectTracker(Mat rgba) 
        hist = new Mat();
        trackRect = new Rect();
        mask = new Mat(rgba.size(), CvType.CV_8UC1);
        prob = new Mat(rgba.size(), CvType.CV_8UC1);
    

    /**
     * 将摄像头传来的图片提取出hue通道,放入hueList中
     * 将摄像头传来的RGB颜色空间的图片转为HSV颜色空间,
     * 然后检查HSV三个通道的值是否在指定范围内,mask中记录了检查结果
     * 再将hsv中的hue提取出来
     * @param rgba
     */
    private List<Mat> rgba2Hue(Mat rgba) 
        // 实例化Mat,显然,hsv是三通道,hue是hsv三通道其中的一个,所以hue是一通道
        Mat hsv = new Mat(rgba.size(), CvType.CV_8UC3);
        Mat hue = new Mat(rgba.size(), CvType.CV_8UC1);

        // 1. 先转换
        // 转换颜色空间,RGB到HSV
        Imgproc.cvtColor(rgba, hsv, Imgproc.COLOR_RGB2HSV);

        int vMin = 65, vMax = 256, sMin = 55;
        //inRange函数的功能是检查输入数组每个元素大小是否在2个给定数值之间,可以有多通道,mask保存0通道的最小值,也就是h分量
        //这里利用了hsv的3个通道,比较h,0~180,s,smin~256,v,min(vmin,vmax),max(vmin,vmax)。如果3个通道都在对应的范围内,
        //则mask对应的那个点的值全为1(0xff),否则为0(0x00).
        Core.inRange(
                hsv,
                new Scalar(0, sMin, Math.min(vMin, vMax)),
                new Scalar(180, 256, Math.max(vMin, vMax)),
                mask
        );

        // 2. 再提取
        // 把hsv的数据放入hsvList中,用于稍后提取出其中的hue
        List<Mat> hsvList = new Vector<>();
        hsvList.add(hsv);

        // 准备好hueList,用于接收通道
        // hue初始化为与hsv大小深度一样的矩阵,色调的度量是用角度表示的,红绿蓝之间相差120度,反色相差180度
        hue.create(hsv.size(), hsv.depth());

        List<Mat> hueList = new Vector<>();
        hueList.add(hue);

        // 描述如何提取:从目标的0位置提取到目的地的0位置
        MatOfInt from_to = new MatOfInt(0, 0);

        // 提取操作:将hsv第一个通道(也就是色调)的数复制到hue中,0索引数组
        Core.mixChannels(hsvList, hueList, from_to);

        return hueList;
    

    /**
     * 当外部调用方确定了人脸在图片中的位置后,就可以调用createTrackedObject开始跟踪,
     * 该方法中会先生成人脸的hue的直方图,用于给后续帧生成反向投影
     * @param mRgba
     * @param region
     */
    public void createTrackedObject(Mat mRgba, Rect region) 
        hist.release();

        //将摄像头的视频帧转化成hsv,然后再提取出其中的hue通道
        List<Mat> hueList = rgba2Hue(mRgba);

        // 人脸区域的mask
        Mat tempMask = mask.submat(region);

        // histSize表示这个直方图分成多少份(即多少个直方柱),就是 bin的个数
        MatOfInt histSize = new MatOfInt(25);
        // 只要头像区域的数据
        List<Mat> images = Collections.singletonList(hueList.get(0).submat(region));
        // 计算头像的hue直方图,结果在hist中
        Imgproc.calcHist(images, new MatOfInt(0), tempMask, hist, histSize, RANGES);

        // 将hist矩阵进行数组范围归一化,都归一化到0~255
        Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX);

        // 这个trackRect记录了人脸最后一次出现的位置,后面新的帧到来时,就从trackRect位置开始做CamShift计算
        trackRect = region;
    

    /**
     * 在开始跟踪后,每当摄像头新的一帧到来时,外部就会调用objectTracking,将新的帧传入,
     * 此时,会用前面准备好的人脸hue直方图,将新的帧计算出反向投影图,
     * 再在反向投影图上执行CamShift计算,找到密度最大处,即人脸在新的帧上的位置,
     * 将这个位置作为返回值,返回
     * @param mRgba 新的一帧
     * @return 人脸在新的一帧上的位置
     */
    public Rect objectTracking(Mat mRgba) 
        // 新的图片,提取hue
        List<Mat> hueList;
        try 
           // 实测此处可能抛出异常,要注意捕获,避免程序退出
            hueList = rgba2Hue(mRgba);
         catch (CvException cvException) 
            log.error("cvtColor exception", cvException);
            trackRect = null;
            return null;
        

        // 用头像直方图在新图片的hue通道数据中计算反向投影。
        Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, prob, RANGES, 1.0);
        // 计算两个数组的按位连接(dst = src1 & src2)计算两个数组或数组和标量的每个元素的逐位连接。
        Core.bitwise_and(prob, mask, prob, new Mat());

        // 在反向投影上进行CamShift计算,返回值就是密度最大处,即追踪结果
        RotatedRect rotatedRect = Video.CamShift(prob, trackRect, new TermCriteria(TermCriteria.EPS, 10, 1));

        // 转为Rect对象
        Rect camShiftRect = rotatedRect.boundingRect();

        // 比较追踪前和追踪后的数据,如果出现太大偏差,就认为追踪失败
        if (lostTrace(trackRect, camShiftRect)) 
            log.info("lost trace!");
            trackRect = null;
            return null;
        

        // 将本次最终到的目标作为下次追踪的对象
        trackRect = camShiftRect;

        return camShiftRect;
    

    /**
     * 变化率的绝对值
     * @param last 变化前
     * @param current 变化后
     * @return
     */
    private static double changeRate(int last, int current) 
        return Math.abs((double)(current-last)/(double) last);
    

    /**
     * 本次和上一次宽度或者高度的变化率,一旦超过阈值就认为跟踪失败
     * @param lastRect
     * @param currentRect
     * @return
     */
    private static boolean lostTrace(Rect lastRect, Rect currentRect) 
        // 0不能做除数,如果发现0就认跟丢了
        if (lastRect.width<1 || lastRect.height<1) 
            return true;
        

        double widthChangeRate = changeRate(lastRect.width, currentRect.width);

        if (widthChangeRate>LOST_GATE) 
            log.info("1. lost trace, old [], new [], rate []", lastRect.width, currentRect.width, widthChangeRate);
            return true;
        

        double heightChangeRate = changeRate(lastRect.height, currentRect.height);

        if (heightChangeRate>LOST_GATE) 
            log.info("2. lost trace, old [], new [], rate []", lastRect.height, currentRect.height, heightChangeRate);
            return true;
        

        return false;
    

  • 最核心的跟踪服务已经完成,接下来要实现完整业务逻辑,即:CamShiftDetectService.java

CamShiftDetectService.java:业务逻辑的提供者

  • 有了核心能力,接下来要做的就是在业务中使用这个能力,前文已设计好完整的业务逻辑,这里先简单回顾一下:
  • 可见主要业务流程可以用两个状态+行为来表示:
  1. 还未开始跟踪:对每一帧做人脸检测,一旦检测到,就进入跟踪状态,并调用ObjectTracker.createTrackedObject生成人脸的hue直方图
  2. 已处于跟踪状态:对每一帧图像,都调用ObjectTracker.objectTracking去检查人脸在图像中的位置,直到到跟丢了为止,一旦跟丢了,就重新进入到还未开始跟踪的状态
  • 现在我们已经清楚了CamShiftDetectService.java要做的具体事情,接下来看看有哪些重要方法:
方法名作用入参返回内部实现
init被主程序调用的初始化方法,在应用启动的时候会调用一次加载人脸检测的模型
convert每当主程序从摄像头拿到新的一帧后,都会调用此方法frame:来自摄像头的最新一帧被处理后的帧,会被主程序展现在预览窗口convert方法内部实现了前面提到的两种状态和行为(还未开始跟踪、已处于跟踪状态)
releaseOutputResource程序结束前,被主程序调用的释放资源的方法释放一些成员变量的资源
  • 再来看看有哪些重要的成员变量,如下所示,isInTracing表示当前是否处于跟踪状态,classifier用于检测人脸:
/**
     * 每一帧原始图片的对象
     */
    private Mat grabbedImage = null;

    /**
     * 分类器
     */
    private CascadeClassifier classifier;

    /**
     * 转换器
     */
    private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();

    /**
     * 模型文件的下载地址
     */
    private String modelFilePath;

    /**
     * 存放RGBA图片Mat
     */
    private Mat mRgba;

    /**
     * 存放灰度图片的Mat,仅用在人脸检测的时候
     */
    private Mat mGray;

    /**
     * 跟踪服务类
     */
    private ObjectTracker objectTracker;

    /**
     * 表示当前是否正在跟踪目标
     */
    private boolean isInTracing = false;
  • 现在可以给出CamShiftDetectService.java的完整代码了:
package com.bolingcavalry.grabpush.extend;

import com.bolingcavalry.grabpush.Util;
import lombok.extern.slf4j.Slf4j;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_core.Mat;
import org.bytedeco.opencv.opencv_core.Rect;
import org.bytedeco.opencv.opencv_core.RectVector;
import org.bytedeco.opencv.opencv_objdetect.CascadeClassifier;
import java.io.File;
import static org.bytedeco.opencv.global.opencv_imgproc.CV_BGR2GRAY;
import static org.bytedeco.opencv.global.opencv_imgproc.cvtColor;

@Slf4j
public class CamShiftDetectService implements DetectService 

    /**
     * 每一帧原始图片的对象
     */
    private Mat grabbedImage = null;

    /**
     * 分类器
     */
    private CascadeClassifier classifier;

    /**
     * 转换器
     */
    private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();

    /**
     * 模型文件的下载地址
     */
    private String modelFilePath;

    /**
     * 存放RGBA图片Mat
     */
    private Mat mRgba;

    /**
     * 存放灰度图片的Mat,仅用在人脸检测的时候
     */
    private Mat mGray;

    /**
     * 跟踪服务类
     */
    private ObjectTracker objectTracker;

    /**
     * 表示当前是否正在跟踪目标
     */
    private boolean isInTracing = false;

    /**
     * 构造方法,在此指定模型文件的下载地址
     * @param modelFilePath
     */
    public CamShiftDetectService(String modelFilePath) 
        this.modelFilePath = modelFilePath;
    

    /**
     * 音频采样对象的初始化
     * @throws Exception
     */
    @Override
    public void init() throws Exception 
        log.info("开始加载模型文件");
        // 模型文件下载后的完整地址
        String classifierName = new File(modelFilePath).getAbsolutePath();

        // 根据模型文件实例化分类器
        classifier = new CascadeClassifier(classifierName);

        if (classifier == null) 
            log.error("Error loading classifier file []", classifierName);
            System.exit(1);
        

        log.info("模型文件加载完毕,初始化完成");
    



    @Override
    public Frame convert(Frame frame) 
        // 由帧转为Mat
        grabbedImage = converter.convert(frame);

        // 初始化灰度Mat
        if (null==mGray) 
            mGray = Util.initGrayImageMat(grabbedImage);
        

        // 初始化RGBA的Mat
        if (null==mRgba) 
            mRgba = Util.initRgbaImageMat(grabbedImage);
        

        // 如果未在追踪状态
        if (!isInTracing) 
            // 存放检测结果的容器
            RectVector objects = new RectVector();

            // 当前图片转为灰度图片
            cvtColor(grabbedImage, mGray, CV_BGR2GRAY);

            // 开始检测
            classifier.detectMultiScale(mGray, objects);

            // 检测结果总数
            long total = objects.size();

            // 当前实例是只追踪一人,因此一旦检测结果不等于一,就不处理,您可以根据自己业务情况修改此处
            if (total!=1) 
                objects.close();
                return frame;
            

            log.info("start new trace");

            Rect r = objects.get(0);
            int x = r.x(), y = r.y(), w = r.width(), h = r.height();

            // 得到opencv的mat,其格式是RGBA
            org.opencv.core.Mat openCVRGBAMat = Util.buildJavacvBGR2OpenCVRGBA(grabbedImage, mRgba);

            // 在buildJavacvBGR2OpenCVRGBA方法内部,有可能在执行native方法的是否发生异常,要做针对性处理
            if (null==openCVRGBAMat) 
                objects.close();
                return frame;
            

            // 如果第一次追踪,要实例化objectTracker
            if (null==objectTracker) 
                objectTracker = new ObjectTracker(openCVRGBAMat);
            

            // 创建跟踪目标
            objectTracker.createTrackedObject(openCVRGBAMat, new org.opencv.core.Rect(x, y, w, h));
            // 根据本次检测结果给原图标注人脸矩形框
            Util.rectOnImage(grabbedImage, x, y, w, h);

            // 释放检测结果资源
            objects.close();

            // 修改标志,表示当前正在跟踪
            isInTracing = true;

            // 将标注过的图片转为帧,返回
            return converter.convert(grabbedImageDocker下RabbitMQ四部曲之三:细说java开发

java版gRPC实战之三:服务端流

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

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

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

LeetCode952三部曲之三:再次优化(122ms -> 96ms,超51% -> 超91%)