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

NVIDIA Jetson Nano深度学习应用-使用OpenCV处理YOLOv4实时影像辨识

作者

张嘉钧

难度

普通

播放影片:custom_yolov4_demo.mp4

详解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的头文件进行搜寻:

0236_cb375d3755b2c26c30f2a702d26603d0bf33703f.png

接着可以看到一系列的副函式上方有 // network.h 的字样,代表要去 /darknet/src/network.c中找到这个副函式的内容,注意程序内容是放在 .c 哦:

1_77a59737c73ac1a56bccad9c97785d16be977c59.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()

 

运行结果

0146_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
DesignSpark Electrical Logolinkedin