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