欢迎访问我的 GitHub
- 这里分类和汇总了欣宸的全部原创(含配套源码):
本篇概览
- 如果您看过 一文,甚至动手实际操作过,您应该会对背后的技术细节感兴趣,开发这样一个应用,咱们总共要做以下三件事:
- 准备好 docker 基础镜像
- 开发java应用
- 将java应用打包成package文件,集成到基础镜像中,得到最终的java应用镜像
- 对于 准备好docker基础镜像 这项工作,咱们在前文 已经完成了,接下来要做的就是开发java应用并将其做成docker镜像
版本信息
- 这个java应用的涉及的版本信息如下:
- spring boot:2.4.8
- javacpp:1.4.3
- javacv:1.4.3
源码下载
- 本篇实战中的完整源码可在 git Hub下载到,地址和链接信息如下表所示(:
- 这个git项目中有多个文件夹,本篇的源码在 javacv-tutorials 文件夹下,如下图红框所示:
编码
- 为了统一管理源码和jar依赖,项目采用了 maven 父子结构,父工程名为 javacv -tutorials ,其pom.xml如下,可见主要是定义了一些 jar 的版本:
<project xmlns="#; | |
xmlns:xsi="#; | |
xsi:schemaLocation=" #;> | |
<modelVersion>.0.0</modelVersion> | |
<groupId>com.bolingcavalry</groupId> | |
<artifactId>javacv-tutorials</artifactId> | |
<packaging>pom</packaging> | |
<version>.0-SNAPSHOT</version> | |
<modules> | |
<module>face-detect-demo</module> | |
</modules> | |
<properties> | |
<java.version>.8</java.version> | |
<maven.compiler.source></maven.compiler.source> | |
<maven.compiler.target></maven.compiler.target> | |
<maven-compiler-plugin.version>.6.1</maven-compiler-plugin.version> | |
<springboot.version>.4.8</springboot.version> | |
<!-- javacpp当前版本 --> | |
<javacpp.version>.4.3</javacpp.version> | |
<!-- opencv 版本 --> | |
<opencv.version>.4.3</opencv.version> | |
<!-- ffmpeg 版本 --> | |
<ffmpeg.version>.0.2</ffmpeg.version> | |
</properties> | |
<dependencyManagement> | |
<dependencies> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
<version>.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如下:
<project xmlns="#; | |
xmlns:xsi="#; | |
xsi:schemaLocation=" #;> | |
<parent> | |
<artifactId>javacv-tutorials</artifactId> | |
<groupId>com.bolingcavalry</groupId> | |
<version>.0-SNAPSHOT</version> | |
</parent> | |
<modelVersion>.0.0</modelVersion> | |
<artifactId>face-detect-demo</artifactId> | |
<packaging>jar</packaging> | |
<dependencyManagement> | |
<dependencies> | |
<dependency> | |
<groupId>org. Spring framework.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=MB | |
# 自定义文件上传路径 | |
web.upload-path=/app/images | |
# 模型路径 | |
opencv.model-path=/app/model/haarcascade_frontalface_default.xml |
- 前端页面文件只有一个 index.ftl ,请原谅欣宸不入流的前端水平,前端只有一个页面,可以提交页面,同时也是展示处理结果的页面:
<head> | |
<meta charset="UTF-" /> | |
<title>图片上传Demo</title> | |
</head> | |
<body> | |
<h >图片上传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:px"/>--> | |
<img src="/show?fileName=${fileName}"/> | |
<#else> | |
<#--<img src="/show" style="width:px"/>--> | |
</#if> | |
</body> | |
</html> |
- 再来看后台代码,先是最常见的应用启动类:
package com.bolingcavalry.facedetect; | |
import org.springframework.boot.SpringApplication; | |
import org.springframework.boot.autoconfigure.SpringBootApplication; | |
public class FaceDetectApplication { | |
public static void main(String[] args) { | |
SpringApplication.run(FaceDetectApplication.class, args); | |
} | |
} |
- 前端上传图片后,后端要做哪些处理呢?先不贴代码,咱们把后端要做的事情捋一遍,如下图:
- 接下来是最核心的业务类 Upload Controller.java ,web接口和业务逻辑处理都在这里面,是按照上图的流程顺序执行的,有几处要注意的地方稍后会提到:
package com.bolingcavalry.facedetect.controller; | |
import lombok.extern.slfj.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.IO Exception ; | |
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; | |
public class UploadController { | |
static { | |
// 加载 动态链接库 | |
System.loadLibrary(Core.NATIVE_LIBRARY_NAME); | |
} | |
private final ResourceLoader resourceLoader; | |
public UploadController(ResourceLoader resourceLoader) { | |
this.resourceLoader = resourceLoader; | |
} | |
private String uploadPath; | |
private String modelPath; | |
/** | |
* 跳转到文件上传页面 | |
* @return | |
*/ | |
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 | |
*/ | |
public String upload(int minneighbors, Map<String, Object> map){ MultipartFile file, | |
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_BGRGRAY); | |
// OpenCv人脸识别分类器 | |
CascadeClassifier classifier = new CascadeClassifier(modelPath); | |
// 用来存放人脸矩形 | |
MatOfRect faceRect = new MatOfRect(); | |
// 特征检测点的最小尺寸 | |
Size minSize = new Size(, 32); | |
// 图像缩放比例,可以理解为相机的X倍镜 | |
double scaleFactor =.2; | |
// 执行人脸检测 | |
classifier.detectMultiScale(dstGrayImg, faceRect, scaleFactor, minneighbors, CV_HAAR_DO_CANNY_PRUNING, minSize); | |
//遍历矩形,画到原图上面 | |
// 定义绘制颜色 | |
Scalar color = new Scalar(, 0, 255); | |
Rect[] rects = faceRect.toArray(); | |
// 没检测到 | |
if (null==rects || rects.length<) { | |
// 显示图片 | |
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,); | |
} | |
// 添加人脸框之后的图片的名字 | |
String newFileName = UUID.randomUUID().toString() + ".png"; | |
// 保存 | |
Imgcodecs.imwrite(uploadPath + "/" + newFileName, srcImg); | |
// 显示图片 | |
map.put("msg", "一共检测到" + rects.length + "个人脸"); | |
// 文件名 | |
map.put("fileName", newFileName); | |
return "forward:/index"; | |
} | |
/** | |
* 显示单张图片 | |
* @return | |
*/ | |
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官方提前训练好的模型,地址是:
- 看似神奇的人脸检测功能,实际上只需一行代码 classifier.detectMultiScale ,就能得到每个人脸在原图中的矩形位置,接下来,咱们只要按照位置在原图上添加矩形框即可
- 现在代码已经写完了,接下来将其做成docker镜像
docker镜像制作
- 首先是编写Dockerfile:
# 基础镜像集成了openjdk和opencv3.4.3 | |
FROM bolingcavalry/opencv.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-.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命令把镜像推送到中央仓库,让更多的人用到:
- 最后,再来回顾一下 一文中启动docker容器的命令,如下可见,通过两个-v参数,将宿主机的目录映射到容器中,因此,容器中的/app/images和/app/model可以保持不变,只要能保证宿主机的目录映射正确即可:
docker run | |
--rm | |
-p:8080 | |
-v /root/temp//17/images:/app/images | |
-v /root/temp//17/model:/app/model | |
bolingcavalry/facedetect:.0.1 |
- 有关SpringBoot官方推荐的docker镜像制作的更多信息,请参考《SpringBoot(2.4)应用制作Docker镜像(Gradle版官方方案)》
需要重点注意的地方
- 请大家关注pom.xml中和javacv相关的几个库的版本,这些版本是不能随便搭配的,建议按照文中的来,就算要改,也请在maven中央仓库检查您所需的版本是否存在;
- 至此,《Java版人脸检测》从体验到开发详解都完成了,小小的功能涉及到不少知识点,也让我们体验到了javacv的便捷和强大,借助docker将环境配置和应用开发分离开来,降低了应用开发和部署的难度(不再花时间到jdk和opencv的部署上),如果您正在寻找简单易用的javacv开发和部署方案,希望本文能给您提供参考;