:YOLOv5攝像頭抓幀與推理全鏈路)
1. 從零打通攝像頭到推理的完整鏈路香橙派RK3588上跑YOLOv5模型轉(zhuǎn)換和推理腳本都調(diào)通了但一直用測試圖片跑總覺得差點意思。真正要把這套東西用起來第一步就是讓板子自己“看見”畫面——接上攝像頭抓一幀送進模型拿到檢測結(jié)果。這個環(huán)節(jié)看著簡單實際上從設(shè)備識別、圖像格式轉(zhuǎn)換到推理輸入適配每一步都有坑。我前后折騰了三四塊不同型號的攝像頭才把這條鏈路徹底跑順。這篇內(nèi)容適合已經(jīng)在香橙派RK3588上完成YOLOv5基礎(chǔ)部署、模型能正常推理靜態(tài)圖片的開發(fā)者。如果你還沒走到這一步建議先把前面的模型轉(zhuǎn)換和基礎(chǔ)推理跑通再來看。整條鏈路的核心關(guān)鍵詞就幾個香橙派、RK3588、YOLOv5、OpenCV、攝像頭。我會從硬件選型開始一步步講到怎么用OpenCV抓幀、怎么把抓到的幀喂給YOLOv5、以及中間那些讓人抓狂的報錯怎么解決。先說清楚最終要實現(xiàn)的效果在香橙派RK3588上運行一個Python腳本調(diào)用USB攝像頭或者MIPI攝像頭實時抓取一幀畫面經(jīng)過預(yù)處理后送入YOLOv5s模型進行推理輸出檢測框和類別信息。整個過程不依賴網(wǎng)絡(luò)全部在板端完成。這個方案可以直接作為后續(xù)視頻流實時檢測的基礎(chǔ)框架也可以用來做定時抓拍分析。2. 硬件選型與攝像頭接入方案2.1 USB攝像頭 vs MIPI攝像頭怎么選香橙派RK3588支持兩種攝像頭接入方式USB接口和MIPI CSI接口。這兩種方案各有優(yōu)劣選哪個取決于你的具體場景。USB攝像頭最大的好處是即插即用系統(tǒng)自帶UVC驅(qū)動基本上插上去就能在/dev/video*下面看到設(shè)備節(jié)點。我手頭有一個羅技C270和一個雜牌1080P攝像頭兩個都是免驅(qū)的插上就能用。缺點是USB帶寬有限高分辨率下幀率會受限而且USB攝像頭的圖像質(zhì)量參差不齊有些便宜貨色彩偏差很大。MIPI攝像頭需要通過FPC排線接到板子的CSI接口上香橙派官方有配套的攝像頭模組比如OV5647就是常見的選擇。MIPI攝像頭的好處是帶寬大、延遲低、圖像質(zhì)量好適合對實時性要求高的場景。但問題是驅(qū)動配置麻煩設(shè)備樹要改不同模組的寄存器配置還不一樣。我第一次接OV5647的時候設(shè)備樹沒配對/dev/video*下面根本看不到節(jié)點折騰了大半天才搞定。提示如果你是第一次在香橙派RK3588上接攝像頭強烈建議先用USB攝像頭把整個鏈路跑通確認軟件層面沒問題之后再換MIPI攝像頭做優(yōu)化。這樣可以避免同時排查硬件和軟件兩個方向的問題。2.2 設(shè)備節(jié)點識別與權(quán)限配置攝像頭接上之后第一件事是確認系統(tǒng)有沒有識別到設(shè)備。打開終端執(zhí)行l(wèi)s /dev/video*正常情況下會看到/dev/video0、/dev/video1這樣的設(shè)備節(jié)點。如果有多個攝像頭編號會依次遞增。我遇到過一種情況板子自帶的HDMI輸入也會占用一個video節(jié)點所以插上USB攝像頭之后可能看到的是/dev/video1而不是/dev/video0。這時候需要用v4l2-ctl工具來確認哪個節(jié)點才是真正的攝像頭v4l2-ctl --list-devices這個命令會列出所有視頻設(shè)備及其對應(yīng)的節(jié)點輸出類似這樣USB Camera: USB Camera (usb-fc800000.usb-1): /dev/video1 /dev/video2看到這個信息就知道/dev/video1是攝像頭的采集節(jié)點。有些攝像頭會同時注冊兩個節(jié)點一個是采集用的一個是元數(shù)據(jù)用的通常采集節(jié)點是第一個。權(quán)限問題也很常見。普通用戶默認沒有訪問/dev/video*的權(quán)限直接跑OpenCV會報“Permission denied”。解決辦法有兩個一是把當前用戶加入video組執(zhí)行sudo usermod -aG video $USER然后重新登錄二是臨時用sudo跑腳本。我推薦第一種一勞永逸。2.3 OpenCV的安裝與攝像頭支持驗證香橙派RK3588的Ubuntu 20.04系統(tǒng)上安裝OpenCV有好幾種方式。最簡單的是用aptsudo apt update sudo apt install python3-opencv但apt裝的OpenCV版本可能比較老而且不一定帶FFmpeg支持。如果你需要處理視頻流或者用一些新特性建議用pip裝pip3 install opencv-python不過要注意pip裝的opencv-python是預(yù)編譯的wheel包在ARM平臺上可能沒有針對RK3588的NPU做優(yōu)化但用來抓幀和預(yù)處理是足夠的。我實測下來pip裝的OpenCV 4.5.x在香橙派上跑攝像頭抓幀沒問題CPU占用也在可接受范圍內(nèi)。裝完之后驗證一下import cv2 print(cv2.__version__) cap cv2.VideoCapture(1) # 注意這里的編號 if cap.isOpened(): print(攝像頭打開成功) ret, frame cap.read() if ret: print(抓幀成功圖像尺寸:, frame.shape) cap.release() else: print(攝像頭打開失敗)這段代碼能跑通說明OpenCV和攝像頭的基本鏈路沒問題。如果cap.isOpened()返回False先檢查設(shè)備節(jié)點編號對不對再檢查權(quán)限。3. 抓幀與預(yù)處理的完整實現(xiàn)3.1 用OpenCV抓取一幀圖像抓幀本身很簡單cap.read()就搞定了。但這里有幾個細節(jié)值得注意。首先是攝像頭的初始化時間。有些USB攝像頭插上之后需要一兩秒才能穩(wěn)定輸出圖像如果VideoCapture之后立刻read()可能會拿到空幀或者花屏。我的做法是在打開攝像頭之后先空轉(zhuǎn)幾幀cap cv2.VideoCapture(1) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) # 預(yù)熱丟棄前10幀 for _ in range(10): cap.read() ret, frame cap.read()設(shè)置CAP_PROP_FOURCC為MJPG很重要。很多USB攝像頭默認輸出YUYV格式在640x480分辨率下YUYV的帶寬占用比MJPG大得多導(dǎo)致幀率上不去。改成MJPG之后同樣分辨率下幀率能翻倍。這個坑我踩過一開始沒設(shè)FOURCC攝像頭只能跑5幀改成MJPG之后直接跑到30幀。分辨率設(shè)置也有講究。YOLOv5s的輸入尺寸是640x640如果你抓到的幀是1920x1080后面需要縮放會浪費CPU資源。直接在攝像頭層面設(shè)置成640x480后續(xù)處理更高效。但要注意有些攝像頭不支持任意分辨率設(shè)置之后實際輸出可能還是默認值需要用cap.get()確認一下。3.2 圖像格式轉(zhuǎn)換與YOLOv5輸入適配OpenCV抓到的幀是BGR格式的numpy數(shù)組而YOLOv5模型通常期望RGB格式的輸入。所以第一步是顏色空間轉(zhuǎn)換img_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)接下來是尺寸調(diào)整。YOLOv5s的標準輸入是640x640但直接resize會改變寬高比導(dǎo)致檢測框位置偏移。正確的做法是保持寬高比縮放然后填充黑邊def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw / 2 dh / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img這個letterbox函數(shù)是YOLOv5官方倉庫里的標準做法。它的邏輯是先按比例縮放讓長邊對齊到640短邊按比例縮放然后在短邊兩側(cè)填充灰色。這樣既保持了寬高比又滿足了模型輸入尺寸要求。歸一化也是必須的。YOLOv5期望輸入像素值在0到1之間img img.astype(np.float32) / 255.0最后是維度變換。模型期望的輸入形狀是(1, 3, 640, 640)也就是batch、channel、height、width的順序img img.transpose(2, 0, 1) # HWC - CHW img np.expand_dims(img, axis0) # 增加batch維度3.3 推理輸入的數(shù)據(jù)類型與內(nèi)存布局這一步是很多人容易忽略的。香橙派RK3588的NPU推理通常通過RKNN API或者ONNX Runtime來調(diào)用。如果你用的是RKNN模型輸入數(shù)據(jù)的類型和內(nèi)存布局有特定要求。RKNN模型通常要求輸入是uint8類型而不是float32。這意味著你不需要做歸一化而是直接把0到255的像素值傳進去。但具體要看模型轉(zhuǎn)換時的配置。我在轉(zhuǎn)換YOLOv5s模型時設(shè)置了mean_values[[0, 0, 0]]和std_values[[255, 255, 255]]這樣模型內(nèi)部會自動做歸一化外部輸入就保持uint8即可。如果你用的是ONNX模型加ONNX Runtime那輸入通常是float32需要手動歸一化。兩種方式的預(yù)處理代碼不一樣一定要根據(jù)實際使用的推理后端來調(diào)整。內(nèi)存布局方面RKNN模型通常要求NHWC格式而ONNX模型要求NCHW格式。這個也要根據(jù)后端來定。我建議在預(yù)處理階段就把數(shù)據(jù)準備好直接喂給推理接口不要在推理接口內(nèi)部再做轉(zhuǎn)換那樣效率低。4. 推理執(zhí)行與結(jié)果解析4.1 調(diào)用RKNN模型執(zhí)行推理假設(shè)你已經(jīng)把YOLOv5s轉(zhuǎn)換成了RKNN模型文件名為yolov5s.rknn。推理的基本流程是加載模型、初始化運行時、設(shè)置輸入、執(zhí)行推理、獲取輸出。from rknnlite.api import RKNNLite rknn RKNNLite() ret rknn.load_rknn(yolov5s.rknn) ret rknn.init_runtime() # 假設(shè)img是預(yù)處理好的NHWC格式uint8數(shù)據(jù) outputs rknn.inference(inputs[img])rknn.inference返回的是一個列表里面包含模型的所有輸出。YOLOv5s通常有三個輸出層分別對應(yīng)不同尺度的特征圖。每個輸出的形狀是(1, 25200, 85)或者類似的形式其中25200是候選框數(shù)量85是(x, y, w, h, obj_conf, class_conf_1, ..., class_conf_80)。4.2 后處理從輸出張量到檢測框模型原始輸出是一堆數(shù)字需要經(jīng)過后處理才能變成人類可讀的檢測框。后處理主要包括三步解碼邊界框、置信度過濾、非極大值抑制。解碼邊界框的公式是x_center (sigmoid(tx) * 2 - 0.5 grid_x) * stride y_center (sigmoid(ty) * 2 - 0.5 grid_y) * stride width (sigmoid(tw) * 2) ** 2 * anchor_w height (sigmoid(th) * 2) ** 2 * anchor_h其中tx, ty, tw, th是模型輸出的原始值grid_x, grid_y是網(wǎng)格坐標stride是特征圖的下采樣倍數(shù)anchor_w, anchor_h是預(yù)設(shè)的錨框尺寸。置信度過濾就是設(shè)定一個閾值比如0.25把obj_conf低于這個值的候選框全部丟掉。非極大值抑制則是把重疊度高的框合并只保留置信度最高的那個。def non_max_suppression(prediction, conf_thres0.25, iou_thres0.45): # 簡化版NMS實現(xiàn) output [] for pred in prediction: # 過濾低置信度 mask pred[:, 4] conf_thres pred pred[mask] if len(pred) 0: continue # 按置信度排序 pred pred[pred[:, 4].argsort(descendingTrue)] # NMS keep [] while len(pred) 0: keep.append(pred[0]) if len(pred) 1: break iou compute_iou(pred[0], pred[1:]) pred pred[1:][iou iou_thres] output.append(np.array(keep)) return output實際項目中我建議直接用YOLOv5官方倉庫里的utils/general.py中的non_max_suppression函數(shù)那個實現(xiàn)經(jīng)過充分測試支持批量處理和多種參數(shù)配置。4.3 檢測結(jié)果可視化與保存拿到檢測框之后用OpenCV畫出來for det in detections: x1, y1, x2, y2, conf, cls_id det label f{class_names[int(cls_id)]} {conf:.2f} cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imwrite(result.jpg, frame)注意坐標要映射回原始圖像尺寸。因為預(yù)處理時做了letterbox檢測框坐標是在640x640空間下的需要反向變換回原始分辨率。反向變換的公式是x1 (x1 - dw) / r y1 (y1 - dh) / r x2 (x2 - dw) / r y2 (y2 - dh) / r其中dw, dh是letterbox時填充的寬度和高度r是縮放比例。這個映射關(guān)系一定要搞清楚否則畫出來的框位置會偏。5. 常見問題與排查技巧實錄5.1 攝像頭打不開的幾種原因攝像頭打不開是最常見的問題表現(xiàn)是cap.isOpened()返回False。排查思路如下現(xiàn)象可能原因解決方法/dev/video*不存在驅(qū)動未加載或硬件未識別檢查USB連接dmesg權(quán)限拒絕用戶不在video組sudo usermod -aG video $USER后重新登錄設(shè)備節(jié)點編號錯誤多個video節(jié)點用v4l2-ctl --list-devices確認打開后讀幀失敗攝像頭被其他進程占用fuser /dev/video1查看占用進程圖像花屏或綠屏格式不匹配設(shè)置FOURCC為MJPG我遇到過一次特別詭異的情況攝像頭在/dev/video1上能打開但讀出來的幀全是綠色的。后來發(fā)現(xiàn)是FOURCC設(shè)置的問題默認的YUYV格式在某個分辨率下驅(qū)動有bug改成MJPG就正常了。5.2 推理結(jié)果異常排查推理結(jié)果異常通常表現(xiàn)為檢測框位置偏移、類別錯誤、置信度異常低。檢測框位置偏移最常見的原因是letterbox的反向映射沒做對。檢查一下dw, dh和r的計算是否正確特別是當原始圖像寬高比和640x640不一致時填充量計算容易出錯。類別錯誤可能是類別標簽映射錯了。YOLOv5的COCO數(shù)據(jù)集有80個類別索引從0到79。如果你自己訓(xùn)練的模型類別數(shù)不一樣要確保class_names列表和模型輸出對應(yīng)。置信度異常低可能是預(yù)處理的問題。檢查一下輸入數(shù)據(jù)的歸一化是否正確uint8和float32有沒有搞混。我有一次把float32的歸一化數(shù)據(jù)喂給了期望uint8的RKNN模型結(jié)果所有檢測框的置信度都低于0.1排查了半天才發(fā)現(xiàn)是數(shù)據(jù)類型的問題。5.3 性能優(yōu)化的一點經(jīng)驗香橙派RK3588的NPU算力足夠跑YOLOv5s實時推理但前提是預(yù)處理和后處理不能成為瓶頸。預(yù)處理的瓶頸通常在cv2.resize和cvtColor上。如果分辨率是1920x1080這兩個操作在CPU上跑會占用不少時間。我的做法是在攝像頭層面直接設(shè)置成640x480省掉resize的開銷。如果攝像頭不支持那就用cv2.INTER_NEAREST插值比默認的INTER_LINEAR快不少。后處理的瓶頸在NMS上。如果檢測框數(shù)量很多NMS的循環(huán)會很耗時??梢杂胣umpy向量化操作來加速或者用Cython寫一個擴展。不過對于YOLOv5s在640x640輸入下的25200個候選框numpy版本的NMS通常在幾毫秒內(nèi)完成問題不大。還有一個容易被忽略的點是內(nèi)存拷貝。從攝像頭讀到幀之后如果經(jīng)過多次copy()操作會累積不少開銷。盡量用原地操作比如img img.transpose(2, 0, 1)之后直接傳給推理接口不要額外np.ascontiguousarray()除非推理接口明確要求連續(xù)內(nèi)存。注意RKNN模型在首次推理時會有一個預(yù)熱過程耗時明顯比后續(xù)推理長。如果你在測幀率記得把前幾幀排除掉否則數(shù)據(jù)不準。5.4 攝像頭熱插拔與異?;謴?fù)在實際部署中攝像頭可能會因為各種原因斷開連接比如USB接觸不良、供電不足等。如果腳本沒有異常處理一旦攝像頭斷開整個程序就會崩潰。我的做法是在主循環(huán)里加一個重連機制def get_camera(cap, device_id): if cap is None or not cap.isOpened(): cap cv2.VideoCapture(device_id) if cap.isOpened(): cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) return cap cap None while True: cap get_camera(cap, 1) if cap is None or not cap.isOpened(): time.sleep(1) continue ret, frame cap.read() if not ret: cap.release() cap None time.sleep(1) continue # 推理處理...這段代碼的邏輯是如果攝像頭沒打開或者讀幀失敗就釋放資源等一秒后重試。這樣即使攝像頭臨時斷開程序也能自動恢復(fù)不需要人工干預(yù)。6. 從單幀抓取到視頻流推理的擴展思路單幀抓取跑通之后擴展到視頻流推理其實很簡單就是把抓幀和推理放到一個循環(huán)里。但這里有幾個實際問題需要考慮。首先是幀率匹配。如果攝像頭輸出30幀但推理只能跑15幀那就會累積延遲。我的做法是丟幀處理每兩幀取一幀做推理保證實時性。或者用多線程一個線程專門抓幀另一個線程專門推理中間用一個隊列做緩沖。其次是顯示問題。如果板子接了HDMI顯示器可以用cv2.imshow直接顯示。但如果是無頭模式就需要把結(jié)果推流出去或者保存成視頻文件。香橙派RK3588支持硬件編碼可以用FFmpeg或者GStreamer把處理后的視頻流推出去CPU占用比軟件編碼低很多。最后是模型切換。YOLOv5s跑通之后你可能會想換更大的模型比如YOLOv5m或者YOLOv5l。這時候要注意NPU的內(nèi)存限制RK3588的NPU內(nèi)存是共享的模型太大可能會初始化失敗。我實測YOLOv5s在RK3588上跑得很流暢YOLOv5m也能跑但幀率會下降不少。我個人在實際操作中的體會是攝像頭抓幀這個環(huán)節(jié)看似簡單但細節(jié)特別多。不同型號的攝像頭行為不一樣同一個攝像頭在不同分辨率下的表現(xiàn)也不一樣。最穩(wěn)妥的做法是先用v4l2-ctl把攝像頭的所有支持格式列出來然后從中選一個最合適的而不是盲目設(shè)置參數(shù)。另外預(yù)處理和后處理的代碼一定要和模型轉(zhuǎn)換時的配置嚴格對應(yīng)否則推理結(jié)果會莫名其妙地出錯。