- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
这里分类和汇总了欣宸的全部原创(含配套源码): https://github.com/zq2599/blog_demos 。
方法名 | 作用 | 入参 | 返回 | 内部实现 |
---|---|---|---|---|
createTrackedObject | 主程序如果从视频帧中首次次检测到人脸,就会调用createTrackedObject方法,表示开始跟踪了 | mRgba:出现人脸的图片 region:人脸在图片中的位置 |
无 | 提取人脸的hue,生成直方图 |
objectTracking | 开始跟踪后,主程序从摄像头取到的每一帧图片后,都会调用此方法,用于得到人脸在这一帧中的位置 | mRgba:图片 | 人脸在输入图片中位置 | 用人脸hue直方图对输入图片进行计算,得到反向投影图,在反向投影图上做CamShift计算得到人脸位置 |
方法名 | 作用 | 入参 | 返回 | 内部实现 |
---|---|---|---|---|
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;
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;
}
}
方法名 | 作用 | 入参 | 返回 | 内部实现 |
---|---|---|---|---|
init | 被主程序调用的初始化方法,在应用启动的时候会调用一次 | 无 | 无 | 加载人脸检测的模型 |
convert | 每当主程序从摄像头拿到新的一帧后,都会调用此方法 | frame:来自摄像头的最新一帧 | 被处理后的帧,会被主程序展现在预览窗口 | convert方法内部实现了前面提到的两种状态和行为(还未开始跟踪、已处于跟踪状态) |
releaseOutputResource | 程序结束前,被主程序调用的释放资源的方法 | 无 | 无 | 释放一些成员变量的资源 |
/**
* 每一帧原始图片的对象
*/
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;
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(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();
}
}
}
protected CanvasFrame previewCanvas
/**
* 检测工具接口
*/
private DetectService detectService;
/**
* 不同的检测工具,可以通过构造方法传入
* @param detectService
*/
public PreviewCameraWithCamShift(DetectService detectService) {
this.detectService = detectService;
}
@Override
protected void initOutput() throws Exception {
previewCanvas = new CanvasFrame("摄像头预览", CanvasFrame.getDefaultGamma() / grabber.getGamma());
previewCanvas.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
previewCanvas.setAlwaysOnTop(true);
// 检测服务的初始化操作
detectService.init();
}
@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;
}
public static void main(String[] args) {
String modelFilePath = System.getProperty("model.file.path");
log.info("模型文件本地路径:{}", modelFilePath);
new PreviewCameraWithCamShift(new CamShiftDetectService(modelFilePath)).action(1000);
}
名称 | 链接 | 备注 |
---|---|---|
项目主页 | 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协议 |
学习路上,你不孤单,欣宸原创一路相伴... 。
最后此篇关于Java版人脸跟踪三部曲之三:编码实战的文章就讲到这里了,如果你想了解更多关于Java版人脸跟踪三部曲之三:编码实战的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在编写一个具有以下签名的 Java 方法。 void Logger(Method method, Object[] args); 如果一个方法(例如 ABC() )调用此方法 Logger,它应该
我是 Java 新手。 我的问题是我的 Java 程序找不到我试图用作的图像文件一个 JButton。 (目前这段代码什么也没做,因为我只是得到了想要的外观第一的)。这是我的主课 代码: packag
好的,今天我在接受采访,我已经编写 Java 代码多年了。采访中说“Java 垃圾收集是一个棘手的问题,我有几个 friend 一直在努力弄清楚。你在这方面做得怎么样?”。她是想骗我吗?还是我的一生都
我的 friend 给了我一个谜语让我解开。它是这样的: There are 100 people. Each one of them, in his turn, does the following
如果我将使用 Java 5 代码的应用程序编译成字节码,生成的 .class 文件是否能够在 Java 1.4 下运行? 如果后者可以工作并且我正在尝试在我的 Java 1.4 应用程序中使用 Jav
有关于why Java doesn't support unsigned types的问题以及一些关于处理无符号类型的问题。我做了一些搜索,似乎 Scala 也不支持无符号数据类型。限制是Java和S
我只是想知道在一个 java 版本中生成的字节码是否可以在其他 java 版本上运行 最佳答案 通常,字节码无需修改即可在 较新 版本的 Java 上运行。它不会在旧版本上运行,除非您使用特殊参数 (
我有一个关于在命令提示符下执行 java 程序的基本问题。 在某些机器上我们需要指定 -cp 。 (类路径)同时执行java程序 (test为java文件名与.class文件存在于同一目录下) jav
我已经阅读 StackOverflow 有一段时间了,现在我才鼓起勇气提出问题。我今年 20 岁,目前在我的家乡(罗马尼亚克卢日-纳波卡)就读 IT 大学。足以介绍:D。 基本上,我有一家提供簿记应用
我有 public JSONObject parseXML(String xml) { JSONObject jsonObject = XML.toJSONObject(xml); r
我已经在 Java 中实现了带有动态类型的简单解释语言。不幸的是我遇到了以下问题。测试时如下代码: def main() { def ks = Map[[1, 2]].keySet()
一直提示输入 1 到 10 的数字 - 结果应将 st、rd、th 和 nd 添加到数字中。编写一个程序,提示用户输入 1 到 10 之间的任意整数,然后以序数形式显示该整数并附加后缀。 public
我有这个 DownloadFile.java 并按预期下载该文件: import java.io.*; import java.net.URL; public class DownloadFile {
我想在 GUI 上添加延迟。我放置了 2 个 for 循环,然后重新绘制了一个标签,但这 2 个 for 循环一个接一个地执行,并且标签被重新绘制到最后一个。 我能做什么? for(int i=0;
我正在对对象 Student 的列表项进行一些测试,但是我更喜欢在 java 类对象中创建硬编码列表,然后从那里提取数据,而不是连接到数据库并在结果集中选择记录。然而,自从我这样做以来已经很长时间了,
我知道对象创建分为三个部分: 声明 实例化 初始化 classA{} classB extends classA{} classA obj = new classB(1,1); 实例化 它必须使用
我有兴趣使用 GPRS 构建车辆跟踪系统。但是,我有一些问题要问以前做过此操作的人: GPRS 是最好的技术吗?人们意识到任何问题吗? 我计划使用 Java/Java EE - 有更好的技术吗? 如果
我可以通过递归方法反转数组,例如:数组={1,2,3,4,5} 数组结果={5,4,3,2,1}但我的结果是相同的数组,我不知道为什么,请帮助我。 public class Recursion { p
有这样的标准方式吗? 包括 Java源代码-测试代码- Ant 或 Maven联合单元持续集成(可能是巡航控制)ClearCase 版本控制工具部署到应用服务器 最后我希望有一个自动构建和集成环境。
我什至不知道这是否可能,我非常怀疑它是否可能,但如果可以,您能告诉我怎么做吗?我只是想知道如何从打印机打印一些文本。 有什么想法吗? 最佳答案 这里有更简单的事情。 import javax.swin
我是一名优秀的程序员,十分优秀!