# 部屬 YOLOV8 至 Jetson Xavier NX 開發板 ## 前言 在本次實作中會重點提到 Docker 容器技術。 現在許多公司行號或是研究團隊都會將他們的專案都包裝 Docker 容器,只需執行幾行指令就可以捨去大量布置環境的時間,並執行他人開發好的專案,那本次會運用 YOLOV8 模型部屬到 Jetson Xavier NX 開發板來執行"實時"的物件偵測。 ## Docker 介紹 假設一台電腦要運行兩個專案: 專案 A 與專案 B,專案 A 與專案 B 都需要 Tensorflow,但專案 A 所需的版本是 1.15,而專案 B 需要的版本卻是 2.5。因此會產生一種問題"每個專案所需要的函式庫都不盡相同即便有相同的函式庫,但還是有版本不同的問題,那該如何在一台電腦上同型運行這些個專案?" 關於以上問題 Docker 容器技術就得以解決。 ### 容器(Container)介紹 容器是種類似虛擬機器的虛擬化技術,可以將程式碼與所有依賴的系統工具、執行環境(Runtime)、函式庫、設定等軟體套件與檔案打包成標準單元,使用者只要在裝置上安裝Docker引擎,就能執行容器。 不同於虛擬機器是將硬體虛擬化,容器是將作業系統層虛擬化,具有更高的可攜性,運作效率也更高,甚至可以無視基礎設施(Infrastructure)的差異,快速部署到任何裝置,發揮讓應用程式可以從某個運算環境快速轉移到另一個運算環境的效果。 ![04bbbc895e5b9629c6bb2b1ff522ea86](https://hackmd.io/_uploads/S1hakZvRT.jpg) 以上取自:[Jetson Xavier NX開發套件動手玩軟體篇:原生雲端與容器功能引爆應用潛力](https://www.techbang.com/posts/78502) :::warning 需要注意的事,容器不會將硬體虛擬化,也就是說容器不會執行不同指令集的機器碼。 講白話就是說,主流的 cpu 指令集都會是 x86-64,但是 Jetson Xavier NX 開發板的 cpu 其指令集卻是 aarch64 (也就是 arm64),因此在可以部屬在 x86-64 的容器是不能部屬在 aarch64 或 arm64 的機器上。 因此每次部屬專案時都要檢查該專案可以在那些機器上執行,並且下載相對應的 Docker Image。 ::: ### Docker 三元素 在了解完 Docker 的基本概念後,接下來我們要進一步了解要使用 Docker 時最重要的三個元素:映像檔、容器、倉庫。 用一個簡單的比喻來解釋,如果映像檔是一個做蛋糕的模具,容器則是用該模具烤出來的蛋糕,而倉庫是用來集中放置模具們的收納櫃。接下來讓我們搭配這個比喻深入一點解釋這三個概念: #### 映像檔 (Image) Docker 映像檔是一個模板,用來重複產生容器實體。例如:一個映像檔裡可以包含一個完整的 MySQL 服務、一個 Golang 的編譯環境、或是一個 Ubuntu 作業系統。 透過 Docker 映像檔,我們可以快速的產生可以執行應用程式的容器。而 Docker 映像檔可以透過撰寫由命令行構成的 Dockerfile 輕鬆建立,或甚至可以從公開的地方下載已經做好的映像檔來使用。 舉例來說,如果我今天想要一個 node.js 的執行環境跑我寫好的程式,我可以直接到上 DockerHub 找到相對應的 node.js 映像檔 ,而不需要自己想辦法打包一個執行環境。 #### 容器 (Container) Docker 映像檔是一個模板,用來重複產生容器實體。例如:一個映像檔裡可以包含一個完整的 MySQL 服務、一個 Golang 的編譯環境、或是一個 Ubuntu 作業系統。 透過 Docker 映像檔,我們可以快速的產生可以執行應用程式的容器。而 Docker 映像檔可以透過撰寫由命令行構成的 Dockerfile 輕鬆建立,或甚至可以從公開的地方下載已經做好的映像檔來使用。 舉例來說,如果我今天想要一個 node.js 的執行環境跑我寫好的程式,我可以直接到上 DockerHub 找到相對應的 node.js 映像檔 ,而不需要自己想辦法打包一個執行環境。 #### 倉庫 (Repository) 倉庫(Repository)是集中存放映像檔檔案的場所,也可以想像成存放蛋糕模具的大本營。倉庫註冊伺服器(Registry)上則存放著多個倉庫。 最大的公開倉庫註冊伺服器是上面提到過的 Docker Hub,存放了數量龐大的映像檔供使用者下載,我們可以輕鬆在上面找到各式各樣現成實用的映像檔。 而 Docker 倉庫註冊伺服器的概念就跟 Github 類似,你可以在上面建立多個倉庫,然後透過 push、pull 的方式上傳、存取。 以上取自:[Docker 基礎教學與介紹 101](https://chengweihu.com/docker-tutorial/) ## 部屬 YOLOV8 在了解以上內容後就可以去試著去部屬 YOLOV8 到 Xavier 上,那我將會分為以下步驟: 1. 更新 Docker 版本 2. 下載 YOLOV8 的 Docker Image 3. 檢查 Docker Image 執行後產生的容器可不可以運行 4. 排除問題 5. 結果展示與容器包裝 ### 1. 更新 Docker 版本 可以去看[官網教學](https://docs.docker.com/engine/install/ubuntu/)或者跟著以下步驟: #### 1.1. 移除舊版本的 Docker 先按下 crtl+alt+t 打開 terminal,然後打下列指令。 ```shell for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done ``` #### 1.2. 設置 apt repository 每當我們需要在 ubuntu 安裝一些工具又或者是函式庫時,都會使用 `apt install` 指令來安裝,安裝的來源都會是 apt repository 所決定的,因此需要去安裝最新的版本時就會去設置 apt repository。 ```shell # Add Docker's official GPG key: sudo apt-get update sudo apt-get install ca-certificates curl sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.asc # Add the repository to Apt sources: echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update ``` #### 1.3. 安裝最新版的 Docker ```shell sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin ``` ### 2. 下載 YOLOV8 的 Docker #### 2.1. 本次部屬的專案來自於 [YOLOV8](https://github.com/ultralytics/ultralytics) ![擷取](https://hackmd.io/_uploads/By932WDAa.png) #### 2.2. 查看是否有 Docker 的連結 ![擷取2](https://hackmd.io/_uploads/Hy9Up-DCT.png) #### 2.3. 檢查是否有可以運行在 aarch64 或 arm64 的機器上的 Docker Image 按下 tag 可以看到各版本的 Docker Image ![擷取4](https://hackmd.io/_uploads/BJYL0WvA6.png) 依序檢查有沒有 aarch64 或 arm64 的字樣,如果有就按下複製指令。 ![擷取6](https://hackmd.io/_uploads/HkiEkMPCp.png) #### 2.4. 在 Xavier 下載該 Docker Image ```shell sudo docker pull ultralytics/ultralytics:latest-arm64 ``` ### 3. 檢查 Docker Image 執行後產生的容器可不可以運行 當前我們實作的目標是做一個實時的 yolov8 模型,那會把 webcam 接在 Xavier 上並且將 webcam 接收到的影像訊號輸入進 yolov8 模型中,最後要跳出一個視窗顯示 webcam 接收到的影像以及 yolov8 模型的輸出。 因此在執行指令時需要額外參數。 #### 3.1. 先建立資料夾 ```shell mkdir yolov8 cd yolov8 ``` #### 3.2. 撰寫程式碼 想法很簡單,跑 while 迴圈,不斷將 webcam 接收到的影像訊號輸入進 yolov8 模型,得到輸出後就將輸出與 webcam 接收到的影像畫在一起並跳出視窗顯示,直至按下 q 停止。 ```shell # 先安裝 vim sudo apt install vim # 創建一個名為 detect.py 的檔案 vim detect.py # 按下 i 進入 insert 模式,並將下列程式碼複製到該文件 ``` [程式碼連結](https://drive.google.com/file/d/1TMqDtYWT9SGa_GRil1YTA03in-ZtgnwG/view?usp=sharing),該程式碼取自於[此](https://dipankarmedh1.medium.com/real-time-object-detection-with-yolo-and-webcam-enhancing-your-computer-vision-skills-861b97c78993)。 ```shell # 貼上程式碼後,便可按下 esc 離開 insert 模式 # 最後打上 wq 代表寫入並離開 # 查看是否有 detect.py ls ``` #### 3.3. 執行專案 現在已經把程式碼撰寫好了,那目前就剩下可以運行程式碼的環境,接下來就是執行容器在我們的 Xavier 上。 ```shell # 執行下列指令,記得路徑 cils00XX 要改掉 sudo docker run -it -v /home/cils00XX/yolov8:/yolov8 --ipc=host --gpus all -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY --runtime=nvidia --device dev/video0:/dev/video0 ultralytics/ultralytics:latest-arm64 # 進入容器後,把 detect.py 複製到容器的資料夾中 cp /yolov8/detect.py /usr/src/ultralytics/detect.py # 接著執行程式碼 python3 detect.py ``` 執行程式碼時需要等一下,後續會下載 yolov8 模型的權重,下載完後執行程式時會看到提示錯誤: ```shell qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "/usr/local/lib/python3.11/dist-packages/cv2/qt/plugins" even though it was found. This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. Available platform plugins are: xcb. Aborted (core dumped) ``` ### 4. 排除問題 很顯然,我們遇到問題了。 那現在來討論現在的情況與問題,我們的目標是把 webcam 接在 Xavier 上並且將 webcam 接收到的影像訊號輸入進 yolov8 模型中,最後要跳出一個視窗顯示 webcam 接收到的影像以及 yolov8 模型的輸出。 但是專案運行的環境是容器,很明顯容器都是文字化界面是不會跳出視窗的,因此我們用到 [x11](https://zh.wikipedia.org/zh-tw/X%E8%A6%96%E7%AA%97%E7%B3%BB%E7%B5%B1) 技術,這點可以從 docker run 的參數來得知: `-v` : 外部授權相關文件掛載進 container * 將 /tmp/.X11-unix 掛載進 container 的 /tmp/.X11-unix `-e` : 設定容器的環境變數指令 * DISPLAY=$DISPLAY: 將 container 內部的 DISPLAY 參數使用的顯示 port 設成和外部使用者同一個 另外為了將 webcam 接收到的影像訊號輸入進容器就需要參數 `--device` : 指定容器能存取的設備 * 指定容器能讀取 dev/video0 (webcam 的訊號) 到容器裡的 /dev/video0 而現在遇到的問題是我們的容器並沒有 Qt platform plugin,因此根據[文章](https://stackoverflow.com/questions/65642916/running-a-qt-gui-in-a-docker-container)需要安裝相關套件 ```shell apt-get install -y libqt5gui5 ``` 安裝好後再執行一次 `detect.py`,接著就會看到有新的錯誤 ```shell # 比起上次錯誤,這次多了這兩行 No protocol specified qt.qpa.xcb: could not connect to display :1 ``` 現在再理解一下,目前是因為無法連線至 display :1,因此跳不出視窗,那再查一下[文章](https://stackoverflow.com/questions/65642916/running-a-qt-gui-in-a-docker-container)。 知道說在 docker run 之前,因該要打 `xhost +`,而 `xhost` 代表可以控制本地端 X server 存取權限,而`xhost + `是讓所有使用者(包含容器)都能存取 Xserver。 總結而言,之所以出現錯誤是因為本地端 X server 沒有開放權限給容器,導致容器無法將視窗畫面傳給本地端,傳不到本地端就無法跳出視窗。 ```shell! # 先離開容器 exit # 在本地端打上 xhost + # 接著再回到容器 # 要知道剛剛執行的容器可以打 sudo docker ps -a # 查看 STATUS 那一行,可以看到容器的狀態,通常狀態為 EXITED 並且時間最短的就是我們剛剛運行的容器 # 再檢查其 CONTAINER ID,接著打 sudo docker start XX # XX 是 CONTAINER ID 的前兩碼,打完後容器就會運行的狀態,那再打 sudo docker attach XX # 就可以回到剛剛執行的容器中,接著執行 python3 detect.py ``` 就可以看到執行畫面。 ![Screenshot from 2024-03-19 12-17-03](https://hackmd.io/_uploads/ryTT94PRp.png) ### 5. 結果展示與容器包裝 目前可以看到專案距離實時還有一段距離(實時在這是假設為 24 偵,跟動畫一樣,但很明顯專案畫一張圖的時間遠超過 1/24 秒)。但不管如何畫面是出來了,現在就是將整個容器包裝成 Docker Image。 ```shell # 先離開容器 exit # 以防忘記,再查看容器的 id sudo docker ps -a # 將容器包裝成 Docker Image sudo docker commit XX(容器的 id) XX(要取的名子) ``` 在包裝成 Docker Image 後,要執行這專案只需要將 docker run 中要執行的 Docker Image 改成我們包裝好 Docker Image 的就好了。 後續如果要讓其他人也可以下載我們做好的 Docker Image,需要先去 docker hub [註冊帳號](https://hub.docker.com/signup)。 ```shell # 註冊帳號後登入 sudo docker login # 將 Docker Image 的名稱改成合格的格式,帳號名稱/Docker Image 的名稱(帳號名稱與Docker Image 的名稱中間塞 /) sudo docker tag XX(原本 Docker Image 的名稱) XX(要改成 帳號名稱/Docker Image 的名稱) # 最後將 Docker Image 傳到 docker hub 的倉庫 sudo docker push XX(帳號名稱/Docker Image 的名稱) ``` 完成上述步驟後,就可以從其他的裝置下載到我們包裝好的 Docker Image 了。 ## 總結 Docker 指令 在這次專案中使用到 Docker 容器技術,能夠使專案運行的環境部屬在 Xavier 上,同時也使用許多指令來布置容器。那接下來會總結本次專案使用到的 Docker 指令。 ### 映像檔 (Image) #### `tag` 將映像檔重新命名。 範例: ```shell sudo docker tag XX(原本 Docker Image 的名稱) XX(要改成 帳號名稱/Docker Image 的名稱) ``` 需要注意若要將映像檔上傳至倉庫,命名名稱就需要遵守 帳號名稱/Docker Image 的名稱:xx,如同範例:ultralytics/ultralytics:latest-arm64。 其中 : 後面是 tag name,用來做版本控制,若沒指定就預設以 latest 來命名。 #### `pull` 將已上傳至倉庫的映像檔下載至本地端上。 範例: ```shell sudo docker pull ultralytics/ultralytics:latest-arm64 ``` 在 pull 指令後的參數,ultralytics/ultralytics:latest-arm64 是指欲下載的映像檔,名稱規則如同將映像檔 push 至倉庫一樣是 帳號名稱/Docker Image 的名稱。 #### `push` 將本地端的映像檔上傳至倉庫。 範例: ```shell sudo docker push XX(帳號名稱/Docker Image 的名稱) ``` 在 push 指令後的參數: XX,名稱規則需要遵守 帳號名稱/Docker Image 的名稱,才能將映像檔 push 至倉庫。 ### 容器 (Container) #### `run` 將映像檔執行,執行後會產生一個容器。 範例: ```shell! sudo docker run -it -v /home/cils00XX/yolov8:/yolov8 --ipc=host --gpus all -v /tmp/.X11-unix:/tmp/.X11-unix -e DISPLAY=$DISPLAY --runtime=nvidia --device dev/video0:/dev/video0 ultralytics/ultralytics:latest-arm64 ``` 其中參數不過多介紹,請至[官網](https://docs.docker.com/reference/cli/docker/container/run/)查看。 #### `start` 當離開容器時,容器的狀態就會變成 EXITED,若要使容器在背景執行就可以使用 `start` 指令。 範例: ```shell sudo docker start XX ``` 其中 XX 是 CONTAINER ID 的前兩碼,可以用 `sudo docker ps -a` 來確認。 #### `attach` 若容器在背景執行時,需要進入至容器時,可以使用 `attach` 來進入。 範例: ```shell sudo docker attach XX ``` 其中 XX 是 CONTAINER ID 的前兩碼,可以用 `sudo docker ps -a` 來確認。 #### `commit` 將容器變成映像檔。 範例: ```shell sudo docker commit XX(容器的 id) XX(要取的名子) ``` ### 倉庫 (Repository) #### `images` 查看本地端倉庫中所有的映像檔。 範例: ```shell sudo docker images ``` #### `ps` 查看本地端的容器。 範例: ```shell sudo docker ps -a ``` 其中參數 -a 代表顯示所有容器,更多參數可見[官網](https://docs.docker.com/reference/cli/docker/container/ls/)。 ## 討論 那可以看到此專案的效果不如預期,比如說顯示的偵數不到 24 偵。如果大家看完有改進的想法,歡迎在下方留言。