樹莓派3B+英特爾神經計算棒進行高速目標檢測python
轉載請註明做者夢裏茶linux
代碼:
訓練數據預處理:
https://gist.github.com/ahangchen/ae1b7562c1f93fdad1de58020e94fbdf
測試:https://github.com/ahangchen/ncs_detectiongit
Star是一種美德。github
最近在作一個項目,要在樹莓派上分析視頻中的圖片,檢測目標,統計目標個數,這是一張樣例圖片:shell
當下效果最好的目標檢測都是基於神經網絡來作的,包括faster rcnn, ssd, yolo2等等,要在樹莓派這種資源緊張的設備上運行檢測模型,首先想到的就是用最輕量的MobileNet SSD,使用Tensorflow object detection api實現的MobileNet SSD雖然已經很是輕,但在樹莓派上推導一張1280x720的圖仍然須要2秒,有興趣的同窗能夠參考這兩個項目:編程
具體的操做在Tensorflow文檔裏都說的很清楚了,在樹莓派上的操做也是同樣的,有問題能夠評論區討論api
極限的模型仍然不能知足性能需求,就須要請出咱們今天的主角了,Intel Movidius Neural Computing Stick
bash
處理器 | 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來訓練模型,部署到樹莓派上。
ncsdk的環境分爲兩部分,訓練端和測試端。
安裝這個過程,說難不難,也就幾行命令的事情,但也有不少坑
在訓練端主機上,插入神經計算棒,而後:
git clone https://github.com/movidius/ncsdk cd ncsdk make install
其中,make install乾的是這些事情:
注意,
/opt/movidius/
這個目錄下,並關聯到系統python3裏邊的(/usr/bin/python3
),若是你電腦裏原來有tf或caffe,也不會被關聯上去這個步驟主要的坑來自萬惡的Caffe,若是你裝過python3版的caffe,大概會有經驗一些,這裏有幾個小坑提示一下:
CAFFE_USE_CUDA=yes
,這樣你以後也能用這個caffe來訓練模型cd /usr/lib/x86_64-linux-gnu/ sudo ln -s libboost_python-py35.so libboost_python3.so
一波操做以後,咱們裝好了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 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
沒報錯就是裝好了,測試端很簡單。
看pyimagesearch這個教程
就是正常的用caffe訓練MobileNet-SSD,主要參考這個倉庫:
README裏將步驟講得很清楚了
labelmap.prototxt
,放到MobileNet-SSD目錄下,好比說,你是在coco預訓練模型上訓練的話,能夠把coco的標籤文件複製過來,將其中與你的目標類(好比個人目標類是Cattle)相近的類(好比Coco中是Cow)改爲對應的名字,並用它的label做爲你的目標類的label。(好比我用21這個類表明Cattle)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
<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中寫對應的圖片名)
這樣一波操做以後,咱們的images
和Annotations
目錄裏都會有數據了,接下來咱們須要把它們一塊複製到all
目錄下
cp images/* all/ cp Annotations/* all/
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
cd MobileNet-SSD ln -s PATH_TO_YOUR_TRAIN_LMDB trainval_lmdb ln -s PATH_TO_YOUR_TEST_LMDB test_lmdb
gen_model.sh
生成三個prototxt(train, test, deploy)# 默認clone下來的目錄是沒有example這個目錄的,而gen_model.sh又會把文件生成到example目錄 mkdir example ./gen_model.sh
./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目錄下
運行merge_bn.py將訓練獲得的模型去除bn層,獲得可部署的Caffe模型,這樣你就能獲得一個名爲MobileNetSSD_deploy.caffemodel
的權重文件,對應的prototxt爲example/MobileNetSSD_deploy.prototxt
離題那麼久,終於來到主題,咱們要把這個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版的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
檢測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
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)
image_np = cv2.imread(image_path) image_np = cv2.cvtColor(image_np, cv2.COLOR_BGR2RGB)
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
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第二條。
看了這麼久,還不快去給個人GitHub點star!