Java版人脸检测详解下篇:编码
阅读原文时间:2021年10月25日阅读:1

欢迎访问我的GitHub

这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  1. 准备好docker基础镜像
  2. 开发java应用
  3. 将java应用打包成package文件,集成到基础镜像中,得到最终的java应用镜像

版本信息

  • 这个java应用的涉及的版本信息如下:
  1. springboot:2.4.8
  2. javacpp:1.4.3
  3. javacv:1.4.3

源码下载

名称

链接

备注

项目主页

https://github.com/zq2599/blog_demos

该项目在GitHub上的主页

git仓库地址(https)

https://github.com/zq2599/blog_demos.git

该项目源码的仓库地址,https协议

git仓库地址(ssh)

git@github.com:zq2599/blog_demos.git

该项目源码的仓库地址,ssh协议

  • 这个git项目中有多个文件夹,本篇的源码在javacv-tutorials文件夹下,如下图红框所示:

编码

  • 为了统一管理源码和jar依赖,项目采用了maven父子结构,父工程名为javacv-tutorials,其pom.xml如下,可见主要是定义了一些jar的版本:


    http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    4.0.0

    <groupId>com.bolingcavalry</groupId>
    <artifactId>javacv-tutorials</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>face-detect-demo</module>
    </modules>
    
    <properties>
        <java.version>1.8</java.version>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <maven-compiler-plugin.version>3.6.1</maven-compiler-plugin.version>
        <springboot.version>2.4.8</springboot.version>
    &lt;!-- javacpp当前版本 --&gt;
    &lt;javacpp.version&gt;1.4.3&lt;/javacpp.version&gt;
    &lt;!-- opencv版本 --&gt;
    &lt;opencv.version&gt;3.4.3&lt;/opencv.version&gt;
    &lt;!-- ffmpeg版本 --&gt;
    &lt;ffmpeg.version&gt;4.0.2&lt;/ffmpeg.version&gt;
    </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.18</version> </dependency>
        &lt;dependency&gt;
            &lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
            &lt;artifactId&gt;javacv-platform&lt;/artifactId&gt;
            &lt;version&gt;${javacpp.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
            &lt;artifactId&gt;javacv&lt;/artifactId&gt;
            &lt;version&gt;${javacpp.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- javacpp --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
            &lt;artifactId&gt;javacpp&lt;/artifactId&gt;
            &lt;version&gt;${javacpp.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;!-- ffmpeg --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.bytedeco.javacpp-presets&lt;/groupId&gt;
            &lt;artifactId&gt;ffmpeg-platform&lt;/artifactId&gt;
            &lt;version&gt;${ffmpeg.version}-${javacpp.version}&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.bytedeco.javacpp-presets&lt;/groupId&gt;
            &lt;artifactId&gt;ffmpeg&lt;/artifactId&gt;
            &lt;version&gt;${ffmpeg.version}-${javacpp.version}&lt;/version&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
    </dependencyManagement>

  • 在javacv-tutorials下面新建名为face-detect-demo的子工程,这里面是咱们今天要开发的应用,其pom.xml如下:


    http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    javacv-tutorials com.bolingcavalry 1.0-SNAPSHOT
    4.0.0

    <artifactId>face-detect-demo</artifactId>
    <packaging>jar</packaging>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${springboot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    <dependencies>
        <!--FreeMarker模板视图依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;/dependency&gt;
    
    &lt;dependency&gt;
        &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
        &lt;artifactId&gt;lombok&lt;/artifactId&gt;
    &lt;/dependency&gt;
    
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    
    &lt;dependency&gt;
        &lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
        &lt;artifactId&gt;javacv-platform&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
        &lt;artifactId&gt;javacv&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- javacpp --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.bytedeco&lt;/groupId&gt;
        &lt;artifactId&gt;javacpp&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- ffmpeg --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.bytedeco.javacpp-presets&lt;/groupId&gt;
        &lt;artifactId&gt;ffmpeg-platform&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.bytedeco.javacpp-presets&lt;/groupId&gt;
        &lt;artifactId&gt;ffmpeg&lt;/artifactId&gt;
    &lt;/dependency&gt;
    </dependencies> <build> <plugins> <!-- 如果父工程不是springboot,就要用以下方式使用插件,才能生成正常的jar --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.bolingcavalry.facedetect.FaceDetectApplication</mainClass> </configuration> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>

  • 配置文件如下,要重点关注前段模板、文件上传大小、模型文件目录等配置:

    FreeMarker 配置

    spring.freemarker.allow-request-override=false
    #Enable template caching.启用模板缓存。
    spring.freemarker.cache=false
    spring.freemarker.check-template-location=true
    spring.freemarker.charset=UTF-8
    spring.freemarker.content-type=text/html
    spring.freemarker.expose-request-attributes=false
    spring.freemarker.expose-session-attributes=false
    spring.freemarker.expose-spring-macro-helpers=false
    #设置面板后缀
    spring.freemarker.suffix=.ftl

    设置单个文件最大内存

    spring.servlet.multipart.max-file-size=100MB

    设置所有文件最大内存

    spring.servlet.multipart.max-request-size=1000MB

    自定义文件上传路径

    web.upload-path=/app/images

    模型路径

    opencv.model-path=/app/model/haarcascade_frontalface_default.xml

  • 前端页面文件只有一个index.ftl,请原谅欣宸不入流的前端水平,前端只有一个页面,可以提交页面,同时也是展示处理结果的页面:


    图片上传Demo

    图片上传Demo

    选择检测文件:

    周围检测数量:

    <#--判断是否上传文件-->
    <#if msg??> ${msg}

    <#else > ${msg!("文件未上传")}

    <#--显示图片,一定要在img中的src发请求给controller,否则直接跳转是乱码-->
    <#if fileName??> <#----> <#else> <#---->

  • 再来看后台代码,先是最常见的应用启动类:

    package com.bolingcavalry.facedetect;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class FaceDetectApplication {

    public static void main(String[] args) {
        SpringApplication.run(FaceDetectApplication.class, args);
    }

    }

  • 前端上传图片后,后端要做哪些处理呢?先不贴代码,咱们把后端要做的事情捋一遍,如下图:

  • 接下来是最核心的业务类UploadController.java,web接口和业务逻辑处理都在这里面,是按照上图的流程顺序执行的,有几处要注意的地方稍后会提到:

    package com.bolingcavalry.facedetect.controller;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.core.io.ResourceLoader;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.multipart.MultipartFile;

    import java.io.File;
    import java.io.IOException;
    import java.util.Map;
    import org.opencv.core.*;
    import org.opencv.imgcodecs.Imgcodecs;
    import org.opencv.imgproc.Imgproc;
    import org.opencv.objdetect.CascadeClassifier;

    import java.util.UUID;

    import static org.bytedeco.javacpp.opencv_objdetect.CV_HAAR_DO_CANNY_PRUNING;

    @Controller
    @Slf4j
    public class UploadController {

    static {
        // 加载 动态链接库
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
    }
    
    private final ResourceLoader resourceLoader;
    
    @Autowired
    public UploadController(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }
    
    @Value("${web.upload-path}")
    private String uploadPath;
    
    @Value("${opencv.model-path}")
    private String modelPath;
    
    /**
     * 跳转到文件上传页面
     * @return
     */
    @RequestMapping("index")
    public String toUpload(){
        return "index";
    }
    
    /**
     * 上次文件到指定目录
     * @param file 文件
     * @param path 文件存放路径
     * @param fileName 源文件名
     * @return
     */
    private static boolean upload(MultipartFile file, String path, String fileName){
        //使用原文件名
        String realPath = path + "/" + fileName;
    File dest = new File(realPath);
    
    //判断文件父目录是否存在
    if(!dest.getParentFile().exists()){
        dest.getParentFile().mkdir();
    }
    
    try {
        //保存文件
        file.transferTo(dest);
        return true;
    } catch (IllegalStateException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        return false;
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        return false;
    }
    } /** * * @param file 要上传的文件 * @return */ @RequestMapping("fileUpload") public String upload(@RequestParam("fileName") MultipartFile file, @RequestParam("minneighbors") int minneighbors, Map<String, Object> map){ log.info("file [{}], size [{}], minneighbors [{}]", file.getOriginalFilename(), file.getSize(), minneighbors);
    String originalFileName = file.getOriginalFilename();
    if (!upload(file, uploadPath, originalFileName)){
        map.put("msg", "上传失败!");
        return "forward:/index";
    }
    
    String realPath = uploadPath + "/" + originalFileName;
    
    Mat srcImg = Imgcodecs.imread(realPath);
    
    // 目标灰色图像
    Mat dstGrayImg = new Mat();
    // 转换灰色
    Imgproc.cvtColor(srcImg, dstGrayImg, Imgproc.COLOR_BGR2GRAY);
    // OpenCv人脸识别分类器
    CascadeClassifier classifier = new CascadeClassifier(modelPath);
    // 用来存放人脸矩形
    MatOfRect faceRect = new MatOfRect();
    
    // 特征检测点的最小尺寸
    Size minSize = new Size(32, 32);
    // 图像缩放比例,可以理解为相机的X倍镜
    double scaleFactor = 1.2;
    // 执行人脸检测
    classifier.detectMultiScale(dstGrayImg, faceRect, scaleFactor, minneighbors, CV_HAAR_DO_CANNY_PRUNING, minSize);
    //遍历矩形,画到原图上面
    // 定义绘制颜色
    Scalar color = new Scalar(0, 0, 255);
    
    Rect[] rects = faceRect.toArray();
    
    // 没检测到
    if (null==rects || rects.length&lt;1) {
        // 显示图片
        map.put("msg", "未检测到人脸");
        // 文件名
        map.put("fileName", originalFileName);
    
        return "forward:/index";
    }
    
    // 逐个处理
    for(Rect rect: rects) {
        int x = rect.x;
        int y = rect.y;
        int w = rect.width;
        int h = rect.height;
        // 单独框出每一张人脸
        Imgproc.rectangle(srcImg, new Point(x, y), new Point(x + w, y + w), color, 2);
    }
    
    // 添加人脸框之后的图片的名字
    String newFileName = UUID.randomUUID().toString() + ".png";
    
    // 保存
    Imgcodecs.imwrite(uploadPath + "/" + newFileName, srcImg);
    
    // 显示图片
    map.put("msg", "一共检测到" + rects.length + "个人脸");
    // 文件名
    map.put("fileName", newFileName);
    
    return "forward:/index";
    } /** * 显示单张图片 * @return */ @RequestMapping("show") public ResponseEntity showPhotos(String fileName){ if (null==fileName) { return ResponseEntity.notFound().build(); }
    try {
        // 由于是读取本机的文件,file是一定要加上的, path是在application配置文件中的路径
        return ResponseEntity.ok(resourceLoader.getResource("file:" + uploadPath + "/" + fileName));
    } catch (Exception e) {
        return ResponseEntity.notFound().build();
    }
    }

    }

  • UploadController.java的代码,有以下几处要关注:

  1. 在静态方法中通过System.loadLibrary加载本地库函,实际开发过程中,这里是最容易报错的地方,一定要确保-Djava.library.path参数配置的路径中的本地库是正常可用的,前文制作的基础镜像中已经准比好了这些本地库,因此只要确保-Djava.library.path参数配置正确即可,这个配置在稍后的Dockerfile中会提到
  2. public String upload方法是处理人脸检测的代码入口,内部按照前面分析的流程顺序执行
  3. new CascadeClassifier(modelPath)是根据指定的模型来实例化分类器,模型文件是从GitHub下载的,opencv官方提前训练好的模型,地址是:https://github.com/opencv/opencv/tree/master/data/haarcascades
  4. 看似神奇的人脸检测功能,实际上只需一行代码classifier.detectMultiScale,就能得到每个人脸在原图中的矩形位置,接下来,咱们只要按照位置在原图上添加矩形框即可
  • 现在代码已经写完了,接下来将其做成docker镜像

docker镜像制作

  • 首先是编写Dockerfile:

    基础镜像集成了openjdk8和opencv3.4.3

    FROM bolingcavalry/opencv3.4.3:0.0.3

    创建目录

    RUN mkdir -p /app/images && mkdir -p /app/model

    指定镜像的内容的来源位置

    ARG DEPENDENCY=target/dependency

    复制内容到镜像

    COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
    COPY ${DEPENDENCY}/META-INF /app/META-INF
    COPY ${DEPENDENCY}/BOOT-INF/classes /app

    指定启动命令

    ENTRYPOINT ["java","-Djava.library.path=/opencv-3.4.3/build/lib","-cp","app:app/lib/*","com.bolingcavalry.facedetect.FaceDetectApplication"]

  • 上述Dockerfile内容很简单,就是一些复制文件的处理,只有一处要格外注意:启动命令中有个参数-Djava.library.path=/opencv-3.4.3/build/lib,指定了本地so库的位置,前面的java代码中,System.loadLibrary加载的本地库就是从这个位置加载的,咱们用的基础镜像是bolingcavalry/opencv3.4.3:0.0.3,已经在该位置准备好了opencv的所有本地库

  • 在父工程目录下执行mvn clean package -U,这是个纯粹的maven操作,和docker没有任何关系

  • 进入face-detect-demo目录,执行以下命令,作用是从jar文件中提取class、配置文件、依赖库等内容到target/dependency目录:

    mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

  • 最后,在Dockerfile文件所在目录执行命令docker build -t bolingcavalry/facedetect:0.0.1 .(命令的最后有个点,不要漏了),即可完成镜像制作

  • 如果您有hub.docker.com的账号,还可以通过docker push命令把镜像推送到中央仓库,让更多的人用到:

  • 最后,再来回顾一下《三分钟极速体验:Java版人脸检测》一文中启动docker容器的命令,如下可见,通过两个-v参数,将宿主机的目录映射到容器中,因此,容器中的/app/images和/app/model可以保持不变,只要能保证宿主机的目录映射正确即可:

    docker run <br /> --rm <br /> -p 18080:8080 <br /> -v /root/temp/202107/17/images:/app/images <br /> -v /root/temp/202107/17/model:/app/model <br /> bolingcavalry/facedetect:0.0.1

  • 有关SpringBoot官方推荐的docker镜像制作的更多信息,请参考《SpringBoot(2.4)应用制作Docker镜像(Gradle版官方方案)》

需要重点注意的地方

  • 请大家关注pom.xml中和javacv相关的几个库的版本,这些版本是不能随便搭配的,建议按照文中的来,就算要改,也请在maven中央仓库检查您所需的版本是否存在;

  • 至此,《Java版人脸检测》从体验到开发详解都完成了,小小的功能涉及到不少知识点,也让我们体验到了javacv的便捷和强大,借助docker将环境配置和应用开发分离开来,降低了应用开发和部署的难度(不再花时间到jdk和opencv的部署上),如果您正在寻找简单易用的javacv开发和部署方案,希望本文能给您提供参考;

你不孤单,欣宸原创一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 数据库+中间件系列
  6. DevOps系列