对象识别

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即可。

对象识别 - 图1

实现方法很简单,我们只需要让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对象,这里面就是检测结果

  1. /**
  2. * 用神经网络执行推理
  3. * @param src
  4. * @return
  5. */
  6. private MatVector doPredict(Mat src) {
  7. // 将图片转为四维blog,并且对尺寸做调整
  8. Mat inputBlob = blobFromImage(src,
  9. 1 / 255.0,
  10. new Size(width, height),
  11. new Scalar(0.0),
  12. true,
  13. false,
  14. CV_32F);
  15. // 神经网络输入
  16. net.setInput(inputBlob);
  17. // 设置输出结果保存的容器
  18. MatVector outs = new MatVector(outNames.size());
  19. // 推理,结果保存在outs
  20. net.forward(outs, outNames);
  21. // 释放资源
  22. inputBlob.release();
  23. return outs;
  24. }

处理原始检测结果

  • 检测结果MatVector对象是个集合,里面有多个Mat对象,每个Mat对象是一个表格,里面有丰富的数据,具体的内容如下图:

对象识别 - 图2

看过上图后,相信我们对如何处理原始的检测结果已经胸有成竹了,只要从MatVector中逐个取出Mat,把每个Mat当做表格,将表格每一行中概率最大的列找到,此列就是该物体的类别了。

  1. /**
  2. * 推理完成后的操作
  3. * @param frame
  4. * @param outs
  5. * @return
  6. */
  7. private List<ObjectDetectionResult> postprocess(Mat frame, MatVector outs) {
  8. final IntVector classIds = new IntVector();
  9. final FloatVector confidences = new FloatVector();
  10. final RectVector boxes = new RectVector();
  11. // 处理神经网络的输出结果
  12. for (int i = 0; i < outs.size(); ++i) {
  13. // extract the bounding boxes that have a high enough score
  14. // and assign their highest confidence class prediction.
  15. // 每个检测到的物体,都有对应的每种类型的置信度,取最高的那种
  16. // 例如检车到猫的置信度百分之九十,狗的置信度百分之八十,那就认为是猫
  17. Mat result = outs.get(i);
  18. FloatIndexer data = result.createIndexer();
  19. // 将检测结果看做一个表格,
  20. // 每一行表示一个物体,
  21. // 前面四列表示这个物体的坐标,后面的每一列,表示这个物体在某个类别上的置信度,
  22. // 每行都是从第五列开始遍历,找到最大值以及对应的列号,
  23. for (int j = 0; j < result.rows(); j++) {
  24. // minMaxLoc implemented in java because it is 1D
  25. int maxIndex = -1;
  26. float maxScore = Float.MIN_VALUE;
  27. for (int k = 5; k < result.cols(); k++) {
  28. float score = data.get(j, k);
  29. if (score > maxScore) {
  30. maxScore = score;
  31. maxIndex = k - 5;
  32. }
  33. }
  34. // 如果最大值大于之前设定的置信度门限,就表示可以确定是这类物体了,
  35. // 然后就把这个物体相关的识别信息保存下来,要保存的信息有:类别、置信度、坐标
  36. if (maxScore > confidenceThreshold) {
  37. int centerX = (int) (data.get(j, 0) * frame.cols());
  38. int centerY = (int) (data.get(j, 1) * frame.rows());
  39. int width = (int) (data.get(j, 2) * frame.cols());
  40. int height = (int) (data.get(j, 3) * frame.rows());
  41. int left = centerX - width / 2;
  42. int top = centerY - height / 2;
  43. // 保存类别
  44. classIds.push_back(maxIndex);
  45. // 保存置信度
  46. confidences.push_back(maxScore);
  47. // 保存坐标
  48. boxes.push_back(new Rect(left, top, width, height));
  49. }
  50. }
  51. // 资源释放
  52. data.release();
  53. result.release();
  54. }
  55. // remove overlapping bounding boxes with NMS
  56. IntPointer indices = new IntPointer(confidences.size());
  57. FloatPointer confidencesPointer = new FloatPointer(confidences.size());
  58. confidencesPointer.put(confidences.get());
  59. // 非极大值抑制
  60. NMSBoxes(boxes, confidencesPointer, confidenceThreshold, nmsThreshold, indices, 1.f, 0);
  61. // 将检测结果放入BO对象中,便于业务处理
  62. List<ObjectDetectionResult> detections = new ArrayList<>();
  63. for (int i = 0; i < indices.limit(); ++i) {
  64. final int idx = indices.get(i);
  65. final Rect box = boxes.get(idx);
  66. final int clsId = classIds.get(idx);
  67. detections.add(new ObjectDetectionResult(
  68. clsId,
  69. names.get(clsId),
  70. confidences.get(idx),
  71. box.x(),
  72. box.y(),
  73. box.width(),
  74. box.height()
  75. ));
  76. // 释放资源
  77. box.releaseReference();
  78. }
  79. // 释放资源
  80. indices.releaseReference();
  81. confidencesPointer.releaseReference();
  82. classIds.releaseReference();
  83. confidences.releaseReference();
  84. boxes.releaseReference();
  85. return detections;
  86. }

可见代码很简单,就是把每个Mat当做表格来处理,有两处特别的地方要处理:

  • confidenceThreshold变量,置信度门限,这里是0.5,如果某一行的最大概率连0.5都达不到,那就相当于已知所有类别的可能性都不大,那就不算识别出来了,所以不会存入detections集合中(不会在结果图片中标注)
  • NMSBoxes:分类器进化为检测器时,在原始图像上从多个尺度产生窗口,这就导致下图左侧的效果,同一个人检测了多张人脸,此时用NMSBoxes来保留最优的一个结果

对于Mat对象对应的表格中,每一列到底是什么类别呢?这个表格是YOLO4的检测结果,所以每一列是什么类别应该由YOLO4来解释,官方提供了名为coco.names的文件,该文件的内容如下图,一共80行,每一行是表示一个类别:

对象识别 - 图3 对象识别 - 图4

postprocess方法执行完毕后,一张照片的识别结果就被放入名为detections的集合中,该集合内的每个元素代表一个识别出的物体,来看看这个元素的数据结构,如下所示,这些数据够我们在照片上标注识别结果了:

  1. @Data
  2. @AllArgsConstructor
  3. public class ObjectDetectionResult {
  4. // 类别索引
  5. int classId;
  6. // 类别名称
  7. String className;
  8. // 置信度
  9. float confidence;
  10. // 物体在照片中的横坐标
  11. int x;
  12. // 物体在照片中的纵坐标
  13. int y;
  14. // 物体宽度
  15. int width;
  16. // 物体高度
  17. int height;
  18. }

把检测结果画在图片上

  • 手里有了检测结果,接下来要做的就是将这些结果画在原图上,这样就有了物体识别的效果,画图分两部分,首先是左上角的总耗时,其次是每个物体识别结果

  • 负责画出总耗时的是printTimeUsed方法,如下,可见总耗时是用多层网络的总次数除以频率得到的,注意,这不是网页上的接口总耗时,而是神经网络识别物体的总耗时,例外画图的putText是个本地方法,这也是OpenCV的常用方法之一:

  1. /**
  2. * 计算出总耗时,并输出在图片的左上角
  3. * @param src
  4. */
  5. private void printTimeUsed(Mat src) {
  6. // 总次数
  7. long totalNums = net.getPerfProfile(new DoublePointer());
  8. // 频率
  9. double freq = getTickFrequency()/1000;
  10. // 总次数除以频率就是总耗时
  11. double t = totalNums / freq;
  12. // 将本次检测的总耗时打印在展示图像的左上角
  13. putText(src,
  14. String.format("Inference time : %.2f ms", t),
  15. new Point(10, 20),
  16. FONT_HERSHEY_SIMPLEX,
  17. 0.6,
  18. new Scalar(255, 0, 0, 0),
  19. 1,
  20. LINE_AA,
  21. false);
  22. }
  • 接下来是画出每个物体识别的结果,有了ObjectDetectionResult对象集合,画图就非常简单了:调用画矩形和文本的本地方法即可:
  1. /**
  2. * 将每一个被识别的对象在图片框出来,并在框的左上角标注该对象的类别
  3. * @param src
  4. * @param results
  5. */
  6. private void markEveryDetectObject(Mat src, List<ObjectDetectionResult> results) {
  7. // 在图片上标出每个目标以及类别和置信度
  8. for(ObjectDetectionResult result : results) {
  9. log.info("类别[{}],置信度[{}%]", result.getClassName(), result.getConfidence() * 100f);
  10. // annotate on image
  11. rectangle(src,
  12. new Point(result.getX(), result.getY()),
  13. new Point(result.getX() + result.getWidth(), result.getY() + result.getHeight()),
  14. Scalar.MAGENTA,
  15. 1,
  16. LINE_8,
  17. 0);
  18. // 写在目标左上角的内容:类别+置信度
  19. String label = result.getClassName() + ":" + String.format("%.2f%%", result.getConfidence() * 100f);
  20. // 计算显示这些内容所需的高度
  21. IntPointer baseLine = new IntPointer();
  22. Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, baseLine);
  23. int top = Math.max(result.getY(), labelSize.height());
  24. // 添加内容到图片上
  25. putText(src, label, new Point(result.getX(), top-4), FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 255, 0, 0), 1, LINE_4, false);
  26. }
  27. }

返回图片给前端

  • 核心工作已经完成,接下来就是保存图片再将图片转为base64传输给前端(当然还有很多方式,这里先用最基本的本地方法):
  1. //path 为图片在服务器的绝对路径
  2. public static String getPhotoBase64(String path) {
  3. try {
  4. File file = new File(path);
  5. FileInputStream fis;
  6. fis = new FileInputStream(file);
  7. long size = file.length();
  8. log.info("文件大小=>" + size);
  9. byte[] temp = new byte[(int) size];
  10. fis.read(temp, 0, (int) size);
  11. fis.close();
  12. return new String(Base64.encodeBase64(temp));
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. return null;
  16. }
  17. }

至此,整个对象识别的功能就开发就完成了,Java在工程化方面的便利性,再结合深度学习领域的优秀模型,为我们们解决视觉图像问题增加了一个备选方案。