Java + opencv 实现年龄识别
一、效果展示
还是同样的图片, 先来一张效果图压压惊. 看过上一篇文章的同学都知道, 前面已经实现了 人脸的性别识别, 所以这篇文章主要讲的是年龄识别. 大部分代码都是一样的, 主要是调用的dnn网络模型不一样, 预测的结果集合不一样. 其他的都一样. 不懂的往下看. 如果觉得本文对你有用, 请一键三连吧.听说点赞关注加收藏的人都是帅哥.(ps: 图片来源于 github 项目 faceai, 顺手就拿来用啦)

二、技术实现思路
1、人脸检测与画框
1.1 图片转换成灰色(降低为一维的灰度,减低计算强度)
1.2 图片上画矩形
1.3 使用训练分类器查找人脸
1.4 如果对于人脸检测不是很熟悉的话可以参考往期文章, 传送门
2、人脸年龄识别
2.1 加载年龄识别网络模型
2.2 遍历图片上检测到的人脸, 逐一检测年龄
2.3 在人脸矩形框上方显示年龄
3、显示年龄
因为opencv 原生的 Imgproc.putText 方法在图片上显示中文会乱码, 也是参考了网上的一些文章, 才找到了解决方案, 具体做法是, 需要将性别文字制作成水印添加到图片上, 这个过程中需要先将图片二位矩阵 Mat 转化成 Image , 添加完水印后再将 Image 转回成二位矩阵Mat
4、模型讲解
上一章的性别预测返回的是一个二分类结果
本章的年龄预测返回的是8个年龄的阶段
这个结果实际上是算出性别检测值在二维矩阵Mat的第1行中最大值的索引.
定义两个结果集合:
/**
* 性别预测返回的是一个二分类结果 Male, Female
*/
private final static List genderList = new ArrayList<>(Arrays.asList("男", "女"));
/**
* 年龄预测返回的是8个年龄的阶段!
*/
private final static List ageList = new ArrayList<>(Arrays.asList("(0-2)", "(4-6)", "(8-12)", "(15-20)", "(25-32)", "(38-43)", "(48-53)", "(60-100)"));
三、核心代码
年龄检测核心代码, 其他代码和上一节的代码完全一样, 如有需要可以去看看, 传送门:
/**
* 年龄检测
* @param img
* @param rect
* @param genderNet
* @return
*/
private static String getAge(Mat img, Rect rect, Net genderNet) {
Mat face = new Mat(img, rect);
// Resizing pictures to resolution of Caffe model
Imgproc.resize(face, face, new Size(140, 140));
// 灰度化
Imgproc.cvtColor(face, face, Imgproc.COLOR_RGBA2BGR);
// blob输入网络进行年龄的检测
Mat inputBlob = Dnn.blobFromImage(face, 1.0f, new Size(227, 227), MODEL_MEAN_VALUES, false, false);
genderNet.setInput(inputBlob, "data");
// 年龄检测进行前向传播
Mat probs = genderNet.forward("prob").reshape(1, 1);
Core.MinMaxLocResult mm = Core.minMaxLoc(probs);
// Result of gender recognition prediction.
double index = mm.maxLoc.x;
return ageList.get((int) index);
}
四、完整代码
package com.biubiu.example;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.imageio.ImageIO;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.core.Size;
import org.opencv.dnn.Dnn;
import org.opencv.dnn.Net;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;
/**
* @author :张音乐
* @date :Created in 2021/4/18 下午1:25
* @description:年龄识别
* @email: zhangyule1993@sina.com
* @version: 1.0
*/
public class ImageAgeDetect {
static {
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
/**
* 年龄识别模型
*/
private final static String ageProto = "D:/workspace/opencv/data/models/age_deploy.prototxt";
private final static String ageModel = "D:/workspace/opencv/data/models/age_net.caffemodel";
/**
* 年龄预测返回的是8个年龄的阶段!
*/
private final static List ageList = new ArrayList<>(Arrays.asList("(0-2)", "(4-6)", "(8-12)", "(15-20)", "(25-32)", "(38-43)", "(48-53)", "(60-100)"));
/**
* 模型均值
*/
private final static Scalar MODEL_MEAN_VALUES = new Scalar(78.4263377603, 87.7689143744, 114.895847746);
public static void main(String[] args) {
// 加载网络模型
Net ageNet = Dnn.readNetFromCaffe(ageProto, ageModel);
if (ageNet.empty()) {
System.out.println("无法打开网络模型...\n");
return;
}
// 加载图片矩阵
String filePath = "D:\\upload\\gather.png";
Mat img = Imgcodecs.imread(filePath);
// 人脸检测
MatOfRect faceRects = facePick(img);
// 定义一个颜色
Scalar color = new Scalar(0, 0, 255);
// 遍历检测到的图片
for(Rect rect : faceRects.toArray()) {
// 人脸画矩形框
drawRect(rect, img, color);
// 检测年龄
String gender = getAge(img, rect, ageNet);
// 图片上显示中文的年龄 ,因为原生的 opencv putText 显示中文会乱码, 所以需要特殊处理一下
img = putChineseTxt(img, gender, rect.x + rect.width / 2 - 5, rect.y - 10);
// Imgproc.putText(img, new String(gender.getBytes(StandardCharsets.UTF_8)), new Point(x, y), 2, 2, color);
}
// 显示图像
HighGui.imshow("预览", img);
HighGui.waitKey(0);
// 释放所有的窗体资源
HighGui.destroyAllWindows();
}
/**
* 在图片上的人脸区域画上矩形框
* @param rect
* @param img
* @param color
*/
private static void drawRect(Rect rect, Mat img, Scalar color) {
int x = rect.x;
int y = rect.y;
int w = rect.width;
int h = rect.height;
Imgproc.rectangle(img, new Point(x, y), new Point(x + h, y + w), color, 2);
}
/**
* 年龄检测
* @param img
* @param rect
* @param genderNet
* @return
*/
private static String getAge(Mat img, Rect rect, Net genderNet) {
Mat face = new Mat(img, rect);
// Resizing pictures to resolution of Caffe model
Imgproc.resize(face, face, new Size(140, 140));
// 颜色空间转换
Imgproc.cvtColor(face, face, Imgproc.COLOR_RGBA2BGR);
// blob输入网络进行年龄的检测
Mat inputBlob = Dnn.blobFromImage(face, 1.0f, new Size(227, 227), MODEL_MEAN_VALUES, false, false);
genderNet.setInput(inputBlob, "data");
// 年龄检测进行前向传播
Mat probs = genderNet.forward("prob").reshape(1, 1);
Core.MinMaxLocResult mm = Core.minMaxLoc(probs);
// Result of gender recognition prediction.
double index = mm.maxLoc.x;
return ageList.get((int) index);
}
/**
* 图片人脸检测
* @return
*/
private static MatOfRect facePick(Mat img) {
// 存放灰度图
Mat tempImg = new Mat();
// 摄像头获取的是彩色图像,所以先灰度化下
Imgproc.cvtColor(img, tempImg, Imgproc.COLOR_BGRA2GRAY);
// OpenCV人脸识别分类器
CascadeClassifier classifier = new CascadeClassifier("D:\\workspace\\opencv\\data\\haarcascades\\haarcascade_frontalface_default.xml");
// # 调用识别人脸
MatOfRect faceRects = new MatOfRect();
// 特征检测点的最小尺寸, 根据实际照片尺寸来选择, 不然测量结果可能不准确。
Size minSize = new Size(140, 140);
// 图像缩放比例,可理解为相机的X倍镜
double scaleFactor = 1.2;
// 对特征检测点周边多少有效点同时检测,这样可避免因选取的特征检测点太小而导致遗漏
int minNeighbors = 3;
// 人脸检测
// CV_HAAR_DO_CANNY_PRUNING
classifier.detectMultiScale(tempImg, faceRects, scaleFactor, minNeighbors, 0, minSize);
return faceRects;
}
/**
* Mat二维矩阵转Image
* @param matrix
* @param fileExtension
* @return
*/
public static BufferedImage matToImg(Mat matrix, String fileExtension) {
// convert the matrix into a matrix of bytes appropriate for
// this file extension
MatOfByte mob = new MatOfByte();
Imgcodecs.imencode(fileExtension, matrix, mob);
// convert the "matrix of bytes" into a byte array
byte[] byteArray = mob.toArray();
BufferedImage bufImage = null;
try {
InputStream in = new ByteArrayInputStream(byteArray);
bufImage = ImageIO.read(in);
} catch (Exception e) {
e.printStackTrace();
}
return bufImage;
}
/**
* BufferedImage转换成 Mat
* @param original
* @param imgType
* @param matType
* @return
*/
public static Mat imgToMat(BufferedImage original, int imgType, int matType) {
if (original == null) {
throw new IllegalArgumentException("original == null");
}
if (original.getType() != imgType){
// Create a buffered image
BufferedImage image = new BufferedImage(original.getWidth(), original.getHeight(), imgType);
// Draw the image onto the new buffer
Graphics2D g = image.createGraphics();
try {
g.setComposite(AlphaComposite.Src);
g.drawImage(original, 0, 0, null);
} finally {
g.dispose();
}
}
byte[] pixels = ((DataBufferByte) original.getRaster().getDataBuffer()).getData();
Mat mat = Mat.eye(original.getHeight(), original.getWidth(), matType);
mat.put(0, 0, pixels);
return mat;
}
/**
* 在图片上显示中文
* @param img
* @param gender
* @param x
* @param y
* @return
*/
private static Mat putChineseTxt(Mat img, String gender, int x, int y) {
Font font = new Font("微软雅黑", Font.PLAIN, 20);
BufferedImage bufImg = matToImg(img,".png");
Graphics2D g = bufImg.createGraphics();
g.drawImage(bufImg, 0, 0, bufImg.getWidth(), bufImg.getHeight(), null);
// 设置字体
g.setColor(new Color(255, 10, 52));
g.setFont(font);
// 设置水印的坐标
g.drawString(gender, x, y);
g.dispose();
// 加完水印再转换回来
return imgToMat(bufImg, BufferedImage.TYPE_3BYTE_BGR, CvType.CV_8UC3);
}
}
五、注意事项
需要性别识别网络模型的同学可以评论区域留下邮箱,我看到的话会给你发的, .代码无法调试通过也可以私信或者评论区提问, 如果我能解决都会提供帮助.
建议先读往期文章系统学习一下, 传送门: