欢迎访问我的GitHub
这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos
本篇概览
作为《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;
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 ;
private static final MatOfFloat RANGES = new MatOfFloat (0f , 256f );
private Mat mask;
private Mat prob;
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<Mat> rgba2Hue (Mat rgba) {
Mat hsv = new Mat (rgba.size(), CvType.CV_8UC3);
Mat hue = new Mat (rgba.size(), CvType.CV_8UC1);
Imgproc.cvtColor(rgba, hsv, Imgproc.COLOR_RGB2HSV);
int vMin = 65 , vMax = 256 , sMin = 55 ;
Core.inRange(
hsv,
new Scalar (0 , sMin, Math.min(vMin, vMax)),
new Scalar (180 , 256 , Math.max(vMin, vMax)),
mask
);
List<Mat> hsvList = new Vector <>();
hsvList.add(hsv);
hue.create(hsv.size(), hsv.depth());
List<Mat> hueList = new Vector <>();
hueList.add(hue);
MatOfInt from_to = new MatOfInt (0 , 0 );
Core.mixChannels(hsvList, hueList, from_to);
return hueList;
}
public void createTrackedObject (Mat mRgba, Rect region) {
hist.release();
List<Mat> hueList = rgba2Hue(mRgba);
Mat tempMask = mask.submat(region);
MatOfInt histSize = new MatOfInt (25 );
List<Mat> images = Collections.singletonList(hueList.get(0 ).submat(region));
Imgproc.calcHist(images, new MatOfInt (0 ), tempMask, hist, histSize, RANGES);
Core.normalize(hist, hist, 0 , 255 , Core.NORM_MINMAX);
trackRect = region;
}
public Rect objectTracking (Mat mRgba) {
List<Mat> hueList;
try {
hueList = rgba2Hue(mRgba);
} catch (CvException cvException) {
log.error("cvtColor exception" , cvException);
trackRect = null ;
return null ;
}
Imgproc.calcBackProject(hueList, new MatOfInt (0 ), hist, prob, RANGES, 1.0 );
Core.bitwise_and(prob, mask, prob, new Mat ());
RotatedRect rotatedRect = Video.CamShift(prob, trackRect, new TermCriteria (TermCriteria.EPS, 10 , 1 ));
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) {
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要做的具体事情,接下来看看有哪些重要方法:
方法名
作用
入参
返回
内部实现
init
被主程序调用的初始化方法,在应用启动的时候会调用一次
无
无
加载人脸检测的模型
convert
每当主程序从摄像头拿到新的一帧后,都会调用此方法
frame:来自摄像头的最新一帧
被处理后的帧,会被主程序展现在预览窗口
convert方法内部实现了前面提到的两种状态和行为(还未开始跟踪、已处于跟踪状态)
releaseOutputResource
程序结束前,被主程序调用的释放资源的方法
无
无
释放一些成员变量的资源
再来看看有哪些重要的成员变量,如下所示,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) {
grabbedImage = converter.convert(frame);
if (null ==mGray) {
mGray = Util.initGrayImageMat(grabbedImage);
}
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();
org.opencv.core.Mat openCVRGBAMat = Util.buildJavacvBGR2OpenCVRGBA(grabbedImage, mRgba);
if (null ==openCVRGBAMat) {
objects.close();
return frame;
}
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);
}
org.opencv.core.Mat openCVRGBAMat = Util.buildJavacvBGR2OpenCVRGBA(grabbedImage, mRgba);
if (null ==openCVRGBAMat) {
return frame;
}
org.opencv.core.Rect rotatedRect = objectTracker.objectTracking(openCVRGBAMat);
if (null ==rotatedRect) {
isInTracing = false ;
return frame;
}
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 );
}
运行程序要注意的地方
下载opencv在windows环境的动态链接库:https://download.csdn.net/download/boling_cavalry/75121158,我这里下载后放在:C:\study\javacv\lib\opencv_java453.dll
人脸检测的模型文件,在GitHub下载,地址是:https://raw.github.com/opencv/opencv/master/data/haarcascades/haarcascade_frontalface_alt.xml,我这里下载后放在:C:\study\javacv\model\haarcascade_frontalface_alt.xml
运行程序的时候,不论是打包成jar,还是直接在IDEA中运行,都要添加下面这两个命令参数,才能确保应用加载到dll和模型文件(请按照您自己的存储位置修改下面参数的值):
-Djava.library.path=C:\study\javacv\lib
-Dmodel.file.path=C:\study\javacv\model\haarcascade_frontalface_alt.xml
程序运行起来后,具体的效果与像《Java版人脸跟踪三部曲之一:极速体验》 中一模一样,这里就不再赘述了,您自行验证就好
其实本篇不运行程序,还有一个原因就是要过年了,用来检测人脸的群众演员临时涨价,要两份盒饭,欣宸实在是负担不起...
源码下载
这个git项目中有多个文件夹,本篇的源码在javacv-tutorials 文件夹下,如下图红框所示:
javacv-tutorials 里面有多个子工程,《JavaCV的摄像头实战》系列的代码在simple-grab-push 工程下:
至此,《Java版人脸跟踪三部曲》完美收官,但是《JavaCV的摄像头实战》系列还会继续呈现更多精彩内容,欢迎关注;
欢迎关注博客园:程序员欣宸
学习路上,你不孤单,欣宸原创一路相伴...
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
2021-07-08 hive学习笔记之九:基础UDF