
## 目錄
[toc]
## 前言
最近公司專案的物件辨識需要使用樹梅派推論,筆者查找了一下資料後決定使用Yolo Fastest V1 這個開源框架來做,因為是第一次使用darknet ,著實吃了許多苦頭,所以詳細記錄一下步驟以及一些網路上查到的坑點,避免自己以後還會犯錯。
## Base
1. 訓練主機 :
CPU : Intel I7–10700
GPU: GTX 1660 Super
OS : Ubuntu 20.04 LTS
CUDA : 11.0
2. 推論主機 :
Raspberry Pi 4 Model B 4GB
OS : raspbian 64 bit
## 訓練主機配置
### 下載相依性套件
1. 下載Yolo Fastest V1 專案,安裝openCV(C++),並打開Makefile配置
```
git clone https://github.com/jerry800416/Yolo-Fastest
sudo apt-get install libopencv-dev
cd Yolo-Fastest
nano MakeFile
```
2. 編譯設定
設定編譯環境 如下,請依照自己的訓練機器配置調整,主要要調整的是GPU和CUDNN,其他的如果有裝OPENCV C++ 版的記得設定為1,其他的設定基本上不用動
```
# set GPU=1 and CUDNN=1 to speedup on GPU
GPU=1
CUDNN=1
# set CUDNN_HALF=1 to further speedup 3 x times (Mixed-precision on Tensor Cores) GPU: Volta, Xavier, Turing and higher
CUDNN_HALF=0
OPENCV=1
# set AVX=1 and OPENMP=1 to speedup on CPU (if error occurs then set AVX=0)
AVX=1
OPENMP=0
LIBSO=0
ZED_CAMERA=0
ZED_CAMERA_v2_8=0
```
### 編譯Darknet
1. 新建build 資料夾,並且編譯Darknet
```
mkdir build2
cd build2/
cmake ../
# j後面帶的是核心數
make -j16
```
2. 這時候如果有跳出cmake版本過低,按照以下步驟繼續
a.請刪除舊版並安裝新版本cmake,網址如下[點我](https://cmake.org/download/)
b.找到`cmake-3.22.0-rc2-linux-x86_64.sh` 並點選下載,然後繼續安裝cmake
```
sudo cp cmake-3.22.0-rc2-linux-x86_64.sh /opt/
cd /opt/
sudo chmod +x cmake-3.22.0-rc2-linux-x86_64.sh
sudo bash cmake-3.22.0-rc2-linux-x86_64.sh
sudo ln -s /opt/cmake-3.22.0-rc2-linux-x86_64/bin/* /usr/local/bin
```
3. 確認 版本正確後就可以繼續編譯
```
cmake — version
```
4. 編譯完成會在build2 資料夾下面發現darknet 可執行檔,代表編譯成功
### 生成預訓練權重
1. 在build2資料夾下打上以下指令,會在build2資料夾下產生yolofastest 預訓練權重
```
./darknet partial ../ModelZoo/yolo-fastest-1.1_coco/yolo-fastest-1.1.cfg ../ModelZoo/yolo-fastest-1.1_coco/yolo-fastest-1.1.weights yolo-fastest.conv.109 109
```
2. 當然fastest - xl 的預訓練權重也可以生成,只需要將指令更改即可
```
./darknet partial ../ModelZoo/yolo-fastest-1.1_coco/yolo-fastest-1.1-xl.cfg ../ModelZoo/yolo-fastest-1.1_coco/yolo-fastest-1.1-xl.weights yolo-fastest-1.1-xl.conv.109 109
```
### 資料前處理(標框)
這裡假設已經蒐集完所有的圖檔,要將圖片中要辨識的物件標出來
1. 安裝Label Image 套件
```
sudo apt-get install labelImg
```
2. labelImg 打開視窗

3. 點選開啟目錄,選擇圖片資料夾 開始標記(記得把資料型態改成yolo)

4. 標記完成後數據會如下圖,資料夾下會有圖檔、txt檔、class檔

### 資料前處理(產生darknet格式的數據)
1. build2 資料夾下新建data 資料夾
2. 在data 資料夾下新建images 和 labels 資料夾、train.txt空白檔案、test.txt空白檔案
3. 將步驟2產出的txt檔案複製到labels資料夾下(不包含 classes.txt)
4. 將步驟2的圖檔複製到images資料夾下
5. 將步驟2的classes.txt複製到data資料夾下,並更名為screw.names
6. 在data 資料夾下新建screw.data 並新增內容如下
```
# classes 代表幾個class , 可以打開screw.names 確認數量對不對
classes= 2
train = data/train.txt
valid = data/test.txt
names = data/screw.names
# backup 代表模型儲存的路徑
backup = backup
```
7. 在data資料夾下新建`makedata.py` 內容如下
```Python
import os
import random
# 自行決定切分比例
trainval_percent = 0.2
train_percent = 1
imgfilepath = './images'
total_img = os.listdir(imgfilepath)
num = len(total_img)
list1 = range(num)
tv = int(num * trainval_percent)
tr = int(tv * train_percent)
trainval = random.sample(list1, tv)
train = random.sample(trainval, tr)
ftest = open('./test.txt', 'w')
ftrain = open('./train.txt', 'w')
for i in list1:
name = total_img[i]
if i in trainval:
if i in train:
ftest.write('data/images/'+name+'\n')
else:
ftrain.write('data/images/'+name+'\n')
ftrain.close()
ftest.close()
```
8. 執行`makedata.py`
* 這時後會發現train.txt 和 test.txt 裡面已經分好train data的路徑和test data的路徑了
9. 將labels 資料夾裡面的txt檔案複製到images資料夾內就完成製作darknet 訓練資料的動作了
### 配置訓練參數
1. 將`Yolo-Fastest/ModelZoo/yolo-fastest-1.1_coco/yolo-fastest-1.1.cfg` 和 `yolo-fastest-1.1-xl.cfg` 複製到`Yolo-Fastest/build2` 資料夾下
2. 打開`yolo-fastest-1.1.cfg` 第2,3行,這裡依你的GPU ram 大小調整,如果後續訓練時出現out of memory 再回來調整
```
[net]
batch=16
subdivisions=8
```
3. 第16~20行 `burn_in` 和 `max_batches` 裡面設定的是訓練趟數(epoch),`steps` 裡面的兩個數字也要改,公式是 `epoch`* 0.8 , `epoch` * 0.9,代表的是learning rate 的開始衰變步數,`scales` 代表衰變幅度
```
burn_in=8000
max_batches=8000
policy=steps
steps=6400,7200
scales=.1,.1
```
4. 第858,926行的`filter`參數,這裡修改的公式為`filter = (class數量+5)*3`,注意只需要修改這兩行的參數,其他的`filter`不需要修改
```
[convolutional]
size=1
stride=1
pad=1
filters=21
```
5. 在866 和934 行的`classes` 也要修改成你自己的class數量
6. 以上是修改`yolo-fastest-1.1.cfg`,若是要用`yolo-fastest-1.1-xl.cfg`,行數會不一樣,只需找到相對應的參數名稱修改即可
### 開始訓練
1. 在build2 資料夾下新建backup 資料夾
```
mkdir backup
```
2. 在build2 資料夾下輸入以下開始訓練(darknet 支援許多後綴參數,詳細情形可以自己google,這裡僅列出最基本的)
```
./darknet detector train data/screw.data yolo-fastest-1.1.cfg yolo-fastest-1.1.conv.109 backup/ -clear
```
3. 訓練時會出現loss下降圖,可以觀察LOSS 下降的情況,注意一開始訓練的時候LOSS 很大不會出現在圖片上,LOSS 低於20才會出現

4. 訓練完成會在backup 資料夾下生成.weights檔案
### 測試
```
./darknet detector test data/screw.data yolo-fastest-1.1.cfg backup/yolo-fastest-1_final.weights <這裡輸入你要測試圖片的路徑 > -thresh 0.55
```

## 推論主機配置
### 安裝系統
1. 拿出你的樹梅派,然後確認它還是可用的
* 還有樹梅派的攝影機或是webcam,我自己是使用羅技的C310
2. 安裝64位元的raspbian系統
* 前往樹梅派官方地址 並下載最新64位元映像檔 [點我](https://downloads.raspberrypi.org/raspios_arm64/images/)
### 編譯NCNN
1. 前置環境配置
```
sudo apt install build-essential git cmake
sudo apt-get install -y gfortran
sudo apt-get install -y libprotobuf-dev libleveldb-dev libsnappy-dev libopencv-dev libhdf5-serial-dev protobuf-compiler libvulkan-dev vulkan-utils
sudo apt-get install — no-install-recommends libboost-all-dev
sudo apt-get install -y libgflags-dev libgoogle-glog-dev liblmdb-dev libatlas-base-dev
```
2. 下載NCNN庫 並編譯
```
git clone https://github.com/Tencent/ncnn
cd ncnn
mkdir build
cd build
cmake ../
make -j4
make install
```
3. 在build資料夾下會出現三個資料夾examples、install、tools,至此ncnn編譯成功。
### 轉換Model
#### 準備檔案
1. clone repo
```
cd ~
git clone https://github.com/jerry800416/Yolo-Fastest
cd ~/ncnn/build/install
```
2. 將install 資料夾下的bin、include、src 資料夾複製到 Yolo-Fastest/sample/ncnn 資料夾下
3. 在ncnny 資料夾下新建model 資料夾
4. 將訓練主機訓練出來的`yolo-fastest-1_final.weights`檔案和`yolo-fastest-1.1.cfg`配置檔複製到剛剛建立的Model 資料夾內
#### 開始轉換
1. 執行轉換程式
```
cd ~/ncnn/build/tools/darknet
./darknet2ncnn ../../../Model/yolo-fastest-1.1.cfg ../../../Model/yolo-fastest-1_last.weights ../../../Model/yolo-fastest-screw.param ../../../Model/yolo-fastest-screw.bin
```
2. 跑完會在Model 資料夾下產生`yolo-fastest-screw.param`、`yolo-fastest-screw.bin` 兩隻檔案
3. 將這兩個檔案複製到Yolo-Fastest/sample/ncnn/model 資料夾內
#### 轉換bf16 浮點數格式
1. 執行`YoloDet.cpp`
```
nano ~/Yolo-Fastest/sample/ncnn/src/YoloDet.cpp
```
2. 在 int YoloDet::init 方法內的開頭新增以下兩行,並保存退出
```
DNet.opt.use_packing_layout = true;
DNet.opt.use_bf16_storage = true;
```
#### 指定運行的CPU核心數
1. 打開`YoloDet.h`
```
nano ~/Yolo-Fastest/sample/ncnn/src/include/YoloDet.h
```
2. 將`# define NUMTHREADS 8` 的 8 改成 4 ,保存退出
### 運行DEMO
#### 新增demo_img.cpp
1. 在~/Yolo-Fastest/sample/ncnn/下新增`demo_img.cpp`
2. code 如下,有需要修改的地方都有註解記得要改
```C++
#include "YoloDet.h"
int main()
{
// 這裡填入class 的名稱,注意 ,這裡第一個必須填unknow,剩下的才是你的class名稱
static const char* class_names[] = {
"unknow","screw","nut"
};
YoloDet api;
// 這裡填入model 的位置
api.init("model/yolo-fastest-screw.param",
"model/yolo-fastest-screw.bin");
// 這裡填入測試照片的位置
cv::Mat cvImg = cv::imread("test.jpg");
std::vector<TargetBox> boxes;
api.detect(cvImg, boxes);
for (int i = 0; i < boxes.size(); i++) {
std::cout<<boxes[i].x1<<" "<<boxes[i].y1<<" "<<boxes[i].x2<<" "<<boxes[i].y2
<<" "<<boxes[i].score<<" "<<boxes[i].cate<<std::endl;
char text[256];
sprintf(text, "%s %.1f%%", class_names[boxes[i].cate], boxes[i].score * 100);
int baseLine = 0;
cv::Size label_size = cv::getTextSize(text, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
int x = boxes[i].x1;
int y = boxes[i].y1 - label_size.height - baseLine;
if (y < 0)
y = 0;
if (x + label_size.width > cvImg.cols)
x = cvImg.cols - label_size.width;
cv::rectangle(cvImg, cv::Rect(cv::Point(x, y), cv::Size(label_size.width, label_size.height + baseLine)),
cv::Scalar(255, 255, 255), -1);
cv::putText(cvImg, text, cv::Point(x, y + label_size.height),
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0));
cv::rectangle (cvImg, cv::Point(boxes[i].x1, boxes[i].y1),
cv::Point(boxes[i].x2, boxes[i].y2), cv::Scalar(255, 255, 0), 2, 2, 0);
}
// 這裡填入結果照片的位置
cv::imwrite("output.png", cvImg);
return 0;
}
```
#### 新增demo_video.cpp
1. 在~/Yolo-Fastest/sample/ncnn/下新增 `demo_video.cpp`
2. code 如下,有需要修改的地方都有註解記得要改
```C++
#include "YoloDet.h"
#include <time.h>
// 這裡填入class 的名稱,注意 ,這裡第一個必須填unknow,剩下的才是你的class名稱
static const char* class_names[] = {
"unknow","screw","nut"
};
int drawBoxes(cv::Mat srcImg, std::vector<TargetBox> boxes)
{
// printf("Detect box num: %d\n", boxes.size());
for (int i = 0; i < boxes.size(); i++)
{
cv::rectangle (srcImg, cv::Point(boxes[i].x1, boxes[i].y1),
cv::Point(boxes[i].x2, boxes[i].y2),
cv::Scalar(255, 255, 0), 2, 2, 0);
std::string cate = class_names[boxes[i].cate];
std::string Ttext = cate;
cv::Point Tp = cv::Point(boxes[i].x1, boxes[i].y1-20);
cv::putText(srcImg, Ttext, Tp, cv::FONT_HERSHEY_TRIPLEX, 0.5,
cv::Scalar(0, 255, 0), 1, CV_AA);
std::string score =std::to_string(boxes[i].score);
std::string Stext = "Score:" + score;
cv::Point Sp = cv::Point(boxes[i].x1, boxes[i].y1-5);
cv::putText(srcImg, Stext, Sp, cv::FONT_HERSHEY_TRIPLEX, 0.5,
cv::Scalar(0, 0, 255), 1, CV_AA);
}
return 0;
}
int testCam() {
YoloDet api;
// 這裡填入model 的位置
api.init("model/yolo-fastest-screw.param",
"model/yolo-fastest-screw.bin");
cv::Mat frame;
std::vector<TargetBox> output;
cv::VideoCapture cap(0);
// 這裡填入你的攝影機畫素
cap.set(CV_CAP_PROP_FRAME_WIDTH, 1080);
cap.set(CV_CAP_PROP_FRAME_HEIGHT, 720);
while (true) {
double start = ncnn::get_current_time();
cap >> frame;
if (frame.empty()) break;
api.detect(frame, output);
drawBoxes(frame, output);
output.clear();
cv::imshow("camera", frame);
cv::waitKey(10);
double end = ncnn::get_current_time();
double time = end - start;
double fps = 1000 / time;
printf("Speed per frame :%7.2f ms\n",time);
printf("FPS :%7.2f \n",fps);
}
cap.release();
return 0;
}
int main() {
testCam();
return 0;
}
```
#### 編譯demo檔案
```
g++ -o demo_img demo_img.cpp src/YoloDet.cpp -I src/include -I include/ncnn lib/libncnn.a `pkg-config --libs --cflags opencv` -fopenmp
g++ -o demo_video demo_video.cpp src/YoloDet.cpp -I src/include -I include/ncnn lib/libncnn.a `pkg-config --libs --cflags opencv` -fopenmp
```
#### 運行demo 檔案
1. 執行`demo_img`
```
./demo_img
```

2. 執行`demo_video`
```
./demo_video
```

3. 分析
可以看到上圖的FPS 不高,主要是因為在C++檔案中把frame 秀出來了,而且我同時開著螢幕錄影,若是只有輸出bbox 和 label 結果,使用yolo fastest 可達15-16 fps ,若是使用yolo fastest-xl 可達10-11 fps ,符合原作者提供的數據

## 結語
至此所有的教學結束,後續或許可以試試樹梅派超頻看看會不會提升FPS,另外作者有發布yolo fastest V2 版本,backbone 改成最近很紅的shuffle-net,聽說又大幅提高了速度,但筆者一直 run不起來,就先放著,下次有時間來試試 yoloX 的表現吧!