注格式互轉(zhuǎn):從JSON到TXT的完整指南與避坑實(shí)踐)
目標(biāo)檢測的數(shù)據(jù)標(biāo)注是每個做視覺項目的人都繞不開的一道坎。我見過太多團(tuán)隊在標(biāo)注工具上反復(fù)橫跳有人習(xí)慣用 labelme 那種點(diǎn)幾下就能畫多邊形、改點(diǎn)、加標(biāo)簽的交互覺得順手也有人認(rèn)準(zhǔn) YOLO 訓(xùn)練時那套一行一個框、歸一化坐標(biāo)的 txt 格式覺得喂給模型最省心。結(jié)果就是標(biāo)注階段用一套格式訓(xùn)練階段要另一套格式中間全靠人肉轉(zhuǎn)換轉(zhuǎn)一次錯一次錯一次查半天。這篇就專門聊 labelme 和 YOLO 兩種標(biāo)注格式之間的互轉(zhuǎn)把 JSON 和 TXT 之間那點(diǎn)事徹底講透包括坐標(biāo)怎么算、類別怎么映射、圖片尺寸從哪來、批量腳本怎么寫、以及我踩過的那些坑。不管你是剛接觸目標(biāo)檢測的新手還是已經(jīng)跑過幾個項目的老手只要你的流程里同時出現(xiàn)過 labelme 和 YOLO這篇內(nèi)容都能直接拿去用。1. 先搞清楚兩種格式到底在存什么很多人轉(zhuǎn)換出錯根本原因不是腳本寫錯了而是壓根沒弄明白這兩種格式各自在表達(dá)什么。labelme 存的是幾何形狀 語義標(biāo)簽YOLO 存的是歸一化后的框 類別索引兩者的坐標(biāo)系、精度、組織方式都不一樣。不把這一層想清楚后面全是玄學(xué) bug。1.1 labelme 的 JSON 結(jié)構(gòu)一個文件裝一張圖的所有標(biāo)注labelme 是典型的一圖一 JSON。你標(biāo)注完一張圖它會生成一個和圖片同名的.json文件里面記錄了圖片路徑、尺寸、以及一個shapes數(shù)組。每個 shape 代表一個標(biāo)注對象核心字段有這么幾個label類別名稱是字符串比如 cat、person、defect。points構(gòu)成這個形狀的點(diǎn)坐標(biāo)數(shù)組單位是像素原點(diǎn)在圖片左上角。shape_type形狀類型常見的有polygon多邊形、rectangle矩形、circle、line、point。imageWidth/imageHeight圖片的寬高這個字段非常關(guān)鍵YOLO 歸一化就靠它。一個典型的矩形標(biāo)注長這樣{ version: 5.1.1, shapes: [ { label: person, points: [[102.5, 88.0], [340.2, 415.7]], shape_type: rectangle, flags: {} } ], imagePath: 0001.jpg, imageWidth: 1280, imageHeight: 720 }注意points里只有兩個點(diǎn)分別是矩形的左上角和右下角。但 labelme 并不強(qiáng)制你按左上、右下的順序點(diǎn)有人習(xí)慣從右下往左上拉那points就是反的。這一點(diǎn)在轉(zhuǎn)換時必須處理否則算出來的寬高會是負(fù)數(shù)。1.2 YOLO 的 TXT 結(jié)構(gòu)一行一個框全部歸一化YOLO 的標(biāo)注是一圖一 TXT文件名和圖片同名內(nèi)容每一行代表一個目標(biāo)格式固定為五個值class_id x_center y_center width height這里有幾個硬性約定新手最容易搞錯所有坐標(biāo)都是歸一化的也就是除以圖片寬高之后的值范圍在 0 到 1 之間。x_center、y_center是框中心點(diǎn)不是左上角。width、height是框的寬高也是歸一化后的。class_id是從 0 開始的整數(shù)索引不是類別名字符串。比如上面那個 person 框假設(shè)它是第 0 類轉(zhuǎn)換后大概是這樣0 0.17285 0.34979 0.18570 0.455141.3 兩種格式的核心差異對照把差異列成表轉(zhuǎn)換時對著看能省很多事維度labelme JSONYOLO TXT文件粒度一圖一 JSON一圖一 TXT坐標(biāo)單位像素絕對值歸一化 0~1框表示左上右下兩點(diǎn)中心點(diǎn)寬高類別表示字符串 label整數(shù) class_id形狀支持多邊形/矩形/圓/線/點(diǎn)僅矩形框尺寸來源JSON 內(nèi)自帶需外部提供或讀圖這張表里最要命的是最后一行YOLO 的 TXT 本身不存圖片尺寸所以從 YOLO 轉(zhuǎn)回 labelme 時你必須知道每張圖的寬高否則沒法還原成像素坐標(biāo)。這是雙向轉(zhuǎn)換里最容易翻車的地方后面會專門講。2. labelme 轉(zhuǎn) YOLO從 JSON 到歸一化 TXT 的完整鏈路labelme 轉(zhuǎn) YOLO 是更常見的需求因為大家通常先用 labelme 標(biāo)再拿去訓(xùn)練 YOLO。這個方向的轉(zhuǎn)換邏輯相對清晰但細(xì)節(jié)坑不少尤其是多邊形和類別映射。2.1 矩形框的坐標(biāo)換算中心點(diǎn)公式別記反矩形框的轉(zhuǎn)換是基礎(chǔ)公式必須記牢。假設(shè) labelme 的兩個點(diǎn)是(x1, y1)和(x2, y2)圖片寬W、高Hx_min min(x1, x2) y_min min(y1, y2) x_max max(x1, x2) y_max max(y1, y2) x_center (x_min x_max) / 2 / W y_center (y_min y_max) / 2 / H width (x_max - x_min) / W height (y_max - y_min) / H這里用min/max就是為了兼容從右下往左上拉的情況。我見過有人直接拿points[0]當(dāng)左上角結(jié)果一半的框位置全錯排查了半天才發(fā)現(xiàn)是標(biāo)注順序問題。所以別偷懶老老實(shí)實(shí)取 min/max。2.2 多邊形怎么變成矩形框取外接矩形labelme 支持多邊形標(biāo)注但 YOLO 檢測只認(rèn)矩形框。這時候通常的做法是取多邊形的外接矩形bounding box也就是所有點(diǎn)里最小的 x、y 和最大的 x、yxs [p[0] for p in points] ys [p[1] for p in points] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys)需要提醒的是外接矩形會引入背景冗余。如果你的目標(biāo)本身是細(xì)長或傾斜的外接框里會混進(jìn)大量無關(guān)像素可能影響檢測精度。這種情況要么改用分割任務(wù)YOLO 也支持 seg 格式存多邊形歸一化點(diǎn)要么在標(biāo)注階段就盡量用矩形框。我個人的經(jīng)驗是檢測任務(wù)就老老實(shí)實(shí)標(biāo)矩形別用多邊形再轉(zhuǎn)轉(zhuǎn)出來的框質(zhì)量不穩(wěn)定。2.3 類別名到 class_id 的映射順序必須固定YOLO 訓(xùn)練時class_id和類別名的對應(yīng)關(guān)系由data.yaml里的names列表決定。所以轉(zhuǎn)換腳本里必須維護(hù)一份固定的映射表比如class_map { person: 0, car: 1, dog: 2, }這里有個大坑映射表的順序必須和訓(xùn)練時names的順序完全一致。我見過一個項目轉(zhuǎn)換腳本里 person 是 0但data.yaml里 person 寫在第 2 位結(jié)果模型把人和狗認(rèn)反了訓(xùn)練 loss 看著正常推理結(jié)果一塌糊涂。所以建議把映射表單獨(dú)抽成一個配置文件轉(zhuǎn)換和訓(xùn)練共用同一份從源頭杜絕不一致。2.4 批量轉(zhuǎn)換腳本遍歷目錄 跳過空標(biāo)注單張轉(zhuǎn)換沒意義實(shí)際都是批量。下面是一個可直接用的腳本骨架import json import os from pathlib import Path class_map {person: 0, car: 1, dog: 2} def convert_one(json_path, out_dir): with open(json_path, r, encodingutf-8) as f: data json.load(f) W data[imageWidth] H data[imageHeight] lines [] for shape in data[shapes]: label shape[label] if label not in class_map: continue pts shape[points] xs [p[0] for p in pts] ys [p[1] for p in pts] x_min, x_max min(xs), max(xs) y_min, y_max min(ys), max(ys) xc (x_min x_max) / 2 / W yc (y_min y_max) / 2 / H w (x_max - x_min) / W h (y_max - y_min) / H lines.append(f{class_map[label]} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}) out_path Path(out_dir) / (Path(json_path).stem .txt) with open(out_path, w, encodingutf-8) as f: f.write(\n.join(lines)) def batch_convert(json_dir, out_dir): os.makedirs(out_dir, exist_okTrue) for name in os.listdir(json_dir): if name.endswith(.json): convert_one(os.path.join(json_dir, name), out_dir)幾個實(shí)操要點(diǎn)空標(biāo)注要生成空文件。如果一張圖沒有任何目標(biāo)YOLO 期望有一個空的 txt或者干脆沒有這個文件取決于訓(xùn)練框架。我建議生成空文件避免訓(xùn)練時找不到標(biāo)簽報錯。坐標(biāo)保留 6 位小數(shù)足夠再多沒意義還占空間。編碼統(tǒng)一用 utf-8否則中文路徑或中文標(biāo)簽會炸。2.5 轉(zhuǎn)換后的自檢別等訓(xùn)練報錯才發(fā)現(xiàn)轉(zhuǎn)完不要直接開訓(xùn)先做幾項檢查隨機(jī)抽幾張圖用可視化腳本把 YOLO 框畫回原圖肉眼確認(rèn)位置對不對。檢查所有坐標(biāo)是否都在 0~1 之間出現(xiàn)負(fù)數(shù)或大于 1 的說明尺寸或公式有問題。統(tǒng)計每個類別的框數(shù)量和 labelme 里的標(biāo)注數(shù)量對一下防止漏轉(zhuǎn)。我一般會寫一個簡單的可視化函數(shù)把歸一化坐標(biāo)乘回寬高再畫框這一步能攔下 90% 的低級錯誤。3. yolo2labelme反向轉(zhuǎn)換里那些容易忽略的細(xì)節(jié)從 YOLO 轉(zhuǎn)回 labelme 的需求相對少但也不是沒有。比如你想用 labelme 的可視化工具檢查 YOLO 標(biāo)注質(zhì)量或者想把已有的 YOLO 數(shù)據(jù)集導(dǎo)入 labelme 做二次修正。這個方向的難點(diǎn)在于TXT 里沒有圖片尺寸也沒有類別名。3.1 圖片尺寸從哪來必須讀圖或查表YOLO 的 TXT 只有歸一化坐標(biāo)要還原成像素必須知道每張圖的W和H。有兩種做法直接讀圖用 PIL 或 OpenCV 打開對應(yīng)圖片取img.size。簡單可靠但要求圖片和 TXT 在同一目錄或能對應(yīng)上。查尺寸表如果圖片不在手邊就得提前準(zhǔn)備一份文件名 - (W, H)的映射表。我推薦直接讀圖因為最不容易出錯。用 PIL 的話from PIL import Image img Image.open(img_path) W, H img.size注意 OpenCV 讀出來是(H, W)順序PIL 是(W, H)別搞混這個坑我踩過不止一次。3.2 類別名從哪來反向查映射表YOLO 的class_id是整數(shù)labelme 需要字符串 label。所以你得有一份id - name的反向映射。如果訓(xùn)練時用的是data.yaml直接讀它的names列表就行id2name {i: name for i, name in enumerate(names)}這里同樣強(qiáng)調(diào)一致性反向映射必須和當(dāng)初正向轉(zhuǎn)換用的是同一份類別定義否則轉(zhuǎn)出來的標(biāo)簽名對不上白忙活。3.3 中心點(diǎn)寬高還原成左上右下反向公式是正向的逆運(yùn)算x_min (x_center - width / 2) * W y_min (y_center - height / 2) * H x_max (x_center width / 2) * W y_max (y_center height / 2) * H還原出來的點(diǎn)是浮點(diǎn)數(shù)labelme 能接受浮點(diǎn)坐標(biāo)但如果你想要整數(shù)可以四舍五入。我一般保留一位小數(shù)既精確又不至于太啰嗦。3.4 生成符合 labelme 規(guī)范的 JSONlabelme 的 JSON 有一些字段是它讀取時依賴的不能省。一個最小可用的結(jié)構(gòu)import json labelme_data { version: 5.1.1, flags: {}, shapes: [], imagePath: img_name, imageData: None, imageWidth: W, imageHeight: H, } for line in lines: cid, xc, yc, w, h line.split() cid int(cid) xc, yc, w, h map(float, (xc, yc, w, h)) x_min (xc - w / 2) * W y_min (yc - h / 2) * H x_max (xc w / 2) * W y_max (yc h / 2) * H labelme_data[shapes].append({ label: id2name[cid], points: [[x_min, y_min], [x_max, y_max]], group_id: None, shape_type: rectangle, flags: {}, }) with open(out_json, w, encodingutf-8) as f: json.dump(labelme_data, f, ensure_asciiFalse, indent2)幾個關(guān)鍵點(diǎn)imageData設(shè)為None就行l(wèi)abelme 會自己去讀圖片不用把 base64 塞進(jìn)去否則 JSON 會巨大。shape_type必須是rectangle否則 labelme 打開時可能不認(rèn)。points順序是左上、右下這個順序 labelme 是認(rèn)的不用糾結(jié)。3.5 反向轉(zhuǎn)換的驗證用 labelme 打開看一眼轉(zhuǎn)完直接拿 labelme 打開生成的 JSON看框的位置和標(biāo)簽對不對。這一步最直觀比寫任何斷言都管用。如果框整體偏移或縮放八成是 W/H 搞反了或者歸一化坐標(biāo)沒乘對。4. 雙向轉(zhuǎn)換里我踩過的坑和對應(yīng)的解法前面講了流程這一節(jié)專門講坑。這些坑都是我在實(shí)際項目里真金白銀踩出來的寫出來能幫你省下大量排查時間。4.1 坐標(biāo)越界歸一化后出現(xiàn)負(fù)數(shù)或大于 1現(xiàn)象轉(zhuǎn)換后某些框的坐標(biāo)是負(fù)數(shù)或者大于 1訓(xùn)練時直接報錯或框跑到圖外。原因通常是標(biāo)注時框超出了圖片邊界。labelme 允許你把框拉到圖片外面但 YOLO 的歸一化坐標(biāo)理論上應(yīng)該在 0~1 之間。解法轉(zhuǎn)換時做一次裁剪clip把坐標(biāo)限制在 0~1xc min(max(xc, 0), 1) yc min(max(yc, 0), 1) w min(max(w, 0), 1) h min(max(h, 0), 1)但更根本的做法是標(biāo)注階段就別讓框出界出界的框本身質(zhì)量就有問題。4.2 圖片尺寸不一致JSON 里的尺寸和實(shí)際圖片對不上現(xiàn)象轉(zhuǎn)換后框的位置整體偏移或縮放。原因labelme 的 JSON 里存了imageWidth和imageHeight但如果圖片后來被裁剪、縮放、替換過JSON 里的尺寸就和實(shí)際圖片不一致了。轉(zhuǎn)換時如果信了 JSON 里的尺寸算出來的歸一化坐標(biāo)就是錯的。解法轉(zhuǎn)換時以實(shí)際圖片的尺寸為準(zhǔn)而不是 JSON 里的。用 PIL 讀一下真實(shí)尺寸覆蓋 JSON 里的值。這個坑很隱蔽因為 JSON 里的尺寸看起來很合理你不會懷疑它。4.3 類別名大小寫和空格看不見的殺手現(xiàn)象某些類別的框全部丟失或者映射到錯誤的 id。原因類別名里有看不見的空格或者大小寫不一致。比如person和person 末尾空格在字典里是兩個不同的 key。解法轉(zhuǎn)換前對 label 做一次清洗label shape[label].strip()同時建議在標(biāo)注規(guī)范里明確要求類別名統(tǒng)一小寫、無空格。我見過一個團(tuán)隊兩個人分別標(biāo)了Car和car結(jié)果訓(xùn)練時被當(dāng)成兩個類模型直接懵了。4.4 多邊形點(diǎn)順序混亂外接矩形算錯現(xiàn)象多邊形轉(zhuǎn)矩形后框的位置或大小明顯不對。原因多邊形的點(diǎn)順序不固定有人順時針點(diǎn)有人逆時針點(diǎn)甚至有人亂序點(diǎn)。如果你假設(shè)點(diǎn)是有序的算外接矩形就會出錯。解法永遠(yuǎn)用min/max遍歷所有點(diǎn)不要假設(shè)點(diǎn)的順序。這是最穩(wěn)妥的做法。4.5 空標(biāo)注和缺失文件訓(xùn)練時的隱形炸彈現(xiàn)象訓(xùn)練時報找不到標(biāo)簽文件或者某些圖被靜默跳過。原因有些圖沒有目標(biāo)labelme 可能沒生成 JSON或者轉(zhuǎn)換時沒生成對應(yīng)的空 TXT。解法轉(zhuǎn)換腳本里顯式處理空標(biāo)注生成空文件。同時訓(xùn)練前做一次完整性檢查確保每張圖都有對應(yīng)的標(biāo)簽文件哪怕是空的。5. 把轉(zhuǎn)換做成可復(fù)用的工具工程化的一點(diǎn)思考單次轉(zhuǎn)換用腳本就夠了但如果你的項目要反復(fù)迭代標(biāo)注會不斷更新每次都手動跑腳本很容易出錯。我的做法是把轉(zhuǎn)換邏輯封裝成一個命令行工具支持配置文件和參數(shù)。5.1 用配置文件管理類別映射把類別映射、輸入輸出目錄、是否裁剪等參數(shù)寫進(jìn)一個 YAMLclass_map: person: 0 car: 1 dog: 2 input_dir: ./labels_json output_dir: ./labels_txt clip: true腳本讀這個配置轉(zhuǎn)換和訓(xùn)練共用同一份類別定義徹底消除不一致。5.2 加日志和統(tǒng)計方便排查轉(zhuǎn)換時打印統(tǒng)計信息比如處理了多少文件、跳過了多少、每個類別多少個框。這些信息在排查問題時非常有用。我一般會在轉(zhuǎn)換結(jié)束后輸出一個匯總處理文件數(shù): 1200 跳過文件數(shù): 3 person: 3400 個框 car: 2100 個框 dog: 800 個框5.3 支持增量轉(zhuǎn)換如果只新增了幾十張標(biāo)注沒必要全量重轉(zhuǎn)??梢杂涗浺艳D(zhuǎn)換的文件列表只處理新增或修改過的。這個優(yōu)化在數(shù)據(jù)集大的時候能省不少時間。6. 轉(zhuǎn)換之外一些關(guān)于標(biāo)注流程的經(jīng)驗格式轉(zhuǎn)換只是標(biāo)注流程里的一環(huán)真正影響效率的是整個流程的設(shè)計。分享幾點(diǎn)我的經(jīng)驗。6.1 標(biāo)注規(guī)范要提前定死類別名、框的松緊程度、是否允許出界、多邊形還是矩形這些都要在標(biāo)注開始前定好寫成文檔發(fā)給所有標(biāo)注人員。我見過太多項目標(biāo)到一半發(fā)現(xiàn)規(guī)范不統(tǒng)一返工的成本遠(yuǎn)高于一開始多花半小時定規(guī)范。6.2 轉(zhuǎn)換腳本要版本化轉(zhuǎn)換腳本本身也是代碼要納入版本管理。因為類別映射、坐標(biāo)處理邏輯可能會變出問題時能回溯到具體版本。別把腳本隨手丟在桌面過兩個月自己都找不到。6.3 轉(zhuǎn)換后一定要可視化抽檢不管腳本寫得多自信轉(zhuǎn)完都要抽幾張圖可視化確認(rèn)。這一步花不了幾分鐘但能攔住絕大多數(shù)低級錯誤。我習(xí)慣抽 10 張覆蓋不同類別和不同尺寸的圖。6.4 保留原始標(biāo)注轉(zhuǎn)換是單向的但原始標(biāo)注要保留。萬一轉(zhuǎn)換邏輯有問題還能從原始 JSON 重新轉(zhuǎn)。別轉(zhuǎn)完就把 JSON 刪了這個教訓(xùn)我吃過一次重標(biāo)了一整天。7. 常見問題速查把高頻問題整理成表遇到時直接對號入座問題現(xiàn)象可能原因排查方向框位置整體偏移圖片尺寸不一致用實(shí)際圖片尺寸覆蓋 JSON 尺寸框大小不對歸一化公式用錯檢查是否除以了 W 和 H部分類別丟失類別名不匹配檢查大小寫、空格、映射表坐標(biāo)為負(fù)或大于 1框超出圖片邊界加 clip 或修正標(biāo)注多邊形轉(zhuǎn)框不準(zhǔn)點(diǎn)順序假設(shè)錯誤用 min/max 遍歷所有點(diǎn)訓(xùn)練找不到標(biāo)簽空標(biāo)注未生成文件顯式生成空 TXT反向轉(zhuǎn)換框錯位W/H 順序搞反PIL 是 (W,H)OpenCV 是 (H,W)這張表基本覆蓋了 90% 的轉(zhuǎn)換問題遇到時先對照排查能省下大量瞎試的時間。8. 寫在最后的一點(diǎn)個人體會labelme 和 YOLO 的格式互轉(zhuǎn)技術(shù)上并不復(fù)雜無非是坐標(biāo)換算和類別映射。但真正讓人頭疼的從來不是公式而是數(shù)據(jù)本身的不規(guī)范尺寸對不上、類別名不統(tǒng)一、框出界、點(diǎn)順序亂。這些問題在單個文件上看不出來一旦批量處理就集中爆發(fā)。我的建議是把轉(zhuǎn)換腳本當(dāng)成項目基礎(chǔ)設(shè)施來對待而不是臨時用的一次性代碼。類別映射抽成配置、加日志統(tǒng)計、轉(zhuǎn)換后可視化抽檢、保留原始標(biāo)注這幾件事做下來轉(zhuǎn)換環(huán)節(jié)基本就不會再出問題了。另外標(biāo)注規(guī)范一定要在動手標(biāo)之前定死這比任何腳本優(yōu)化都管用。格式轉(zhuǎn)換只是手段干凈、一致的數(shù)據(jù)才是目標(biāo)檢測項目真正的地基。