NVIDAI Jetson Nano深度學習應用-使用OpenCV處理YOLOv4即時影像辨識

*本文由RS components 贊助發表,轉載自DesignSpark部落格原文連結

作者/攝影 張嘉鈞
難度

★★☆☆☆(普通)

材料表

RK-NVIDIA® Jetson Nano™ Developer Kit B01 套件

 

詳解darknet.py

首先我們先來詳解一下 darknet.py,由於他是由C去做封裝的所以比較難理解一些,但是他已經幫我們整理好一些Python的副函式可以使用,這邊將會一一介紹。

取得神經網路的輸入寬高 ( network_width、network_height )

如同標題所示,其中 lib會連結到darknetlib.so,是作者用c封裝好的函式庫,最後面會稍微介紹一下怎麼樣去查看各函式,這邊功能簡單就不多說了:

[pastacode lang=”python” manual=”def%20network_width(net)%3A%0A%20%20%20%20return%20lib.network_width(net)%0A%0Adef%20network_height(net)%3A%0A%20%20%20%20return%20lib.network_height(net)” message=”” highlight=”” provider=”manual”/]

邊界框座標與標籤色彩 ( bbox2point、class_color)

由於神經網路模型輸出的是中心點的位置以及物件的寬高大小,這邊需要一個副函式來做轉換;接著通常物件辨識會變是一種以上的物件,所以通常會使用不同的顏色來做區隔,所以也提供了一個隨機色彩的副函式:

[pastacode lang=”python” manual=”def%20bbox2points(bbox)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20From%20bounding%20box%20yolo%20format%0A%20%20%20%20to%20corner%20points%20cv2%20rectangle%0A%20%20%20%20%22%22%22%0A%20%20%20%20x%2C%20y%2C%20w%2C%20h%20%3D%20bbox%0A%20%20%20%20xmin%20%3D%20int(round(x%20-%20(w%20%2F%202)))%0A%20%20%20%20xmax%20%3D%20int(round(x%20%2B%20(w%20%2F%202)))%0A%20%20%20%20ymin%20%3D%20int(round(y%20-%20(h%20%2F%202)))%0A%20%20%20%20ymax%20%3D%20int(round(y%20%2B%20(h%20%2F%202)))%0A%20%20%20%20return%20xmin%2C%20ymin%2C%20xmax%2C%20ymax%0A%0Adef%20class_colors(names)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20Create%20a%20dict%20with%20one%20random%20BGR%20color%20for%20each%0A%20%20%20%20class%20name%0A%20%20%20%20%22%22%22%0A%20%20%20%20return%20%7Bname%3A%20(%0A%20%20%20%20%20%20%20%20random.randint(0%2C%20255)%2C%0A%20%20%20%20%20%20%20%20random.randint(0%2C%20255)%2C%0A%20%20%20%20%20%20%20%20random.randint(0%2C%20255))%20for%20name%20in%20names%7D” message=”” highlight=”” provider=”manual”/]

載入神經網路模型 ( load_network )

在執行命令的時候可以發現每一次都需要給予 data、cfg、weight,原因就在這個副函式上拉,在load_net_custom的部分會透過config ( 配置檔 )、weight ( 權重檔 ) 導入神經網路模型,這邊是他寫好的liberary我也不再深入探討;接著metadata存放 .data 檔案後就可以取得所有的標籤,這邊用Python的簡寫來完成,將所有的標籤都存放在陣列裡面 ( class_names ),metadata 的部分可以搭配下一列的coco.data內容去理解:

[pastacode lang=”python” manual=”def%20load_network(config_file%2C%20data_file%2C%20weights%2C%20batch_size%3D1)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20load%20model%20description%20and%20weights%20from%20config%20files%0A%20%20%20%20args%3A%0A%20%20%20%20%20%20%20%20config_file%20(str)%3A%20path%20to%20.cfg%20model%20file%0A%20%20%20%20%20%20%20%20data_file%20(str)%3A%20path%20to%20.data%20model%20file%0A%20%20%20%20%20%20%20%20weights%20(str)%3A%20path%20to%20weights%0A%20%20%20%20returns%3A%0A%20%20%20%20%20%20%20%20network%3A%20trained%20model%0A%20%20%20%20%20%20%20%20class_names%0A%20%20%20%20%20%20%20%20class_colors%0A%20%20%20%20%22%22%22%0A%20%20%20%20network%20%3D%20load_net_custom(%0A%20%20%20%20%20%20%20%20config_file.encode(%22ascii%22)%2C%0A%20%20%20%20%20%20%20%20weights.encode(%22ascii%22)%2C%200%2C%20batch_size)%0A%20%20%20%20metadata%20%3D%20load_meta(data_file.encode(%22ascii%22))%0A%20%20%20%20class_names%20%3D%20%5Bmetadata.names%5Bi%5D.decode(%22ascii%22)%20for%20i%20in%20range(metadata.classes)%5D%0A%20%20%20%20colors%20%3D%20class_colors(class_names)%0A%20%20%20%20return%20network%2C%20class_names%2C%20colors%0A%20″ message=”” highlight=”” provider=”manual”/]

這邊可以稍微帶一下各個檔案的內容,下列是coco.data,這邊就不多作介紹了,應該都可以看得懂:

[pastacode lang=”python” manual=”classes%3D%2080%0Atrain%20%20%3D%20%2Fhome%2Fpjreddie%2Fdata%2Fcoco%2Ftrainvalno5k.txt%0Avalid%20%20%3D%20coco_testdev%0A%23valid%20%3D%20data%2Fcoco_val_5k.list%0A%0Anames%20%3D%20data%2Fcoco.names%0Abackup%20%3D%20%2Fhome%2Fpjreddie%2Fbackup%2F%0Aeval%3Dcoco” message=”” highlight=”” provider=”manual”/]

將辨識結果顯示出來 ( print_detections )

將推論後的結果顯示在終端機上面,如果要學習怎麼提取資料可以參考這個部分,在推論後的結果 ( detection ) 中可以解析出三個內容 標籤 ( labels)、信心指數 ( confidence )、邊界框 ( bbox ),取得到之後將所有內容顯示出來,這邊提供了一個變數是coordinates讓使用者自己確定是否要顯示邊界框資訊:

[pastacode lang=”python” manual=”def%20print_detections(detections%2C%20coordinates%3DFalse)%3A%0A%20%20%20%20print(%22%5CnObjects%3A%22)%0A%20%20%20%20for%20label%2C%20confidence%2C%20bbox%20in%20detections%3A%0A%20%20%20%20%20%20%20%20x%2C%20y%2C%20w%2C%20h%20%3D%20bbox%0A%20%20%20%20%20%20%20%20if%20coordinates%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(%22%7B%7D%3A%20%7B%7D%25%20%20%20%20(left_x%3A%20%7B%3A.0f%7D%20%20%20top_y%3A%20%20%7B%3A.0f%7D%20%20%20width%3A%20%20%20%7B%3A.0f%7D%20%20%20height%3A%20%20%7B%3A.0f%7D)%22.format(label%2C%20confidence%2C%20x%2C%20y%2C%20w%2C%20h))%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20print(%22%7B%7D%3A%20%7B%7D%25%22.format(label%2C%20confidence))” message=”” highlight=”” provider=”manual”/]

將邊界框繪製到圖片上 ( draw_boxes )

將邊界框繪製到圖片上面,針對 bbox進行轉換 ( 使用 bbox2point ) 取得四個邊角座標,方便繪製邊界框使用 ( cv2.rectangle );接著要將標籤資訊給繪製上去 ( cv2.putText ),最後將繪製完的圖片回傳:

[pastacode lang=”python” manual=”def%20draw_boxes(detections%2C%20image%2C%20colors)%3A%0A%20%20%20%20import%20cv2%0A%20%20%20%20for%20label%2C%20confidence%2C%20bbox%20in%20detections%3A%0A%20%20%20%20%20%20%20%20left%2C%20top%2C%20right%2C%20bottom%20%3D%20bbox2points(bbox)%0A%20%20%20%20%20%20%20%20cv2.rectangle(image%2C%20(left%2C%20top)%2C%20(right%2C%20bottom)%2C%20colors%5Blabel%5D%2C%201)%0A%20%20%20%20%20%20%20%20cv2.putText(image%2C%20%22%7B%7D%20%5B%7B%3A.2f%7D%5D%22.format(label%2C%20float(confidence))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(left%2C%20top%20-%205)%2C%20cv2.FONT_HERSHEY_SIMPLEX%2C%200.5%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20colors%5Blabel%5D%2C%202)%0A%20%20%20%20return%20image” message=”” highlight=”” provider=”manual”/]

解析辨識結果並回傳 ( decode_detection )

這部分是待會加上GPIO互動最重要的環節了,他會解析detection並將各個值做好處理後回傳讓使用者去做後續的應用;這邊比較不常看到的是round( x, 2 ) 為取小數點後2位,乘以100則是轉換成百分比:

[pastacode lang=”python” manual=”def%20decode_detection(detections)%3A%0A%20%20%20%20decoded%20%3D%20%5B%5D%0A%20%20%20%20for%20label%2C%20confidence%2C%20bbox%20in%20detections%3A%0A%20%20%20%20%20%20%20%20confidence%20%3D%20str(round(confidence%20*%20100%2C%202))%0A%20%20%20%20%20%20%20%20decoded.append((str(label)%2C%20confidence%2C%20bbox))%0A%20%20%20%20return%20decoded” message=”” highlight=”” provider=”manual”/]

取得乾淨的辨識結果 ( remove_negatives )

這邊的目的是因為coco dataset有91種類別,但辨識出來的東西可能僅有3個,這樣就會有88個0,資料稍微肥大不好查看,所以她這邊提供一個副函式將所有有信心指數為0的給除去,留下有辨識到的物件:

[pastacode lang=”python” manual=”def%20remove_negatives(detections%2C%20class_names%2C%20num)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20Remove%20all%20classes%20with%200%25%20confidence%20within%20the%20detection%0A%20%20%20%20%22%22%22%0A%20%20%20%20predictions%20%3D%20%5B%5D%0A%20%20%20%20for%20j%20in%20range(num)%3A%0A%20%20%20%20%20%20%20%20for%20idx%2C%20name%20in%20enumerate(class_names)%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20detections%5Bj%5D.prob%5Bidx%5D%20%3E%200%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bbox%20%3D%20detections%5Bj%5D.bbox%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20bbox%20%3D%20(bbox.x%2C%20bbox.y%2C%20bbox.w%2C%20bbox.h)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20predictions.append((name%2C%20detections%5Bj%5D.prob%5Bidx%5D%2C%20(bbox)))%0A%20%20%20%20return%20predictions” message=”” highlight=”” provider=”manual”/]

進行辨識回傳結果 ( detect_image )

最重要的環節來了,這邊是主要推論的地方,將模型、標籤、圖片都丟進副函式當中就可以獲得結果了,這邊比較多C函式庫的內容,如果不想深入研究的話只需要知道透過這個副函式可以獲得predict的結果,知道這個點我們就可以客製化程式了:

[pastacode lang=”python” manual=”def%20detect_image(network%2C%20class_names%2C%20image%2C%20thresh%3D.5%2C%20hier_thresh%3D.5%2C%20nms%3D.45)%3A%0A%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20Returns%20a%20list%20with%20highest%20confidence%20class%20and%20their%20bbox%0A%20%20%20%20%22%22%22%0A%20%20%20%20pnum%20%3D%20pointer(c_int(0))%0A%20%20%20%20predict_image(network%2C%20image)%0A%20%20%20%20detections%20%3D%20get_network_boxes(network%2C%20image.w%2C%20image.h%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20thresh%2C%20hier_thresh%2C%20None%2C%200%2C%20pnum%2C%200)%0A%20%20%20%20num%20%3D%20pnum%5B0%5D%0A%20%20%20%20if%20nms%3A%0A%20%20%20%20%20%20%20%20do_nms_sort(detections%2C%20num%2C%20len(class_names)%2C%20nms)%0A%20%20%20%20predictions%20%3D%20remove_negatives(detections%2C%20class_names%2C%20num)%0A%20%20%20%20predictions%20%3D%20decode_detection(predictions)%0A%20%20%20%20free_detections(detections%2C%20num)%0A%20%20%20%20return%20sorted(predictions%2C%20key%3Dlambda%20x%3A%20x%5B1%5D)” message=”” highlight=”” provider=”manual”/]

進行辨識回傳結果 – 進階

這裡多是用C的函式庫內容,在程式的最頂端有導入了ctypes這個函式庫,這是個與C兼容的數據類型,並且可以透過這個函式庫調用DLL等用C建置好的函式庫,如果想要了解更多可以去include資料夾中尋找C的函式,並且在 darknet/src當中找到對應的內容。

舉例來說,我現在想了解get_network_boxes,先開啟 darknet/include/darknet.h的標頭檔進行搜尋:

接著可以看到一系列的副函式上方有 // network.h 的字樣,代表要去 /darknet/src/network.c中找到這個副函式的內容,注意程式內容是放在 .c 哦:

自己撰寫一個最易理解的yolov4即時影像辨識

礙於原本github提供的程式碼對於一些新手來說還是不太好理解,因為新手也比較少用到 Threading跟Queue,除此之外原本的darknet_video.py我在Jetson Nano上執行非常的卡頓 ( 原因待查證 ),所以我決定來帶大家撰寫一個較好理解的版本,使用OpenCV就可以搞定。

這個程式需要放在darknet的資料夾當中,並且確保已經有build過了 ( 是否有 libdarknet.so ),詳細的使用方法可以參考github或我之前的yolov4文章。

 

正式開始

最陽春的版本就是直接導入darknet.py之後開始撰寫,因為我直接寫即時影像辨識,所以還需要導入opencv:

[pastacode lang=”python” manual=”import%20cv2%0Aimport%20darknet%0Aimport%20time” message=”” highlight=”” provider=”manual”/]

一些基本的參數可以先宣告,像是導入神經網路模型的配置、權重、資料集等:

[pastacode lang=”python” manual=”%23%20Parameters%0A%0Awin_title%20%3D%20’YOLOv4%20CUSTOM%20DETECTOR’%0Acfg_file%20%3D%20’cfg%2Fyolov4-tiny.cfg’%0Adata_file%20%3D%20’cfg%2Fcoco.data’%0Aweight_file%20%3D%20’yolov4-tiny.weights’%0Athre%20%3D%200.25%0Ashow_coordinates%20%3D%20True” message=”” highlight=”” provider=”manual”/]

接著我們可以先宣告神經網路模型並且取得輸入的維度大小,注意我們是以darknet.py進行客製,所以如果不知道load_network的功用可以往回去了解

[pastacode lang=”python” manual=”%23%20Load%20Network%0A%0Anetwork%2C%20class_names%2C%20class_colors%20%3D%20darknet.load_network(%0A%20%20%20%20%20%20%20%20cfg_file%2C%0A%20%20%20%20%20%20%20%20data_file%2C%0A%20%20%20%20%20%20%20%20weight_file%2C%0A%20%20%20%20%20%20%20%20batch_size%3D1%0A%20%20%20%20)%0A%0A%23%20Get%20Nets%20Input%20dimentions%0A%0Awidth%20%3D%20darknet.network_width(network)%0Aheight%20%3D%20darknet.network_height(network)” message=”” highlight=”” provider=”manual”/]

有了模型、輸入維度之後就可以開始取得輸入圖像,第一個版本中我們使用OpenCV進行即時影像辨識,所以需要先取得到Webcam的物件並使用While來完成:

[pastacode lang=”python” manual=”%23%20Video%20Stream%0A%0Awhile%20cap.isOpened()%3A%0A%20%20%20%20%0A%20%20%20%20%0A%23%20Get%20current%20frame%2C%20quit%20if%20no%20frame%20%0A%0A%20%20%20%20ret%2C%20frame%20%3D%20cap.read()%0A%0A%20%20%20%20if%20not%20ret%3A%20break%0A%0A%20%20%20%20t_prev%20%3D%20time.time()” message=”” highlight=”” provider=”manual”/]

接著需要對圖像進行前處理,在主要是OpenCV格式是BGR需要轉換成RGB,除此之外就是輸入的大小需要跟神經網路模型相同:

[pastacode lang=”python” manual=”%23%20Fix%20image%20format%0A%0A%20%20%20%20frame_rgb%20%3D%20cv2.cvtColor(%20frame%2C%20cv2.COLOR_BGR2RGB)%0A%20%20%20%20frame_resized%20%3D%20cv2.resize(%20frame_rgb%2C%20(width%2C%20height))” message=”” highlight=”” provider=”manual”/]

接著轉換成darknet的格式,透過make_image事先宣告好輸入的圖片,再透過copy_image_from_bytes將位元組的形式複製到剛剛宣告好的圖片當中:

[pastacode lang=”python” manual=”%20%20%20%20%0A%23%20convert%20to%20darknet%20format%2C%20save%20to%20%22%20darknet_image%20%22%0A%0A%20%20%20%20darknet_image%20%3D%20darknet.make_image(width%2C%20height%2C%203)%0A%20%20%20%20darknet.copy_image_from_bytes(darknet_image%2C%20frame_resized.tobytes())%20″ message=”” highlight=”” provider=”manual”/]

再來就是Inference的部分,直接調用 detect_image即可獲得結果,可以使用print_detections將資訊顯示在終端機上,最後要記得用free_image將圖片給清除:

[pastacode lang=”python” manual=”%23%20inference%0A%0A%20%20%20%20detections%20%3D%20darknet.detect_image(network%2C%20class_names%2C%20darknet_image%2C%20thresh%3Dthre)%0A%20%20%20%20darknet.print_detections(detections%2C%20show_coordinates)%0A%20%20%20%20darknet.free_image(darknet_image)” message=”” highlight=”” provider=”manual”/]

最後就是將bounding box繪製到圖片上並顯示,這邊使用的是frame_resized而不是剛剛的darknet_image,那個變數的內容會專門用來辨識所使用,況且剛剛free_image已經將其清除了;用OpenCV顯示圖片前記得要轉換回BGR格式哦:

[pastacode lang=”python” manual=”%20%20%20%20%0A%23%20draw%20bounding%20box%0A%20%20%20%20image%20%3D%20darknet.draw_boxes(detections%2C%20frame_resized%2C%20class_colors)%0A%20%20%20%20image%20%3D%20cv2.cvtColor(image%2C%20cv2.COLOR_BGR2RGB)” message=”” highlight=”” provider=”manual”/]

顯示之前計算一下FPS並顯示在左上角:

[pastacode lang=”python” manual=”%20%20%20%20%23%20Show%20Image%20and%20FPS%0A%0A%20%20%20%20fps%20%3D%20int(1%2F(time.time()-t_prev))%0A%20%20%20%20cv2.rectangle(image%2C%20(5%2C%205)%2C%20(75%2C%2025)%2C%20(0%2C0%2C0)%2C%20-1)%0A%20%20%20%20cv2.putText(image%2C%20f’FPS%20%7Bfps%7D’%2C%20(10%2C%2020)%2C%20cv2.FONT_HERSHEY_SIMPLEX%2C%200.5%2C%20(0%2C%200%2C%20255)%2C%202)%0A%20%20%20%20cv2.imshow(win_title%2C%20image)” message=”” highlight=”” provider=”manual”/]

程式的最後是按下小寫q離開迴圈並且刪除所有視窗、釋放Webcam的物件:

[pastacode lang=”python” manual=”%20%20%20%20if%20cv2.waitKey(1)%20%3D%3D%20ord(‘q’)%3A%0A%20%20%20%20%20%20%20%20break%0Acv2.destroyAllWindows()%0Acap.release()” message=”” highlight=”” provider=”manual”/]

運行結果

結論

其實搞清楚darknet.py之後再進行客製化的修改已經就不難了,已經使用常見的OpenCV來取得圖像,如果想要改成視窗程式也很簡單,使用Tkinter、PyQt5等都可以更快結合yolov4;順道提一下,如果想要追求更高的效能可以使用Gstreamer搭配多線程或是轉換到TensorRT引擎上加速,可以參考之前的yolov4基礎介紹,後半段有提供TensorRT的Github。

 

相關文章

https://github.com/AlexeyAB/darknet

 

*本文由RS components 贊助發表,轉載自DesignSpark部落格原文連結

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *