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:
1 parent
7867c3bfaf
commit
a64690477a
43 files changed
+2691
-205
No files matched your search
@@ -0,0 +1,2 @@
|
||||
test/
|
||||
train/
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user