機(jī)檢測實(shí)戰(zhàn):YOLO格式轉(zhuǎn)換與YOLOv8訓(xùn)練全流程)
簡介這份資源是面向無人機(jī)俯視視角的農(nóng)場場景目標(biāo)檢測數(shù)據(jù)集適合從事農(nóng)業(yè)智能化、無人機(jī)巡檢及目標(biāo)檢測算法實(shí)踐的開發(fā)者與研究者使用可解決農(nóng)場中車輛、農(nóng)機(jī)與行人識(shí)別任務(wù)缺乏高質(zhì)量標(biāo)注數(shù)據(jù)的問題。數(shù)據(jù)集包含1000余張圖像壓縮包共2000個(gè)文件其中497張jpg為無人機(jī)航拍原圖1502個(gè)txt為對(duì)應(yīng)的YOLO格式標(biāo)注文件另有1個(gè)yaml配置文件整體約522.36MB。目錄已按train、val、test劃分完畢data.yaml中定義了car、people、tractor、van四類目標(biāo)yolov5、yolov7、yolov8等主流框架可直接加載訓(xùn)練省去格式轉(zhuǎn)換與劃分工作。目前已有451人學(xué)習(xí)下載配套博文提供了數(shù)據(jù)集說明與檢測結(jié)果參考便于讀者快速驗(yàn)證模型效果并復(fù)現(xiàn)實(shí)驗(yàn)流程適合作為農(nóng)業(yè)場景目標(biāo)檢測的入門與進(jìn)階訓(xùn)練素材。1. 農(nóng)場里的農(nóng)機(jī)檢測VisDrone 數(shù)據(jù)集為什么值得你花一個(gè)周末跑通去年幫一個(gè)做智慧農(nóng)業(yè)的朋友看模型他拿 COCO 預(yù)訓(xùn)練的 YOLO 直接去數(shù)拖拉機(jī)結(jié)果把收割機(jī)認(rèn)成卡車、把旋耕機(jī)認(rèn)成摩托mAP 掉到 0.2 出頭。問題不在模型在數(shù)據(jù)——通用數(shù)據(jù)集里農(nóng)業(yè)機(jī)械的樣本少得可憐類別定義也對(duì)不上。VisDrone 原本是無人機(jī)視角的視覺目標(biāo)檢測數(shù)據(jù)集里面包含大量俯拍場景下的車輛、行人、農(nóng)機(jī)等目標(biāo)其中農(nóng)業(yè)機(jī)械這一類在農(nóng)田、果園、牧場場景下有相當(dāng)數(shù)量的標(biāo)注。這個(gè)標(biāo)題指向的就是把 VisDrone 里跟農(nóng)業(yè)機(jī)械相關(guān)的子集抽出來整理成 YOLO 格式直接用于訓(xùn)練自己的農(nóng)機(jī)檢測模型。適合誰做農(nóng)業(yè)無人機(jī)巡檢、農(nóng)機(jī)自動(dòng)駕駛、果園管理機(jī)器人的一線開發(fā)者以及想找一個(gè)真實(shí)場景數(shù)據(jù)集練手 YOLO 全流程的工程師。你不需要從零標(biāo)注幾千張圖但需要理解這個(gè)數(shù)據(jù)集的格式、類別映射和轉(zhuǎn)換邏輯否則拿到手也是一堆看不懂的 txt。2. VisDrone 原始標(biāo)注拆解與 YOLO 格式對(duì)齊2.1 VisDrone 的標(biāo)注文件長什么樣VisDrone 數(shù)據(jù)集常見發(fā)布形式是每張圖片對(duì)應(yīng)一個(gè) txt 標(biāo)注文件文件名與圖片同名。標(biāo)注內(nèi)容按行組織每行 8 個(gè)字段用逗號(hào)分隔。字段順序依次是bbox_left、bbox_top、bbox_width、bbox_height、score、object_category、truncation、occlusion。其中 score 表示標(biāo)注置信度1 表示有效object_category 是類別編號(hào)truncation 和 occlusion 分別表示截?cái)喑潭群驼趽醭潭热≈?0 或 1。類別編號(hào)在 VisDrone 官方定義中通常有 10 類左右包括 ignored regions、pedestrian、people、bicycle、car、van、truck、tricycle、awning-tricycle、bus、motor、others。農(nóng)業(yè)機(jī)械并沒有單獨(dú)一個(gè)類而是分散在 truck、van、others 等類別里。這意味著你不能直接拿 VisDrone 的類別定義去訓(xùn)練農(nóng)機(jī)檢測必須做一步類別篩選和重映射。我一般會(huì)先寫一個(gè)統(tǒng)計(jì)腳本看看每個(gè)類別在標(biāo)注里出現(xiàn)多少次再?zèng)Q定哪些類別歸入農(nóng)機(jī)。比如 truck 里可能包含拖拉機(jī)、收割機(jī)van 里可能包含農(nóng)用運(yùn)輸車others 里可能包含旋耕機(jī)、播種機(jī)等。這一步不做后面訓(xùn)練出來的模型類別就是亂的。2.2 從 VisDrone 格式轉(zhuǎn) YOLO 格式的完整腳本YOLO 格式要求每張圖片對(duì)應(yīng)一個(gè) txt每行 5 個(gè)字段class_id、x_center、y_center、width、height。所有坐標(biāo)都要?dú)w一化到 0 到 1 之間。轉(zhuǎn)換的核心邏輯是讀取 VisDrone 標(biāo)注行過濾掉不需要的類別把 bbox 的左上角坐標(biāo)和寬高轉(zhuǎn)成中心點(diǎn)坐標(biāo)和寬高再除以圖片寬高做歸一化。下面是我常用的轉(zhuǎn)換腳本依賴 Pillow 和 tqdm直接放在數(shù)據(jù)集根目錄跑就行。import os from PIL import Image from tqdm import tqdm # VisDrone 類別編號(hào)到名稱的映射按你實(shí)際拿到的數(shù)據(jù)集版本調(diào)整 VISDRONE_CATEGORIES { 0: ignored, 1: pedestrian, 2: people, 3: bicycle, 4: car, 5: van, 6: truck, 7: tricycle, 8: awning-tricycle, 9: bus, 10: motor, 11: others } # 你希望保留并映射到 YOLO 的類別這里只保留農(nóng)機(jī)相關(guān) # 鍵是 VisDrone 類別編號(hào)值是 YOLO 類別編號(hào) CATEGORY_MAP { 5: 0, # van - agricultural_machine 6: 0, # truck - agricultural_machine 11: 0, # others - agricultural_machine } # YOLO 類別名稱順序與 CATEGORY_MAP 的值對(duì)應(yīng) YOLO_NAMES [agricultural_machine] def convert_annotation(anno_path, img_path, output_path): # 讀取圖片尺寸用于歸一化 with Image.open(img_path) as im: img_w, img_h im.size lines_out [] with open(anno_path, r) as f: for line in f: parts line.strip().split(,) if len(parts) 8: continue # VisDrone 字段順序left, top, width, height, score, category, truncation, occlusion left, top, w, h map(float, parts[:4]) score int(parts[4]) category int(parts[5]) # 跳過無效標(biāo)注和不需要的類別 if score 0 or category not in CATEGORY_MAP: continue # 跳過寬高為 0 的異???if w 0 or h 0: continue # 轉(zhuǎn)成中心點(diǎn)坐標(biāo)并歸一化 x_center (left w / 2.0) / img_w y_center (top h / 2.0) / img_h norm_w w / img_w norm_h h / img_h # 裁剪到 0-1 范圍防止越界 x_center min(max(x_center, 0.0), 1.0) y_center min(max(y_center, 0.0), 1.0) norm_w min(max(norm_w, 0.0), 1.0) norm_h min(max(norm_h, 0.0), 1.0) class_id CATEGORY_MAP[category] lines_out.append(f{class_id} {x_center:.6f} {y_center:.6f} {norm_w:.6f} {norm_h:.6f}) # 即使沒有目標(biāo)也要生成空文件YOLO 訓(xùn)練時(shí)要求圖片和標(biāo)簽一一對(duì)應(yīng) with open(output_path, w) as f: f.write(\n.join(lines_out)) def main(): # 假設(shè)目錄結(jié)構(gòu)dataset/images/ 和 dataset/annotations/ img_dir dataset/images anno_dir dataset/annotations label_dir dataset/labels os.makedirs(label_dir, exist_okTrue) img_files [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .png, .jpeg))] for img_file in tqdm(img_files, descConverting): base os.path.splitext(img_file)[0] img_path os.path.join(img_dir, img_file) anno_path os.path.join(anno_dir, base .txt) out_path os.path.join(label_dir, base .txt) if not os.path.exists(anno_path): # 沒有標(biāo)注文件的圖片生成空標(biāo)簽 open(out_path, w).close() continue convert_annotation(anno_path, img_path, out_path) # 生成 classes.txt方便后續(xù)訓(xùn)練和可視化 with open(os.path.join(label_dir, classes.txt), w) as f: f.write(\n.join(YOLO_NAMES)) if __name__ __main__: main()邏輯說明腳本先讀取圖片真實(shí)寬高因?yàn)?VisDrone 的 bbox 是絕對(duì)像素坐標(biāo)必須除以圖片尺寸才能歸一化。CATEGORY_MAP 是核心你把哪些 VisDrone 類別歸入農(nóng)機(jī)就改這個(gè)字典。score 為 0 的行直接跳過那是無效標(biāo)注。寬高為 0 的框也要跳過否則歸一化后會(huì)出現(xiàn) NaN。最后生成空標(biāo)簽文件是必須的YOLO 訓(xùn)練時(shí)如果某張圖沒有對(duì)應(yīng)標(biāo)簽文件會(huì)被當(dāng)成負(fù)樣本處理但空文件表示這張圖確實(shí)沒有目標(biāo)兩者語義不同。參數(shù)說明VISDRONE_CATEGORIES 只用于打印和調(diào)試不影響轉(zhuǎn)換結(jié)果。CATEGORY_MAP 的鍵是 VisDrone 原始類別編號(hào)值是你自定義的 YOLO 類別編號(hào)從 0 開始連續(xù)編號(hào)。YOLO_NAMES 的順序必須與 CATEGORY_MAP 的值一一對(duì)應(yīng)。歸一化后的坐標(biāo)保留 6 位小數(shù)足夠YOLO 官方實(shí)現(xiàn)也是這個(gè)精度。2.3 轉(zhuǎn)換后必須做的三項(xiàng)校驗(yàn)轉(zhuǎn)換腳本跑完不代表數(shù)據(jù)就能用。我每次都會(huì)做三項(xiàng)校驗(yàn)少一項(xiàng)都可能在后頭翻車。第一項(xiàng)檢查標(biāo)簽文件數(shù)量是否與圖片數(shù)量一致。如果 labels 目錄下的 txt 數(shù)量少于 images 目錄下的圖片數(shù)量說明有圖片沒生成標(biāo)簽訓(xùn)練時(shí)會(huì)報(bào)錯(cuò)。用一條 shell 命令就能查echo images: $(ls dataset/images | wc -l) echo labels: $(ls dataset/labels/*.txt | wc -l)第二項(xiàng)隨機(jī)抽 10 張圖做可視化把 YOLO 格式的框畫回原圖看位置和類別對(duì)不對(duì)。我一般用 Python 的 PIL 畫框不依賴 matplotlib速度快。from PIL import Image, ImageDraw import os, random img_dir dataset/images label_dir dataset/labels names open(os.path.join(label_dir, classes.txt)).read().strip().split(\n) for img_file in random.sample(os.listdir(img_dir), 10): base os.path.splitext(img_file)[0] img Image.open(os.path.join(img_dir, img_file)).convert(RGB) draw ImageDraw.Draw(img) w, h img.size label_path os.path.join(label_dir, base .txt) if os.path.exists(label_path): for line in open(label_path): parts line.strip().split() if len(parts) ! 5: continue cid, xc, yc, nw, nh int(parts[0]), *map(float, parts[1:]) x1 (xc - nw / 2) * w y1 (yc - nh / 2) * h x2 (xc nw / 2) * w y2 (yc nh / 2) * h draw.rectangle([x1, y1, x2, y2], outlinered, width2) draw.text((x1, y1), names[cid], fillred) img.save(fcheck_{base}.jpg)第三項(xiàng)統(tǒng)計(jì)每個(gè)類別的框數(shù)量確認(rèn)沒有類別為空。如果 agricultural_machine 的框數(shù)為 0說明 CATEGORY_MAP 配錯(cuò)了或者這個(gè)子集里根本沒有農(nóng)機(jī)標(biāo)注。這時(shí)候要么換子集要么重新審視類別映射。3. 用 YOLOv8 在 VisDrone 農(nóng)機(jī)子集上跑通訓(xùn)練3.1 環(huán)境配置與數(shù)據(jù)目錄組織YOLOv8 的環(huán)境配置現(xiàn)在比兩年前省事多了但 Anaconda 環(huán)境下還是有幾個(gè)版本坑。我一般用 Python 3.9 到 3.10PyTorch 選 2.0 以上CUDA 版本跟顯卡驅(qū)動(dòng)匹配就行。安裝命令就一行pip install ultralyticsUltralytics 包會(huì)自動(dòng)拉取匹配的 PyTorch 版本但如果你之前裝過舊版 torch建議先卸載再裝否則容易出現(xiàn) CUDA 版本沖突。裝完后跑yolo checks看一眼環(huán)境確認(rèn) CUDA 可用。數(shù)據(jù)目錄按 YOLO 官方推薦的結(jié)構(gòu)組織dataset/ images/ train/ val/ labels/ train/ val/ data.yamltrain 和 val 的劃分比例我一般用 8:2如果圖片總數(shù)少于 500 張用 7:3驗(yàn)證集大一點(diǎn)更能反映真實(shí)泛化能力。劃分腳本很簡單用 sklearn 的 train_test_split 或者自己寫隨機(jī)抽樣都行。注意圖片和標(biāo)簽要同步移動(dòng)不能只移圖片。data.yaml 的內(nèi)容如下path: /absolute/path/to/dataset train: images/train val: images/val nc: 1 names: [agricultural_machine]path 必須寫絕對(duì)路徑Y(jié)OLOv8 對(duì)相對(duì)路徑的處理有時(shí)候會(huì)出玄學(xué)問題。nc 是類別數(shù)names 的順序必須與轉(zhuǎn)換腳本里的 YOLO_NAMES 一致。3.2 訓(xùn)練命令與關(guān)鍵參數(shù)怎么設(shè)訓(xùn)練命令本身不復(fù)雜但參數(shù)設(shè)不對(duì)結(jié)果差很多。我常用的起手命令yolo detect train \ datadataset/data.yaml \ modelyolov8s.pt \ epochs100 \ imgsz640 \ batch16 \ lr00.01 \ lrf0.01 \ patience20 \ device0 \ projectruns/visdrone_agri \ nameexp1model 選 yolov8s.pt 還是 yolov8n.pt取決于你的部署端算力。如果最終要跑在 RK3588 或者 Atlas 這類邊緣設(shè)備上建議從 yolov8n.pt 開始精度不夠再往上換。imgsz 設(shè) 640 是通用起點(diǎn)VisDrone 圖片分辨率普遍偏高如果農(nóng)機(jī)目標(biāo)在圖中占比很小可以嘗試 1280但顯存占用會(huì)翻倍。batch 根據(jù)顯存調(diào)16 是 8GB 顯存的保守值。lr0 初始學(xué)習(xí)率 0.01 是 YOLOv8 的默認(rèn)值如果 loss 震蕩厲害降到 0.005。patience 設(shè) 20 表示 20 個(gè) epoch 沒有提升就早停省時(shí)間。訓(xùn)練過程中重點(diǎn)看三個(gè)指標(biāo)box_loss、cls_loss、mAP50。box_loss 下降說明框位置在收斂cls_loss 下降說明類別判斷在改善。如果 box_loss 一直不降檢查標(biāo)簽格式是不是有越界坐標(biāo)。如果 cls_loss 降不下去可能是類別映射有問題或者某些類別樣本太少。3.3 訓(xùn)練完怎么驗(yàn)證模型真的學(xué)到了農(nóng)機(jī)訓(xùn)練結(jié)束后runs 目錄下會(huì)生成權(quán)重文件和驗(yàn)證結(jié)果。不要只看 mAP 數(shù)字一定要做可視化推理。用訓(xùn)練好的 best.pt 跑一批驗(yàn)證集圖片yolo detect predict \ modelruns/visdrone_agri/exp1/weights/best.pt \ sourcedataset/images/val \ conf0.25 \ saveTrue \ projectruns/visdrone_agri/predictconf 設(shè) 0.25 是起點(diǎn)如果發(fā)現(xiàn)漏檢多降到 0.15 試試如果誤檢多升到 0.4。VisDrone 的俯拍視角下農(nóng)機(jī)目標(biāo)有時(shí)候會(huì)被樹冠或建筑遮擋模型容易漏檢。這時(shí)候可以看預(yù)測結(jié)果里哪些圖漏了單獨(dú)拿出來分析是標(biāo)注問題還是模型能力問題。我還會(huì)做一件事把驗(yàn)證集里所有包含農(nóng)機(jī)的圖片單獨(dú)抽出來算一個(gè)子集的 mAP。因?yàn)檎w mAP 可能被大量簡單樣本拉高掩蓋了農(nóng)機(jī)類別的真實(shí)表現(xiàn)。如果農(nóng)機(jī)子集 mAP 明顯低于整體說明模型對(duì)農(nóng)機(jī)特征學(xué)習(xí)不夠需要增加農(nóng)機(jī)樣本或做數(shù)據(jù)增強(qiáng)。4. 農(nóng)機(jī)檢測訓(xùn)練中的避坑與排查記錄4.1 標(biāo)簽越界導(dǎo)致訓(xùn)練直接報(bào)錯(cuò)現(xiàn)象訓(xùn)練啟動(dòng)后幾秒鐘就報(bào)錯(cuò)提示坐標(biāo)超出 0 到 1 范圍。原因VisDrone 原始標(biāo)注里有些框的左上角坐標(biāo)加寬高超過了圖片邊界轉(zhuǎn)換時(shí)沒有做裁剪。解決在轉(zhuǎn)換腳本里加一步裁剪把 x_center、y_center、width、height 都限制在 0 到 1 之間。更穩(wěn)妥的做法是先算 x1、y1、x2、y2裁剪到圖片范圍內(nèi)再反算中心點(diǎn)和寬高。我上面給的腳本已經(jīng)做了簡單裁剪但如果原始框完全在圖片外裁剪后寬高會(huì)變成 0需要跳過。4.2 類別映射錯(cuò)位導(dǎo)致模型學(xué)出四不像現(xiàn)象訓(xùn)練 loss 正常下降但推理時(shí)把所有農(nóng)機(jī)都標(biāo)成同一個(gè)類或者把背景標(biāo)成農(nóng)機(jī)。原因CATEGORY_MAP 的鍵值對(duì)寫反了或者 YOLO_NAMES 的順序與 CATEGORY_MAP 的值不對(duì)應(yīng)。解決轉(zhuǎn)換完先跑一遍統(tǒng)計(jì)腳本打印每個(gè) YOLO 類別對(duì)應(yīng)的框數(shù)量確認(rèn)沒有類別為空、沒有類別數(shù)量異常偏多。另外classes.txt 里的名稱順序必須與 data.yaml 里的 names 完全一致大小寫和空格都要對(duì)齊。4.3 驗(yàn)證集 mAP 虛高但實(shí)際部署誤檢嚴(yán)重現(xiàn)象驗(yàn)證集 mAP50 到 0.85但拿真實(shí)農(nóng)田視頻跑誤檢率很高把石頭、草垛都認(rèn)成農(nóng)機(jī)。原因VisDrone 的驗(yàn)證集和訓(xùn)練集來自同一分布圖片風(fēng)格、光照、角度都相似模型過擬合了。解決從真實(shí)場景里抽 50 到 100 張圖手工標(biāo)注后作為獨(dú)立測試集不參與訓(xùn)練。如果測試集 mAP 掉到 0.5 以下說明泛化能力不夠需要加數(shù)據(jù)增強(qiáng)比如隨機(jī)裁剪、色彩抖動(dòng)、馬賽克增強(qiáng)。YOLOv8 默認(rèn)開啟了馬賽克增強(qiáng)但你可以調(diào) mosaic 參數(shù)控制強(qiáng)度。4.4 小目標(biāo)漏檢嚴(yán)重現(xiàn)象大農(nóng)機(jī)檢測沒問題但遠(yuǎn)處的小農(nóng)機(jī)基本漏檢。原因VisDrone 俯拍視角下遠(yuǎn)處農(nóng)機(jī)可能只有幾十個(gè)像素YOLO 的下采樣倍率導(dǎo)致小目標(biāo)特征丟失。解決把 imgsz 從 640 提到 1280或者用 YOLOv8 的 P2 檢測頭。P2 檢測頭會(huì)增加計(jì)算量但小目標(biāo)召回率提升明顯。另一個(gè)辦法是切片推理把大圖切成小塊分別檢測再合并適合離線批量處理場景。4.5 訓(xùn)練到一半 loss 突然變成 NaN現(xiàn)象前幾十個(gè) epoch 正常突然 loss 變成 NaN訓(xùn)練中斷。原因?qū)W習(xí)率過高導(dǎo)致梯度爆炸或者某批數(shù)據(jù)里有異常標(biāo)簽。解決先把 lr0 降到 0.001 重跑如果還出 NaN檢查標(biāo)簽文件里有沒有空行、有沒有非數(shù)字字符。VisDrone 轉(zhuǎn)換后的標(biāo)簽文件如果某行只有 4 個(gè)字段YOLO 解析時(shí)會(huì)出錯(cuò)。用腳本掃一遍所有標(biāo)簽文件確保每行要么為空要么正好 5 個(gè)字段。5. 把農(nóng)機(jī)檢測模型推到邊緣設(shè)備前的最后幾步訓(xùn)練出一個(gè)能跑的模型只是開始真正落地到農(nóng)場場景還有幾件事必須做。第一件是模型導(dǎo)出。YOLOv8 支持導(dǎo)出 ONNX、TensorRT、OpenVINO 等格式如果你要部署到 RK3588一般走 ONNX 再轉(zhuǎn) RKNN。導(dǎo)出命令yolo export modelruns/visdrone_agri/exp1/weights/best.pt formatonnx opset12 simplifyTrueopset 選 12 是兼容性比較好的版本simplify 會(huì)做一層圖優(yōu)化減小模型體積。導(dǎo)出后先用 onnxruntime 跑一遍推理確認(rèn)輸出與 PyTorch 一致再往邊緣設(shè)備上轉(zhuǎn)。第二件是置信度門限的調(diào)整。YOLO 默認(rèn) conf 是 0.25但在農(nóng)田場景下光照變化大、背景雜亂我一般會(huì)把部署時(shí)的 conf 提到 0.4 到 0.5犧牲一點(diǎn)召回率換準(zhǔn)確率。如果業(yè)務(wù)允許漏檢但不能誤檢就設(shè)高如果寧可誤檢也不能漏就設(shè)低。這個(gè)門限沒有標(biāo)準(zhǔn)答案拿一批真實(shí)場景圖跑一遍畫 precision-recall 曲線選 F1 最高的點(diǎn)。第三件是輸入尺寸的權(quán)衡。邊緣設(shè)備算力有限1280 的輸入可能跑不到實(shí)時(shí)。我一般會(huì)做一次 benchmark測 640、960、1280 三檔的推理耗時(shí)和 mAP選一個(gè)滿足幀率要求且精度可接受的尺寸。RK3588 上跑 YOLOv8n 加 640 輸入一般能做到 30 FPS 以上夠用了。最后說一個(gè)我自己的習(xí)慣每次訓(xùn)練完把 data.yaml、轉(zhuǎn)換腳本、訓(xùn)練命令、驗(yàn)證結(jié)果截圖全部歸檔到一個(gè)文件夾命名帶上日期和數(shù)據(jù)集版本。農(nóng)機(jī)檢測這個(gè)方向數(shù)據(jù)集會(huì)不斷更新模型也會(huì)迭代沒有歸檔三個(gè)月后你根本記不清當(dāng)時(shí)是怎么跑的。這個(gè)習(xí)慣幫我省過很多次后悔藥。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取