写点什么

Java 版人脸检测详解下篇:开发 java 应用并做成 docker 镜像同步

用户头像
编程菌
关注
发布于: 1 小时前

本篇概览

如果您看过《Java 版人脸检测上篇》一文,甚至动手实际操作过,那么你应该会对背后的技术细节感兴趣,开发这样一个应用,咱们总共要做以下三件事:

1.准备好 docker 基础镜像

2.开发 java 应用

3.将 java 应用打包成 package 文件,集成到基础镜像中,得到最终的 java 应用镜像


对于准备好 docker 基础镜像这项工作,咱们在前文《Java 版人脸检测详解上篇:运行环境的 Docker 镜像(CentOS+JDK+OpenCV)》已经完成了,接下来要做的就是开发 java 应用并将其做成 docker 镜像

版本信息这个 java 应用的涉及的版本信息如下:

  • springboot:2.4.8

  • javacpp:1.4.3

  • javacv:1.4.3 源码下载本篇实战中的完整源码可在 GitHub 下载到,地址和链接信息如下表所示:



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


编码

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

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>
<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>
<!-- javacpp当前版本 --> <javacpp.version>1.4.3</javacpp.version> <!-- opencv版本 --> <opencv.version>3.4.3</opencv.version> <!-- ffmpeg版本 --> <ffmpeg.version>4.0.2</ffmpeg.version> </properties>
<dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.18</version> </dependency>
<dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> <version>${javacpp.version}</version> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> <version>${javacpp.version}</version> </dependency> <!-- javacpp --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> <version>${javacpp.version}</version> </dependency> <!-- ffmpeg --> <dependency> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>ffmpeg-platform</artifactId> <version>${ffmpeg.version}-${javacpp.version}</version> </dependency> <dependency> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>ffmpeg</artifactId> <version>${ffmpeg.version}-${javacpp.version}</version> </dependency> </dependencies>
</dependencyManagement></project>
复制代码

在 javacv-tutorials 下面新建名为 face-detect-demo 的子工程,这里面是咱们今天要开发的应用,其 pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>javacv-tutorials</artifactId>        <groupId>com.bolingcavalry</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>
<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>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
<dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv-platform</artifactId> </dependency> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacv</artifactId> </dependency> <!-- javacpp --> <dependency> <groupId>org.bytedeco</groupId> <artifactId>javacpp</artifactId> </dependency> <!-- ffmpeg --> <dependency> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>ffmpeg-platform</artifactId> </dependency> <dependency> <groupId>org.bytedeco.javacpp-presets</groupId> <artifactId>ffmpeg</artifactId> </dependency> </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></project>
复制代码

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

###FreeMarker 配置spring.freemarker.allow-request-override=false#Enable template caching.启用模板缓存。spring.freemarker.cache=falsespring.freemarker.check-template-location=truespring.freemarker.charset=UTF-8spring.freemarker.content-type=text/htmlspring.freemarker.expose-request-attributes=falsespring.freemarker.expose-session-attributes=falsespring.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,请原谅欣宸不入流的前端水平,前端只有一个页面,可以提交页面,同时也是展示处理结果的页面:

<!DOCTYPE html><head>    <meta charset="UTF-8" />    <title>图片上传Demo</title></head><body><h1 >图片上传Demo</h1><form action="fileUpload" method="post" enctype="multipart/form-data">    <p>选择检测文件: <input type="file" name="fileName"/></p>    <p>周围检测数量: <input type="number" value="32" name="minneighbors"/></p>    <p><input type="submit" value="提交"/></p></form><#--判断是否上传文件--><#if msg??>    <span>${msg}</span><br><br><#else >    <span>${msg!("文件未上传")}</span><br></#if><#--显示图片,一定要在img中的src发请求给controller,否则直接跳转是乱码--><#if fileName??><#--<img src="/show?fileName=${fileName}" style="width: 100px"/>--><img src="/show?fileName=${fileName}"/><#else><#--<img src="/show" style="width: 200px"/>--></#if></body></html>
复制代码

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

package com.bolingcavalry.facedetect;
import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplicationpublic 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@Slf4jpublic 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<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 官方提前训练好的模型,地址是:好像不能放网址哈哈哈哈哈哈哈


4.看似神奇的人脸检测功能,实际上只需一行代码 classifier.detectMultiScale,就能得到每个人脸在原图中的矩形位置,接下来,咱们只要按照位置在原图上添加矩形框即可


  • 现在代码已经写完了,接下来将其做成 docker 镜像

docker 镜像制作

  • 首先是编写 Dockerfile:

# 基础镜像集成了openjdk8和opencv3.4.3FROM 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/libCOPY ${DEPENDENCY}/META-INF /app/META-INFCOPY ${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 \--rm \-p 18080:8080 \-v /root/temp/202107/17/images:/app/images \-v /root/temp/202107/17/model:/app/model \bolingcavalry/facedetect:0.0.1
复制代码
  • 有关 SpringBoot 官方推荐的 docker 镜像制作的更多信息,请参考《SpringBoot(2.4)应用制作 Docker 镜像(Gradle 版官方方案)》

需要重点注意的地方

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

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

用户头像

编程菌

关注

还未添加个人签名 2021.07.13 加入

还未添加个人简介

评论

发布
暂无评论
Java版人脸检测详解下篇:开发java应用并做成docker镜像同步