基于OpenCv做照片分析(Java)

前言

现在基于人体照片做相关业务的场景越来越多了,但是开源的、精细化的那就很少。基于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公开的模型就没有手指、鼻子、耳朵等部位的模型。
相关推荐
THMAIL9 分钟前
深度剖析Spring AI源码(七):化繁为简,Spring Boot自动配置的实现之秘
人工智能·spring boot·spring
爱炸薯条的小朋友22 分钟前
C#由Dictionary不正确释放造成的内存泄漏问题与GC代系
开发语言·opencv·c#
fatfishccc1 小时前
Spring MVC 全解析:从核心原理到 SSM 整合实战 (附完整源码)
java·spring·ajax·mvc·ssm·过滤器·拦截器interceptor
机器之心1 小时前
谷歌nano banana正式上线:单图成本不到3毛钱,比OpenAI便宜95%
人工智能·openai
兰亭妙微1 小时前
从线到机:AI 与多模态交互如何重塑 B 端与 App 界面设计
人工智能·小程序·交互·用户体验设计公司
没有bug.的程序员1 小时前
MyBatis 初识:框架定位与核心原理——SQL 自由掌控的艺术
java·数据库·sql·mybatis
MansFlower1 小时前
Gemini 2.5 Flash Image Preview:nano banana
人工智能
机器之心2 小时前
拒稿警告,靠大模型「偷摸水论文」被堵死,ICLR最严新规来了
人工智能·openai
执键行天涯2 小时前
从双重检查锁定的设计意图、锁的作用、第一次检查提升性能的原理三个角度,详细拆解单例模式的逻辑
java·前端·github
程序员江鸟2 小时前
Java面试实战系列【JVM篇】- JVM内存结构与运行时数据区详解(私有区域)
java·jvm·面试