1. 程式人生 > >毫秒級檢測!你見過帶GPU的樹莓派嗎?

毫秒級檢測!你見過帶GPU的樹莓派嗎?

load 定義 interval += 編譯 lena 驗證 iss 另一個

樹莓派3B+英特爾神經計算棒進行高速目標檢測

轉載請註明作者夢裏茶

技術分享圖片

代碼:
訓練數據預處理:
https://gist.github.com/ahangchen/ae1b7562c1f93fdad1de58020e94fbdf
測試:https://github.com/ahangchen/ncs_detection

Star是一種美德。

Background

最近在做一個項目,要在樹莓派上分析視頻中的圖片,檢測目標,統計目標個數,這是一張樣例圖片:

技術分享圖片

Motivation

當下效果最好的目標檢測都是基於神經網絡來做的,包括faster rcnn, ssd, yolo2等等,要在樹莓派這種資源緊張的設備上運行檢測模型,首先想到的就是用最輕量的MobileNet SSD,使用Tensorflow object detection api實現的MobileNet SSD雖然已經非常輕,但在樹莓派上推導一張1280x720的圖仍然需要2秒,有興趣的同學可以參考這兩個項目:

  • armv7版Tensorflow(必須是1.4及以上):https://github.com/lhelontra/tensorflow-on-arm/releases
  • Tensorflow Object detection API: https://github.com/tensorflow/models/tree/master/research/object_detection

具體的操作在Tensorflow文檔裏都說的很清楚了,在樹莓派上的操作也是一樣的,有問題可以評論區討論

Hardware

極限的模型仍然不能滿足性能需求,就需要請出我們今天的主角了,Intel Movidius Neural Computing Stick
技術分享圖片

處理器 Intel Movidius VPU
支持框架 TensorFlow, Caffe
連接方式 USB 3.0 Type-A
尺寸 USB stick (72.5mm X 27mm X 14mm)
工作溫度 0° - 40° C
x86_64 Ubuntu 16.04主機
Raspberry Pi 3B Stretch desktop
Ubuntu 16.04 虛擬機
系統要求 USB 2.0 以上 (推薦 USB 3.0)
1GB 內存
4GB 存儲

實際上這不是一個GPU,而是一個專用計算芯片,但能起到類似GPU對神經網絡運算的加速作用。

京東上搜名字可以買到,只要500元左右,想想一塊GPU都要幾千塊錢,就會覺得很值了。

SDK是開源的:https://github.com/movidius/ncsdk

提問不在GitHub issue裏,而是在一個專門的論壇:https://ncsforum.movidius.com/

雖然目前NCSDK支持的框架包含Tensorflow和Caffe,但並不是支持所有的模型,目前已支持的模型列表可以在這裏查到:https://github.com/movidius/ncsdk/releases

截止到2018年3月15日,NCSDK還沒有支持Tensorflow版的MobileNet SSD(比如tf.cast這個操作還未被支持),所以我們需要用Caffe來訓練模型,部署到樹莓派上。

Environment

ncsdk的環境分為兩部分,訓練端和測試端。

  • 訓練端通常是一個Ubuntu 帶GPU主機,訓練Caffe或TensorFlow模型,編譯成NCS可以執行的graph;
  • 測試端則面向ncs python mvnc api編程,可以運行在樹莓派上raspbian stretch版本,也可以運行在訓練端這種機器上。

訓練端

安裝

安裝這個過程,說難不難,也就幾行命令的事情,但也有很多坑

在訓練端主機上,插入神經計算棒,然後:

git clone https://github.com/movidius/ncsdk
cd ncsdk
make install 

其中,make install幹的是這些事情:

  • 檢查安裝Tensorflow
  • 檢查安裝Caffe(SSD-caffe)
  • 編譯安裝ncsdk(不包含inference模塊,只包含mvNCCompile相關模塊,用來將Caffe或Tensorflow模型轉成NCS graph的)

註意,

  • 這些庫都是安裝到/opt/movidius/這個目錄下,並關聯到系統python3裏邊的(/usr/bin/python3),如果你電腦裏原來有tf或caffe,也不會被關聯上去
  • NCSDK mvNCCompile模塊目前只兼容python3,我嘗試過將安裝完的SDK改成兼容python2的版本,可以將模型編譯出來,但是在運行時會報錯,所以暫時放棄兼容python2了,也建議大家用默認的python3版本
  • 這個步驟主要的坑來自萬惡的Caffe,如果你裝過python3版的caffe,大概會有經驗一些,這裏有幾個小坑提示一下:

  • 最好在ncsdk目錄中的ncsdk.conf中,開啟caffe的cuda支持,即設置CAFFE_USE_CUDA=yes,這樣你之後也能用這個caffe來訓練模型
  • caffe的依賴會在腳本中安裝,但有些Debian兼容問題要解決
  • 開啟CUDA支持後,編譯caffe會找不到libboost-python3,因為在Ubuntu16.04裏,它叫libboost-python3.5,所以要軟鏈接一下:

cd /usr/lib/x86_64-linux-gnu/
sudo ln -s libboost_python-py35.so libboost_python3.so
  • 其他可能出現的caffe的坑,可以在我博客找找答案,如果沒有的話,就去caffe的GitHub issue搜吧

測試

一波操作之後,我們裝好了ncsdk編譯模塊,可以下載我訓練的caffe模型,嘗試編譯成ncs graph

git clone https://github.com/ahangchen/MobileNetSSD
mvNCCompile example/MobileNetSSD_deploy.prototxt -w MobileNetSSD_deploy.caffemodel -s 12 -is 300 300 -o ncs_mobilenet_ssd_graph

這裏其實是調用python3去執行/usr/local/bin/ncsdk/mvNCCompile.py這個文件, 不出意外在當前版本(1.12.00)你會遇到這個錯誤:

[Error 17] Toolkit Error: Internal Error: Could not build graph. Missing link: conv11_mbox_conf

這是因為NCSDK在處理caffe模型的時候,會把conv11_mbox_conf_new節點叫做conv11_mbox_conf,所以build graph的時候就會找不著。因此需要為這種節點起一個別名,即,將conv11_mbox_conf_new起別名為conv11_mbox_conf,修改SDK代碼中的/usr/local/bin/ncsdk/Models/NetworkStage.py,在第85行後面添加:

if ‘‘_new‘ in name:
    self.alias.append(name[:-4])

於是就能編譯生成graph了,你會看到一個名為ncs_mobilenet_ssd_graph的文件。

上邊這個bug我已經跟NCSDK的工程師講了,他們在跟進修這個bug:
技術分享圖片

測試端

NCSDK

測試端要安裝ncsdk python api,用於inference,實際上測試端能做的操作,訓練端也都能做

git clone https://github.com/movidius/ncsdk
cd api/src
make install

從輸出日誌可以發現,將ncsdk的lib和include文件分別和系統的python2(/usr/bin/python2)和python3(/usr/bin/python3)做了關聯。

然後你可以下一個GitHub工程來跑一些測試:

git clone https://github.com/movidius/ncappzoo
cd ncappzoo/apps/hello_ncs_py
python3 hello_ncs.py
python2 hello_ncs.py

沒報錯就是裝好了,測試端很簡單。

OpenCV

看pyimagesearch這個教程

Caffe模型訓練

就是正常的用caffe訓練MobileNet-SSD,主要參考這個倉庫:

  • MobileNet-SSD: https://github.com/chuanqi305/MobileNet-SSD

README裏將步驟講得很清楚了

  1. 下載SSD-caffe(這個我們已經在NCSDK裏裝了)
  2. 下載chuanqi在VOC0712上預訓練的模型
  3. 把MobileNet-SSD這個項目放到SSD-Caffe的examples目錄下,這一步可以不做,但是要對應修改train.sh裏的caffe目錄位置
  4. 創建你自己的labelmap.prototxt,放到MobileNet-SSD目錄下,比如說,你是在coco預訓練模型上訓練的話,可以把coco的標簽文件復制過來,將其中與你的目標類(比如我的目標類是Cattle)相近的類(比如Coco中是Cow)改成對應的名字,並用它的label作為你的目標類的label。(比如我用21這個類代表Cattle)
  5. 用你自己的數據訓練MobileNet-SSD,參考SSD-caffe的wiki,主要思路還是把你的數據轉換成類似VOC或者COCO的格式,然後生成lmdb,坑也挺多的:
  • 假設你的打的標簽是這樣一個文件raw_label.txt,假裝我們數據集只有兩張圖片:
data/strange_animal/1017.jpg 0.487500   0.320675    0.670000    0.433193
data/strange_animal/1018.jpg 0.215000   0.293952    0.617500    0.481013
  • 我們的目標是將標簽中涉及的圖片和位置信息轉成這樣一個目錄(在ssd-caffe/data/coco目錄基礎上生成的):

    coco_cattle
    ├── all # 存放全部圖片和xml標簽文件
    │   ├── 1017.jpg
    │   ├── 1017.xml
    │   ├── 1018.jpg
    │   └── 1018.xml
    ├── Annotations # 存放全部標簽xml
    │   ├── 1017.xml
    │   └── 1018.xml
    ├── create_data.sh # 將圖片轉為lmdb的腳本
    ├── create_list.py # 根據ImageSets裏的數據集劃分文件,生成jpg和xml的對應關系文件到coco_cattle目錄下,但我發現這個對應關系文件用不上
    ├── images  # 存放全部圖片
    │   ├── 1017.jpg
    │   └── 1018.jpg
    ├── ImageSets # 劃分訓練集,驗證集和測試集等,如果只想分訓練和驗證的話,可以把minival.txt,testdev.txt,test.txt內容改成一樣的
    │   ├── minival.txt 
    │   ├── testdev.txt
    │   ├── test.txt
    │   └── train.txt
    ├── labelmap_coco.prototxt # 如前所述的標簽文件,改一下可以放到MobileNet-SSD目錄下
    ├── labels.txt
    ├── lmdb # 手動創建這個目錄
    │   ├── coco_cattle_minival_lmdb # 自動創建的,由圖片和標簽轉換來的LMDB文件
    │   ├── coco_cattle_testdev_lmdb
    │   ├── coco_cattle_test_lmdb
    │   └── coco_cattle_train_lmdb
    ├── minival.log
    ├── README.md
    ├── testdev.log
    ├── test.log
    └── train.log
  • 其中,標簽xml的格式如下:

<annotation>
  <folder>train</folder>
  <filename>86</filename>
  <source>
    <database>coco_cattle</database>
  </source>
  <size>
    <width>720</width>
    <height>1280</height>
    <depth>3</depth>
  </size>
  <segmented>0</segmented>
  <object>
    <name>21</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>169</xmin>
      <ymin>388</ymin>
      <xmax>372</xmax>
      <ymax>559</ymax>
    </bndbox>
  </object>
  <object>
    <name>21</name>
    <pose>Unspecified</pose>
    <truncated>0</truncated>
    <difficult>0</difficult>
    <bndbox>
      <xmin>169</xmin>
      <ymin>388</ymin>
      <xmax>372</xmax>
      <ymax>559</ymax>
    </bndbox>
  </object>
</annotation>

代表一張圖中多個對象所在位置(bndbox節點表示),以及類別(name)。

  • 一開始,all, Annotations, images, ImageSets,lmdb四個目錄都是空的,你可以把自己的圖片放到隨便哪個地方,只要在raw_label.txt裏寫好圖片路徑就行
  • 讀取raw_label.txt,利用lxml構造一棵dom tree,然後寫到Annotations對應的xml裏,並將對應的圖片移動到image目錄裏,可以參考這份代碼。並根據我們設置的train or not標誌符將當前這張圖片分配到訓練集或測試集中(也就是往ImageSet/train.txt中寫對應的圖片名)

  • 這樣一波操作之後,我們的imagesAnnotations目錄裏都會有數據了,接下來我們需要把它們一塊復制到all目錄下

cp images/* all/
cp Annotations/* all/
  • 然後用create_data.sh將all中的數據,根據ImageSet中的數據集劃分,創建訓練集和測試集的lmdb,這裏對coco的create_data.sh做了一點修改:
cur_dir=$(cd $( dirname ${BASH_SOURCE[0]} ) && pwd )
root_dir=$cur_dir/../..

cd $root_dir

redo=true
# 這裏改成all目錄
data_root_dir="$cur_dir/all"
# 這裏改成自己的數據集名,也是我們這個目錄的名字
dataset_name="coco_cattle"
# 指定標簽文件
mapfile="$root_dir/data/$dataset_name/labelmap_coco.prototxt"
anno_type="detection"
label_type="xml"
db="lmdb"
min_dim=0
max_dim=0
width=0
height=0

extra_cmd="--encode-type=jpg --encoded"
if $redo
then
  extra_cmd="$extra_cmd --redo"
fi
for subset in minival testdev train test
do
  python3 $root_dir/scripts/create_annoset.py --anno-type=$anno_type --label-type=$label_type --label-map-file=$mapfile --min-dim=$min_dim --max-dim=$max_dim --resize-width=$width --resize-height=$height --check-label $extra_cmd $data_root_dir $root_dir/data/$dataset_name/ImageSets/$subset.txt $data_root_dir/../$db/$dataset_name"_"$subset"_"$db examples/$dataset_name 2>&1 | tee $root_dir/data/$dataset_name/$subset.log
done

於是會lmdb目錄下會為每個劃分集合創建一個目錄,存放數據

├── lmdb
│   ├── coco_cattle_minival_lmdb
│   │   ├── data.mdb
│   │   └── lock.mdb
│   ├── coco_cattle_testdev_lmdb
│   │   ├── data.mdb
│   │   └── lock.mdb
│   ├── coco_cattle_test_lmdb
│   │   ├── data.mdb
│   │   └── lock.mdb
│   └── coco_cattle_train_lmdb
│       ├── data.mdb
│       └── lock.mdb
  1. 將5生成的lmdb鏈接到MobileNet-SSD的目錄下:
cd MobileNet-SSD
ln -s PATH_TO_YOUR_TRAIN_LMDB trainval_lmdb
ln -s PATH_TO_YOUR_TEST_LMDB test_lmdb
  1. 運行gen_model.sh生成三個prototxt(train, test, deploy)
# 默認clone下來的目錄是沒有example這個目錄的,而gen_model.sh又會把文件生成到example目錄
mkdir example
./gen_model.sh
  1. 訓練

    ./train.sh

    這裏如果爆顯存了,可以到example/MobileNetSSD_train.prototxt修改batch size,假如你batch size改到20,剛好可以吃滿GTX1060的6G顯存,但是跑到一定步數(設置在solver_test.prototxt裏的test_interval變量),會執行另一個小batch的test(這個batch size定義在example/MobileNetSSD_test.prototxt裏),這樣就會再爆顯存,所以如果你的train_batch_size + test_batch_size <= 20的話才可以保證你在6G顯存上能順利完成訓練,我的設置是train_batch_size=16, test_batch_size=4

一開始的training loss可能比較大,30左右,等到loss下降到2.x一段時間就可以ctrl+c退出訓練了,模型權重會自動保存在snapshot目錄下

  1. 運行merge_bn.py將訓練得到的模型去除bn層,得到可部署的Caffe模型,這樣你就能得到一個名為MobileNetSSD_deploy.caffemodel的權重文件,對應的prototxt為example/MobileNetSSD_deploy.prototxt

  2. 離題那麽久,終於來到主題,我們要把這個caffemodel編譯成NCS可運行的graph,這個操作之前在搭環境的部分也提過:

mvNCCompile example/MobileNetSSD_deploy.prototxt -w MobileNetSSD_deploy.caffemodel -s 12 -is 300 300 -o ncs_mobilenet_ssd_graph

參數格式:

mvNCCompile prototxt路徑 -w 權重文件路徑 -s 最大支持的NCS數目 -is 輸入圖片寬度 輸入圖片高度 -o 輸出graph路徑

其實訓練端相對於chuanqi的MobileNet-SSD沒啥改動,甚至訓練參數也不用怎麽改動,主要工作還是在數據預處理上,可以參考我的預處理代碼

樹莓派NCS模型測試

現在我們要用ncs版的ssd模型在樹莓派上進行對圖片做檢測,這個目標一旦達成我們自然也能對視頻或攝像頭數據進行檢測了。

倉庫結構

ncs_detection
├── data # 標簽文件
│   └── mscoco_label_map.pbtxt
├── file_helper.py # 文件操作輔助函數
├── model # 訓練好的模型放在這裏
│   ├── ncs_mobilenet_ssd_graph
│   └── README.md
├── ncs_detection.py # 主入口
├── object_detection # 改了一下TF的Object detection包中的工具類來用
│   ├── __init__.py
│   ├── protos
│   │   ├── __init__.py
│   │   ├── string_int_label_map_pb2.py
│   │   └── string_int_label_map.proto
│   └── utils
│       ├── __init__.py
│       ├── label_map_util.py
│       └── visualization_utils.py
├── r10 # 圖片數據
│   ├── 00000120.jpg
│   ├── 00000133.jpg
│   ├── 00000160.jpg
│   ├── 00000172.jpg
│   ├── 00000192.jpg
│   ├── 00000204.jpg
│   ├── 00000220.jpg
│   └── 00000236.jpg
├── README.md
└── total_cnt.txt
  • 由於這個工程一開始是用Tensorflow Object Detection API做的,所以改了其中的幾個文件來讀標簽和畫檢測框,將其中跟tf相關的代碼去掉。
  • TF的圖片IO是用pillow做的,在樹莓派上速度奇慢,對一張1280x720的圖使用Image的get_data這個函數獲取數據需要7秒,所以我改成了OpenCV來做IO。

任務目標

檢測r10目錄中的圖片中的對象,標記出來,存到r10_tmp目錄裏

流程

  • 準備目標目錄
def config_init(dataset_pref):
    os.system(‘mkdir %s_tmp‘ % dataset_pref)
    os.system(‘rm %s_tmp/*‘ % dataset_pref)
  • 指定模型路徑,標簽位置,類別總數,測試圖片路徑
PATH_TO_CKPT = ‘model/ncs_mobilenet_ssd_graph‘
PATH_TO_LABELS = os.path.join(‘data‘, ‘mscoco_label_map.pbtxt‘)
NUM_CLASSES = 81
TEST_IMAGE_PATHS = [os.path.join(img_dir, %08d.jpg‘ % i) for i in range(start_index, end_index)]
  • 發現並嘗試打開神經計算棒
def ncs_prepare():
    print("[INFO] finding NCS devices...")
    devices = mvnc.EnumerateDevices()

    if len(devices) == 0:
        print("[INFO] No devices found. Please plug in a NCS")
        quit()

    print("[INFO] found {} devices. device0 will be used. "
          "opening device0...".format(len(devices)))
    device = mvnc.Device(devices[0])
    device.OpenDevice()
    return device
  • 將NCS模型加載到NCS中
def graph_prepare(PATH_TO_CKPT, device):
    print("[INFO] loading the graph file into RPi memory...")
    with open(PATH_TO_CKPT, mode="rb") as f:
        graph_in_memory = f.read()

    # load the graph into the NCS
    print("[INFO] allocating the graph on the NCS...")
    detection_graph = device.AllocateGraph(graph_in_memory)
    return detection_graph
  • 準備好標簽與類名對應關系
category_index = label_prepare(PATH_TO_LABELS, NUM_CLASSES)
  • 讀取圖片,由於Caffe訓練圖片采用的通道順序是RGB,而OpenCV模型通道順序是BGR,需要轉換一下
image_np = cv2.imread(image_path)
image_np = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
  • 使用NCS模型為輸入圖片推斷目標位置
def predict(image, graph):
    image = preprocess_image(image)
    graph.LoadTensor(image, None)
    (output, _) = graph.GetResult()
    num_valid_boxes = output[0]
    predictions = []
    for box_index in range(num_valid_boxes):
        base_index = 7 + box_index * 7

        if (not np.isfinite(output[base_index]) or
                not np.isfinite(output[base_index + 1]) or
                not np.isfinite(output[base_index + 2]) or
                not np.isfinite(output[base_index + 3]) or
                not np.isfinite(output[base_index + 4]) or
                not np.isfinite(output[base_index + 5]) or
                not np.isfinite(output[base_index + 6])):
            continue

        (h, w) = image.shape[:2]
        x1 = max(0, output[base_index + 3])
        y1 = max(0, output[base_index + 4])
        x2 = min(w, output[base_index + 5])
        y2 = min(h, output[base_index + 6])
        pred_class = int(output[base_index + 1]) + 1
        pred_conf = output[base_index + 2]
        pred_boxpts = (y1, x1, y2, x2)

        prediction = (pred_class, pred_conf, pred_boxpts)
        predictions.append(prediction)

    return predictions

其中,首先將圖片處理為Caffe輸入格式,縮放到300x300,減均值,縮放到0-1範圍,轉浮點數

def preprocess_image(input_image):
    PREPROCESS_DIMS = (300, 300)
    preprocessed = cv2.resize(input_image, PREPROCESS_DIMS)
    preprocessed = preprocessed - 127.5
    preprocessed = preprocessed * 0.007843
    preprocessed = preprocessed.astype(np.float16)
    return preprocessed

graph推斷得到目標位置,類別,分數

graph.LoadTensor(image, None)
(output, _) = graph.GetResult()

其中的output格式為,

[
    目標數量,
    class,score,xmin, ymin, xmax, ymax,
    class,score,xmin, ymin, xmax, ymax,
    ...
]
  • 根據我們感興趣的類別和分數進行過濾
def predict_filter(predictions, score_thresh):
    num = 0
    boxes = list()
    scores = list()
    classes = list()
    for (i, pred) in enumerate(predictions):
        (cl, score, box) = pred
        if cl == 21 or cl == 45 or cl == 19 or cl == 76 or cl == 546 or cl == 32:
            if score > score_thresh:
                boxes.append(box)
                scores.append(score)
                classes.append(cl)
                num += 1
    return num, boxes, classes, scores
  • 用OpenCV將當前圖片的對象數量寫到圖片右上角,用pillow(tf庫中的實現)將當前圖片的對象位置和類別在圖中標出
def add_str_on_img(image, total_cnt):
    cv2.putText(image, %d % total_cnt, (image.shape[1] - 100, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
result = vis_util.visualize_boxes_and_labels_on_image_array(
                image_np,
                np.squeeze(valid_boxes).reshape(num, 4),
                np.squeeze(valid_classes).astype(np.int32).reshape(num, ),
                np.squeeze(valid_scores).reshape(num, ),
                category_index,
                use_normalized_coordinates=True,
                min_score_thresh=score_thresh,
                line_thickness=8)
  • 保存圖片
 cv2.imwrite(%s_tmp/%s % (dataset_pref, image_path.split(‘/‘)[-1]),
                        cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
  • 釋放神經計算棒
def ncs_clean(detection_graph, device):
    detection_graph.DeallocateGraph()
    device.CloseDevice()

運行

python2 ncs_detection.py

結果

框架 圖片數量/張 耗時
TensorFlow 1800 60min
NCS 1800 10min
TensorFlow 1 2sec
NCS 1 0.3sec

性能提升6倍!單張圖300毫秒,可以說是毫秒級檢測了。在論壇上有霓虹國的同行嘗試後,甚至評價其為“超爆速”。

擴展

單根NCS一次只能運行一個模型,但是我們可以用多根NCS,多線程做檢測,達到更高的速度,具體可以看Reference第二條。

Reference

  • https://www.pyimagesearch.com/2018/02/19/real-time-object-detection-on-the-raspberry-pi-with-the-movidius-ncs/
  • https://qiita.com/PINTO/items/b97b3334ed452cb555e2

看了這麽久,還不快去給我的GitHub點star!

毫秒級檢測!你見過帶GPU的樹莓派嗎?