ToB企服应用市场:ToB评测及商务社交产业平台

标题: Java版人脸跟踪三部曲之三:编码实战 [打印本页]

作者: 半亩花草    时间: 2023-7-8 12:52
标题: Java版人脸跟踪三部曲之三:编码实战
欢迎访问我的GitHub

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

程序主框架和关键类

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

方法名作用入参返回内部实现createTrackedObject主程序如果从视频帧中首次次检测到人脸,就会调用createTrackedObject方法,表示开始跟踪了mRgba:出现人脸的图片
region:人脸在图片中的位置无提取人脸的hue,生成直方图objectTracking开始跟踪后,主程序从摄像头取到的每一帧图片后,都会调用此方法,用于得到人脸在这一帧中的位置mRgba:图片人脸在输入图片中位置用人脸hue直方图对输入图片进行计算,得到反向投影图,在反向投影图上做CamShift计算得到人脸位置
方法名作用入参返回内部实现rgba2Hue将RGB颜色空间的图片转为HSV,再提取出hue通道,生成直方图rgba:人脸图片无List:直方图lostTrace对比objectTracking方法返回的结果与上次出现的位置,确定人有没有跟丢lastRect:上次出现的位置
currentRect:objectTracking方法检测到的当前帧上的位置true表示跟丢了,false表示没有跟丢对比两个矩形的差距是否超过一个门限,正常情况下连续两帧中的人脸差别不会太大,所以一旦差别大了就表示跟丢了,currentRect的位置上不是人脸
  1.     // 每一帧图像的反向投影图都用这个成员变量来保存
  2.     private Mat prob;
  3.     // 保存最近一次确认的头像的位置,每当新的一帧到来时,都从这个位置开始追踪(也就是反向投影图做CamShift计算的起始位置)
  4.     private Rect trackRect;
  5.     // 直方图,在跟丢之前,每一帧图像都要用到这个直方图来生成反向投影
  6.     private Mat hist;
复制代码
  1. package com.bolingcavalry.grabpush.extend;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.opencv.core.*;
  4. import org.opencv.imgproc.Imgproc;
  5. import org.opencv.video.Video;
  6. import java.util.Collections;
  7. import java.util.List;
  8. import java.util.Vector;
  9. /**
  10. * @author willzhao
  11. * @version 1.0
  12. * @description TODO
  13. * @date 2022/1/8 21:21
  14. */
  15. @Slf4j
  16. public class ObjectTracker {
  17.     /**
  18.      * 上一个矩形和当前矩形的差距达到多少的时候,才算跟丢,您可以自行调整
  19.      */
  20.     private static final double LOST_GATE = 0.8d;
  21.     // [0.0, 256.0]表示直方图能表示像素值从0.0到256的像素
  22.     private static final MatOfFloat RANGES = new MatOfFloat(0f, 256f);
  23.     private Mat mask;
  24.     // 保存用来追踪的每一帧的反向投影图
  25.     private Mat prob;
  26.     // 保存最近一次确认的头像的位置,每当新的一帧到来时,都从这个位置开始追踪(也就是反向投影图做CamShift计算的起始位置)
  27.     private Rect trackRect;
  28.     // 直方图
  29.     private Mat hist;
  30.     public ObjectTracker(Mat rgba) {
  31.         hist = new Mat();
  32.         trackRect = new Rect();
  33.         mask = new Mat(rgba.size(), CvType.CV_8UC1);
  34.         prob = new Mat(rgba.size(), CvType.CV_8UC1);
  35.     }
  36.     /**
  37.      * 将摄像头传来的图片提取出hue通道,放入hueList中
  38.      * 将摄像头传来的RGB颜色空间的图片转为HSV颜色空间,
  39.      * 然后检查HSV三个通道的值是否在指定范围内,mask中记录了检查结果
  40.      * 再将hsv中的hue提取出来
  41.      * @param rgba
  42.      */
  43.     private List<Mat> rgba2Hue(Mat rgba) {
  44.         // 实例化Mat,显然,hsv是三通道,hue是hsv三通道其中的一个,所以hue是一通道
  45.         Mat hsv = new Mat(rgba.size(), CvType.CV_8UC3);
  46.         Mat hue = new Mat(rgba.size(), CvType.CV_8UC1);
  47.         // 1. 先转换
  48.         // 转换颜色空间,RGB到HSV
  49.         Imgproc.cvtColor(rgba, hsv, Imgproc.COLOR_RGB2HSV);
  50.         int vMin = 65, vMax = 256, sMin = 55;
  51.         //inRange函数的功能是检查输入数组每个元素大小是否在2个给定数值之间,可以有多通道,mask保存0通道的最小值,也就是h分量
  52.         //这里利用了hsv的3个通道,比较h,0~180,s,smin~256,v,min(vmin,vmax),max(vmin,vmax)。如果3个通道都在对应的范围内,
  53.         //则mask对应的那个点的值全为1(0xff),否则为0(0x00).
  54.         Core.inRange(
  55.                 hsv,
  56.                 new Scalar(0, sMin, Math.min(vMin, vMax)),
  57.                 new Scalar(180, 256, Math.max(vMin, vMax)),
  58.                 mask
  59.         );
  60.         // 2. 再提取
  61.         // 把hsv的数据放入hsvList中,用于稍后提取出其中的hue
  62.         List<Mat> hsvList = new Vector<>();
  63.         hsvList.add(hsv);
  64.         // 准备好hueList,用于接收通道
  65.         // hue初始化为与hsv大小深度一样的矩阵,色调的度量是用角度表示的,红绿蓝之间相差120度,反色相差180度
  66.         hue.create(hsv.size(), hsv.depth());
  67.         List<Mat> hueList = new Vector<>();
  68.         hueList.add(hue);
  69.         // 描述如何提取:从目标的0位置提取到目的地的0位置
  70.         MatOfInt from_to = new MatOfInt(0, 0);
  71.         // 提取操作:将hsv第一个通道(也就是色调)的数复制到hue中,0索引数组
  72.         Core.mixChannels(hsvList, hueList, from_to);
  73.         return hueList;
  74.     }
  75.     /**
  76.      * 当外部调用方确定了人脸在图片中的位置后,就可以调用createTrackedObject开始跟踪,
  77.      * 该方法中会先生成人脸的hue的直方图,用于给后续帧生成反向投影
  78.      * @param mRgba
  79.      * @param region
  80.      */
  81.     public void createTrackedObject(Mat mRgba, Rect region) {
  82.         hist.release();
  83.         //将摄像头的视频帧转化成hsv,然后再提取出其中的hue通道
  84.         List<Mat> hueList = rgba2Hue(mRgba);
  85.         // 人脸区域的mask
  86.         Mat tempMask = mask.submat(region);
  87.         // histSize表示这个直方图分成多少份(即多少个直方柱),就是 bin的个数
  88.         MatOfInt histSize = new MatOfInt(25);
  89.         // 只要头像区域的数据
  90.         List<Mat> images = Collections.singletonList(hueList.get(0).submat(region));
  91.         // 计算头像的hue直方图,结果在hist中
  92.         Imgproc.calcHist(images, new MatOfInt(0), tempMask, hist, histSize, RANGES);
  93.         // 将hist矩阵进行数组范围归一化,都归一化到0~255
  94.         Core.normalize(hist, hist, 0, 255, Core.NORM_MINMAX);
  95.         // 这个trackRect记录了人脸最后一次出现的位置,后面新的帧到来时,就从trackRect位置开始做CamShift计算
  96.         trackRect = region;
  97.     }
  98.     /**
  99.      * 在开始跟踪后,每当摄像头新的一帧到来时,外部就会调用objectTracking,将新的帧传入,
  100.      * 此时,会用前面准备好的人脸hue直方图,将新的帧计算出反向投影图,
  101.      * 再在反向投影图上执行CamShift计算,找到密度最大处,即人脸在新的帧上的位置,
  102.      * 将这个位置作为返回值,返回
  103.      * @param mRgba 新的一帧
  104.      * @return 人脸在新的一帧上的位置
  105.      */
  106.     public Rect objectTracking(Mat mRgba) {
  107.         // 新的图片,提取hue
  108.         List<Mat> hueList;
  109.         try {
  110.            // 实测此处可能抛出异常,要注意捕获,避免程序退出
  111.             hueList = rgba2Hue(mRgba);
  112.         } catch (CvException cvException) {
  113.             log.error("cvtColor exception", cvException);
  114.             trackRect = null;
  115.             return null;
  116.         }
  117.         // 用头像直方图在新图片的hue通道数据中计算反向投影。
  118.         Imgproc.calcBackProject(hueList, new MatOfInt(0), hist, prob, RANGES, 1.0);
  119.         // 计算两个数组的按位连接(dst = src1 & src2)计算两个数组或数组和标量的每个元素的逐位连接。
  120.         Core.bitwise_and(prob, mask, prob, new Mat());
  121.         // 在反向投影上进行CamShift计算,返回值就是密度最大处,即追踪结果
  122.         RotatedRect rotatedRect = Video.CamShift(prob, trackRect, new TermCriteria(TermCriteria.EPS, 10, 1));
  123.         // 转为Rect对象
  124.         Rect camShiftRect = rotatedRect.boundingRect();
  125.         // 比较追踪前和追踪后的数据,如果出现太大偏差,就认为追踪失败
  126.         if (lostTrace(trackRect, camShiftRect)) {
  127.             log.info("lost trace!");
  128.             trackRect = null;
  129.             return null;
  130.         }
  131.         // 将本次最终到的目标作为下次追踪的对象
  132.         trackRect = camShiftRect;
  133.         return camShiftRect;
  134.     }
  135.     /**
  136.      * 变化率的绝对值
  137.      * @param last 变化前
  138.      * @param current 变化后
  139.      * @return
  140.      */
  141.     private static double changeRate(int last, int current) {
  142.         return Math.abs((double)(current-last)/(double) last);
  143.     }
  144.     /**
  145.      * 本次和上一次宽度或者高度的变化率,一旦超过阈值就认为跟踪失败
  146.      * @param lastRect
  147.      * @param currentRect
  148.      * @return
  149.      */
  150.     private static boolean lostTrace(Rect lastRect, Rect currentRect) {
  151.         // 0不能做除数,如果发现0就认跟丢了
  152.         if (lastRect.width<1 || lastRect.height<1) {
  153.             return true;
  154.         }
  155.         double widthChangeRate = changeRate(lastRect.width, currentRect.width);
  156.         if (widthChangeRate>LOST_GATE) {
  157.             log.info("1. lost trace, old [{}], new [{}], rate [{}]", lastRect.width, currentRect.width, widthChangeRate);
  158.             return true;
  159.         }
  160.         double heightChangeRate = changeRate(lastRect.height, currentRect.height);
  161.         if (heightChangeRate>LOST_GATE) {
  162.             log.info("2. lost trace, old [{}], new [{}], rate [{}]", lastRect.height, currentRect.height, heightChangeRate);
  163.             return true;
  164.         }
  165.         return false;
  166.     }
  167. }
复制代码
CamShiftDetectService.java:业务逻辑的提供者


方法名作用入参返回内部实现init被主程序调用的初始化方法,在应用启动的时候会调用一次无无加载人脸检测的模型convert每当主程序从摄像头拿到新的一帧后,都会调用此方法frame:来自摄像头的最新一帧被处理后的帧,会被主程序展现在预览窗口convert方法内部实现了前面提到的两种状态和行为(还未开始跟踪、已处于跟踪状态)releaseOutputResource程序结束前,被主程序调用的释放资源的方法无无释放一些成员变量的资源
  1. /**
  2.      * 每一帧原始图片的对象
  3.      */
  4.     private Mat grabbedImage = null;
  5.     /**
  6.      * 分类器
  7.      */
  8.     private CascadeClassifier classifier;
  9.     /**
  10.      * 转换器
  11.      */
  12.     private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
  13.     /**
  14.      * 模型文件的下载地址
  15.      */
  16.     private String modelFilePath;
  17.     /**
  18.      * 存放RGBA图片Mat
  19.      */
  20.     private Mat mRgba;
  21.     /**
  22.      * 存放灰度图片的Mat,仅用在人脸检测的时候
  23.      */
  24.     private Mat mGray;
  25.     /**
  26.      * 跟踪服务类
  27.      */
  28.     private ObjectTracker objectTracker;
  29.     /**
  30.      * 表示当前是否正在跟踪目标
  31.      */
  32.     private boolean isInTracing = false;
复制代码
  1. 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;@Slf4jpublic class CamShiftDetectService implements DetectService {    /**
  2.      * 每一帧原始图片的对象
  3.      */
  4.     private Mat grabbedImage = null;
  5.     /**
  6.      * 分类器
  7.      */
  8.     private CascadeClassifier classifier;
  9.     /**
  10.      * 转换器
  11.      */
  12.     private OpenCVFrameConverter.ToMat converter = new OpenCVFrameConverter.ToMat();
  13.     /**
  14.      * 模型文件的下载地址
  15.      */
  16.     private String modelFilePath;
  17.     /**
  18.      * 存放RGBA图片Mat
  19.      */
  20.     private Mat mRgba;
  21.     /**
  22.      * 存放灰度图片的Mat,仅用在人脸检测的时候
  23.      */
  24.     private Mat mGray;
  25.     /**
  26.      * 跟踪服务类
  27.      */
  28.     private ObjectTracker objectTracker;
  29.     /**
  30.      * 表示当前是否正在跟踪目标
  31.      */
  32.     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(grabbedImage);        }        // 代码走到这里,表示已经在追踪状态了        // 得到opencv的mat,其格式是RGBA        org.opencv.core.Mat openCVRGBAMat = Util.buildJavacvBGR2OpenCVRGBA(grabbedImage, mRgba);        // 在buildJavacvBGR2OpenCVRGBA方法内部,有可能在执行native方法的是否发生异常,要做针对性处理        if (null==openCVRGBAMat) {            return frame;        }        // 基于上一次的检测结果开始跟踪        org.opencv.core.Rect rotatedRect = objectTracker.objectTracking(openCVRGBAMat);        // 如果rotatedRect为空,表示跟踪失败,此时要修改状态为"未跟踪"        if (null==rotatedRect) {            isInTracing = false;            // 返回原始帧            return frame;        }        // 代码能走到这里,表示跟踪成功,拿到的新的一帧上的目标的位置,此时就在新位置上//        Util.rectOnImage(grabbedImage, rotatedRect.x, rotatedRect.y, rotatedRect.width, rotatedRect.height);        // 矩形框的整体向下放一些(总高度的五分之一),另外跟踪得到的高度过大,画出的矩形框把脖子也框上了,这里改用宽度作为高度        Util.rectOnImage(grabbedImage, rotatedRect.x, rotatedRect.y + rotatedRect.height/5, rotatedRect.width, rotatedRect.width);        return converter.convert(grabbedImage);    }    /**     * 程序结束前,释放人脸识别的资源     */    @Override    public void releaseOutputResource() {        if (null!=grabbedImage) {            grabbedImage.release();        }        if (null!=mGray) {            mGray.release();        }        if (null!=mRgba) {            mRgba.release();        }        if (null==classifier) {            classifier.close();        }    }}
复制代码
PreviewCameraWithCamShift.java:主程序

  1. protected CanvasFrame previewCanvas
复制代码
  1.     /**
  2.      * 检测工具接口
  3.      */
  4.     private DetectService detectService;
复制代码
  1.     /**
  2.      * 不同的检测工具,可以通过构造方法传入
  3.      * @param detectService
  4.      */
  5.     public PreviewCameraWithCamShift(DetectService detectService) {
  6.         this.detectService = detectService;
  7.     }
复制代码
  1.     @Override
  2.     protected void initOutput() throws Exception {
  3.         previewCanvas = new CanvasFrame("摄像头预览", CanvasFrame.getDefaultGamma() / grabber.getGamma());
  4.         previewCanvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  5.         previewCanvas.setAlwaysOnTop(true);
  6.         // 检测服务的初始化操作
  7.         detectService.init();
  8.     }
复制代码
  1.     @Override
  2.     protected void output(Frame frame) {
  3.         // 原始帧先交给检测服务处理,这个处理包括物体检测,再将检测结果标注在原始图片上,
  4.         // 然后转换为帧返回
  5.         Frame detectedFrame = detectService.convert(frame);
  6.         // 预览窗口上显示的帧是标注了检测结果的帧
  7.         previewCanvas.showImage(detectedFrame);
  8.     }
复制代码
  1.     @Override
  2.     protected void releaseOutputResource() {
  3.         if (null!= previewCanvas) {
  4.             previewCanvas.dispose();
  5.         }
  6.         // 检测工具也要释放资源
  7.         detectService.releaseOutputResource();
  8.     }
复制代码
  1.     @Override
  2.     protected int getInterval() {
  3.         return super.getInterval()/8;
  4.     }
复制代码
  1.     public static void main(String[] args) {
  2.         String modelFilePath = System.getProperty("model.file.path");
  3.         log.info("模型文件本地路径:{}", modelFilePath);
  4.         new PreviewCameraWithCamShift(new CamShiftDetectService(modelFilePath)).action(1000);
  5.     }
复制代码
运行程序要注意的地方


源码下载

名称链接备注项目主页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协议
欢迎关注博客园:程序员欣宸

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

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4