超詳細的編碼實戰,讓你的springboot應用識別圖片中的行人、汽車、狗子、喵星人(JavaCV+YOLO4)

程式設計師欣宸發表於2022-01-14

歡迎訪問我的GitHub

https://github.com/zq2599/blog_demos

內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;

本篇概覽

在這裡插入圖片描述

  • 如果您之前對深度學習和YOLO、darknet等有過了解,相信您會產生疑問:Java能實現這些?
  • 沒錯,今天我們們就從零開始,開發一個SpringBoot應用實現上述功能,該應用名為<font color="blue">yolo-demo</font>
  • 讓SpringBoot應用識別圖片中的物體,其關鍵在如何使用已經訓練好的神經網路模型,好在OpenCV整合的DNN模組可以載入和使用YOLO4模型,我們只要找到使用OpenCV的辦法即可
  • 我這裡的方法是使用JavaCV庫,因為JavaCV本身封裝了OpenCV,最終可以使用YOLO4模型進行推理,依賴情況如下圖所示:

在這裡插入圖片描述

關鍵技術

  • 本篇涉及到JavaCV、OpenCV、YOLO4等,從上圖可以看出JavaCV已將這些做了封裝,包括最終推理時所用的模型也是YOLO4官方提前訓練好的,我們們只要知道如何使用JavaCV的API即可
  • YOVO4的paper在此:https://arxiv.org/pdf/2004.10...

版本資訊

  • 這裡給出我的開發環境供您參考:
  • 作業系統:Ubuntu 16(MacBook Pro也可以,版本是11.2.3,macOS Big Sur)
  • docker:20.10.2 Community
  • java:1.8.0_211
  • springboot:2.4.8
  • javacv:1.5.6
  • opencv:4.5.3

實戰步驟

  • 在正式動手前,先把本次實戰的步驟梳理清楚,後面按部就班執行即可;
  • 為了減少環境和軟體差異的影響,讓程式的執行除錯更簡單,這裡會把SpringBoot應用製作成docker映象,然後在docker環境執行,所以,整個實戰簡單來說分為三步 :製做基礎映象、開發SpringBoot應用、把應用做成映象,如下圖:

在這裡插入圖片描述

  • 上述流程中的第一步<font color="blue">製做基礎映象</font>,已經在《製作JavaCV應用依賴的基礎Docker映象(CentOS7+JDK8+OpenCV4)》一文中詳細介紹,我們們直接使用映象<font color="red">bolingcavalry/opencv4.5.3:0.0.1</font>即可,接下來的內容將會聚焦SpringBoot應用的開發;
  • 這個SpringBoot應用的功能很單一,如下圖所示:

在這裡插入圖片描述

  • 整個開發過程涉及到這些步驟:提交照片的網頁、神經網路初始化、檔案處理、圖片檢測、處理檢測結果、在圖片上標準識別結果、前端展示圖片等,完整步驟已經整理如下圖:

在這裡插入圖片描述

  • 內容很豐富,收穫也不會少,更何況前文已確保可以成功執行,那麼,別猶豫啦,我們們開始吧!

原始碼下載

名稱連結備註
專案主頁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>資料夾下,如下圖紅框所示:

在這裡插入圖片描述

  • <font color="blue">javacv-tutorials</font>裡面有多個子工程,今天的程式碼在<font color="red">yolo-demo</font>工程下:

在這裡插入圖片描述

新建SpringBoot應用

  • 新建名為<font color="blue">yolo-demo</font>的maven工程,首先這是個標準的SpringBoot工程,其次新增了javacv的依賴庫,pom.xml內容如下,重點是javacv、opencv等庫的依賴和準確的版本匹配:
<?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>
    <version>1.0-SNAPSHOT</version>
    <artifactId>yolo-demo</artifactId>
    <packaging>jar</packaging>

    <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>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    </properties>

    <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>
            <version>1.5.6</version>
        </dependency>

        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>opencv-platform-gpu</artifactId>
            <version>4.5.3-1.5.6</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- 如果父工程不是springboot,就要用以下方式使用外掛,才能生成正常的jar -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.bolingcavalry.yolodemo.YoloDemoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>
  • 接下來的重點是配置檔案<font color="blue">application.properties</font>,如下可見,除了常見的spring配置,還有幾個檔案路徑配置,實際執行時,這些路徑都要存放對應的檔案給程式使用,這些檔案如何獲取稍後會講到:
### 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
# 模型路徑
# yolo的配置檔案所在位置
opencv.yolo-cfg-path=/app/model/yolov4.cfg
# yolo的模型檔案所在位置
opencv.yolo-weights-path=/app/model/yolov4.weights
# yolo的分類檔案所在位置
opencv.yolo-coconames-path=/app/model/coco.names
# yolo模型推理時的圖片寬度
opencv.yolo-width=608
# yolo模型推理時的圖片高度
opencv.yolo-height=608
  • 啟動類<font color="blue">YoloDemoApplication.java</font>:
package com.bolingcavalry.yolodemo;

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

@SpringBootApplication
public class YoloDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(YoloDemoApplication.class, args);
    }
}
  • 工程已建好,接下來開始編碼,先從前端頁面開始

前端頁面

  • 只要涉及到前端,欣宸一般都會發個自保宣告:請大家原諒欣宸不入流的前端水平,頁面做得我自己都不忍直視,但為了功能的完整,請您忍忍,也不是不能用,我們們總要有個地方提交照片並且展示識別結果不是?
  • 新增名為<font color="blue">index.ftl</font>的前端模板檔案,位置如下圖紅框:

在這裡插入圖片描述

  • <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="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>
  • 頁面的效果,就像下面這樣:

在這裡插入圖片描述

後端邏輯:初始化

  • 為了保持簡單,所有後端邏輯放在一個java檔案中:YoloServiceController.java,按照前面梳理的流程,我們們先看初始化部分
  • 首先是成員變數和依賴
private final ResourceLoader resourceLoader;

    @Autowired
    public YoloServiceController(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Value("${web.upload-path}")
    private String uploadPath;

    @Value("${opencv.yolo-cfg-path}")
    private String cfgPath;

    @Value("${opencv.yolo-weights-path}")
    private String weightsPath;

    @Value("${opencv.yolo-coconames-path}")
    private String namesPath;

    @Value("${opencv.yolo-width}")
    private int width;

    @Value("${opencv.yolo-height}")
    private int height;

    /**
     * 置信度門限(超過這個值才認為是可信的推理結果)
     */
    private float confidenceThreshold = 0.5f;

    private float nmsThreshold = 0.4f;

    // 神經網路
    private Net net;

    // 輸出層
    private StringVector outNames;

    // 分類名稱
    private List<String> names;
  • 接下來是初始化方法init,可見會從之前配置的幾個檔案路徑中載入神經網路所需的配置、訓練模型等檔案,關鍵方法是readNetFromDarknet的呼叫,還有就是檢查是否有支援CUDA的裝置,如果有就在神經網路中做好設定:
    @PostConstruct
    private void init() throws Exception {
        // 初始化列印一下,確保編碼正常,否則日誌輸出會是亂碼
        log.error("file.encoding is " + System.getProperty("file.encoding"));

        // 神經網路初始化
        net = readNetFromDarknet(cfgPath, weightsPath);

        // 檢查網路是否為空
        if (net.empty()) {
            log.error("神經網路初始化失敗");
            throw new Exception("神經網路初始化失敗");
        }

        // 輸出層
        outNames = net.getUnconnectedOutLayersNames();

        // 檢查GPU
        if (getCudaEnabledDeviceCount() > 0) {
            net.setPreferableBackend(opencv_dnn.DNN_BACKEND_CUDA);
            net.setPreferableTarget(opencv_dnn.DNN_TARGET_CUDA);
        }

        // 分類名稱
        try {
            names = Files.readAllLines(Paths.get(namesPath));
        } catch (IOException e) {
            log.error("獲取分類名稱失敗,檔案路徑[{}]", namesPath, e);
        }
    }

處理上傳檔案

  • 前端將二進位制格式的圖片檔案提交上來後如何處理?這裡整理了一個簡單的檔案處理方法upload,會將檔案儲存在伺服器的指定位置,後面會呼叫:
/**
     * 上傳檔案到指定目錄
     * @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;
        }
    }

物體檢測

  • 準備工作都完成了,來寫最核心的物體檢測程式碼,這些程式碼放在yolo-demo應用處理web請求的方法中,如下所示,可見這裡只是個大綱,將推理、結果處理、圖片標註等功能串起來形成完整流程,但是不涉及每個具體功能的細節:
@RequestMapping("fileUpload")
    public String upload(@RequestParam("fileName") MultipartFile file, Map<String, Object> map){
        log.info("檔案 [{}], 大小 [{}]", file.getOriginalFilename(), file.getSize());

        // 檔名稱
        String originalFileName = file.getOriginalFilename();

        if (!upload(file, uploadPath, originalFileName)){
            map.put("msg", "上傳失敗!");
            return "forward:/index";
        }

        // 讀取檔案到Mat
        Mat src = imread(uploadPath + "/" + originalFileName);

        // 執行推理
        MatVector outs = doPredict(src);

        // 處理原始的推理結果,
        // 對檢測到的每個目標,找出置信度最高的類別作為改目標的類別,
        // 還要找出每個目標的位置,這些資訊都儲存在ObjectDetectionResult物件中
        List<ObjectDetectionResult> results = postprocess(src, outs);

        // 釋放資源
        outs.releaseReference();

        // 檢測到的目標總數
        int detectNum = results.size();

        log.info("一共檢測到{}個目標", detectNum);

        // 沒檢測到
        if (detectNum<1) {
            // 顯示圖片
            map.put("msg", "未檢測到目標");
            // 檔名
            map.put("fileName", originalFileName);

            return "forward:/index";
        } else {
            // 檢測結果頁面的提示資訊
            map.put("msg", "檢測到" + results.size() + "個目標");
        }

        // 計算出總耗時,並輸出在圖片的左上角
        printTimeUsed(src);

        // 將每一個被識別的物件在圖片框出來,並在框的左上角標註該物件的類別
        markEveryDetectObject(src, results);

        // 將新增了標註的圖片保持在磁碟上,並將圖片資訊寫入map(給跳轉頁面使用)
        saveMarkedImage(map, src);

        return "forward:/index";
    }
  • 這裡已經可以把整個流程弄明白了,接下來展開每個細節

用神經網路檢測物體

  • 由上面的程式碼可見,圖片被轉為Mat物件後(OpenCV中的重要資料結構,可以理解為矩陣,裡面存放著圖片每個畫素的資訊),被送入<font color="blue">doPredict</font>方法,該方法執行完畢後就得到了物體識別的結果
  • 細看doPredict方法,可見核心是用blobFromImage方法得到四維blob物件,再將這個物件送給神經網路去檢測(net.setInput、net.forward)
/**
     * 用神經網路執行推理
     * @param src
     * @return
     */
    private MatVector doPredict(Mat src) {
        // 將圖片轉為四維blog,並且對尺寸做調整
        Mat inputBlob = blobFromImage(src,
                1 / 255.0,
                new Size(width, height),
                new Scalar(0.0),
                true,
                false,
                CV_32F);

        // 神經網路輸入
        net.setInput(inputBlob);

        // 設定輸出結果儲存的容器
        MatVector outs = new MatVector(outNames.size());

        // 推理,結果儲存在outs中
        net.forward(outs, outNames);

        // 釋放資源
        inputBlob.release();

        return outs;
    }
  • 要注意的是,blobFromImage、net.setInput、net.forward這些都是native方法,是OpenCV的dnn模組提供的
  • doPredict方法返回的是MatVector物件,這裡面就是檢測結果

處理原始檢測結果

  • 檢測結果MatVector物件是個集合,裡面有多個Mat物件,每個Mat物件是一個表格,裡面有豐富的資料,具體的內容如下圖:

在這裡插入圖片描述

  • 看過上圖後,相信您對如何處理原始的檢測結果已經胸有成竹了,只要從MatVector中逐個取出Mat,把每個Mat當做表格,將表格每一行中概率最大的列找到,此列就是該物體的類別了(至於每一列到底是啥東西,為啥上面表格中第五列是人,第六列是自行車,最後一列是牙刷?這個稍後會講到):
    /**
     * 推理完成後的操作
     * @param frame
     * @param outs
     * @return
     */
    private List<ObjectDetectionResult> postprocess(Mat frame, MatVector outs) {
        final IntVector classIds = new IntVector();
        final FloatVector confidences = new FloatVector();
        final RectVector boxes = new RectVector();

        // 處理神經網路的輸出結果
        for (int i = 0; i < outs.size(); ++i) {
            // extract the bounding boxes that have a high enough score
            // and assign their highest confidence class prediction.

            // 每個檢測到的物體,都有對應的每種型別的置信度,取最高的那種
            // 例如檢車到貓的置信度百分之九十,狗的置信度百分之八十,那就認為是貓
            Mat result = outs.get(i);
            FloatIndexer data = result.createIndexer();

            // 將檢測結果看做一個表格,
            // 每一行表示一個物體,
            // 前面四列表示這個物體的座標,後面的每一列,表示這個物體在某個類別上的置信度,
            // 每行都是從第五列開始遍歷,找到最大值以及對應的列號,
            for (int j = 0; j < result.rows(); j++) {
                // minMaxLoc implemented in java because it is 1D
                int maxIndex = -1;
                float maxScore = Float.MIN_VALUE;
                for (int k = 5; k < result.cols(); k++) {
                    float score = data.get(j, k);
                    if (score > maxScore) {
                        maxScore = score;
                        maxIndex = k - 5;
                    }
                }

                // 如果最大值大於之前設定的置信度門限,就表示可以確定是這類物體了,
                // 然後就把這個物體相關的識別資訊儲存下來,要儲存的資訊有:類別、置信度、座標
                if (maxScore > confidenceThreshold) {
                    int centerX = (int) (data.get(j, 0) * frame.cols());
                    int centerY = (int) (data.get(j, 1) * frame.rows());
                    int width = (int) (data.get(j, 2) * frame.cols());
                    int height = (int) (data.get(j, 3) * frame.rows());
                    int left = centerX - width / 2;
                    int top = centerY - height / 2;

                    // 儲存類別
                    classIds.push_back(maxIndex);
                    // 儲存置信度
                    confidences.push_back(maxScore);
                    // 儲存座標
                    boxes.push_back(new Rect(left, top, width, height));
                }
            }

            // 資源釋放
            data.release();
            result.release();
        }

        // remove overlapping bounding boxes with NMS
        IntPointer indices = new IntPointer(confidences.size());
        FloatPointer confidencesPointer = new FloatPointer(confidences.size());
        confidencesPointer.put(confidences.get());

        // 非極大值抑制
        NMSBoxes(boxes, confidencesPointer, confidenceThreshold, nmsThreshold, indices, 1.f, 0);

        // 將檢測結果放入BO物件中,便於業務處理
        List<ObjectDetectionResult> detections = new ArrayList<>();
        for (int i = 0; i < indices.limit(); ++i) {
            final int idx = indices.get(i);
            final Rect box = boxes.get(idx);

            final int clsId = classIds.get(idx);

            detections.add(new ObjectDetectionResult(
               clsId,
               names.get(clsId),
               confidences.get(idx),
               box.x(),
               box.y(),
               box.width(),
               box.height()
            ));

            // 釋放資源
            box.releaseReference();
        }

        // 釋放資源
        indices.releaseReference();
        confidencesPointer.releaseReference();
        classIds.releaseReference();
        confidences.releaseReference();
        boxes.releaseReference();

        return detections;
    }
  • 可見程式碼很簡單,就是把每個Mat當做表格來處理,有兩處特別的地方要處理:
  1. confidenceThreshold變數,置信度門限,這裡是0.5,如果某一行的最大概率連0.5都達不到,那就相當於已知所有類別的可能性都不大,那就不算識別出來了,所以不會存入detections集合中(不會在結果圖片中標註)
  2. NMSBoxes:分類器進化為檢測器時,在原始影像上從多個尺度產生視窗,這就導致下圖左側的效果,同一個人檢測了多張人臉,此時用NMSBoxes來保留最優的一個結果

在這裡插入圖片描述

  • 現在解釋一下Mat物件對應的表格中,每一列到底是什麼類別:這個表格是YOLO4的檢測結果,所以每一列是什麼類別應該由YOLO4來解釋,官方提供了名為<font color="blue">coco.names</font>的檔案,該檔案的內容如下圖,一共80行,每一行是表示一個類別:

在這裡插入圖片描述

  • 此刻聰明的您肯定已經明白Mat表格中的每一列代表什麼類別了:Mat表格中的每一列對應<font color="blue">coco.names</font>的每一行,如下圖:

在這裡插入圖片描述

  • postprocess方法執行完畢後,一張照片的識別結果就被放入名為detections的集合中,該集合內的每個元素代表一個識別出的物體,來看看這個元素的資料結構,如下所示,這些資料夠我們在照片上標註識別結果了:
@Data
@AllArgsConstructor
public class ObjectDetectionResult {
    // 類別索引
    int classId;
    // 類別名稱
    String className;
    // 置信度
    float confidence;
    // 物體在照片中的橫座標
    int x;
    // 物體在照片中的縱座標
    int y;
    // 物體寬度
    int width;
    // 物體高度
    int height;
}

把檢測結果畫在圖片上

  • 手裡有了檢測結果,接下來要做的就是將這些結果畫在原圖上,這樣就有了物體識別的效果,畫圖分兩部分,首先是左上角的總耗時,其次是每個物體識別結果
  • 先在圖片的上角畫出本次檢測的總耗時,效果如下圖所示:

在這裡插入圖片描述

  • 負責畫出總耗時的是printTimeUsed方法,如下,可見總耗時是用多層網路的總次數除以頻率得到的,注意,這不是網頁上的介面總耗時,而是神經網路識別物體的總耗時,例外畫圖的putText是個本地方法,這也是OpenCV的常用方法之一:
    /**
     * 計算出總耗時,並輸出在圖片的左上角
     * @param src
     */
    private void printTimeUsed(Mat src) {
        // 總次數
        long totalNums = net.getPerfProfile(new DoublePointer());
        // 頻率
        double freq = getTickFrequency()/1000;
        // 總次數除以頻率就是總耗時
        double t =  totalNums / freq;

        // 將本次檢測的總耗時列印在展示影像的左上角
        putText(src,
                String.format("Inference time : %.2f ms", t),
                new Point(10, 20),
                FONT_HERSHEY_SIMPLEX,
                0.6,
                new Scalar(255, 0, 0, 0),
                1,
                LINE_AA,
                false);
    }
  • 接下來是畫出每個物體識別的結果,有了ObjectDetectionResult物件集合,畫圖就非常簡單了:呼叫畫矩形和文字的本地方法即可:
   /**
     * 將每一個被識別的物件在圖片框出來,並在框的左上角標註該物件的類別
     * @param src
     * @param results
     */
    private void markEveryDetectObject(Mat src, List<ObjectDetectionResult> results) {
        // 在圖片上標出每個目標以及類別和置信度
        for(ObjectDetectionResult result : results) {
            log.info("類別[{}],置信度[{}%]", result.getClassName(), result.getConfidence() * 100f);

            // annotate on image
            rectangle(src,
                    new Point(result.getX(), result.getY()),
                    new Point(result.getX() + result.getWidth(), result.getY() + result.getHeight()),
                    Scalar.MAGENTA,
                    1,
                    LINE_8,
                    0);

            // 寫在目標左上角的內容:類別+置信度
            String label = result.getClassName() + ":" + String.format("%.2f%%", result.getConfidence() * 100f);

            // 計算顯示這些內容所需的高度
            IntPointer baseLine = new IntPointer();

            Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, baseLine);
            int top = Math.max(result.getY(), labelSize.height());

            // 新增內容到圖片上
            putText(src, label, new Point(result.getX(), top-4), FONT_HERSHEY_SIMPLEX, 0.5, new Scalar(0, 255, 0, 0), 1, LINE_4, false);
        }
    }

展示結果

  • 核心工作已經完成,接下來就是儲存圖片再跳轉到展示網頁:

在這裡插入圖片描述

  • 至此SpringBoot工程編碼完成,接下來要做的就是將整個工程做成docker映象

將SpringBoot工程做成docker映象

# 基礎映象整合了openjdk8和opencv4.5.3
FROM bolingcavalry/opencv4.5.3:0.0.1

# 建立目錄
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

ENV LANG C.UTF-8
ENV LANGUAGE zh_CN.UTF-8
ENV LC_ALL C.UTF-8
ENV TZ Asia/Shanghai

# 指定啟動命令(注意要執行編碼,否則日誌是亂碼)
ENTRYPOINT ["java","-Dfile.encoding=utf-8","-cp","app:app/lib/*","com.bolingcavalry.yolodemo.YoloDemoApplication"]
  • 控制檯進入pom.xml所在目錄,執行命令<font color="blue">mvn clean package -U</font>,這是個普通的maven命令,會編譯原始碼,在target目錄下生成檔案<font color="red">yolo-demo-1.0-SNAPSHOT.jar</font>
  • 執行以下命令,可以從jar檔案中提取出製作docker映象所需的內容:
mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)
  • 執行以下命令即可構建映象:
docker build -t bolingcavalry/yolodemo:0.0.1 .
  • 構建成功:
will@willMini yolo-demo % docker images        
REPOSITORY                  TAG       IMAGE ID       CREATED              SIZE
bolingcavalry/yolodemo      0.0.1     d0ef6e734b53   About a minute ago   2.99GB
bolingcavalry/opencv4.5.3   0.0.1     d1518ffa4699   6 days ago           2.01GB
  • 此刻,具備完整物體識別能力的SpringBoot應用已經開發完成了,還記得application.properties中的那幾個檔案路徑配置麼?我們們要去下載這幾個檔案,有兩種下載方式,您二選一即可
  • 第一種是從官方下載,從下面這三個地址分別下下載:
  1. YOLOv4配置檔案: https://raw.githubusercontent...
  2. YOLOv4權重: https://github.com/AlexeyAB/d...
  3. 分類名稱: https://raw.githubusercontent...
  • 第二種是從csdn下載(無需積分),上述三個檔案我已打包放在此:https://download.csdn.net/dow...
  • 上述兩種方式無論哪種,最終都會得到三個檔案:yolov4.cfg、yolov4.weights、coco.names,請將它們放在同一目錄下,我是放在這裡:/home/will/temp/202110/19/model
  • 新建一個目錄用來存放照片,我這裡新建的目錄是:<font color="blue">/home/will/temp/202110/19/images</font>,注意要確保該目錄可以讀寫
    最終目錄結構如下所示:
/home/will/temp/202110/19/
├── images
└── model
    ├── coco.names
    ├── yolov4.cfg
    └── yolov4.weights
  • 萬事俱備,執行以下命令即可執行服務:
sudo docker run \
--rm \
--name yolodemo \
-p 8080:8080 \
-v /home/will/temp/202110/19/images:/app/images \
-v /home/will/temp/202110/19/model:/app/model \
bolingcavalry/yolodemo:0.0.1
  • 服務執行起來後,操作過程和效果與《三分鐘:極速體驗JAVA版目標檢測(YOLO4)》一文完全相同,就不多贅述了
  • 至此,整個物體識別的開發實戰就完成了,Java在工程化方面的便利性,再結合深度學習領域的優秀模型,為我們們解決視覺影像問題增加了一個備選方案,如果您是一位對視覺和影像感興趣的Java程式設計師,希望本文能給您一些參考

你不孤單,欣宸原創一路相伴

  1. Java系列
  2. Spring系列
  3. Docker系列
  4. kubernetes系列
  5. 資料庫+中介軟體系列
  6. DevOps系列

歡迎關注公眾號:程式設計師欣宸

微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos

相關文章