Java版人脸检测详解下篇:编码
欢迎访问我的GitHub
https://github.com/zq2599/blog_demos
内容:所有原创文章分类汇总及配套源码,涉及Java、Docker、Kubernetes、DevOPS等;
本篇概览
- 如果您看过《三分钟极速体验:Java版人脸检测》一文,甚至动手实际操作过,您应该会对背后的技术细节感兴趣,开发这样一个应用,咱们总共要做以下三件事:
- 准备好docker基础镜像
- 开发java应用
- 将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下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称 | 链接 | 备注 |
---|---|---|
项目主页 | 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的版本:
<?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=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,请原谅欣宸不入流的前端水平,前端只有一个页面,可以提交页面,同时也是展示处理结果的页面:
<!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;
@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<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的代码,有以下几处要关注:
- 在静态方法中通过System.loadLibrary加载本地库函,实际开发过程中,这里是最容易报错的地方,一定要确保-Djava.library.path参数配置的路径中的本地库是正常可用的,前文制作的基础镜像中已经准比好了这些本地库,因此只要确保-Djava.library.path参数配置正确即可,这个配置在稍后的Dockerfile中会提到
- public String upload方法是处理人脸检测的代码入口,内部按照前面分析的流程顺序执行
- new CascadeClassifier(modelPath)是根据指定的模型来实例化分类器,模型文件是从GitHub下载的,opencv官方提前训练好的模型,地址是:https://github.com/opencv/opencv/tree/master/data/haarcascades
- 看似神奇的人脸检测功能,实际上只需一行代码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 \
--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开发和部署方案,希望本文能给您提供参考;
你不孤单,欣宸原创一路相伴
欢迎关注公众号:程序员欣宸
微信搜索「程序员欣宸」,我是欣宸,期待与您一同畅游Java世界...
https://github.com/zq2599/blog_demos