檢測數(shù)據(jù)集實戰(zhàn):從清洗到訓(xùn)練與避坑指南)
簡介建筑工地目標(biāo)檢測數(shù)據(jù)集面向智慧工地與工程自動化場景提供YOLO格式標(biāo)注數(shù)據(jù)適合目標(biāo)檢測學(xué)習(xí)者、算法工程師及施工安全系統(tǒng)開發(fā)者使用。數(shù)據(jù)源自真實工地現(xiàn)場覆蓋工程機械、鋼管、塔吊、人員、鋼筋、腳手架、護欄、模板、汽車、自行車等10類核心目標(biāo)包含機械與人員交互的復(fù)雜場景可支撐安全預(yù)警、施工進度管理、機器人環(huán)境感知等任務(wù)。壓縮包共478個文件包括238張jpg現(xiàn)場圖片、238個txt邊界框標(biāo)注、1個yaml配置及1份docx說明文檔其中訓(xùn)練集201張、驗證集37張整體19.82MB可直接導(dǎo)入YOLOv5等主流檢測框架訓(xùn)練驗證。已有194人學(xué)習(xí)瀏覽其精細標(biāo)注和高風(fēng)險要素專項標(biāo)注塔吊作業(yè)區(qū)、鋼筋裸露區(qū)對人員闖入預(yù)警、機械碰撞預(yù)防等安全類算法研發(fā)尤為實用。1. 建筑工地目標(biāo)檢測數(shù)據(jù)集-2.zip拆開湊不出成品模型先把這份監(jiān)督信號審清楚收到建筑工地目標(biāo)檢測數(shù)據(jù)集-2.zip不少人以為解壓后丟進訓(xùn)練框架就能直接出模型。我實際跑過幾次之后發(fā)現(xiàn)這類壓縮包里最常見的不是模型問題而是數(shù)據(jù)本身的問題標(biāo)簽格式混排、空標(biāo)簽一大堆、類別編號和 classes.txt 對不上甚至同一段監(jiān)控視頻的連續(xù)幀被隨機分進了訓(xùn)練集和驗證集。文件名叫“數(shù)據(jù)集-2”多半是指第二個版本但它仍然只是圖像加標(biāo)注框的監(jiān)督信號不是能直接部署的成品模型。這篇文章不聊大而全的算法原理就按我處理工地監(jiān)控數(shù)據(jù)的習(xí)慣從解壓、校驗、劃分、訓(xùn)練、排坑到最后的模型校準(zhǔn)講一遍能把這份數(shù)據(jù)用起來的完整路徑。適合正在做安全帽、反光衣、工程機械目標(biāo)檢測或者剛拿到一份歷史標(biāo)注數(shù)據(jù)準(zhǔn)備訓(xùn)練基準(zhǔn)模型的開發(fā)者參考。如果你期待的是“下載即用”那要先調(diào)整預(yù)期數(shù)據(jù)集整理這件事往往決定了后面所有訓(xùn)練工作的下限。2. 解壓之后的第一步先摸清目錄布局、命名規(guī)則和標(biāo)注格式2.1 目錄布局圖片、標(biāo)簽和劃分清單各在哪個位置先別急著訓(xùn)練把壓縮包解開用find看一下整體目錄結(jié)構(gòu)。大概率會看到兩種風(fēng)格一種是把 images 和 labels 分開放另帶一個 classes.txt另一種是按 train/val/test 分好子目錄里面再各放圖片和標(biāo)注。不同打包風(fēng)格直接決定了你后續(xù)寫校驗?zāi)_本時該去哪找文件。unzip -q 建筑工地目標(biāo)檢測數(shù)據(jù)集-2.zip -d>data-2/ images/ 000001.jpg 000002.jpg labels/ 000001.txt 000002.txt classes.txt也有的版本會把標(biāo)注放在annotations/下用 XML 或 JSON 格式保存。看到這種結(jié)構(gòu)不要慌你先記住一個原則訓(xùn)練框架要的只是一份“圖片路徑 對應(yīng)標(biāo)注”的索引原始目錄怎么擺都可以后面通過腳本轉(zhuǎn)成標(biāo)準(zhǔn)格式就行。真正需要警惕的是 labels 目錄里文件數(shù)明顯少于 images或者某個子目錄里混著不同后綴的標(biāo)注文件這種數(shù)據(jù)源大多經(jīng)歷過手工補標(biāo)一致性最差。2.2 判斷標(biāo)注格式先抽三個樣本再決定要不要寫轉(zhuǎn)換腳本建筑工地目標(biāo)檢測數(shù)據(jù)集的標(biāo)注格式一般逃不出三種YOLO 的 txt、VOC 的 xml、COCO 的 json。YOLO txt 最常見每一行是“類別ID 中心點x 中心點y 寬 高”四個坐標(biāo)都是相對圖片寬高的歸一化值VOC xml 則把目標(biāo)框坐標(biāo)寫成了絕對像素值。兩者混在一個數(shù)據(jù)集里時訓(xùn)練會直接報錯或者默默丟掉一部分框。我一般會寫一段極短的探查腳本先隨機抽幾張圖看看對應(yīng)標(biāo)簽長什么樣再決定后續(xù)怎么統(tǒng)一格式import os for img_file in [images/000001.jpg, images/000002.jpg, images/000003.jpg]: base os.path.splitext(img_file)[0].replace(images/, labels/) for ext in [.txt, .xml, .json]: p f{base}{ext} if os.path.exists(p): print(img_file, -, p) with open(p, r, encodingutf-8) as f: lines f.read().strip().splitlines()[:5] print(\n.join(lines)) break這段代碼的核心邏輯是同一個圖片名依次去 labels 目錄下找 txt、xml、json 三種后綴找到哪個就打印哪個的前五行。你不需要把所有文件都解析一遍三個樣本就能區(qū)分出數(shù)據(jù)集的標(biāo)注體系。如果三張圖分別出現(xiàn)不同格式說明這個包經(jīng)歷過多次導(dǎo)出后面必須寫統(tǒng)一腳本。如果 txt 里的第一列數(shù)字大于 classes.txt 里列出的類目數(shù)減一那就是版本沖突常見于“數(shù)據(jù)集-2”是在舊版基礎(chǔ)上追加了類別卻沒有重新生成所有標(biāo)簽。3. 清洗數(shù)據(jù)集-2校驗、劃分與增強缺一環(huán)都會白訓(xùn)3.1 清掉空標(biāo)簽、壞圖和越界類別編號拿到一份建筑工地檢測數(shù)據(jù)集第一件事不是標(biāo)新立異而是用腳本把所有文件過一遍。工地監(jiān)控照片有個特點大量畫面是空鏡頭、逆光拍糊、或者只有遠處塔吊但沒有可標(biāo)注物體。這些圖里如果有標(biāo)簽文件很可能是空文件或者寫了一個不存在的類別 ID。如果直接把這樣一堆數(shù)據(jù)丟給訓(xùn)練框架輕則損失一部分訓(xùn)練樣本重則讓模型學(xué)到“看到空白就輸出一個奇怪框”。我用這段腳本做第一輪清理import os import cv2 img_dir images label_dir labels problems [] for img_name in sorted(os.listdir(img_dir)): base os.path.splitext(img_name)[0] label_path os.path.join(label_dir, base .txt) if not os.path.exists(label_path): problems.append((img_name, label missing)) continue with open(label_path) as f: lines [line.strip() for line in f if line.strip()] if not lines: problems.append((img_name, empty label)) continue img cv2.imread(os.path.join(img_dir, img_name)) if img is None: problems.append((img_name, broken image)) continue height, width img.shape[:2] for line in lines: parts list(map(float, line.split())) cls, x, y, w, h parts if int(cls) 0 or int(cls) 3: problems.append((img_name, fclass id overflow: {cls})) break if not (0 x 1 and 0 y 1): problems.append((img_name, fcenter out of range: {line})) break if w 0 or h 0 or w 1 or h 1: problems.append((img_name, fbox size out of range: {line})) break print(bad samples:, len(problems)) for p in problems[:20]: print(p)這段腳本把每個圖片名對應(yīng)的標(biāo)簽文件讀出來先看是否存在、是否為空再讀圖片、檢查類別 ID 是否越界、框坐標(biāo)是否在合法區(qū)間。具體執(zhí)行時把里面的3換成你 classes.txt 的實際類別數(shù)。這里有個經(jīng)驗值空標(biāo)簽和壞圖一般能清掉 5% 到 15% 的樣本清完之后數(shù)據(jù)集才談得上可用。不要覺得這是浪費把臟數(shù)據(jù)留著訓(xùn)練出來的模型就會把誤檢當(dāng)成正常輸出。3.2 按來源劃分數(shù)據(jù)而不是隨機打散建筑工地目標(biāo)檢測數(shù)據(jù)集里的圖片很多是從監(jiān)控視頻里抽幀出來的相鄰幀的內(nèi)容幾乎一樣。如果直接用train_test_split隨機分同一個場景的連續(xù)幀會同時出現(xiàn)在訓(xùn)練集和驗證集里最后指標(biāo)會異常好看但模型一上真實場景立刻露餡。正確做法是按“拍攝來源”分組讓同一個攝像頭、同一段時間的畫面只出現(xiàn)在一個集合里。如果文件名里帶了攝像頭編號或時間前綴可以按前綴分組import os import random from collections import defaultdict images [f for f in os.listdir(images)] groups defaultdict(list) for img in images: group_key img.split(_)[0] if _ in img else scene_ img[:4] groups[group_key].append(img) keys list(groups.keys()) random.shuffle(keys) num len(keys) train_keys keys[: int(num * 0.8)] val_keys keys[int(num * 0.8): int(num * 0.9)] test_keys keys[int(num * 0.9):] def write_list(name, key_list): with open(name, w) as f: for key in key_list: for img in groups[key]: f.write(img \n) write_list(train.txt, train_keys) write_list(val.txt, val_keys) write_list(test.txt, test_keys)這段腳本先按文件名第一個下劃線前的字段分組然后把組隨機分到訓(xùn)練、驗證、測試三個文件里。核心參數(shù)是 8:1:1 的比例如果你的標(biāo)注量很大可以改成 7:1:2。這里容易踩的坑是不要按幀索引切分一定要按場景 key 切分。如果文件名里沒有明顯分組信息就按時間排序后整段切前 80% 當(dāng)訓(xùn)練后 20% 當(dāng)驗證這樣至少能避免最嚴重的跨場景泄漏。3.3 數(shù)據(jù)增強小目標(biāo)靠切圖逆光靠光度擾動工地場景里最難檢的往往不是機械大目標(biāo)而是遠處工人頭上的安全帽十幾像素甚至幾像素的帽子比比皆是。針對小目標(biāo)增強策略不能只靠隨機翻轉(zhuǎn)和裁剪我會顯式開啟 Mosaic 和比例縮放用大圖切塊訓(xùn)練讓模型在更小的目標(biāo)尺寸上也見過足夠多樣本。逆光和傍晚場景則靠亮度對比度擾動來補不然模型白天好用傍晚就失靈。常見的增強庫配置可以等價寫成這樣aug dict( mosaic1.0, random_brightness_contrast(0.6, 0.6), hsv_h0.02, hsv_s0.5, hsv_v0.5, fliplr0.5, flip_ud0.0, scale_min0.2, )幾個參數(shù)要單獨說明flip_ud0.0意味著不做上下翻轉(zhuǎn)因為安全帽永遠在頭頂上下翻轉(zhuǎn)會強行讓模型學(xué)習(xí)“帽子長在腳底”的錯誤規(guī)律反而增加誤檢。mosaic1.0表示每張訓(xùn)練圖都來自四張圖拼接能顯著提升小目標(biāo)附近的上下文多樣性但也會讓本來就小的目標(biāo)變得更小所以需要配合scale_min0.2避免把所有目標(biāo)都縮沒了。亮度對比度擾動控制在 0.6 左右既能模擬傍晚逆光又不會把白天樣本壓成黑乎乎一團。4. 訓(xùn)練參數(shù)怎么設(shè)類別清單、錨框、輸入尺寸一起定4.1 讀取 classes.txt生成訓(xùn)練用的數(shù)據(jù)聲明文件建筑工地目標(biāo)檢測數(shù)據(jù)集-2 的類別數(shù)不會太多常見的是安全帽、反光衣、人、挖掘機、塔吊這幾類。先讀一下 classes.txt把類別列表和數(shù)量輸出再生成訓(xùn)練框架需要的數(shù)據(jù)聲明文件。這樣做看似多了一步實際上是為了避免在命令行里手寫nc4這類參數(shù)一旦類別數(shù)寫錯訓(xùn)練過程不報錯但驗證時所有框都會錯位。with open(classes.txt) as f: classes [x.strip() for x in f if x.strip()] print(f類別數(shù): {len(classes)}, 列表: {classes}) data_yaml f path: ./ train: train.txt val: val.txt test: test.txt nc: {len(classes)} names: {classes} with open(data.yaml, w) as f: f.write(data_yaml)這段代碼把classes.txt解析成列表并生成一個 data.yaml 給訓(xùn)練框架用。path指向當(dāng)前數(shù)據(jù)集根目錄train和val指向剛生成的索引文件names里的順序必須和標(biāo)注文件里的類別 ID 嚴格一致。如果你的標(biāo)注是 VOC 或 COCO這一步要先把標(biāo)注轉(zhuǎn)成 YOLO 格式再生成這個文件順序不能反不然類別順序會錯位訓(xùn)練出的模型等于每個目標(biāo)都預(yù)測錯了名字。4.2 錨框不要抄通用預(yù)置值對小目標(biāo)重新聚類很多訓(xùn)練框架默認錨框是從自然圖片數(shù)據(jù)集上統(tǒng)計出來的尺寸偏大建筑工地里的安全帽、反光背心這類小目標(biāo)占比高繼續(xù)用默認錨框會讓小目標(biāo)分支的回歸非常吃力。我習(xí)慣在每次訓(xùn)練前用標(biāo)注框重新聚類一組錨框幾行代碼就能跑出來。import numpy as np from pathlib import Path boxes [] for lp in Path(labels).glob(*.txt): for line in lp.read_text().strip().splitlines(): parts line.split() if len(parts) 5: _, _, _, w, h map(float, parts) boxes.append([w, h]) boxes np.array(boxes, dtypenp.float64) def kmeans_anchor(boxes, k, max_iter100, seed7): rng np.random.default_rng(seed) anchors boxes[rng.choice(len(boxes), k, replaceFalse)].copy() for _ in range(max_iter): w boxes[:, 0] h boxes[:, 1] aw anchors[:, 0] ah anchors[:, 1] inter np.minimum(w[:, None], aw[None, :]) * np.minimum(h[:, None], ah[None, :]) union w[:, None] * h[:, None] aw[None, :] * ah[None, :] - inter iou inter / (union 1e-6) assign np.argmax(iou, axis1) new_anchors [] for j in range(k): group boxes[assign j] if len(group) 0: new_anchors.append(anchors[j]) else: new_anchors.append(group.mean(axis0)) anchors np.array(new_anchors) return anchors anchors kmeans_anchor(boxes, k9) print(anchors:) print(anchors)這段腳本讀取所有 YOLO 格式的標(biāo)簽取出歸一化后的寬和高然后用 IoU 距離做 k-means 聚類最終輸出 9 組錨框。k9 是一般檢測框架的常見取值如果你用輕量模型可以改成 6 組。這里要提醒一句聚類出來的錨框尺寸依賴輸入分辨率如果你打算用 1280 訓(xùn)練就按 1280 下的歸一化坐標(biāo)去換算如果訓(xùn)練和推理的輸入尺寸不一致錨框也要同步縮放否則前面的聚類就白做了。配合錨框重聚類我一般把輸入尺寸從默認 640 提到 1280尤其安全帽這類小目標(biāo)640 下常常只有 6 到 8 個像素寬1280 下能翻倍到可辨識范圍。訓(xùn)練命令可以按這個思路設(shè)置# 常見 YOLO 系框架的參數(shù)示意具體寫法以你使用的版本為準(zhǔn) python train.py --data data.yaml --model yolov8n.pt \ --img-size 1280 --batch-size 8 --epochs 100 \ --patience 20 --weights pretrained.ptimg-size 1280是工地小目標(biāo)場景下最值得優(yōu)先加的參數(shù)但顯存不夠時不要硬上可以把 batch-size 降到 4配合梯度累積來緩解。patience 20表示驗證集指標(biāo)連續(xù) 20 輪不提升就早停省時間也避免過擬合到最后開始背訓(xùn)練樣本。5. 建筑工地目標(biāo)檢測避坑指南四個比模型結(jié)構(gòu)更影響結(jié)果的問題5.1 安全帽類別漂移戴帽和不戴帽的邊界不統(tǒng)一現(xiàn)象訓(xùn)練時安全帽類別指標(biāo)很高但一到新工地視頻模型頻繁把頭頂陰影、安全帽反光、甚至黃色塑料凳識別成安全帽。原因標(biāo)注者對“戴帽”的界定不一致。有的標(biāo)注只要帽子出現(xiàn)在頭頂就算正樣本有的要求遮蓋住 60% 以上的頭圍才標(biāo)還有的把帽帶的反光條單獨標(biāo)成另一個物體。數(shù)據(jù)源里混了多種標(biāo)準(zhǔn)模型學(xué)到的不是帽子的本質(zhì)而是各種黃色塊和弧形紋理。解決統(tǒng)一標(biāo)注規(guī)則至少按“帽子覆蓋頭部面積過半”才算正樣本。如果數(shù)據(jù)集里已經(jīng)有大量模棱兩可的框最簡單的做法是把它們當(dāng)作難例保留但要在訓(xùn)練時給安全帽類別加一點權(quán)重或者干脆把安全帽拆成“戴帽”“不戴帽”兩個類別讓模型先分清人再判斷帽子狀態(tài)。5.2 小目標(biāo)密集工人擠在一起被模型“團滅”現(xiàn)象畫面里有十幾個工人模型只輸出三四個框另外幾個被漏掉或者兩個相近工人被框成一個巨大矩形。原因工地多人作業(yè)時標(biāo)注框之間重疊率高模型在 NMS 階段把置信度略低的框壓掉了。另一個原因是訓(xùn)練時用的輸入尺寸太小幾像素寬的帽子在特征圖里已經(jīng)退化成一個點。解決提高輸入分辨率到 1280同時在增強階段開 Mosaic 并保證每張訓(xùn)練圖里保留小目標(biāo)樣本。推理階段如果還是漏把 NMS 的 IoU 閾值從默認的 0.45 調(diào)高到 0.6能減少高密度場景下的框合并。注意不要調(diào)得太高否則同一個目標(biāo)會產(chǎn)生重復(fù)框。5.3 把工地設(shè)備誤檢成人或安全帽現(xiàn)象藍色鐵皮圍擋、反光錐桶、混凝土攪拌車滾筒有時被誤檢成工人或安全帽。原因圍擋和工服的藍色色塊接近反光衣和錐桶的橙色接近模型學(xué)到的顏色特征沒有和“人體結(jié)構(gòu)”綁定于是看到大面積近似色就觸發(fā)檢測。解決不要急著改網(wǎng)絡(luò)結(jié)構(gòu)先看訓(xùn)練數(shù)據(jù)里的負樣本夠不夠多。我通常會在訓(xùn)練集里額外放一批沒有目標(biāo)的背景圖和難負例圖讓模型明確“這塊區(qū)域即使有顏色也不是目標(biāo)”。另一個辦法是把類別定義寫得更窄不是檢測“所有反光物體”而是檢測“穿反光衣的人”標(biāo)注時只標(biāo)人體范圍內(nèi)的區(qū)域不標(biāo)單獨的反光布。5.4 驗證集虛高同一段視頻漏進了兩個集合現(xiàn)象訓(xùn)練日志里 mAP 到 0.95部署到新攝像頭后準(zhǔn)確率直接掉到 0.6 左右差得離譜。原因圖片來自監(jiān)控視頻抽幀隨機劃分時同一個攝像頭同一天的連續(xù)幀被分到訓(xùn)練集和驗證集模型等于已經(jīng)在考場上見過驗證題。解決按場景劃分數(shù)據(jù)而不是按幀隨機分。具體操作見 3.2 節(jié)文件名里有攝像頭編號就按編號分沒有編號就按時間戳整段切。強烈建議在訓(xùn)練完成后再留一個“完全陌生的攝像頭”的測試文件夾只有它在 mAP 上也保持住才算真的有效。6. 把數(shù)據(jù)集-2 變成自己的模型遷移、置信度校準(zhǔn)與驗收技巧6.1 先小批量跑通端到端再談精度完整地訓(xùn)一個工地目標(biāo)檢測模型之前我最先做的事是從清洗后的數(shù)據(jù)里取 100 張圖跑一個 10 輪的快速實驗。這個小實驗不是為了出精度而是驗證整條鏈路圖片路徑是否正確、類別順序是否對得上、損失能否正常下降、驗證階段能不能輸出預(yù)測結(jié)果。如果這個小批量過程里出現(xiàn)nanloss 或者驗證階段一張圖都沒有輸出基本就是標(biāo)注越界、類別數(shù)不一致或路徑配置錯誤趁早停下來比訓(xùn)到一半再排查省得多。跑通小批量之后再用全量數(shù)據(jù)訓(xùn)練。個人習(xí)慣是先凍結(jié)骨干網(wǎng)絡(luò)微調(diào) 20 輪等驗證集損失不再下降再解凍全部層微調(diào) 50 到 100 輪。這樣能避免新工地數(shù)據(jù)量不足時把預(yù)訓(xùn)練權(quán)重里的通用特征全部沖掉。6.2 用驗證集錯誤樣本調(diào)置信度閾值最后一個值得做的技巧是給推理階段選一個合適的置信度閾值。默認 0.5 不一定適合你的工地場景如果客戶更在意不能漏人就把閾值降到 0.3用誤檢換召回如果誤報會導(dǎo)致告警平臺被刷屏就提到 0.7。別靠肉眼拍腦袋把驗證集預(yù)測結(jié)果導(dǎo)出來看不同閾值下的精確率和召回率曲線。import json preds json.load(open(val_predictions.json)) labels json.load(open(val_labels.json)) for threshold in [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]: tp fp fn 0 for pred, label in zip(preds, labels): matched set() for p in pred: if p[conf] threshold: continue # 這里省略 IoU 匹配邏輯實際按框重疊面積計算 matched.add(p[cls]) for target in label[classes]: if target in matched: tp 1 else: fn 1 fp max(0, len(matched) - len(label[classes])) precision tp / (tp fp) if tp fp 0 else 0 recall tp / (tp fn) if tp fn 0 else 0 print(fthreshold{threshold}, precision{precision:.3f}, recall{recall:.3f})這段代碼按置信度閾值過濾預(yù)測框然后粗略統(tǒng)計精確率和召回率實際工程里要把 IoU 匹配補上。你真正要觀察的是閾值從 0.5 移動到 0.3 時召回率提升了多少、誤檢增加了多少。如果 0.3 時誤檢暴增說明模型學(xué)到的是顏色特征而不是結(jié)構(gòu)特征該回頭查標(biāo)注質(zhì)量如果 0.7 時召回率仍然很高說明檢測結(jié)果很可靠可以放心提高閾值減少告警。把這份建筑工地目標(biāo)檢測數(shù)據(jù)集-2 跑完我最深的感受是真正決定模型能不能用的往往是數(shù)據(jù)組織、場景劃分、標(biāo)注邊界這些最不起眼的地方。我習(xí)慣在最后一輪訓(xùn)練時留下一個從沒參與過任何訓(xùn)練和驗證的攝像頭來驗收如果它在第一幀就丟目標(biāo)那前面所有指標(biāo)都只是數(shù)字。希望幫到你。本文還有配套的精品資源點擊獲取