使用python&C++對bubbliiiing的yolo系列進行opencv.dnn進行推理部署
theme: nico highlight: atelier-forest-dark
前言
相必大家在對yolo專案部署的過程中是否存在過:有沒有一個程式碼能將當前github上的yolov4至v7的onnx推理統一一下。在這裡由於官網提供的yolo程式碼 編寫習慣不盡相同,大家的學習成本比較大,在這裡我為大家帶來的是bubbliiiing的yolo系列程式碼。 目前經我實際測試驗證了yolov4、yolov5、yolov5-6.1、yolov7都可以用這套程式碼實現onnx部署(在這裡不多贅述onnx部署的優點了) 本文以yolov7為例子進行講解(其他版本的實現思路相同)
部署之前
在進行onnx的部署前我們需要匯出onnx,這部分可以參考我github專案中的README.md部分
推理框架
整個opencv推理模組分為四部分: 1. Anchors、Stride和置信度等引數配置 2. 載入onnx權重檔案 3. 對影象進行推理得到目標座標點和置信度、目標ID 4. 對推理結果進行在影象上繪製相關資訊
引數配置
這一部分實現起來比較簡單,但是非常重要!
這裡我們對網路目標類別進行載入,className檔案格式同官網即可,Anchors和Stride填入相對於的版本的資訊即可,後續的Width、Height設定為匯出onnx時設定的大小,nmsThreshold、boxThreshold、classThreshold為自己設定的閾值,這部分大家可以根據自己實際應用中進行更改 ``` className = list(map(lambda x: x.strip(), open('coco.names', 'r').readlines())) # 可替換自己的類別檔案
替換對應yolo的Anchors值
netAnchors = np.asarray([[12.0, 16.0, 19.0, 36.0, 40.0, 28.0], [36.0, 75.0, 76.0, 55.0, 72.0, 146.0], [142.0, 110.0, 192.0, 243.0, 459.0, 401.0]], dtype=np.float32)
netStride = np.asarray([8.0, 16.0, 32.0], dtype=np.float32)
netWidth = 640
netHeight = 640
nmsThreshold = 0.80
boxThreshold = 0.80
classThreshold = 0.80
  在這裡初始化相關配置的時候,也需要生成我們繪製目標框時框的顏色,這裡的color_num為自己類別總數。Sigmoid函式這裡大家應該都很熟悉了。
def GetColors(color_num):
ColorList = []
for num in range(color_num):
R = random.randint(100, 255)
G = random.randint(100, 255)
B = random.randint(100, 255)
BGR = (B, G, R)
ColorList.append(BGR)
return ColorList
def Sigmoid(x): x = float(x) out = (float(1.) / (float(1.) + exp(-x))) return float(out) ```
讀取ONNX
大家在這裡的讀取onnx時候我踩到的一個最大的坑就是直接pip install opencv-python。直接pip得到的opencv的版本為4.6.0,當了兩天的小白鼠實踐最新版本的 opencv,總之計算的結果是莫名其妙。這裡推薦大家使用pip install opencv-python==4.5.5.62
在這裡定義了一個呼叫onnx函式,輸入onnx網路的路徑以及是否呼叫GPU模組,函式返回值為net ```
def readModel(netPath, isCuda=False): try: net = cv2.dnn.readNetFromONNX(netPath) except: return False
if isCuda: # GPU
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_CUDA)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CUDA)
else: # CPU
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_DEFAULT)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
return net
```
推理模組
由於整個推理模組比較多,在這裡我為大家分成多個部分進行講解
影象輸入及輸出
這裡定義推理函式,輸入分別是輸入影象、讀取onnx的網路,網路設點給的寬以及高。由於需要對影象進行推理,在使用opencv讀取影象的通道格式為BGR,以及影象維度需要轉換為為(1,3,640,640),這裡我們需要藉助 cv2.dnn.blobFromImage 函式進行快速轉換(當大家進行批量讀取的時候可以更換為cv2.dnn.blobFromImages)
對轉換好的影象進行網路推理並得到輸出,這裡我們得到的輸出為:(1,25200,85)
ps: 25200 = 3 * 80 * 80 * 85 + 3 * 40 * 40 * 85 + 3 * 20 * 20 * 85\
80 = 640 / 8 \
40 = 640 / 16\
20 = 640 / 32\
85 = 80(類別總數) + 5(四個位置資訊 + 一個置信度)
def Detect(SrcImg, net, netWidth, netHeight):
netInputImg = SrcImg
blob = cv2.dnn.blobFromImage(netInputImg, scalefactor=1 / 255.0, size=(netWidth, netHeight), mean=[104, 117, 123], swapRB=True, crop=False)
net.setInput(blob) # 把影象放入網路
netOutputImg = net.forward(net.getUnconnectedOutLayersNames()) # 獲取輸出結果集
netOutputImg = np.array(netOutputImg, dtype=np.float32)
pdata = netOutputImg[0]
計算網路輸出結果
上述我們對影象進行了歸一化操作,那麼我們需要計算影象的縮放比;在一個25200 * 85的陣列中我們需要遍歷計算我們需要的資訊,迴圈前定義空的表用作儲存使用
# 記錄縮放比
ratio_h = float(netInputImg.shape[1] / netHeight)
ratio_w = float(netInputImg.shape[0] / netWidth)
classIds = [] # 定義結果表
confidences = [] # 定義置信度表
boxes = [] # 定義座標表
count = 0 # 標記符
我們根據原始碼的輸出計算可以有如下for迴圈:
1. 首先是通過三個Stride值計算得到grid值(80 40 20 的由來)
2. 然後是對Anchors的值進行提取為了座標的計算
3. 對每一次的grid進行迴圈計算
對640 * 640的影象我們總共遍歷的次數是25200次,也就對應了25200行的輸出資訊。我們需要對每一行的box閾值進行計算,對符合要求的box閾值 再進行提取座標以及類別最大可能值資訊進計算。
大家需要注意下,由於我們提取了很多個座標框,如果這樣我們就進行繪製在影象上的話會十分糟糕的,不夠優雅,這裡我們就需要呼叫cv2.dnn.NMSBoxes 模組,由於cv2.dnn.NMSBoxes模組裡對座標進行抑制的話座標的排列順序為:BOX = (left, top, W, H) ,那麼我們在寫入BOX中的時候也應該按照這個順序進行填入
```
for stride in range(3): # netStride = {8.0, 16.0, 32.0} = 3
grid_x = netWidth / netStride[stride]
grid_y = netHeight / netStride[stride]
grid_x, grid_y = int(grid_x), int(grid_y) # 系統預設是float32,這裡是為了下面的迴圈轉為int
for anchor in range(3): # netAnchors 的層數 = 3
anchor_w = netAnchors[stride][anchor * 2]
anchor_h = netAnchors[stride][anchor * 2 + 1]
anchor_w, anchor_h = float(anchor_w), float(anchor_h)
for i in range(grid_x):
for j in range(grid_y): # 到這的下一行總執行次數是25200 = (80*80*3) + (40*40*3) + (20*20*3)
pdatabox = pdata[0][count][4]
box_score = Sigmoid(pdatabox) # 獲取每一行的box框中含有某個物體的概率
if box_score > boxThreshold: # box的閾值起作用了
scores = pdata[0][count][5:] # 這裡的scores理應是一個多維矩陣
_, max_class_socre, _, classIdPoint = cv2.minMaxLoc(scores) # 求最大值以及最大值的位置&位置是元組
max_class_socre = np.asarray(max_class_socre, dtype=np.float64)
max_class_socre = Sigmoid(max_class_socre)
if max_class_socre > classThreshold: # 類別的置信度起作用
# rect[x,y,w,h]
pdatax=pdata[0][count][0]
x = (Sigmoid(pdatax) * float(2.) - float(0.5) + j) * netStride[stride] # x
pdatay = np.asarray(pdata[0][count][1], dtype=np.float64)
y = (Sigmoid(pdatay) * float(2.) - float(0.5) + i) * netStride[stride] # y
pdataw=pdata[0][count][2]
w = pow(Sigmoid(pdataw) * float(2.), float(2.0)) * anchor_w # w
pdatah = pdata[0][count][3]
h = pow(Sigmoid(pdatah) * float(2.), float(2.0)) * anchor_h # h
left = (x - 0.5 * w) * ratio_w
top = (y - 0.5 * h) * ratio_h
left, top, W, H = int(left), int(top), int(w * ratio_w), int(h * ratio_h)
# 對classIds & confidences & boxes
classIds.append(classIdPoint[1]) # 獲取最大值的位置
confidences.append(max_class_socre * box_score)
boxes.append((left, top, W, H))
count += 1
```
極大值抑制
這裡我們需要對25200次迴圈的結果進行極大值抑制,參考opencv官網我們需要輸入boxes, confidences, classThreshold, nmsThreshold
最後得到的結果為:
1. 返回結果的ID(便於從className中找到對應的類別名稱);
2. 該ID的置信度
3. 該ID的座標框
在這裡我們對結果進行判斷,因為存在輸出為空的情況,即:在影象中沒有檢測出符合要求的目標
nms_result = cv2.dnn.NMSBoxes(boxes, confidences, classThreshold, nmsThreshold) # 抑制處理 返回的是一個數組
result_id, result_confidence, result_boxs = [], [], []
for idx in nms_result:
result_id.append(classIds[idx])
result_confidence.append(confidences[idx])
result_boxs.append(boxes[idx])
if len(result_id) == 0:
return False
else:
return result_id, result_confidence, result_boxs
結果繪製
我們終終於將結果獲取到了我們需要的結果,這裡就需要進行繪製了。在對BOX的結果進行分解: BOX = (left, top, W, H),在繪製的時候需要用到cv2.rectangle和cv2.putText,那麼我們需要一張影象的Xmin Xmax Ymin Ymax 其中:Xmin = left Ymin = top Xmax = left + W Ymax = top + H 在這裡大家可以對繪製的顏色以及字型相關資訊自己按照喜好調整! ```
def drawPred(img, result_id, result_confidence, result_boxs, color): for i in range(len(result_id)): class_id = result_id[i] confidence = round(result_confidence[i], 2) # 保留兩位有效數字 box = result_boxs[i] pt1 = (box[0], box[1]) pt2 = (box[0] + box[2], box[1] + box[3]) # 繪製圖像目標位置 cv2.rectangle(img, pt1, pt2, color[i], 2, 2) # x1, y1, x2, y2 = box[0], box[1], box[0] + box[2], box[1] + box[3] cv2.rectangle(img, (box[0], box[1]-18), (box[0] + box[2], box[1]), color[i], -1)
label = "%s:%s" % (className[class_id], confidence) # 給目標進行新增類別名稱以及置信度
FONT_FACE = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img, label, (box[0] - 2, box[1] - 5), FONT_FACE, 0.5, (0, 0, 0), 1)
cv2.imwrite("my640.jpg", img)
cv2.imshow("outPut", img)
cv2.waitKey(0)
```
入口呼叫
在這裡我們呼叫定義的函式,輸入影象的路徑和網路的路徑以及標籤類別總數
if __name__ == "__main__":
img_path = "bus.jpg"
model_path = "yolov7.onnx"
classNum = 80
color = GetColors(classNum)
Mynet = readModel(model_path, isCuda=False)
img = cv2.imread(img_path)
result_id, result_confidence, result_boxs = Detect(img, Mynet, 640, 640)
drawPred(img, result_id, result_confidence, result_boxs, color)
附錄:
檢測結果資訊表:
專案地址:
https://github.com/kivenyangming/yolo-bubbliiiing-onnx-py
https://github.com/kivenyangming/yolo-bubbliiiing-onnx
本文正在參加「金石計劃 . 瓜分6萬現金大獎」
- 基於實時狀態下人臉檢測完成人流量統計
- 使用python&C 對bubbliiiing的yolo系列進行opencv.dnn進行推理部署
- 影片檢測 定位 測距 控制滑鼠移動
- 不要傻乎乎的去找不同了,一起來用程式碼完成“找不同”遊戲吧
- 解決小目標檢測常用手段——影象平鋪
- 分析單雙步目標檢測效果差異
- 三個小Trick助力解決動作檢測【摔倒檢測】
- 七夕禮物:火柴人特效製作
- 學習:時間序列模型綜述
- 一起來學MediaPipe(一)人臉及五官定位檢測
- 實現虛擬空間穿梭不是夢:人物影片背景替換
- 資料倍化術——提升資料數量同時滿足小目標影象構建
- Study - 基於Harris的角點特徵檢測
- Study-基於塊匹配的全景影象拼接
- Study-基於主成分分析的影象壓縮和重建
- Study-基於小波技術進行影象融合
- Study-基於直方圖優化的影象去霧技術
- Study-基於形態學的權重自適應影象去噪
- 學習——Anycost Gan 風格遷移
- 定區域法快速實現資料集的製作