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封裝好的函式庫,最後面會稍微介紹一下怎麼樣去查看各函式,這邊功能簡單就不多說了:

def network_width(net):
    return lib.network_width(net)

def network_height(net):
    return lib.network_height(net)

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

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

def bbox2points(bbox):
    """
    From bounding box yolo format
    to corner points cv2 rectangle
    """
    x, y, w, h = bbox
    xmin = int(round(x - (w / 2)))
    xmax = int(round(x + (w / 2)))
    ymin = int(round(y - (h / 2)))
    ymax = int(round(y + (h / 2)))
    return xmin, ymin, xmax, ymax

def class_colors(names):
    """
    Create a dict with one random BGR color for each
    class name
    """
    return {name: (
        random.randint(0, 255),
        random.randint(0, 255),
        random.randint(0, 255)) for name in names}

載入神經網路模型 ( load_network )

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

def load_network(config_file, data_file, weights, batch_size=1):
    """
    load model description and weights from config files
    args:
        config_file (str): path to .cfg model file
        data_file (str): path to .data model file
        weights (str): path to weights
    returns:
        network: trained model
        class_names
        class_colors
    """
    network = load_net_custom(
        config_file.encode("ascii"),
        weights.encode("ascii"), 0, batch_size)
    metadata = load_meta(data_file.encode("ascii"))
    class_names = [metadata.names[i].decode("ascii") for i in range(metadata.classes)]
    colors = class_colors(class_names)
    return network, class_names, colors
 

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

classes= 80
train  = /home/pjreddie/data/coco/trainvalno5k.txt
valid  = coco_testdev
#valid = data/coco_val_5k.list

names = data/coco.names
backup = /home/pjreddie/backup/
eval=coco

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

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

def print_detections(detections, coordinates=False):
    print("\nObjects:")
    for label, confidence, bbox in detections:
        x, y, w, h = bbox
        if coordinates:
            print("{}: {}%    (left_x: {:.0f}   top_y:  {:.0f}   width:   {:.0f}   height:  {:.0f})".format(label, confidence, x, y, w, h))
        else:
            print("{}: {}%".format(label, confidence))

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

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

def draw_boxes(detections, image, colors):
    import cv2
    for label, confidence, bbox in detections:
        left, top, right, bottom = bbox2points(bbox)
        cv2.rectangle(image, (left, top), (right, bottom), colors[label], 1)
        cv2.putText(image, "{} [{:.2f}]".format(label, float(confidence)),
                    (left, top - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5,
                    colors[label], 2)
    return image

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

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

def decode_detection(detections):
    decoded = []
    for label, confidence, bbox in detections:
        confidence = str(round(confidence * 100, 2))
        decoded.append((str(label), confidence, bbox))
    return decoded

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

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

def remove_negatives(detections, class_names, num):
    """
    Remove all classes with 0% confidence within the detection
    """
    predictions = []
    for j in range(num):
        for idx, name in enumerate(class_names):
            if detections[j].prob[idx] > 0:
                bbox = detections[j].bbox
                bbox = (bbox.x, bbox.y, bbox.w, bbox.h)
                predictions.append((name, detections[j].prob[idx], (bbox)))
    return predictions

進行辨識回傳結果 ( detect_image )

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

def detect_image(network, class_names, image, thresh=.5, hier_thresh=.5, nms=.45):
    """
        Returns a list with highest confidence class and their bbox
    """
    pnum = pointer(c_int(0))
    predict_image(network, image)
    detections = get_network_boxes(network, image.w, image.h,
                                   thresh, hier_thresh, None, 0, pnum, 0)
    num = pnum[0]
    if nms:
        do_nms_sort(detections, num, len(class_names), nms)
    predictions = remove_negatives(detections, class_names, num)
    predictions = decode_detection(predictions)
    free_detections(detections, num)
    return sorted(predictions, key=lambda x: x[1])

進行辨識回傳結果 – 進階

這裡多是用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:

import cv2
import darknet
import time

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

# Parameters

win_title = 'YOLOv4 CUSTOM DETECTOR'
cfg_file = 'cfg/yolov4-tiny.cfg'
data_file = 'cfg/coco.data'
weight_file = 'yolov4-tiny.weights'
thre = 0.25
show_coordinates = True

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

# Load Network

network, class_names, class_colors = darknet.load_network(
        cfg_file,
        data_file,
        weight_file,
        batch_size=1
    )

# Get Nets Input dimentions

width = darknet.network_width(network)
height = darknet.network_height(network)

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

# Video Stream

while cap.isOpened():
    
    
# Get current frame, quit if no frame 

    ret, frame = cap.read()

    if not ret: break

    t_prev = time.time()

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

# Fix image format

    frame_rgb = cv2.cvtColor( frame, cv2.COLOR_BGR2RGB)
    frame_resized = cv2.resize( frame_rgb, (width, height))

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

    
# convert to darknet format, save to " darknet_image "

    darknet_image = darknet.make_image(width, height, 3)
    darknet.copy_image_from_bytes(darknet_image, frame_resized.tobytes()) 

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

# inference

    detections = darknet.detect_image(network, class_names, darknet_image, thresh=thre)
    darknet.print_detections(detections, show_coordinates)
    darknet.free_image(darknet_image)

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

    
# draw bounding box
    image = darknet.draw_boxes(detections, frame_resized, class_colors)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

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

    # Show Image and FPS

    fps = int(1/(time.time()-t_prev))
    cv2.rectangle(image, (5, 5), (75, 25), (0,0,0), -1)
    cv2.putText(image, f'FPS {fps}', (10, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
    cv2.imshow(win_title, image)

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

    if cv2.waitKey(1) == ord('q'):
        break
cv2.destroyAllWindows()
cap.release()

運行結果

結論

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

 

相關文章

https://github.com/AlexeyAB/darknet

 

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

發佈留言

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