欢送拜访我的GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,波及Java、Docker、Kubernetes、DevOPS等;

本篇概览

  • 如果您看过《三分钟极速体验:Java版人脸检测》一文,甚至入手实际操作过,您应该会对背地的技术细节感兴趣,开发这样一个利用,咱们总共要做以下三件事:
  • 筹备好docker根底镜像
  • 开发java利用
  • 将java利用打包成package文件,集成到根底镜像中,失去最终的java利用镜像
  • 对于<font color="blue">筹备好docker根底镜像</font>这项工作,咱们在前文《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/blo...):
名称链接备注
我的项目主页https://github.com/zq2599/blo...该我的项目在GitHub上的主页
git仓库地址(https)https://github.com/zq2599/blo...该我的项目源码的仓库地址,https协定
git仓库地址(ssh)git@github.com:zq2599/blog_demos.git该我的项目源码的仓库地址,ssh协定
  • 这个git我的项目中有多个文件夹,本篇的源码在<font color="blue">javacv-tutorials</font>文件夹下,如下图红框所示:

编码

  • 为了对立治理源码和jar依赖,我的项目采纳了maven父子构造,父工程名为<font color="blue">javacv-tutorials</font>,其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>
  • 在<font color="blue">javacv-tutorials</font>上面新建名为<font color="red">face-detect-demo</font>的子工程,这外面是咱们明天要开发的利用,其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
  • 前端页面文件只有一个<font color="blue">index.ftl</font>,请原谅欣宸不入流的前端程度,前端只有一个页面,能够提交页面,同时也是展现处理结果的页面:
<!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);    }}
  • 前端上传图片后,后端要做哪些解决呢?先不贴代码,咱们把后端要做的事件捋一遍,如下图:

  • 接下来是最外围的业务类<font color="blue">UploadController.java</font>,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();        }    }}
  • <font color="blue">UploadController.java</font>的代码,有以下几处要关注:
  1. 在静态方法中通过<font color="blue">System.loadLibrary</font>加载本地库函,理论开发过程中,这里是最容易报错的中央,肯定要确保<font color="red">-Djava.library.path</font>参数配置的门路中的本地库是失常可用的,前文制作的根底镜像中曾经准比好了这些本地库,因而只有确保<font color="red">-Djava.library.path</font>参数配置正确即可,这个配置在稍后的Dockerfile中会提到
  2. <font color="blue">public String upload</font>办法是解决人脸检测的代码入口,外部依照后面剖析的流程程序执行
  3. <font color="blue">new CascadeClassifier(modelPath)</font>是依据指定的模型来实例化分类器,模型文件是从GitHub下载的,opencv官网提前训练好的模型,地址是:https://github.com/opencv/ope...
  4. 看似神奇的人脸检测性能,实际上只需一行代码<font color="blue">classifier.detectMultiScale</font>,就能失去每个人脸在原图中的矩形地位,接下来,咱们只有依照地位在原图上增加矩形框即可
  • 当初代码曾经写完了,接下来将其做成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内容很简略,就是一些复制文件的解决,只有一处要分外留神:启动命令中有个参数<font color="blue">-Djava.library.path=/opencv-3.4.3/build/lib</font>,指定了本地so库的地位,后面的java代码中,<font color="blue">System.loadLibrary</font>加载的本地库就是从这个地位加载的,咱们用的根底镜像是<font color="blue">bolingcavalry/opencv3.4.3:0.0.3</font>,曾经在该地位筹备好了opencv的所有本地库
  • 在父工程目录下执行<font color="blue">mvn clean package -U</font>,这是个纯正的maven操作,和docker没有任何关系
  • 进入<font color="blue">face-detect-demo</font>目录,执行以下命令,作用是从jar文件中提取class、配置文件、依赖库等内容到target/dependency目录:
mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
  • 最初,在Dockerfile文件所在目录执行命令<font color="blue">docker build -t bolingcavalry/facedetect:0.0.1 .</font>(命令的最初有个点,不要漏了),即可实现镜像制作
  • 如果您有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开发和部署计划,心愿本文能给您提供参考;

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

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

欢送关注公众号:程序员欣宸

微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游Java世界...
https://github.com/zq2599/blog_demos