前言
现在基于人体照片做相关业务的场景越来越多了,但是开源的、精细化的那就很少。基于OpenCV实际上是借助它暴露的Api做业务,简化过程,它的成功就在于暴露Api,让专门的事情专门做,业务的事情业务做。
专业知识
1.图片分析原理
基于 OpenCV 的图片分析原理涉及多个计算机视觉领域的核心技术。以下是详细的原理分析:
1.1图像表示原理
数字图像基础
OpenCV 使用 Mat(矩阵) 对象表示图像。
像素存储格式:
•灰度图:单通道,每个像素 0-255 的亮度值
•彩色图:三通道(BGR),每个通道 0-255
•透明度:四通道(BGRA),增加 alpha 通道
1.2图像预处理原理
1.2.1色彩空间转换
1.2.2图像滤波原理
**高斯模糊:**使用高斯函数加权平均,减少噪声
**中值滤波:**用邻域中值替换当前像素,有效去除椒盐噪声。
1.3特征检测原理
1.3.1边缘检测(Canny算法)
1.3.2角点检测
Harris角点检测原理:
- 计算图像在各个方向的梯度变化
- 通过特征值判断是否为角点
- 响应函数:R = det(M) - k*(trace(M))^2
1.4机器学习检测原理
1.4.1 Haar级联分类器原理
工作原理:
- Haar特征:计算矩形区域的像素和之差
- 积分图:快速计算任意矩形区域的像素和
- AdaBoost:组合多个弱分类器形成强分类器
- 级联结构:快速排除非目标区域
1.4.2HOG(方向梯度直方图)+ SVM
原理: - 计算图像的梯度方向和幅值
- 将图像分成小细胞单元,统计梯度方向直方图
- 对细胞单元进行对比度归一化
- 使用SVM分类器进行目标检测
1.5深度学习原理
1.5.1基于深度神经网络的目标检测
YOLO/SSD原理:
•单次检测:直接在网络中预测边界框和类别概率
•锚点框:预定义不同尺度和长宽比的候选框
•非极大值抑制:去除重叠的检测框
1.6图像分割原理
1.6.1阈值分割
1.6.2基于边缘的分割
1.6.3分水岭算法
1.7特征匹配原理
1.7.1关键点检测和描述
1.7.2特征匹配
1.8模板匹配原理
相似度度量方法:
•平方差匹配(TM_SQDIFF)
•相关匹配(TM_CCORR)
•相关系数匹配(TM_CCOEFF)
1.9光学字符识别(OCR)原理
1.9.1基于Tesseract的OCR
1.10三维视觉原理
1.10.1相机标定
1.10.2立体视觉
2.核心技术原理总结
- 数学基础:线性代数(矩阵运算)、概率统计、微积分
- 信号处理:傅里叶变换、卷积运算、滤波器设计
- 机器学习:特征工程、分类算法、聚类分析
- 优化理论:梯度下降、非线性优化
- 几何原理:投影几何、坐标系变换
OpenCV 的成功在于它将这些复杂的计算机视觉原理封装成简单易用的API,让开发者可以专注于应用逻辑而不是底层数学实现。
3.基于OpenCv实现检测(Java)
3.1建一个SpringBoot项目
pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.rs.cv</groupId>
<artifactId>OpenCvPreStudy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>OpenCvPreStudy</name>
<description>OpenCvPreStudy</description>
<properties>
<java.version>21</java.version>
<skipTests>true</skipTests>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.openpnp</groupId>
<artifactId>opencv</artifactId>
<version>4.9.0-0</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
OpenCvResult
java
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* @author zwmac
*/
@Data
@AllArgsConstructor
public class OpenCvResult {
/**
* 是否健全
**/
private boolean healthy;
/**
* 提示信息
**/
private String message;
}
IOpenCvService
java
import com.rs.cv.entity.OpenCvResult;
import java.io.File;
/**
* @author zwmac
*/
public interface IOpenCvService {
/**
* 手掌检测
*
* @param imageFile 照片文件
* @return 结果
*/
OpenCvResult analyzeHand(File imageFile);
/**
* 人脸检测
*
* @param imageFile 照片文件
* @return 结果
*/
OpenCvResult analyzeFace(File imageFile);
/**
* 模型检测
*
* @param imageFile 待检图片文件
* @param cascadeName 模型名称
* @return 检测结果
*/
OpenCvResult analyze(File imageFile, String cascadeName);
}
OpenCvServiceImpl
java
import cn.hutool.core.date.DateUtil;
import com.rs.cv.entity.OpenCvResult;
import com.rs.cv.service.IOpenCvService;
import com.rs.cv.util.OpenCVUtil;
import org.opencv.core.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import java.io.File;
/**
* @author zwmac
*/
@Service
public class OpenCvServiceImpl implements IOpenCvService {
@Override
public OpenCvResult analyzeHand(File imageFile) {
//OpenCV.loadLocally();
Mat src = Imgcodecs.imread(imageFile.getAbsolutePath());
if (src.empty()) {
return new OpenCvResult(false, "无法读取图片");
}
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
CascadeClassifier classifier = OpenCVUtil.getHandCascade();
Assert.notNull(classifier, "手掌模型加载失败");
MatOfRect matOfRect = new MatOfRect();
classifier.detectMultiScale(gray, matOfRect);
if (matOfRect.toArray().length == 0) {
return new OpenCvResult(false, "未检测到手掌");
}
// TODO: 这里可以接入 MediaPipe Hands 获取关键点,判断 5 指是否健全
// 目前简化为检测到手掌就返回"可能健全"
return new OpenCvResult(true, "检测到手掌,可能健全");
}
@Override
public OpenCvResult analyzeFace(File imageFile) {
Mat src = Imgcodecs.imread(imageFile.getAbsolutePath());
if (src.empty()) {
return new OpenCvResult(false, "无法读取图片");
}
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
CascadeClassifier classifier = OpenCVUtil.getFaceCascade();
Assert.notNull(classifier, "人脸模型加载失败");
MatOfRect matOfRect = new MatOfRect();
classifier.detectMultiScale(gray, matOfRect);
if (matOfRect.toArray().length == 0) {
return new OpenCvResult(false, "未检测到人脸");
}
//TOTO 还以做其他处理
return new OpenCvResult(true, "检测到人脸");
}
@Override
public OpenCvResult analyze(File imageFile, String cascadeName) {
//读取检测项图片
Mat src = Imgcodecs.imread(imageFile.getAbsolutePath());
if (src.empty()) {
return new OpenCvResult(false, "无法读取图片");
}
//灰度化
Mat gray = new Mat();
Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
//加载训练好的检测模型
CascadeClassifier classifier = OpenCVUtil.getCascade(cascadeName);
Assert.notNull(classifier, "模型:"+cascadeName+"加载失败");
//检测检测项
MatOfRect matOfRect = new MatOfRect();
classifier.detectMultiScale(gray, matOfRect);
//判断检测结果
if (matOfRect.toArray().length == 0) {
return new OpenCvResult(false, "未检测到测试项");
}
//TODO 还可以继续判断关键点,
//这里先保存结果图
// 5. 打印检测到的矩形框
Rect[] rects = matOfRect.toArray();
System.out.println("检测到检测项数量: " + rects.length);
for (Rect rect : rects) {
System.out.println("x=" + rect.x + ", y=" + rect.y +
", width=" + rect.width + ", height=" + rect.height);
// 在原图上画矩形
Imgproc.rectangle(src,
new Point(rect.x, rect.y),
new Point(rect.x + rect.width, rect.y + rect.height),
new Scalar(0, 255, 0), 2);
}
// 6. 保存结果图
String resultTime = DateUtil.format(DateUtil.date(), "yyyyMMddHHmmss");
String resultPath = "/Users/zwmac/Downloads/test/result_"+resultTime+".jpg";
Imgcodecs.imwrite(resultPath, src);
System.out.println("结果已保存: "+resultPath);
return new OpenCvResult(true, "检测到测试项");
}
}
OpenCVUtil
java
import org.opencv.objdetect.CascadeClassifier;
import java.net.URL;
/**
* @author zwmac
*/
public class OpenCVUtil {
static {
//有OpenCV.loadLocally();就不需要这个
//System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
}
private static CascadeClassifier cascadeClassifier;
public static CascadeClassifier getHandCascade() {
if (cascadeClassifier == null) {
//cascadeClassifier = new CascadeClassifier("src/main/resources/haarcascades/haarcascade_hand.xml");
// 获取 Haar cascade 文件的类路径 URL
URL url = OpenCVUtil.class.getClassLoader().getResource("haarcascades/haarcascade_hand.xml");
if (url != null) {
String cascadePath = url.getPath();
System.out.println("文件路径: " + cascadePath);
// 对于 OpenCV CascadeClassifier
cascadeClassifier = new CascadeClassifier();
boolean loaded = cascadeClassifier.load(cascadePath);
if (loaded) {
System.out.println("Haar cascade 文件加载成功");
} else {
System.out.println("Haar cascade 文件加载失败");
}
} else {
System.out.println("未找到 Haar cascade 文件");
}
}
return cascadeClassifier;
}
public static CascadeClassifier getFaceCascade() {
if (cascadeClassifier == null) {
// 获取 Haar cascade 文件的类路径 URL
URL url = OpenCVUtil.class.getClassLoader().getResource("haarcascades/haarcascade_frontalface_alt.xml");
if (url != null) {
String cascadePath = url.getPath();
System.out.println("文件路径: " + cascadePath);
// 对于 OpenCV CascadeClassifier
cascadeClassifier = new CascadeClassifier();
boolean loaded = cascadeClassifier.load(cascadePath);
if (loaded) {
System.out.println("Haar cascade 文件加载成功");
} else {
System.out.println("Haar cascade 文件加载失败");
}
} else {
System.out.println("未找到 Haar cascade 文件");
}
}
return cascadeClassifier;
}
public static CascadeClassifier getEyeCascade() {
if (cascadeClassifier == null) {
// 获取 Haar cascade 文件的类路径 URL
URL url = OpenCVUtil.class.getClassLoader().getResource("haarcascades/haarcascade_eye.xml");
if (url != null) {
String cascadePath = url.getPath();
System.out.println("文件路径: " + cascadePath);
// 对于 OpenCV CascadeClassifier
cascadeClassifier = new CascadeClassifier();
boolean loaded = cascadeClassifier.load(cascadePath);
if (loaded) {
System.out.println("Haar cascade 文件加载成功");
} else {
System.out.println("Haar cascade 文件加载失败");
}
} else {
System.out.println("未找到 Haar cascade 文件");
}
}
return cascadeClassifier;
}
public static CascadeClassifier getCascade(String cascadeName) {
if (cascadeClassifier == null) {
// 获取 Haar cascade 文件的类路径 URL
URL url = OpenCVUtil.class.getClassLoader().getResource("haarcascades/"+cascadeName);
if (url != null) {
String cascadePath = url.getPath();
System.out.println("文件路径: " + cascadePath);
// 对于 OpenCV CascadeClassifier
cascadeClassifier = new CascadeClassifier();
boolean loaded = cascadeClassifier.load(cascadePath);
if (loaded) {
System.out.println("Haar cascade 文件加载成功");
} else {
System.out.println("Haar cascade 文件加载失败");
}
} else {
System.out.println("未找到 Haar cascade 文件");
}
}
return cascadeClassifier;
}
}
OpenCvPreStudyApplicationTests
java
import com.rs.cv.entity.OpenCvResult;
import com.rs.cv.service.IOpenCvService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import nu.pattern.OpenCV;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
@Slf4j
@SpringBootTest
class OpenCvPreStudyApplicationTests {
@Resource
private IOpenCvService openCvService;
@Test
void contextLoads() {
log.info("--- OpenCvPreStudyApplicationTests ---");
OpenCV.loadLocally();
// 现在可以使用 OpenCV 了
System.out.println("OpenCV 加载成功!");
System.out.println("版本: " + org.opencv.core.Core.VERSION);
/*
File temp1 = new File("/Users/zwmac/Downloads/test/hand-01.png");
OpenCvResult handResult = openCvService.analyzeHand(temp1);
System.out.println(handResult);
log.info("-- test face --");
File temp2 = new File("/Users/zwmac/Downloads/test/hand-01.png");
OpenCvResult faceResult = openCvService.analyzeFace(temp2);
log.info("-- faceResult:{} --", faceResult);
log.info("-- test eye --");
File temp3 = new File("/Users/zwmac/Downloads/test/hand-03.png");
OpenCvResult eyeResult = openCvService.analyzeEye(temp3);
log.info("-- eyeResult:{} --", eyeResult);*/
log.info("-- test --");
String imgPath = "/Users/zwmac/Downloads/test/hand-03.png";
//String cascadeName = "haarcascade_eye.xml";
//String cascadeName = "haarcascade_frontalcatface.xml";
//String cascadeName = "haarcascade_lefteye_2splits.xml";
//String cascadeName = "haarcascade_righteye_2splits.xml";
String cascadeName = "haarcascade_smile.xml";
//String cascadeName = "haarcascade_fullbody.xml";
File temp = new File(imgPath);
OpenCvResult result = openCvService.analyze(temp, cascadeName);
System.out.println("-- test result:" + result);
}
}
3.2设计思路
其实这个检测是非常粗的,但是可以作为初检。细化关键点范围,然后可以配合其他算法做进一步验证检测。
流程:
- 本地加载OpenCV
- 待检图片读取
- 灰度化处理
- 加载训练好的检测模型
- 检测检测项
- 判断检测结果
- 其他算法判断
- 保存检测结果图片(这个是在原图上标记检测位置,上面的结果已经可以用于程序业务了)
3.3结果图片
上面的设计思路,其实还有很多算法、细节可以提高精度,先看下这个初检效果。效果涉及肖像权,我就不展示了,大家自己可以试试。
反正你会看到笑脸检测几乎就乱了,这个模型显然不行,还需要训练。
总结
- 首先,基于OpenCV的检测,不管是什么语种,肯定是可行的。要谈性能那就要看业务、配置、语种等细节了。
- 其次,自研的难点倒不是应用,百度也就是对照片文件分析,对我们暴露的也就是传文件的Api接口,内部的一些接口应该属于高级了,一般自己花钱了也应该不会自己去写、去调用了。
- 最后,基于OpenCV的自研检测关键是模型。目前OpenCV公开的模型就没有手指、鼻子、耳朵等部位的模型。