Dataset: add READMEs, organize scripts, update .gitignore

- Add README.md to all 16 dataset directories with script references
- Centralize generation scripts under scripts/ with full README
- Update .gitignore to cover all data directories, keep only metadata
- Remove redundant CCPD/.gitignore (parent covers same rules)
- Remove stale scripts/test/ (254MB pipeline residue)
- Fix label parser reference: scripts are authoritative source of truth
This commit is contained in:
kirl committed 2026-06-23 14:41:39 +08:00
1 parent 7867c3bfaf
commit a64690477a
43 files changed
+2691 -205

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
test/
train/
+61
View File
@@ -0,0 +1,61 @@
# CCPD_19K_EXP_CUT_TRANS — ICE-COR 角点矫正 + 透视变换
> **以脚本为准**:生成逻辑以 `ICE_Dataset/scripts/build_ccpd_19k_exp_cut_trans.py` 为准。
由 CCPD_19K_EXP_CUT 经 ICE-COR 角点推理 → 中心外扩 8%(x)/5%(y) → 透视变换生成。
## 处理流程
```
CCPD_19K_EXP_CUT (~18,887 张)
→ ICE-COR best.pth 推理 (192×64)
→ 角点排序 (TL→TR→BR→BL)
→ 中心外扩: 8% (x) / 5% (y)
→ 透视变换 → 192×64
→ 文件名追加 _trans
→ CCPD_19K_EXP_CUT_TRANS
```
## 目录结构
```
CCPD_19K_EXP_CUT_TRANS/
├── train/ # 与源数据集对应
│ ├── blue/
│ ├── green/
│ ├── police/
│ ├── army/
│ ├── gangao/
│ ├── shiguan_old/
│ ├── shiguan_new/
│ ├── lingguan_old/
│ ├── lingguan_new/
│ ├── coach/
│ └── new_energy/
├── test/
│ └── ... (同上 11 类)
└── README.md
```
## 生成脚本
```bash
cd ICE_Dataset/CCPD_19K_EXP_CUT_TRANS
python build_ccpd_19k_exp_cut_trans.py --dry-run # 预览 (每类2张)
python build_ccpd_19k_exp_cut_trans.py --n 100 # 预览 100 张
python build_ccpd_19k_exp_cut_trans.py --full # 全量处理
```
## 参数说明
| 参数 | 说明 |
|------|------|
| 外扩比例 | 8% (x) / 5% (y),从四角点中心向外径向扩展 |
| 输出尺寸 | 192×64 (对齐车牌生成器 440×140 底图比例) |
| 文件名 | 原文件名追加 `_trans`,不重映射角点 |
| 跳过策略 | ICE-COR 推理失败 / 角点越界 → 直接跳过 |
## 模型
- ICE-COR: `ICE_NNv2/ICE-COR/output/1_CCPD_19K_EXP_CUT/best.pth`
- 专为 CCPD_19K_EXP_CUT 裁剪车牌 ROI 训练
@@ -0,0 +1,344 @@
#!/usr/bin/env python3
"""
CCPD_19K_EXP_CUT → ICE-COR 角点推理 → 中心外扩 → 透视变换 → CCPD_19K_EXP_CUT_TRANS
基于 CCPD_19K_EXP_CUT 裁剪车牌图片,使用 ICE-COR 检测四角点,
按 8%(x)/5%(y) 中心外扩后进行透视变换矫正至 192×64,
文件名追加 _trans 后缀,保持原文件夹结构。
外扩算法参考: Linux/App/main.cc rectify_plate_pfld()
用法:
python build_ccpd_19k_exp_cut_trans.py --dry-run # 预览 (每类2张)
python build_ccpd_19k_exp_cut_trans.py --n 100 # 预览 100 张
python build_ccpd_19k_exp_cut_trans.py --full # 全量处理 (~19K)
"""
import argparse
import os
import sys
import time
from collections import defaultdict
from pathlib import Path
import cv2
import numpy as np
# ---- Unicode-safe I/O ----
def _imread_unicode(filepath, flags=cv2.IMREAD_COLOR):
try:
data = np.fromfile(filepath, dtype=np.uint8)
if len(data) == 0:
return None
return cv2.imdecode(data, flags)
except Exception:
return cv2.imread(filepath, flags) # fallback
def _imwrite_unicode(filepath, img, params=None):
"""Unicode-safe imwrite — NEVER falls back to cv2.imwrite."""
try:
ext = os.path.splitext(filepath)[1]
ok, buf = cv2.imencode(ext, img, params or [])
if ok:
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'wb') as f:
f.write(buf.tobytes())
return True
# imencode failed — try writing via temp file
import tempfile, shutil
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
tmp.write(cv2.imencode(ext, img, params or [])[1].tobytes())
shutil.move(tmp.name, filepath)
return True
except Exception:
# Last resort: imencode to bytes, write with Python I/O
try:
ext = os.path.splitext(filepath)[1]
ok, buf = cv2.imencode(ext, img, params or [])
if ok:
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'wb') as f:
f.write(buf.tobytes())
return True
except Exception:
pass
return False
import torch
# ---------------------------------------------------------------------------
# 路径
# ---------------------------------------------------------------------------
SCRIPT_DIR = Path(__file__).resolve().parent # CCPD_19K_EXP_CUT_TRANS/
PROJ_ROOT = SCRIPT_DIR.parents[1] # ICEv2/
ICE_COR_DIR = PROJ_ROOT / "ICE_NNv2" / "ICE-COR"
MODEL_PATH = ICE_COR_DIR / "output" / "1_CCPD_19K_EXP_CUT" / "best.pth"
SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT" # 源数据集
DST_DIR = SCRIPT_DIR # 本目录
if str(ICE_COR_DIR) not in sys.path:
sys.path.insert(0, str(ICE_COR_DIR))
from corner_rectify import load_corner_model, predict_plate_corners, order_corners
# ---------------------------------------------------------------------------
# 参数
# ---------------------------------------------------------------------------
OUT_W = 192
OUT_H = 64
CORNER_PAD_X = 0.08 # 从中心向外扩展 8% (x)
CORNER_PAD_Y = 0.05 # 从中心向外扩展 5% (y)
SPLITS = ["train", "test"]
# ===================================================================
# 核心处理: 单张图片
# ===================================================================
def process_single(model, device, img_path: Path, out_img_path: Path,
draw_corners: bool = False):
"""
对单张裁剪车牌图片:
1. ICE-COR 推理 → 4 角点像素坐标
2. order_corners → TL, TR, BR, BL
3. 中心外扩 8%(x)/5%(y), clip 到图像边界
4. 透视变换 → 192×64
5. (可选) 在输出图上绘制角点
6. 保存, 文件名追加 _trans
返回:
"ok" — 成功
"skip" — 跳过 (推理失败/角点异常)
"""
img = _imread_unicode(str(img_path))
if img is None:
return "skip"
H, W = img.shape[:2]
# ---- 1. ICE-COR 角点推理 ----
try:
corners_raw = predict_plate_corners(img, model, device) # (4,2) 像素坐标
except Exception:
return "skip"
# ---- 2. 排序 (TL, TR, BR, BL) ----
ordered = order_corners(corners_raw) # (4,2) float32
# 校验: 角点是否有效 (非退化四边形)
if not _valid_quad(ordered, W, H):
return "skip"
# ---- 3. 中心外扩 ----
src_pts = _expand_corners(ordered, W, H, CORNER_PAD_X, CORNER_PAD_Y)
# ---- 4. 透视变换 ----
dst_pts = np.float32([[0, 0], [OUT_W - 1, 0], [OUT_W - 1, OUT_H - 1], [0, OUT_H - 1]])
M = cv2.getPerspectiveTransform(src_pts, dst_pts)
warped = cv2.warpPerspective(img, M, (OUT_W, OUT_H))
# ---- 5. 绘制角点 (预览模式) ----
if draw_corners:
# 将原始角点 (ordered) 和外扩后角点 (src_pts) 映射到 warped 上绘制
_draw_debug(warped, ordered, src_pts, M)
# ---- 6. 保存 ----
stem = img_path.stem
new_name = stem + "_trans" + img_path.suffix
out_img_path.parent.mkdir(parents=True, exist_ok=True)
final_out = out_img_path.parent / new_name
_imwrite_unicode(str(final_out), warped)
return "ok"
def _valid_quad(pts, W, H):
"""检查四边形是否有效 (非退化, 不越界太离谱)"""
pts = np.asarray(pts, dtype=np.float32)
# 所有点必须在图像范围内 (允许少许越界)
if (pts[:, 0] < -0.5 * W).any() or (pts[:, 0] > 1.5 * W).any():
return False
if (pts[:, 1] < -0.5 * H).any() or (pts[:, 1] > 1.5 * H).any():
return False
# 面积不能太小 (退化)
area = cv2.contourArea(pts.reshape(-1, 1, 2).astype(np.float32))
if area < 0.01 * W * H:
return False
return True
def _expand_corners(pts, W, H, pad_x, pad_y):
"""
从四角点中心出发, 各点沿径向向外扩展 pad_x/pad_y 比例.
参考: Linux/App/main.cc rectify_plate_pfld()
"""
pts = np.asarray(pts, dtype=np.float32).copy()
center = pts.mean(axis=0)
for i in range(4):
pts[i, 0] = center[0] + (pts[i, 0] - center[0]) * (1.0 + pad_x)
pts[i, 1] = center[1] + (pts[i, 1] - center[1]) * (1.0 + pad_y)
# clip 到图像边界
pts[:, 0] = np.clip(pts[:, 0], 0.0, W - 1.0)
pts[:, 1] = np.clip(pts[:, 1], 0.0, H - 1.0)
return pts
def _draw_debug(warped, ordered, src_pts, M):
"""在 warped 图上绘制 ICE-COR 原始角点和外扩后角点"""
# 原始角点 (ordered) → warped 坐标系
ordered_h = cv2.perspectiveTransform(
ordered.reshape(1, -1, 2).astype(np.float32), M
).reshape(-1, 2).astype(np.int32)
# 外扩角点 (src_pts) → warped 坐标系 (应该映射到四角)
src_h = cv2.perspectiveTransform(
src_pts.reshape(1, -1, 2).astype(np.float32), M
).reshape(-1, 2).astype(np.int32)
# 绿色: 外扩后的四边形
cv2.polylines(warped, [src_h], isClosed=True, color=(0, 255, 0), thickness=1)
# 蓝色: 原始 ICE-COR 角点
for pt in ordered_h:
cv2.circle(warped, tuple(pt), 2, (255, 0, 0), -1)
# 标签
cv2.putText(warped, "blue=ICE-COR green=expanded",
(3, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1)
# ===================================================================
# 主流程
# ===================================================================
def main():
parser = argparse.ArgumentParser(
description="CCPD_19K_EXP_CUT → ICE-COR角点 + 外扩 + 透视变换 → CCPD_19K_EXP_CUT_TRANS")
parser.add_argument("--dry-run", action="store_true",
help="预览 (每类2张, 图上绘制角点)")
parser.add_argument("--n", type=int, default=None,
help="预览 N 张")
parser.add_argument("--full", action="store_true",
help="全量处理 (~19K)")
args = parser.parse_args()
if not (args.dry_run or args.n or args.full):
print("请指定 --dry-run, --n N, 或 --full")
print(f" python {Path(__file__).name} --dry-run")
print(f" python {Path(__file__).name} --full")
return
draw = not args.full
label = ("DRY-RUN (每类2张)" if args.dry_run else
f"预览 ({args.n} 张)" if args.n else "全量处理")
print("=" * 70)
print(f"CCPD_19K_EXP_CUT → ICE-COR 角点 + 外扩 {CORNER_PAD_X:.0%}(x)/{CORNER_PAD_Y:.0%}(y) → 透视变换 {OUT_W}×{OUT_H}")
print(f"模式: {label}")
print("=" * 70)
# ---- 加载 ICE-COR 模型 ----
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"\n加载 ICE-COR: {MODEL_PATH}")
model, _ = load_corner_model(str(MODEL_PATH), device)
print(f"设备: {device} | 输出: {OUT_W}×{OUT_H}")
# ---- 收集图片 ----
print("\n收集图片...")
all_tasks = []
for split in SPLITS:
split_dir = SRC_DIR / split
if not split_dir.exists():
continue
for cls_dir in sorted(split_dir.iterdir()):
if not cls_dir.is_dir():
continue
for jpg in sorted(cls_dir.glob("*.jpg")):
rel = jpg.relative_to(SRC_DIR)
out = DST_DIR / rel
all_tasks.append((jpg, out))
print(f"共 {len(all_tasks)} 张")
# ---- 限制数量 ----
if args.dry_run:
by_class = defaultdict(list)
for src, dst in all_tasks:
by_class[src.parent.name].append((src, dst))
limited = []
for cls in sorted(by_class):
limited.extend(by_class[cls][:2])
all_tasks = limited
print(f"预览: 限制 {len(all_tasks)} 张 (每类 2 张)")
elif args.n:
all_tasks = all_tasks[:args.n]
print(f"预览: 限制 {len(all_tasks)} 张")
# ---- 清理旧输出 (非 full 模式) ----
if not args.full:
import shutil
for split in SPLITS:
sp = DST_DIR / split
if sp.exists():
shutil.rmtree(sp)
# ---- 处理 ----
print(f"\n开始处理...")
t0 = time.perf_counter()
stats = {"total": 0, "ok": 0, "skip": 0}
sc = defaultdict(lambda: {"ok": 0, "skip": 0})
for idx, (src_path, out_path) in enumerate(all_tasks):
rel = src_path.relative_to(SRC_DIR)
parts = rel.parts
split_name, cls_name = parts[0], parts[1]
result = process_single(model, device, src_path, out_path,
draw_corners=draw)
stats["total"] += 1
key = f"{split_name}/{cls_name}"
if result == "ok":
stats["ok"] += 1
sc[key]["ok"] += 1
else:
stats["skip"] += 1
sc[key]["skip"] += 1
if (idx + 1) % 500 == 0 or (idx + 1) == len(all_tasks):
elapsed = time.perf_counter() - t0
rate = (idx + 1) / elapsed if elapsed > 0 else 0
eta = (len(all_tasks) - (idx + 1)) / rate if rate > 0 else 0
print(f" [{idx+1}/{len(all_tasks)}] "
f"成功={stats['ok']} 跳过={stats['skip']} "
f"速率={rate:.1f}张/s ETA={eta:.0f}s")
t1 = time.perf_counter()
# ---- 统计 ----
print(f"\n{'='*70}")
print(f"完成! {t1 - t0:.1f}s")
print(f" 总图: {stats['total']}")
print(f" 成功: {stats['ok']} ({stats['ok'] / max(stats['total'], 1) * 100:.1f}%)")
print(f" 跳过: {stats['skip']}")
print(f"\n 按类别:")
for key in sorted(sc):
s = sc[key]
print(f" {key:30s} ok={s['ok']:5d} skip={s['skip']:5d}")
print(f"\n 输出: {DST_DIR}")
for split in SPLITS:
sp = DST_DIR / split
if sp.exists():
jpg_n = sum(1 for _ in sp.rglob("*.jpg"))
cls_n = len([d for d in sp.iterdir() if d.is_dir()])
print(f" {split}/ : {jpg_n} 图, {cls_n} 类")
print(f"\n{'='*70}")
if __name__ == "__main__":
main()