嘿!您似乎在 United States,您想使用我们的 English 网站吗?
Switch to English site
Skip to main content

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

作者

張嘉鈞

難度

普通

詳解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的標頭檔進行搜尋:

0235_cb375d3755b2c26c30f2a702d26603d0bf33703f.png

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

_a7f1a487eb6f8319ffd7f30627a7d712a0171056.png

自己撰寫一個最易理解的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()

運行結果

0145_b982dd2ccfc59d86f2522a06b5d8a2ea24d5c055.png

結論

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

相關文章

https://github.com/AlexeyAB/darknet

CAVEDU Education is devoted into robotics education and maker movement since 2008, and is intensively active in teaching fundamental knowledge and skills. We had published many books for readers in all ages, topics including Deep Learning, edge computing, App Inventor, IoT and robotics. Please check CAVEDU's website for more information: http://www.cavedu.com, http://www.appinventor.tw