你觉得这篇文章怎么样? 帮助我们为您提供更好的内容。
Thank you! Your feedback has been received.
There was a problem submitting your feedback, please try again later.
你觉得这篇文章怎么样?
作者 |
张嘉钧 |
难度 |
普通 |
播放影片: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的头文件进行搜寻:
接着可以看到一系列的副函式上方有 // 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。