前言
现在基于人体照片做相关业务的场景越来越多了,但是开源的、精细化的那就很少。基于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公开的模型就没有手指、鼻子、耳朵等部位的模型。