对象识别
YOVO4的算法介绍文档链接:https://arxiv.org/pdf/2004.10934v1.pdf
YOLOv4配置文件下载地址: https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4.cfg
YOLOv4权重文件下载地址: https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights
分类名称文件下载地址: https://raw.githubusercontent.com/AlexeyAB/darknet/master/data/coco.names
此功能所实现的算法参考文章:https://xinchen.blog.csdn.net/article/details/120929514
功能简介
对象识别包括分类和检测两项任 务,分类用于判断一幅图像是否包含某类对象 ,检测则要求标出这些对象的位置和大小。对象识别是理解图像和场景的关键 ,具有广泛的应用前景 ,可用于 Web图像自动标注 、海量图像搜索 、图像内容过滤 、机器人 、安全监视 、医学远程会诊等多种领域。通用对象识别面临很多困难,迄今没有完善的解决方案,这些困难包括:(1)光照变化 、视点变化 、尺度变化 、物体变形 、遮挡 、背景嘈杂等多种因素使 同一 物体在不同图像中存在很大的差异 ;(2)同类物体之间存在较大差异 ,这要求识别模型即能体现 同类物体之间的共性 ,又不能混淆相似的物体类别 ;(3)大量的类别增加了系统实现的难度。近几年来 ,通用对象识别的研究非常活跃 ,新的方法不断涌现。
技术流程
对象识别系统使用训练图像,训练出识别模型 ,并利用这个模型识别新图像中的对象 ,这个过程一般包括预处理、特征提取、模型训练和对象识别四个阶段。
特征提取
特征提取提取图像的亮度模式,纹理细节、形状和轮廓等信息,包括特征选取和特征描述两部分内容。
原始的训练图像往往需要人工进行预处理 ,预处理后 ,可从训练图像中提取特征集 。特征集可以是全局特征 ,体现整幅图像的特点 ,也可以是局部特征 ,代表图像局部的特点。
常用的特征有包括:
(1)Difference of Gaussians(DoG):最早由Lowe提出,具有平移、尺度不变性,检测速度很快;
(2)Kadir&Brady检测子:通过圆形区域亮度直方图的局部最大熵寻找特征区域,能输出稳定、少量的圆形特征区域;
(3)多尺度Harris检测子:具有尺度不变性,适于检测角形区域;
(4)Hessian-Laplace类似于DoG,这两种方法都检测类似于元球(blob-like)的结构,但Hessian-Laplace方法在尺度一空间定位精度更高;
(5)Harris-Affine区域和Hessian-Affine区域对图像仿射变换具有不变性。
模型训练
不同的对象识别系统有不同的训练方法。很多方法来源于基本的机器学习技术,如boost、Winnow、支持向量机、RVM、贝叶斯理论、高斯混合模型、EM算法、决策树、决策树桩等技术。训练方法大致可分为两大类:求异法(discriminative approach)和泛化法(generative approach)。求异法试图在特征空间找到一条决策边界,将特征矢量分类,判断它是否属于某类物体。滑动窗口模型常采用求异法训练模型,SVM、决策树、决策树桩及boost类技术常用于求异法泛化法则尽可能多地找到某类对象的特征,根据这些特征出现的概率,使用贝叶斯理论、高斯混合模型判断对象的类别。基于部件的方法常采用泛化法设置、优化模型参数,EM算法常用来处理部件及其之间的关系,这种方法是一种迭代估计参数的方法,它可以处理数据缺失的问题,但不能保证找到全局最大值。
对象识别
提取了训练图像的特征集后,就可以利用这些特征集训练识别模型。识别模型有很多种,为了描述方便,本文大致把它们分成三大类,分别是基于特征袋(bag of feature)的识别模型,基于部件(part-based)的识别模型,基于滑动窗:(sliding-windows)的识别模型。
(1)特征袋模型又称为单词袋(bag of word),近几年广泛地应用于各种识别任务,这种模型非常适合多类对象的同时识别,它将自动文档分类技术引入对象识别,将一幅图像看成由大量视觉单词组成,每个视觉单词是矢量量化后的局部特征描述子,在对象识别中,某类对象相当于某类文档主题,寻找某类对象类似于根据某类单词出现的频率寻找文档的主题。该方法在多类对象识别测试中获得了较好的结果。特征袋模型的识别步骤一般为:特征提取、矢量量化、直方图计算、模型训练、对象识别。
(2)基于部件的模型:这种模型学习识别给定类对象共同的相似的部件,通过它们是否存在判断是否存在给定类对象。这种方法很适合具有固定结构和形状的物体。实现细节主要包括如何检测某个部件,采用何种空间结构模型处理部件之间的共存(co-occurrence)关系。在通用对象识别系统,一般采用相对较松散的空间结构模型,如星形网、树形结构、K-fans结构等来表示部件之间的关系。
(3)基于滑动窗口的模型,这种模型隐含地采用固定模板将空间信息表示成特征矢量。这种方法一般采用稠密的特征提取法。然后采用机器学习算法,如支持向量机(SVM),对隐含模板进行匹配,这种模型往往包含大量的特征集,因此常采用adaboost及级联等技术从大量的特征窗选择最有识别性的特征窗。基于滑动窗口的模型只需要少量的指导,检测精度也较高,但这类方法的定位能力较弱,需要专门的算法将检测到的多个窗口整合起来。
功能实现
本功能涉及到技术有 JavaCV、OpenCV、YOLO4等,但JavaCV已将这些做了封装,包括最终推理时所用的模型也是YOLO4官方提前训练好的,我们只需要知道如何使用JavaCV的API即可。
实现方法很简单,我们只需要让SpringBoot应用识别图片中的物体,其关键在如何使用已经训练好的神经网络模型,其实在OpenCV集成的DNN模块可以加载和使用YOLO4模型,我们只要找到使用OpenCV的办法即可。
核心代码
用神经网络检测物体
首先,上传的图片被转为Mat对象后(OpenCV中的重要数据结构,可以理解为矩阵,里面存放着图片每个像素的信息),被送入doPredict方法,该方法执行完毕后就得到了物体识别的结果。
细看doPredict方法,可见核心是用blobFromImage方法得到四维blob对象,再将这个对象送给神经网络去检测(net.setInput、net.forward)
要注意的是,blobFromImage、net.setInput、net.forward这些都是native方法,是OpenCV的dnn模块提供的
doPredict方法返回的是MatVector对象,这里面就是检测结果
/**
* 用神经网络执行推理
* @param src
* @return
*/
private MatVector doPredict(Mat src) {
// 将图片转为四维blog,并且对尺寸做调整
Mat inputBlob = blobFromImage(src,
1 / 255.0,
new Size(width, height),
new Scalar(0.0),
true,
false,
CV_32F);
// 神经网络输入
net.setInput(inputBlob);
// 设置输出结果保存的容器
MatVector outs = new MatVector(outNames.size());
// 推理,结果保存在outs中
net.forward(outs, outNames);
// 释放资源
inputBlob.release();
return outs;
}
处理原始检测结果
- 检测结果MatVector对象是个集合,里面有多个Mat对象,每个Mat对象是一个表格,里面有丰富的数据,具体的内容如下图:
看过上图后,相信我们对如何处理原始的检测结果已经胸有成竹了,只要从MatVector中逐个取出Mat,把每个Mat当做表格,将表格每一行中概率最大的列找到,此列就是该物体的类别了。
/**
* 推理完成后的操作
* @param frame
* @param outs
* @return
*/
private List<ObjectDetectionResult> postprocess(Mat frame, MatVector outs) {
final IntVector classIds = new IntVector();
final FloatVector confidences = new FloatVector();
final RectVector boxes = new RectVector();
// 处理神经网络的输出结果
for (int i = 0; i < outs.size(); ++i) {
// extract the bounding boxes that have a high enough score
// and assign their highest confidence class prediction.
// 每个检测到的物体,都有对应的每种类型的置信度,取最高的那种
// 例如检车到猫的置信度百分之九十,狗的置信度百分之八十,那就认为是猫
Mat result = outs.get(i);
FloatIndexer data = result.createIndexer();
// 将检测结果看做一个表格,
// 每一行表示一个物体,
// 前面四列表示这个物体的坐标,后面的每一列,表示这个物体在某个类别上的置信度,
// 每行都是从第五列开始遍历,找到最大值以及对应的列号,
for (int j = 0; j < result.rows(); j++) {
// minMaxLoc implemented in java because it is 1D
int maxIndex = -1;
float maxScore = Float.MIN_VALUE;
for (int k = 5; k < result.cols(); k++) {
float score = data.get(j, k);
if (score > maxScore) {
maxScore = score;
maxIndex = k - 5;
}
}
// 如果最大值大于之前设定的置信度门限,就表示可以确定是这类物体了,
// 然后就把这个物体相关的识别信息保存下来,要保存的信息有:类别、置信度、坐标
if (maxScore > confidenceThreshold) {
int centerX = (int) (data.get(j, 0) * frame.cols());
int centerY = (int) (data.get(j, 1) * frame.rows());
int width = (int) (data.get(j, 2) * frame.cols());
int height = (int) (data.get(j, 3) * frame.rows());
int left = centerX - width / 2;
int top = centerY - height / 2;
// 保存类别
classIds.push_back(maxIndex);
// 保存置信度
confidences.push_back(maxScore);
// 保存坐标
boxes.push_back(new Rect(left, top, width, height));
}
}
// 资源释放
data.release();
result.release();
}
// remove overlapping bounding boxes with NMS
IntPointer indices = new IntPointer(confidences.size());
FloatPointer confidencesPointer = new FloatPointer(confidences.size());
confidencesPointer.put(confidences.get());
// 非极大值抑制
NMSBoxes(boxes, confidencesPointer, confidenceThreshold, nmsThreshold, indices, 1.f, 0);
// 将检测结果放入BO对象中,便于业务处理
List<ObjectDetectionResult> detections = new ArrayList<>();
for (int i = 0; i < indices.limit(); ++i) {
final int idx = indices.get(i);
final Rect box = boxes.get(idx);
final int clsId = classIds.get(idx);
detections.add(new ObjectDetectionResult(
clsId,
names.get(clsId),
confidences.get(idx),
box.x(),
box.y(),
box.width(),
box.height()
));
// 释放资源
box.releaseReference();
}
// 释放资源
indices.releaseReference();
confidencesPointer.releaseReference();
classIds.releaseReference();
confidences.releaseReference();
boxes.releaseReference();
return detections;
}
可见代码很简单,就是把每个Mat当做表格来处理,有两处特别的地方要处理:
- confidenceThreshold变量,置信度门限,这里是0.5,如果某一行的最大概率连0.5都达不到,那就相当于已知所有类别的可能性都不大,那就不算识别出来了,所以不会存入detections集合中(不会在结果图片中标注)
- NMSBoxes:分类器进化为检测器时,在原始图像上从多个尺度产生窗口,这就导致下图左侧的效果,同一个人检测了多张人脸,此时用NMSBoxes来保留最优的一个结果
对于Mat对象对应的表格中,每一列到底是什么类别呢?这个表格是YOLO4的检测结果,所以每一列是什么类别应该由YOLO4来解释,官方提供了名为coco.names的文件,该文件的内容如下图,一共80行,每一行是表示一个类别:
postprocess方法执行完毕后,一张照片的识别结果就被放入名为detections的集合中,该集合内的每个元素代表一个识别出的物体,来看看这个元素的数据结构,如下所示,这些数据够我们在照片上标注识别结果了:
@Data
@AllArgsConstructor
public class ObjectDetectionResult {
// 类别索引
int classId;
// 类别名称
String className;
// 置信度
float confidence;
// 物体在照片中的横坐标
int x;
// 物体在照片中的纵坐标
int y;
// 物体宽度
int width;
// 物体高度
int height;
}
把检测结果画在图片上
手里有了检测结果,接下来要做的就是将这些结果画在原图上,这样就有了物体识别的效果,画图分两部分,首先是左上角的总耗时,其次是每个物体识别结果
负责画出总耗时的是printTimeUsed方法,如下,可见总耗时是用多层网络的总次数除以频率得到的,注意,这不是网页上的接口总耗时,而是神经网络识别物体的总耗时,例外画图的putText是个本地方法,这也是OpenCV的常用方法之一:
/**
* 计算出总耗时,并输出在图片的左上角
* @param src
*/
private void printTimeUsed(Mat src) {
// 总次数
long totalNums = net.getPerfProfile(new DoublePointer());
// 频率
double freq = getTickFrequency()/1000;
// 总次数除以频率就是总耗时
double t = totalNums / freq;
// 将本次检测的总耗时打印在展示图像的左上角
putText(src,
String.format("Inference time : %.2f ms", t),
new Point(10, 20),
FONT_HERSHEY_SIMPLEX,
0.6,
new Scalar(255, 0, 0, 0),
1,
LINE_AA,
false);
}
- 接下来是画出每个物体识别的结果,有了ObjectDetectionResult对象集合,画图就非常简单了:调用画矩形和文本的本地方法即可:
/**
* 将每一个被识别的对象在图片框出来,并在框的左上角标注该对象的类别
* @param src
* @param results
*/
private void markEveryDetectObject(Mat src, List<ObjectDetectionResult> results) {
// 在图片上标出每个目标以及类别和置信度
for(ObjectDetectionResult result : results) {
log.info("类别[{}],置信度[{}%]", result.getClassName(), result.getConfidence() * 100f);
// annotate on image
rectangle(src,
new Point(result.getX(), result.getY()),
new Point(result.getX() + result.getWidth(), result.getY() + result.getHeight()),
Scalar.MAGENTA,
1,
LINE_8,
0);
// 写在目标左上角的内容:类别+置信度
String label = result.getClassName() + ":" + String.format("%.2f%%", result.getConfidence() * 100f);
// 计算显示这些内容所需的高度
IntPointer baseLine = new IntPointer();
Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, baseLine);
int top = Math.max(result.getY(), labelSize.height());
// 添加内容到图片上
putText(src, label, new Point(result.getX(), top-4), FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 255, 0, 0), 1, LINE_4, false);
}
}
返回图片给前端
- 核心工作已经完成,接下来就是保存图片再将图片转为base64传输给前端(当然还有很多方式,这里先用最基本的本地方法):
//path 为图片在服务器的绝对路径
public static String getPhotoBase64(String path) {
try {
File file = new File(path);
FileInputStream fis;
fis = new FileInputStream(file);
long size = file.length();
log.info("文件大小=>" + size);
byte[] temp = new byte[(int) size];
fis.read(temp, 0, (int) size);
fis.close();
return new String(Base64.encodeBase64(temp));
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
至此,整个对象识别的功能就开发就完成了,Java在工程化方面的便利性,再结合深度学习领域的优秀模型,为我们们解决视觉图像问题增加了一个备选方案。