
《Java版人脸跟踪三部曲》全文链接这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
- 《极速体验》《开发设计》《编码实战》
作为《Java版人脸跟踪三部曲》系列的终篇,本文会与大家一起写出完整的人脸跟踪应用代码前文《开发设计》中,已经对人脸跟踪的核心技术、应用主流程、异常处理等方方面面做了详细设计,建议您简单回顾一下接下来,自顶向下,先整体设计好主框架和关键类 程序主框架和关键类
听欣宸唠叨了两篇文章,终于要看具体代码了,整体上看,最关键的三个类如下图:
可见把功能、流程、知识点梳理清楚后,代码其实并不多,而且各司其职,分工明确,接下来开始编码,ObejctTracker负责实现跟踪功能,就从它开始
ObejctTracker.java:跟踪能力的提供者
从前面的图中可知,与跟踪有关的服务都是ObejctTracker类提供的,此类涉及知识点略多,在编写代码前,先做一下简单的设计从功能看,ObejctTracker会对外提供如下两个方法:
方法名 作用 入参 返回 内部实现
region:人脸在图片中的位置无提取人脸的hue,生成直方图
除了上述两个对外方法,ObejctTracker内部还要准备如下两个辅助方法:
方法名 作用 入参 返回 内部实现
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;
@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);
}
private List 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 hsvList = new Vector<>();
hsvList.add(hsv);
// 准备好hueList,用于接收通道
// hue初始化为与hsv大小深度一样的矩阵,色调的度量是用角度表示的,红绿蓝之间相差120度,反色相差180度
hue.create(hsv.size(), hsv.depth());
List 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;
}
public void createTrackedObject(Mat mRgba, Rect region) {
hist.release();
//将摄像头的视频帧转化成hsv,然后再提取出其中的hue通道
List hueList = rgba2Hue(mRgba);
// 人脸区域的mask
Mat tempMask = mask.submat(region);
// histSize表示这个直方图分成多少份(即多少个直方柱),就是 bin的个数
MatOfInt histSize = new MatOfInt(25);
// 只要头像区域的数据
List 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;
}
public Rect objectTracking(Mat mRgba) {
// 新的图片,提取hue
List 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;
}
private static double changeRate(int last, int current) {
return Math.abs((double)(current-last)/(double) last);
}
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:业务逻辑的提供者
有了核心能力,接下来要做的就是在业务中使用这个能力,前文已设计好完整的业务逻辑,这里先简单回顾一下:
可见主要业务流程可以用两个状态+行为来表示:
- 还未开始跟踪:对每一帧做人脸检测,一旦检测到,就进入跟踪状态,并调用ObjectTracker.createTrackedObject生成人脸的hue直方图已处于跟踪状态:对每一帧图像,都调用ObjectTracker.objectTracking去检查人脸在图像中的位置,直到到跟丢了为止,一旦跟丢了,就重新进入到还未开始跟踪的状态
现在我们已经清楚了CamShiftDetectService.java要做的具体事情,接下来看看有哪些重要方法:
方法名 作用 入参 返回 内部实现
再来看看有哪些重要的成员变量,如下所示,isInTracing表示当前是否处于跟踪状态,classifier用于检测人脸:
private Mat grabbedImage = null;
private CascadeClassifier classifier;
private OpenCVframeConverter.ToMat converter = new OpenCVframeConverter.ToMat();
private String modelFilePath;
private Mat mRgba;
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;
private Mat mRgba;
private Mat mGray;
private ObjectTracker objectTracker;
private boolean isInTracing = false;
public CamShiftDetectService(String modelFilePath) {
this.modelFilePath = modelFilePath;
}
@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:主程序
《JavaCV的摄像头实战之一:基础》创建的simple-grab-push工程中已经准备好了父类AbstractCameraApplication,所以本篇继续使用该工程,创建子类PreviewCameraWithCamShift实现那些抽象方法即可编码前先回顾父类的基础结构,如下图,粗体是父类定义的各个方法,红色块都是需要子类来实现抽象方法,所以接下来,咱们以本地窗口预览为目标实现这三个红色方法即可:
新建文件PreviewCameraWithCamShift.java,这是AbstractCameraApplication的子类,其代码很简单,接下来按上图顺序依次说明先定义Canvasframe类型的成员变量previewCanvas,这是展示视频帧的本地窗口:
protected Canvasframe previewCanvas
把前面创建的DetectService作为成员变量,后面检测的时候会用到:
private DetectService detectService;
PreviewCameraWithCamShift的构造方法,接受DetectService的实例:
public PreviewCameraWithCamShift(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方法,代码如下,请注意人脸检测所需的模型文件的路径来自系统变量:
public static void main(String[] args) {
String modelFilePath = System.getProperty("model.file.path");
log.info("模型文件本地路径:{}", modelFilePath);
new PreviewCameraWithCamShift(new CamShiftDetectService(modelFilePath)).action(1000);
}
至此,《Java版人脸跟踪三部曲》的代码已经全部写完了,可以像《Java版人脸跟踪三部曲之一:极速体验》文中那样运行起来了 运行程序要注意的地方
- 下载opencv在windows环境的动态链接库:https://download.csdn.net/download/boling_cavalry/75121158,我这里下载后放在:C:studyjavacvlibopencv_java453.dll人脸检测的模型文件,在GitHub下载,地址是:https://raw.github.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt.xml,我这里下载后放在:C:studyjavacvmodelhaarcascade_frontalface_alt.xml运行程序的时候,不论是打包成jar,还是直接在IDEA中运行,都要添加下面这两个命令参数,才能确保应用加载到dll和模型文件(请按照您自己的存储位置修改下面参数的值):
-Djava.library.path=C:studyjavacvlib-Dmodel.file.path=C:studyjavacvmodelhaarcascade_frontalface_alt.xml程序运行起来后,具体的效果与像《Java版人脸跟踪三部曲之一:极速体验》中一模一样,这里就不再赘述了,您自行验证就好其实本篇不运行程序,还有一个原因就是要过年了,用来检测人脸的群众演员临时涨价,要两份盒饭,欣宸实在是负担不起… 源码下载
《JavaCV人脸识别三部曲》的完整源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称 链接 备注
这个git项目中有多个文件夹,本篇的源码在javacv-tutorials文件夹下,如下图红框所示:
javacv-tutorials里面有多个子工程,《JavaCV的摄像头实战》系列的代码在simple-grab-push工程下:
至此,《Java版人脸跟踪三部曲》完美收官,但是《JavaCV的摄像头实战》系列还会继续呈现更多精彩内容,欢迎关注;马上过年了,欣宸祝各位读者们新年快乐!
你不孤单,欣宸原创一路相伴
- Java系列Spring系列Docker系列kubernetes系列数据库+中间件系列DevOps系列
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)