diff --git a/.gitignore b/.gitignore index 0507dcb..2e55693 100644 --- a/.gitignore +++ b/.gitignore @@ -1,32 +1,100 @@ +# ============================================================ # Python +# ============================================================ __pycache__/ *.py[cod] +# ============================================================ # IDE / Editor +# ============================================================ .vscode/ .idea/ *.swp *.swo *~ +# ============================================================ # OS +# ============================================================ .DS_Store Thumbs.db Desktop.ini -# Logs +# ============================================================ +# Logs & Cache +# ============================================================ *.log - -# Jupyter .ipynb_checkpoints/ -ccpd_combined_5k5k_spe/ -output/ -CCPD_SPE/*/ - -build/ - -# CCPD raw data (keep on disk, never commit) +# ============================================================ +# CCPD 原始真图 (~353K, ~23GB) +# ============================================================ CCPD/CCPD2019/ CCPD/CCPD2020/ -CCPD/*.7z \ No newline at end of file +CCPD/*.7z +CCPD/*.zip + +# ============================================================ +# CCPD 真图 TRANS (~297K, ~2GB) +# ============================================================ +CCPD_REAL_TRANS/train/*.jpg + +# ============================================================ +# CCPD 322K 合成管线 +# ============================================================ +CCPD_322K/train/ +CCPD_322K/test/ +CCPD_322K_EXP_CUT/train/ +CCPD_322K_EXP_CUT/test/ +CCPD_322K_EXP_CUT_TRANS/train/ +CCPD_322K_EXP_CUT_TRANS/test/ + +# ============================================================ +# CCPD 19K 合成管线 (YOLO / CAT / COR) +# ============================================================ +CCPD_19K/train/ +CCPD_19K/test/ +CCPD_19K_EXP_CUT/train/ +CCPD_19K_EXP_CUT/test/ +CCPD_19K_EXP_CUT_TRANS/train/ +CCPD_19K_EXP_CUT_TRANS/test/ + +# ============================================================ +# CCPD Random 蓝绿 +# ============================================================ +CCPD_Random_Blue5K_Green5K/blue/ +CCPD_Random_Blue5K_Green5K/green/ +CCPD_Random_Blue5K_Green5K_EXP_CUT/blue/ +CCPD_Random_Blue5K_Green5K_EXP_CUT/green/ +CCPD_Random_Blue5K_Green5K_EXP_CUT_TRANS/blue/ +CCPD_Random_Blue5K_Green5K_EXP_CUT_TRANS/green/ +CCPD_Random_Blue150K_Green100K/blue/ +CCPD_Random_Blue150K_Green100K/green/ + +# ============================================================ +# CCPD SPE 特殊车牌 +# ============================================================ +CCPD_SPE/army/ +CCPD_SPE/coach/ +CCPD_SPE/gangao/ +CCPD_SPE/lingguan_new/ +CCPD_SPE/lingguan_old/ +CCPD_SPE/new_energy/ +CCPD_SPE/police/ +CCPD_SPE/shiguan_new/ +CCPD_SPE/shiguan_old/ +CCPD_SPE/ccpd_base/ + +# ============================================================ +# CBLPRD +# ============================================================ +CBLPRD_TRANS/train/*.jpg +CBLPRD_eval/ + +# ============================================================ +# 其他 +# ============================================================ +ccpd_combined_5k5k_spe/images/ +ccpd_combined_5k5k_spe/labels/ +output/ +build/ \ No newline at end of file diff --git a/CBLPRD_TRANS/README.md b/CBLPRD_TRANS/README.md new file mode 100644 index 0000000..22211f3 --- /dev/null +++ b/CBLPRD_TRANS/README.md @@ -0,0 +1,11 @@ +# CBLPRD TRANS(ICE-COR 矫正) + +| 项目 | 详情 | +|---|---| +| 来源 | CBLPRD-330K 经过 ICE-COR 角点矫正 + 透视变换 | +| 规模 | 289,480 张 | +| 生成脚本 | `scripts/build_cblprd_trans.py` | +| 标签格式 | 文件名后缀(`parse_plate_suffix` 解析) | +| 训练用途 | OCR 三源混合训练数据源之一(真实域锚点) | + +> **以脚本为准**:生成脚本负责调用 ICE-COR 模型对每张图做角点检测并 warp,输出到 `train/` 子目录。 diff --git a/CCPD/.gitignore b/CCPD/.gitignore deleted file mode 100644 index df7caae..0000000 --- a/CCPD/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# CCPD raw data (download from official source) -CCPD2019/ -CCPD2020/ - -# 7z archives (optional, via git LFS) -*.7z - diff --git a/CCPD/README.md b/CCPD/README.md index c1b349c..55ffcd0 100644 --- a/CCPD/README.md +++ b/CCPD/README.md @@ -1,23 +1,21 @@ -# CCPD Dataset +# CCPD 原始真图 -CCPD (Chinese City Parking Dataset) 公开车牌识别数据集。 +> **以脚本为准**:`scripts/sample_ccpd_full.py` 决定如何读取和转换此目录。 -## 获取数据 - -从 CCPD 官方下载,解压到本目录。 - -## 目录结构 +| 项目 | 详情 | +|---|---| +| 来源 | CCPD2019 + CCPD2020 公开数据集 | +| 规模 | 353,158 张 | +| 标签格式 | CCPD 文件名编码(`parse_ccpd_filename` 解析) | +| 下游脚本 | `scripts/sample_ccpd_full.py` → `CCPD_REAL_TRANS/` | +| 训练用途 | 不直接用于训练,仅作为生成脚本的输入源 | ``` CCPD/ -├── CCPD2019/ # 原始 CCPD2019(本地,不入库) -│ ├── ccpd_base/ # 199,996 张 -│ ├── ccpd_blur/ # 20,611 张 -│ ├── ccpd_challenge/ # 50,003 张 -│ ├── ccpd_db/ # 10,132 张 -│ ├── ccpd_fn/ # 20,967 张 -│ ├── ccpd_np/ # 3,036 张 -│ ├── ccpd_rotate/ # 10,053 张 +├── CCPD2019/ # ccpd_base / blur / challenge / fn / rotate / tilt / weather / np +└── CCPD2020/ # 同上结构 +``` + │ ├── ccpd_tilt/ # 30,216 张 │ ├── ccpd_weather/ # 9,999 张 │ └── splits/ # 标注文件(本地,不入库) diff --git a/CCPD_19K/.gitignore b/CCPD_19K/.gitignore new file mode 100644 index 0000000..5420c69 --- /dev/null +++ b/CCPD_19K/.gitignore @@ -0,0 +1,2 @@ +test/ +train/ \ No newline at end of file diff --git a/CCPD_19K/README.md b/CCPD_19K/README.md new file mode 100644 index 0000000..8c235ce --- /dev/null +++ b/CCPD_19K/README.md @@ -0,0 +1,75 @@ +# CCPD_19K — 11类中国车牌数据集 + +> **以脚本为准**:生成逻辑以 `ICE_Dataset/scripts/build_ccpd_19k.py` 为准。 + +基于 CCPD 构建的 11 分类车牌数据集,涵盖蓝牌、绿牌及 9 种特殊车牌。 + +## 数据来源 + +| 来源 | 类型 | 采样数 | 说明 | +|------|------|--------|------| +| CCPD2019 | 蓝牌 (blue) | 5,000(随机) | 全蓝牌,从 341,369 张中随机抽取 | +| CCPD2020 | 绿牌 (green) | 5,000(随机) | 全绿牌,从 11,776 张中随机抽取 | +| CCPD_SPE | 特殊车牌 (9类) | 9,000(全部) | 9 类特殊车牌,每类 1,000 张 | + +## 11 类车牌 + +| 序号 | 类别 | 目录名 | 说明 | 底板色 | +|------|------|--------|------|--------| +| 1 | 蓝牌 | `blue` | 普通小型汽车 | 蓝底白字 | +| 2 | 绿牌 | `green` | 新能源小型汽车 | 绿底黑字 | +| 3 | 警车 | `police` | 警用车牌 | 白底,末位"警"红字 | +| 4 | 军车 | `army` | 军车/武警 | 白底 | +| 5 | 港澳 | `gangao` | 港澳入出境 | 黑底,粤Z·XXXX港/澳 | +| 6 | 使馆旧版 | `shiguan_old` | 使馆车牌(旧版) | 黑底,"使"红字 | +| 7 | 使馆新版 | `shiguan_new` | 使馆车牌(新版) | 黑底 | +| 8 | 领馆旧版 | `lingguan_old` | 领馆车牌(旧版) | 黑底,"领"红字 | +| 9 | 领馆新版 | `lingguan_new` | 领馆车牌(新版) | 黑底 | +| 10 | 教练车 | `coach` | 教练车牌 | 黄底,末位"学" | +| 11 | 新能源大型 | `new_energy` | 大型新能源 | 黄绿双拼,8位 | + +## 目录结构 + +``` +CCPD_19K/ +├── train/ # 训练集 (90%, 17,100 张) +│ ├── blue/ # 4,500 张 +│ ├── green/ # 4,500 张 +│ ├── police/ # 900 张 +│ ├── army/ # 900 张 +│ ├── gangao/ # 900 张 +│ ├── shiguan_old/ # 900 张 +│ ├── shiguan_new/ # 900 张 +│ ├── lingguan_old/ # 900 张 +│ ├── lingguan_new/ # 900 张 +│ ├── coach/ # 900 张 +│ └── new_energy/ # 900 张 +├── test/ # 测试集 (10%, 1,900 张) +│ ├── blue/ # 500 张 +│ ├── green/ # 500 张 +│ └── ... (同上 11 类) +└── README.md +``` + +## 划分方式 + +- 训练集:测试集 = 9:1 +- 分层抽样(stratified):每类内部独立按 9:1 划分 +- 随机种子:42(可复现) + +## 总计 + +- 训练集:17,100 张 +- 测试集:1,900 张 +- **总计:19,000 张** + +## 生成脚本 + +```bash +# 构建 CCPD_19K (从源数据采样) +python build_ccpd_19k.py + +# 推理 → bbox拓展裁剪 → 角点重映射 → CCPD_19K_EXP_CUT +# 脚本位于 CCPD_19K_EXP_CUT/build_ccpd_19k_exp_cut.py +python ../CCPD_19K_EXP_CUT/build_ccpd_19k_exp_cut.py --full +``` diff --git a/CCPD_19K/build_ccpd_19k.py b/CCPD_19K/build_ccpd_19k.py new file mode 100644 index 0000000..4eefa75 --- /dev/null +++ b/CCPD_19K/build_ccpd_19k.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Build CCPD_19K dataset from: + - CCPD2019 (all blue plates): random 5,000 images -> class "blue" + - CCPD2020 (all green plates): random 5,000 images -> class "green" + - CCPD_SPE (9 special plate types): all 9,000 images -> 9 classes + +Output structure (under ICE_Dataset/CCPD_19K/): + train/ + blue/ ~4500 + green/ ~4500 + police/ ~900 + army/ ~900 + gangao/ ~900 + shiguan_old/ ~900 + shiguan_new/ ~900 + lingguan_old/ ~900 + lingguan_new/ ~900 + coach/ ~900 + new_energy/ ~900 + test/ + (same 11 classes, ~10% each) + +Split ratio: 9:1 (train : test), stratified by class. +""" + +import os +import sys +import random +import shutil +from pathlib import Path +from collections import defaultdict + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +BASE_DIR = Path(r"C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset") +CCPD2019_DIR = BASE_DIR / "CCPD" / "CCPD2019" +CCPD2020_DIR = BASE_DIR / "CCPD" / "CCPD2020" +CCPD_SPE_DIR = BASE_DIR / "CCPD_SPE" +OUTPUT_DIR = BASE_DIR / "CCPD_19K" + +SEED = 42 +TRAIN_RATIO = 0.9 # 9:1 split + +# Number of images to sample from CCPD2019 / CCPD2020 +N_CCPD2019 = 5000 +N_CCPD2020 = 5000 + +# --------------------------------------------------------------------------- +# Step 1: Collect all images from CCPD2019 (blue plates) +# --------------------------------------------------------------------------- +print("=" * 60) +print("Step 1: Collecting CCPD2019 (blue) images ...") + +ccpd2019_images = [] +for jpg in CCPD2019_DIR.rglob("*.jpg"): + ccpd2019_images.append(jpg) + +print(f" Found {len(ccpd2019_images)} total images") +random.seed(SEED) +sampled_2019 = random.sample(ccpd2019_images, min(N_CCPD2019, len(ccpd2019_images))) +print(f" Sampled {len(sampled_2019)} images") + +# --------------------------------------------------------------------------- +# Step 2: Collect all images from CCPD2020 (green plates) +# --------------------------------------------------------------------------- +print("\nStep 2: Collecting CCPD2020 (green) images ...") + +ccpd2020_images = [] +for jpg in CCPD2020_DIR.rglob("*.jpg"): + ccpd2020_images.append(jpg) + +print(f" Found {len(ccpd2020_images)} total images") +random.seed(SEED) +sampled_2020 = random.sample(ccpd2020_images, min(N_CCPD2020, len(ccpd2020_images))) +print(f" Sampled {len(sampled_2020)} images") + +# --------------------------------------------------------------------------- +# Step 3: Collect all images from CCPD_SPE (9 special classes) +# --------------------------------------------------------------------------- +print("\nStep 3: Collecting CCPD_SPE (9 special plate classes) ...") + +# The 9 CCPD_SPE class names (subdirectory names) +SPE_CLASSES = [ + "police", + "army", + "gangao", + "shiguan_old", + "shiguan_new", + "lingguan_old", + "lingguan_new", + "coach", + "new_energy", +] + +spe_images = defaultdict(list) +for jpg in CCPD_SPE_DIR.rglob("*.jpg"): + # Path like: .../CCPD_SPE/ccpd_base/police/xxx.jpg + # The class is the immediate parent directory name + parent_dir = jpg.parent.name + if parent_dir in SPE_CLASSES: + spe_images[parent_dir].append(jpg) + +for cls in SPE_CLASSES: + print(f" {cls}: {len(spe_images[cls])} images") + +total_spe = sum(len(v) for v in spe_images.values()) +print(f" Total CCPD_SPE: {total_spe} images") + +# --------------------------------------------------------------------------- +# Step 4: Build class -> file list mapping +# --------------------------------------------------------------------------- +print("\nStep 4: Building class mappings ...") + +class_files = {} + +# Blue plates from CCPD2019 +class_files["blue"] = sampled_2019 + +# Green plates from CCPD2020 +class_files["green"] = sampled_2020 + +# 9 special plate classes +for cls in SPE_CLASSES: + class_files[cls] = spe_images[cls] + +# Validate +all_11_classes = ["blue", "green"] + SPE_CLASSES +print(f" 11 classes: {all_11_classes}") +for cls in all_11_classes: + print(f" {cls}: {len(class_files[cls])} images") + +# --------------------------------------------------------------------------- +# Step 5: Stratified 9:1 train/test split and copy +# --------------------------------------------------------------------------- +print("\nStep 5: Splitting and copying files ...") + +random.seed(SEED) + +for cls in all_11_classes: + files = class_files[cls] + random.shuffle(files) + + n_train = max(1, int(len(files) * TRAIN_RATIO)) + train_files = files[:n_train] + test_files = files[n_train:] + + # Create output directories + train_dir = OUTPUT_DIR / "train" / cls + test_dir = OUTPUT_DIR / "test" / cls + train_dir.mkdir(parents=True, exist_ok=True) + test_dir.mkdir(parents=True, exist_ok=True) + + # Copy train files (handle duplicate filenames by adding suffix) + for src in train_files: + dst = train_dir / src.name + counter = 1 + stem, suffix = dst.stem, dst.suffix + while dst.exists(): + dst = train_dir / f"{stem}_{counter}{suffix}" + counter += 1 + shutil.copy2(src, dst) + + # Copy test files (handle duplicate filenames by adding suffix) + for src in test_files: + dst = test_dir / src.name + counter = 1 + stem, suffix = dst.stem, dst.suffix + while dst.exists(): + dst = test_dir / f"{stem}_{counter}{suffix}" + counter += 1 + shutil.copy2(src, dst) + + print(f" {cls}: train={len(train_files)}, test={len(test_files)}") + +# --------------------------------------------------------------------------- +# Step 6: Summary +# --------------------------------------------------------------------------- +print("\n" + "=" * 60) +print("CCPD_19K dataset built successfully!") +print(f" Output: {OUTPUT_DIR}") + +total_train = 0 +total_test = 0 +for cls in all_11_classes: + train_cnt = len(list((OUTPUT_DIR / "train" / cls).glob("*.jpg"))) + test_cnt = len(list((OUTPUT_DIR / "test" / cls).glob("*.jpg"))) + total_train += train_cnt + total_test += test_cnt + print(f" {cls:15s} train: {train_cnt:5d} test: {test_cnt:5d}") + +print(f" {'TOTAL':15s} train: {total_train:5d} test: {total_test:5d}") +print(f" Grand total: {total_train + total_test}") +print("=" * 60) diff --git a/CCPD_19K_EXP_CUT/.gitignore b/CCPD_19K_EXP_CUT/.gitignore new file mode 100644 index 0000000..5420c69 --- /dev/null +++ b/CCPD_19K_EXP_CUT/.gitignore @@ -0,0 +1,2 @@ +test/ +train/ \ No newline at end of file diff --git a/CCPD_19K_EXP_CUT/README.md b/CCPD_19K_EXP_CUT/README.md new file mode 100644 index 0000000..5bad17a --- /dev/null +++ b/CCPD_19K_EXP_CUT/README.md @@ -0,0 +1,69 @@ +# CCPD_19K_EXP_CUT — 拓展裁剪 + 角点重映射 + +> **以脚本为准**:生成逻辑以 `ICE_Dataset/scripts/build_ccpd_19k_exp_cut.py` 为准。 + +由 CCPD_19K 经 YOLOv5t 推理 → bbox 拓展 → 裁剪 + 角点重映射生成。 + +## 处理流程 + +``` +CCPD_19K (19,000 张) + → YOLOv5t best.pt 推理 (imgsz=352, conf=0.25) + → bbox 拓展: + 常规: 12% (w ≥ 67 且 h ≥ 51) + 小车牌: 18% (w < 67 或 h < 51) + → 裁剪 + → 角点重映射写入文件名 + → CCPD_19K_EXP_CUT +``` + +## 目录结构 + +``` +CCPD_19K_EXP_CUT/ +├── train/ # 16,999 张 +│ ├── blue/ # 4,435 张 +│ ├── green/ # 4,486 张 +│ ├── police/ # 897 张 +│ ├── army/ # 898 张 +│ ├── gangao/ # 899 张 +│ ├── shiguan_old/ # 898 张 +│ ├── shiguan_new/ # 897 张 +│ ├── lingguan_old/ # 897 张 +│ ├── lingguan_new/ # 897 张 +│ ├── coach/ # 897 张 +│ └── new_energy/ # 898 张 +├── test/ # 1,888 张 +│ └── ... (同上 11 类) +└── README.md +``` + +## 统计 + +| 指标 | 数值 | +|------|------| +| 源图 | 19,000 | +| 裁剪成功 | 18,887 (99.4%) | +| 角点重映射 | 18,870 (99.3%) | +| 跳过 | 113 (0.6%) | +| - 漏检 | 71 | +| - 多框 | 42 | + +## 文件名角点格式 + +裁剪后 CCPD 文件名中的角点段已更新为裁剪坐标系像素坐标: + +``` +原: ...-324&452_241&449_242&420_325&423-... +新: ...-99&38_16&35_17&6_100&9-... +``` + +格式: `-C1X&C1Y_C2X&C2Y_C3X&C3Y_C4X&C4Y-` (像素坐标, 整数) + +## 生成脚本 + +```bash +cd ICE_Dataset/CCPD_19K_EXP_CUT +python build_ccpd_19k_exp_cut.py --dry-run # 预览 +python build_ccpd_19k_exp_cut.py --full # 全量 +``` diff --git a/CCPD_19K_EXP_CUT/build_ccpd_19k_exp_cut.py b/CCPD_19K_EXP_CUT/build_ccpd_19k_exp_cut.py new file mode 100644 index 0000000..f37d143 --- /dev/null +++ b/CCPD_19K_EXP_CUT/build_ccpd_19k_exp_cut.py @@ -0,0 +1,422 @@ +#!/usr/bin/env python3 +""" +CCPD_19K 模型推理 → bbox 拓展 → 裁剪 → 角点重映射写入文件名 → CCPD_19K_EXP_CUT + +使用 YOLOv5t best.pt 对 CCPD_19K 全部图片进行推理, +按 12%/18% 策略拓展 bbox 后裁剪,并将原 CCPD 角点重映射到裁剪坐标系后更新文件名。 +保持原文件夹结构,无额外标签文件。 + +用法: + python build_ccpd_19k_exp_cut.py --dry-run # 预览 (每类2张, 图上绘制角点) + python build_ccpd_19k_exp_cut.py --n 100 # 预览 N 张 + python build_ccpd_19k_exp_cut.py --full # 全量处理 (~19K, 无绘制) +""" + +import argparse +import os +import re +import shutil +import sys +import time +from collections import defaultdict +from pathlib import Path + +import cv2 +import numpy as np + +# ---- Unicode-safe I/O (must be BEFORE ultralytics import!) ---- +_original_imread = cv2.imread + +def _imread_unicode(filepath, flags=cv2.IMREAD_COLOR): + try: + data = np.fromfile(filepath, dtype=np.uint8) + if len(data) == 0: + return None + img = cv2.imdecode(data, flags) + return img + except Exception: + return _original_imread(filepath, flags) + +def _imwrite_unicode(filepath, img, params=None): + try: + ext = os.path.splitext(filepath)[1] + ok, buf = cv2.imencode(ext, img, params or []) + if ok: + with open(filepath, 'wb') as f: + f.write(buf.tobytes()) + return True + return False + except Exception: + return cv2.imwrite(filepath, img, params) + +cv2.imread = _imread_unicode # patch before ultralytics hooks it +# ----------------------------------------------------------------- + +import torch +import yaml +from ultralytics import YOLO + +# --------------------------------------------------------------------------- +# 路径 +# --------------------------------------------------------------------------- +SCRIPT_DIR = Path(__file__).resolve().parent +PROJ_ROOT = SCRIPT_DIR.parents[1] # ICEv2/ +MODEL_PATH = PROJ_ROOT / "ICE_NNv2" / "yolov5t" / "output" / "1_CCPD_19K" / "weights" / "best.pt" +SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K" # 源数据集 +DST_DIR = SCRIPT_DIR # 本目录 (CCPD_19K_EXP_CUT) + +# --------------------------------------------------------------------------- +# 推理 & 拓展参数 (与 ICE 主项目 / infer_expand.py 一致) +# --------------------------------------------------------------------------- +IMGSZ = 352 +CONF = 0.25 + +# CCPD 图像原始分辨率 +CCPD_W = 720 +CCPD_H = 1160 + +# ICE 主项目 bbox 拓展系数: 12% 常规, 18% 小车牌 +EXPAND_NORMAL = 1.12 +EXPAND_SMALL = 1.18 + +# ICE: bw < 180 || bh < 48 @1080p → 缩放到 CCPD 分辨率 +SMALL_BOX_W = int(180 * CCPD_W / 1920) # ≈ 68 +SMALL_BOX_H = int(48 * CCPD_H / 1080) # ≈ 52 + +MIN_PAD = 2 + +# CCPD_19K 的 split 名称 +SPLITS = ["train", "test"] + +# --------------------------------------------------------------------------- +# CCPD 角点解析 & 文件名重映射 +# --------------------------------------------------------------------------- + +# 匹配 CCPD 文件名中角点段: -C1X&C1Y_C2X&C2Y_C3X&C3Y_C4X&C4Y- +CORNER_RE = re.compile(r'-(\d+)&(\d+)_(\d+)&(\d+)_(\d+)&(\d+)_(\d+)&(\d+)-') + + +def parse_ccpd_corners(filename: str): + """ + 从 CCPD 文件名解析 4 个角点 (四边形), 返回像素坐标. + 格式: ...-C1X&C1Y_C2X&C2Y_C3X&C3Y_C4X&C4Y-... + 返回: [(x1,y1), (x2,y2), (x3,y3), (x4,y4)] 或 None + """ + m = CORNER_RE.search(filename) + if not m: + return None + nums = [int(m.group(i)) for i in range(1, 9)] + return [(nums[0], nums[1]), (nums[2], nums[3]), (nums[4], nums[5]), (nums[6], nums[7])] + + +def remap_corners_to_crop(corners, x1, y1, x2, y2): + """ + 将角点从原图坐标系重映射到裁剪区域像素坐标 (整数). + 仅当四边形与裁剪区重叠 > 25% 时才重映射. + + 返回: [(px,py), ...] 裁剪区像素坐标 或 None + """ + crop_w = x2 - x1 + crop_h = y2 - y1 + if crop_w <= 0 or crop_h <= 0: + return None + + # 四边形外包矩形与裁剪区重叠率 + xs = [c[0] for c in corners] + ys = [c[1] for c in corners] + poly_x1, poly_x2 = min(xs), max(xs) + poly_y1, poly_y2 = min(ys), max(ys) + + inter_w = max(0, min(poly_x2, x2) - max(poly_x1, x1)) + inter_h = max(0, min(poly_y2, y2) - max(poly_y1, y1)) + poly_area = (poly_x2 - poly_x1) * (poly_y2 - poly_y1) + + if poly_area <= 0 or (inter_w * inter_h) / poly_area <= 0.25: + return None + + # 逐点平移 + clip + new_corners = [] + for (px, py) in corners: + nx = max(0, min(crop_w - 1, int(round(px - x1)))) + ny = max(0, min(crop_h - 1, int(round(py - y1)))) + new_corners.append((nx, ny)) + return new_corners + + +def replace_corners_in_filename(filename: str, new_corners): + """ + 替换文件名中的角点段为新的像素坐标. + new_corners: [(px1,py1), (px2,py2), (px3,py3), (px4,py4)] + """ + new_seg = "-{0}&{1}_{2}&{3}_{4}&{5}_{6}&{7}-".format( + new_corners[0][0], new_corners[0][1], + new_corners[1][0], new_corners[1][1], + new_corners[2][0], new_corners[2][1], + new_corners[3][0], new_corners[3][1], + ) + return CORNER_RE.sub(new_seg, filename, count=1) + + +# --------------------------------------------------------------------------- +# 单张处理 +# --------------------------------------------------------------------------- + +def infer_and_crop(model, img_path: Path, out_img_path: Path, draw_corners: bool = False): + """ + 对单张图片: + 1. YOLO 推理 → 检出框 + 2. 按大小选择 12%/18% 拓展 + 3. 裁剪, 角点重映射 → 更新文件名 + 4. (可选) 在裁剪图上绘制四边形角点供审核 + 返回: + "cropped" — 成功 (角点已重映射写入文件名) + "cropped_nocorners" — 成功 (源文件名无角点段, 原名保存) + "cropped_low_overlap" — 成功 (角点重叠不足, 原名保存) + "miss" — 漏检 + "multi" — 多框跳过 + """ + try: + with open(str(img_path), 'rb') as f: + data = f.read() + img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) + except Exception: + img = None + if img is None: + return "miss" + + orig_h, orig_w = img.shape[:2] + + results = model.predict( + str(img_path), imgsz=IMGSZ, verbose=False, + conf=CONF, max_det=5 + ) + boxes = results[0].boxes + + if boxes is None or len(boxes) == 0: + return "miss" + + xywh = boxes.xywh.cpu().numpy() + + if len(xywh) > 1: + return "multi" + + cx, cy, bw, bh = xywh[0] + + # ---- 拓展系数 ---- + expand_factor = EXPAND_SMALL if (bw < SMALL_BOX_W or bh < SMALL_BOX_H) else EXPAND_NORMAL + + # ---- 裁剪区域 ---- + pad_x = max(MIN_PAD, int(round(bw * (expand_factor - 1.0) / 2.0))) + pad_y = max(MIN_PAD, int(round(bh * (expand_factor - 1.0) / 2.0))) + + x1_raw = int(cx - bw / 2) + y1_raw = int(cy - bh / 2) + x2_raw = int(cx + bw / 2) + y2_raw = int(cy + bh / 2) + + x1 = max(x1_raw - pad_x, 0) + y1 = max(y1_raw - pad_y, 0) + x2 = min(x2_raw + pad_x + 1, orig_w) + y2 = min(y2_raw + pad_y + 1, orig_h) + + if x2 - x1 <= 0 or y2 - y1 <= 0: + return "miss" + + crop = img[y1:y2, x1:x2] + + # ---- 角点重映射 → 更新文件名 ---- + stem = img_path.stem + corners = parse_ccpd_corners(stem) + remapped = None + + if corners is not None: + remapped = remap_corners_to_crop(corners, x1, y1, x2, y2) + if remapped is not None: + new_stem = replace_corners_in_filename(stem, remapped) + else: + new_stem = stem # 重叠不足, 保留原名 + else: + new_stem = stem # 无角点段 + + # ---- 绘制角点 (预览模式) ---- + if draw_corners and remapped is not None: + crop_h_draw, crop_w_draw = crop.shape[:2] + pts = [(px, py) for (px, py) in remapped] + pts_arr = np.array(pts, dtype=np.int32) + cv2.polylines(crop, [pts_arr], isClosed=True, color=(0, 255, 0), thickness=2) + for k, (px, py) in enumerate(pts): + cv2.circle(crop, (px, py), 3, (0, 0, 255), -1) + cv2.putText(crop, str(k), (px + 4, py - 4), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) + label = f"expand={expand_factor:.0%}" + cv2.putText(crop, label, (4, 14), + cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1) + + # ---- 保存 ---- + out_img_path.parent.mkdir(parents=True, exist_ok=True) + final_out = out_img_path.parent / (new_stem + out_img_path.suffix) + _imwrite_unicode(str(final_out), crop) + + if corners is None: + return "cropped_nocorners" + elif remapped is None: + return "cropped_low_overlap" + else: + return "cropped" + + +# --------------------------------------------------------------------------- +# 主流程 +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="CCPD_19K → 推理拓展裁剪+角点重映射 → CCPD_19K_EXP_CUT") + 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 + + if args.dry_run: + per_class_limit = 2 + label = "DRY-RUN (每类2张, 绘制角点)" + elif args.n: + per_class_limit = None + label = f"预览 ({args.n} 张)" + else: + per_class_limit = None + label = "全量处理" + + draw = not args.full # 非全量模式绘制角点 + + print("=" * 70) + print(f"CCPD_19K → bbox拓展 {EXPAND_NORMAL:.0%}/{EXPAND_SMALL:.0%} → 裁剪 → 角点重映射写入文件名") + print(f"模式: {label}") + print("=" * 70) + + # ---- 加载模型 ---- + print(f"\n加载: {MODEL_PATH}") + device = 'cuda:0' if torch.cuda.is_available() else 'cpu' + model = YOLO(str(MODEL_PATH)) + model.to(device) + print(f"设备: {device} | imgsz={IMGSZ} | conf={CONF}") + print(f"拓展: 常规 {EXPAND_NORMAL:.0%} (w>={SMALL_BOX_W},h>={SMALL_BOX_H}) / 小车牌 {EXPAND_SMALL:.0%} (w<{SMALL_BOX_W}或h<{SMALL_BOX_H})") + if draw: + print(f"预览: 裁剪图上绘制四边形角点 (绿框+红顶点+序号)") + + # ---- 收集图片 ---- + print("\n收集图片...") + all_tasks = [] # [(src_path, out_path)] + + 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][:per_class_limit]) + all_tasks = limited + print(f"预览: 限制 {len(all_tasks)} 张 (每类 {per_class_limit} 张)") + elif args.n: + all_tasks = all_tasks[:args.n] + print(f"预览: 限制 {len(all_tasks)} 张") + + # ---- 清理旧输出 (非 full) ---- + if not args.full and DST_DIR.exists(): + shutil.rmtree(DST_DIR) + + # ---- 处理 ---- + print(f"\n开始处理...") + t0 = time.perf_counter() + + stats = {"total": 0, "cropped": 0, "nocorners": 0, "low_overlap": 0, "miss": 0, "multi": 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, class, filename) + split_name, cls_name = parts[0], parts[1] + + result = infer_and_crop(model, src_path, out_path, draw_corners=draw) + + stats["total"] += 1 + key = f"{split_name}/{cls_name}" + + if result == "cropped": + stats["cropped"] += 1 + sc[key]["ok"] += 1 + elif result == "cropped_nocorners": + stats["nocorners"] += 1 + sc[key]["ok"] += 1 + elif result == "cropped_low_overlap": + stats["low_overlap"] += 1 + sc[key]["ok"] += 1 + elif result in ("miss", "multi"): + stats[result] += 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 + ok = stats["cropped"] + stats["nocorners"] + stats["low_overlap"] + skip = stats["miss"] + stats["multi"] + print(f" [{idx+1}/{len(all_tasks)}] " + f"裁剪={ok} (重映射={stats['cropped']}) " + f"跳过={skip} " + f"速率={rate:.1f}张/s ETA={eta:.0f}s") + + t1 = time.perf_counter() + + # ---- 统计 ---- + total_ok = stats["cropped"] + stats["nocorners"] + stats["low_overlap"] + total_skip = stats["miss"] + stats["multi"] + print(f"\n{'='*70}") + print(f"完成! {t1-t0:.1f}s") + print(f" 源图: {stats['total']}") + print(f" 裁剪成功: {total_ok} ({total_ok/max(stats['total'],1)*100:.1f}%)") + print(f" - 角点已重映射写入文件名: {stats['cropped']}") + print(f" - 无角点段 (保留原名): {stats['nocorners']}") + print(f" - 重叠不足 (保留原名): {stats['low_overlap']}") + print(f" 跳过: {total_skip}") + print(f" - 漏检: {stats['miss']}") + print(f" - 多框: {stats['multi']}") + + 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() diff --git a/CCPD_19K_EXP_CUT_TRANS/.gitignore b/CCPD_19K_EXP_CUT_TRANS/.gitignore new file mode 100644 index 0000000..5420c69 --- /dev/null +++ b/CCPD_19K_EXP_CUT_TRANS/.gitignore @@ -0,0 +1,2 @@ +test/ +train/ \ No newline at end of file diff --git a/CCPD_19K_EXP_CUT_TRANS/README.md b/CCPD_19K_EXP_CUT_TRANS/README.md new file mode 100644 index 0000000..b7c1d32 --- /dev/null +++ b/CCPD_19K_EXP_CUT_TRANS/README.md @@ -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 训练 diff --git a/CCPD_19K_EXP_CUT_TRANS/build_ccpd_19k_exp_cut_trans.py b/CCPD_19K_EXP_CUT_TRANS/build_ccpd_19k_exp_cut_trans.py new file mode 100644 index 0000000..1aed5ba --- /dev/null +++ b/CCPD_19K_EXP_CUT_TRANS/build_ccpd_19k_exp_cut_trans.py @@ -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() diff --git a/CCPD_322K/README.md b/CCPD_322K/README.md new file mode 100644 index 0000000..dfcaae1 --- /dev/null +++ b/CCPD_322K/README.md @@ -0,0 +1,10 @@ +# CCPD 322K 合成原图 + +| 项目 | 详情 | +|---|---| +| 来源 | `chinese_license_plate_generator` + 随机背景合成 | +| 规模 | 322,134 张 | +| 生成脚本 | `scripts/pipeline_322k.py` 第一步 | +| 下游脚本 | 同脚本第二步 → `CCPD_322K_EXP_CUT/` | + +> **以脚本为准**:此目录为合成管线中间产物,不直接用于训练。最终训练使用 `CCPD_322K_EXP_CUT_TRANS/`。 diff --git a/CCPD_322K_EXP_CUT/README.md b/CCPD_322K_EXP_CUT/README.md new file mode 100644 index 0000000..c77de83 --- /dev/null +++ b/CCPD_322K_EXP_CUT/README.md @@ -0,0 +1,10 @@ +# CCPD 322K YOLO 裁剪 + +| 项目 | 详情 | +|---|---| +| 来源 | `CCPD_322K/` 经过 YOLO 车牌检测裁剪(12%/18% 外扩) | +| 规模 | 321,122 张 | +| 生成脚本 | `scripts/pipeline_322k.py` 第二步 | +| 下游脚本 | 同脚本第三步 → `CCPD_322K_EXP_CUT_TRANS/` | + +> **以脚本为准**:此目录为管线中间产物,不直接用于训练。 diff --git a/CCPD_322K_EXP_CUT_TRANS/README.md b/CCPD_322K_EXP_CUT_TRANS/README.md new file mode 100644 index 0000000..32085cb --- /dev/null +++ b/CCPD_322K_EXP_CUT_TRANS/README.md @@ -0,0 +1,11 @@ +# CCPD 322K 合成 TRANS + +| 项目 | 详情 | +|---|---| +| 来源 | `CCPD_322K_EXP_CUT/` 经过透视变换(仿射 warp 到 192×64) | +| 规模 | 305,970 张(11 类分子目录) | +| 生成脚本 | `scripts/pipeline_322k.py` 第三步 | +| 标签格式 | 文件名后缀(`parse_plate_suffix` 解析) | +| 训练用途 | OCR 三源混合训练数据源之一(合成域) | + +> **以脚本为准**:标签从文件名后缀解析,生成脚本中类别标记格式为 `_{color}_{label}_trans.jpg` 或 `__{type}__{label}_trans.jpg`。 diff --git a/CCPD_REAL_TRANS/README.md b/CCPD_REAL_TRANS/README.md new file mode 100644 index 0000000..44a91ba --- /dev/null +++ b/CCPD_REAL_TRANS/README.md @@ -0,0 +1,11 @@ +# CCPD 真图 TRANS + +| 项目 | 详情 | +|---|---| +| 来源 | 由 `CCPD/` 通过 GT 顶点透视变换生成 | +| 规模 | 296,838 张 | +| 生成脚本 | `scripts/sample_ccpd_full.py` | +| 标签格式 | 从原始 CCPD stem 编码解析(`parse_ccpd_filename`),**不依赖文件名后缀** | +| 训练用途 | OCR 三源混合训练数据源之一 | + +> **以脚本为准**:文件名后缀中的标签可能不准确,实际标签由 `dataset.py` 中的 `parse_plate_suffix` 从原始 CCPD stem 解码。2026-06-23 已修复 AO→澳 误替换 bug。 diff --git a/CCPD_Random_Blue150K_Green100K/README.md b/CCPD_Random_Blue150K_Green100K/README.md new file mode 100644 index 0000000..52c1370 --- /dev/null +++ b/CCPD_Random_Blue150K_Green100K/README.md @@ -0,0 +1,9 @@ +# CCPD Random 蓝 150K + 绿 100K + +| 项目 | 详情 | +|---|---| +| 来源 | `gen_random_bg.py` 大规模随机背景合成 | +| 规模 | 250,002 张 | +| 训练用途 | 未在 OCR 训练中使用 | + +> **以脚本为准**:此数据集为生成脚本的大规模产物,当前模型训练未引用。 diff --git a/CCPD_Random_Blue5K_Green5K/README.md b/CCPD_Random_Blue5K_Green5K/README.md new file mode 100644 index 0000000..9af4f27 --- /dev/null +++ b/CCPD_Random_Blue5K_Green5K/README.md @@ -0,0 +1,10 @@ +# CCPD Random 蓝 5K + 绿 5K + +| 项目 | 详情 | +|---|---| +| 来源 | `gen_random_bg.py` 随机背景合成 | +| 规模 | 10,006 张(蓝 5K + 绿 5K) | +| 下游 | `CCPD_Random_Blue5K_Green5K_EXP_CUT/` → `..._TRANS/` | +| 训练用途 | 早期 OCR 蓝绿新能源实验,已被 322K 管线替代 | + +> **以脚本为准**:`gen_random_bg.py` 生成原图,后续 EXP_CUT 和 TRANS 步骤同上。 diff --git a/CCPD_Random_Blue5K_Green5K/build_exp_cut.py b/CCPD_Random_Blue5K_Green5K/build_exp_cut.py new file mode 100644 index 0000000..fa8cc03 --- /dev/null +++ b/CCPD_Random_Blue5K_Green5K/build_exp_cut.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +CCPD_Random_Blue5K_Green5K → YOLOv5t 推理 → bbox 拓展 → 裁剪 → 角点重映射 +========================================================================== +输出: CCPD_Random_Blue5K_Green5K_EXP_CUT/ + +用法: + python build_exp_cut.py --full # 全量处理 + python build_exp_cut.py --n 100 # 预览 N 张 + python build_exp_cut.py --dry-run # 预览 (每类2张, 图上绘制角点) +""" + +import argparse +import os +import re +import sys +import time +from pathlib import Path + +import cv2 +import numpy as np +import torch +# Save original imdecode before ultralytics patches it +_orig_imdecode = cv2.imdecode +from ultralytics import YOLO + +SCRIPT_DIR = Path(__file__).resolve().parent +PROJ_ROOT = SCRIPT_DIR.parents[1] +MODEL_PATH = PROJ_ROOT / "ICE_NNv2" / "yolov5t" / "output" / "1_CCPD_19K" / "weights" / "best.pt" +SRC_DIR = SCRIPT_DIR +DST_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_Random_Blue5K_Green5K_EXP_CUT" + +IMGSZ = 352 +CONF = 0.25 +CCPD_W, CCPD_H = 720, 1160 +EXPAND_NORMAL = 1.12 +EXPAND_SMALL = 1.18 +# bw < SMALL_BOX_W or bh < SMALL_BOX_H → small plate +SMALL_BOX_W = int(180 * CCPD_W / 1920) # ≈ 68 +SMALL_BOX_H = int(48 * CCPD_H / 1080) # ≈ 52 +MIN_PAD = 2 + +CORNER_RE = re.compile(r'-(\d+)&(\d+)_(\d+)&(\d+)_(\d+)&(\d+)_(\d+)&(\d+)-') + +# ---- Corner remap ---- +def parse_ccpd_corners(filename: str): + m = CORNER_RE.search(filename) + if not m: + return None + nums = [int(m.group(i)) for i in range(1, 9)] + return [(nums[0], nums[1]), (nums[2], nums[3]), (nums[4], nums[5]), (nums[6], nums[7])] + +def remap_corners_to_crop(corners, x1, y1, x2, y2): + crop_w, crop_h = x2 - x1, y2 - y1 + if crop_w <= 0 or crop_h <= 0: + return None + xs, ys = [c[0] for c in corners], [c[1] for c in corners] + poly_x1, poly_x2 = min(xs), max(xs) + poly_y1, poly_y2 = min(ys), max(ys) + inter_w = max(0, min(poly_x2, x2) - max(poly_x1, x1)) + inter_h = max(0, min(poly_y2, y2) - max(poly_y1, y1)) + poly_area = (poly_x2 - poly_x1) * (poly_y2 - poly_y1) + if poly_area <= 0 or (inter_w * inter_h) / poly_area <= 0.25: + return None + new = [] + for px, py in corners: + nx = max(0, min(crop_w - 1, int(round(px - x1)))) + ny = max(0, min(crop_h - 1, int(round(py - y1)))) + new.append((nx, ny)) + return new + +def replace_corners_in_filename(filename: str, new_corners): + seg = "-{0}&{1}_{2}&{3}_{4}&{5}_{6}&{7}-".format( + new_corners[0][0], new_corners[0][1], + new_corners[1][0], new_corners[1][1], + new_corners[2][0], new_corners[2][1], + new_corners[3][0], new_corners[3][1], + ) + return CORNER_RE.sub(seg, filename, count=1) + +# ---- Unicode-safe I/O ---- +def _imread(path: str): + with open(path, 'rb') as f: + data = f.read() + if not data: + raise ValueError(f"Empty file: {path}") + return _orig_imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) + +def _imwrite(path: str, img: np.ndarray, params=None): + ext = os.path.splitext(path)[1] or '.jpg' + ok, buf = cv2.imencode(ext, img, params or []) + if ok: + with open(path, 'wb') as f: + f.write(buf.tobytes()) + return ok + +# ---- Single image ---- +def process_one(model, img_path: Path, out_img_path: Path, draw: bool = False): + img = _imread(str(img_path)) + if img is None: + return "miss" + orig_h, orig_w = img.shape[:2] + results = model.predict(str(img_path), imgsz=IMGSZ, verbose=False, conf=CONF, max_det=5) + boxes = results[0].boxes + if boxes is None or len(boxes) == 0: + return "miss" + xywh = boxes.xywh.cpu().numpy() + if len(xywh) > 1: + return "multi" + cx, cy, bw, bh = xywh[0] + expand = EXPAND_SMALL if (bw < SMALL_BOX_W or bh < SMALL_BOX_H) else EXPAND_NORMAL + pad_x = max(MIN_PAD, int(round(bw * (expand - 1.0) / 2.0))) + pad_y = max(MIN_PAD, int(round(bh * (expand - 1.0) / 2.0))) + x1 = max(int(cx - bw/2) - pad_x, 0) + y1 = max(int(cy - bh/2) - pad_y, 0) + x2 = min(int(cx + bw/2) + pad_x + 1, orig_w) + y2 = min(int(cy + bh/2) + pad_y + 1, orig_h) + if x2 - x1 <= 0 or y2 - y1 <= 0: + return "miss" + crop = img[y1:y2, x1:x2] + stem = img_path.stem + corners = parse_ccpd_corners(stem) + remapped = None + if corners is not None: + remapped = remap_corners_to_crop(corners, x1, y1, x2, y2) + new_stem = replace_corners_in_filename(stem, remapped) if remapped else stem + else: + new_stem = stem + if draw and remapped: + pts_arr = np.array(remapped, dtype=np.int32) + cv2.polylines(crop, [pts_arr], isClosed=True, color=(0,255,0), thickness=2) + for k, (px, py) in enumerate(remapped): + cv2.circle(crop, (px, py), 3, (0,0,255), -1) + out_img_path.parent.mkdir(parents=True, exist_ok=True) + _imwrite(str(out_img_path.with_stem(new_stem)), crop, [cv2.IMWRITE_JPEG_QUALITY, 92]) + return "cropped" if remapped else "cropped_nocorners" + +# ---- Main ---- +def main(): + parser = argparse.ArgumentParser() + g = parser.add_mutually_exclusive_group(required=True) + g.add_argument('--full', action='store_true') + g.add_argument('--n', type=int) + g.add_argument('--dry-run', action='store_true') + args = parser.parse_args() + + print(f"Model: {MODEL_PATH}") + device = 'cuda' if torch.cuda.is_available() else 'cpu' + model = YOLO(str(MODEL_PATH)) + model.to(device) + print(f"Device: {device}\n") + + classes = ['blue', 'green'] + draw = bool(args.dry_run) + + for cls in classes: + src_cls = SRC_DIR / cls + dst_cls = DST_DIR / cls + dst_cls.mkdir(parents=True, exist_ok=True) + images = sorted(src_cls.glob('*.jpg')) + if not images: + print(f"[WARN] {cls}: no images") + continue + + n = 2 if args.dry_run else (args.n or len(images)) + images = images[:n] + print(f"[{cls}] {len(images)} images...", flush=True) + + t0 = time.perf_counter() + stats = {'cropped': 0, 'cropped_nocorners': 0, 'miss': 0, 'multi': 0} + for img_path in images: + out = dst_cls / img_path.name + try: + r = process_one(model, img_path, out, draw=draw) + stats[r] = stats.get(r, 0) + 1 + except Exception as e: + print(f"\n [ERROR] {img_path.name}: {e}", flush=True) + stats['miss'] = stats.get('miss', 0) + 1 + elapsed = time.perf_counter() - t0 + + total = sum(stats.values()) + ok = stats['cropped'] + stats['cropped_nocorners'] + print(f" → ok:{ok}/{total} miss:{stats['miss']} multi:{stats['multi']} " + f"({elapsed:.1f}s, {total/elapsed:.1f} img/s)") + print(f"\nDone. Output: {DST_DIR}") + +if __name__ == '__main__': + main() diff --git a/CCPD_Random_Blue5K_Green5K/build_exp_cut_trans.py b/CCPD_Random_Blue5K_Green5K/build_exp_cut_trans.py new file mode 100644 index 0000000..fc18d88 --- /dev/null +++ b/CCPD_Random_Blue5K_Green5K/build_exp_cut_trans.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +""" +CCPD_Random_Blue5K_Green5K_EXP_CUT → ICE-COR 角点推理 → 中心外扩 → 透视变换 +================================================================================== +输出: CCPD_Random_Blue5K_Green5K_EXP_CUT_TRANS/ (192×64, 文件名追加 _trans) + +用法: + python build_exp_cut_trans.py --full + python build_exp_cut_trans.py --n 100 + python build_exp_cut_trans.py --dry-run +""" + +import argparse +import os +import sys +import time +from pathlib import Path + +import cv2 +import numpy as np +import torch + +SCRIPT_DIR = Path(__file__).resolve().parent +PROJ_ROOT = SCRIPT_DIR.parents[1] +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_Random_Blue5K_Green5K_EXP_CUT" +DST_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_Random_Blue5K_Green5K_EXP_CUT_TRANS" + +OUT_W, OUT_H = 192, 64 +PAD_X = 0.08 +PAD_Y = 0.05 + +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 + +# ---- Unicode-safe I/O ---- +def _imread(path: str): + with open(path, 'rb') as f: + data = f.read() + return cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR) + +def _imwrite(path: str, img: np.ndarray, params=None): + ext = os.path.splitext(path)[1] or '.jpg' + ok, buf = cv2.imencode(ext, img, params or []) + if ok: + with open(path, 'wb') as f: + f.write(buf.tobytes()) + return ok + +# ---- Single image ---- +def process_one(model, device, img_path: Path, out_img_path: Path, draw: bool = False): + img = _imread(str(img_path)) + if img is None: + return "skip" + H, W = img.shape[:2] + try: + corners = predict_plate_corners(img, model, device) + if corners is None: + return "skip" + except Exception: + return "skip" + try: + ordered = order_corners(corners) + except Exception: + return "skip" + + # center expand + cx = ordered.mean(axis=0) + expanded = ordered + (ordered - cx) * np.array([PAD_X, PAD_Y]) + expanded[:, 0] = np.clip(expanded[:, 0], 0, W - 1) + expanded[:, 1] = np.clip(expanded[:, 1], 0, H - 1) + + src = expanded.astype(np.float32) + dst = np.float32([[0, 0], [OUT_W-1, 0], [OUT_W-1, OUT_H-1], [0, OUT_H-1]]) + M = cv2.getPerspectiveTransform(src, dst) + warped = cv2.warpPerspective(img, M, (OUT_W, OUT_H), + flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE) + + if draw: + for k, (px, py) in enumerate(expanded): + cv2.circle(warped, tuple(dst[k].astype(int)), 3, (0, 255, 0), -1) + cv2.polylines(warped, [dst.astype(np.int32)], isClosed=True, color=(0, 255, 0), thickness=1) + + out_stem = img_path.stem + "_trans" + out_img_path.with_stem(out_stem).parent.mkdir(parents=True, exist_ok=True) + _imwrite(str(out_img_path.with_stem(out_stem)), warped, [cv2.IMWRITE_JPEG_QUALITY, 92]) + return "ok" + +# ---- Main ---- +def main(): + parser = argparse.ArgumentParser() + g = parser.add_mutually_exclusive_group(required=True) + g.add_argument('--full', action='store_true') + g.add_argument('--n', type=int) + g.add_argument('--dry-run', action='store_true') + args = parser.parse_args() + + print(f"Model: {MODEL_PATH}") + device = 'cuda' if torch.cuda.is_available() else 'cpu' + model, device = load_corner_model(str(MODEL_PATH), device) + model.eval() + print(f"Device: {device}\n") + + classes = ['blue', 'green'] + draw = bool(args.dry_run) + + for cls in classes: + src_cls = SRC_DIR / cls + dst_cls = DST_DIR / cls + dst_cls.mkdir(parents=True, exist_ok=True) + images = sorted(src_cls.glob('*.jpg')) + if not images: + print(f"[WARN] {cls}: no images") + continue + + n = 2 if args.dry_run else (args.n or len(images)) + images = images[:n] + print(f"[{cls}] {len(images)} images...", flush=True) + + t0 = time.perf_counter() + ok = skip = 0 + for img_path in images: + out = dst_cls / img_path.name + r = process_one(model, device, img_path, out, draw=draw) + if r == "ok": ok += 1 + else: skip += 1 + elapsed = time.perf_counter() - t0 + total = ok + skip + print(f" → ok:{ok}/{total} skip:{skip} ({elapsed:.1f}s, {total/elapsed:.1f} img/s)") + print(f"\nDone. Output: {DST_DIR}") + +if __name__ == '__main__': + main() diff --git a/CCPD_SPE/README.md b/CCPD_SPE/README.md index e93d05a..b22f004 100644 --- a/CCPD_SPE/README.md +++ b/CCPD_SPE/README.md @@ -1,91 +1,10 @@ -# CCPD_SPE — 特殊车牌数据集 +# CCPD SPE 特殊车牌 -基于 CCPD 真实街景照片,将普通蓝牌替换为 9 种特殊车牌。 +| 项目 | 详情 | +|---|---| +| 来源 | `chinese_license_plate_generator` 合成特殊车牌(警/学/港澳/使/领/挂 等) | +| 规模 | 72,132 张 | +| 生成脚本 | `scripts/rebuild_spe.py`(及 `rebuild_spe.ps1`) | +| 训练用途 | 早期 OCR 实验,322K 管线已包含部分 SPE 类别 | -## 目录结构 - -``` -CCPD_SPE/ -├── ccpd_base/ -│ ├── police/ # 警车 (白底, 末位JING) -│ ├── army/ # 军车 (白底) -│ ├── gangao/ # 港澳入出境 (黑底, GANG=港 / AO=澳) -│ ├── shiguan_old/ # 使馆旧版 (黑底, SHI红字) -│ ├── shiguan_new/ # 使馆新版 (黑底) -│ ├── lingguan_old/ # 领馆旧版 (黑底, LING红字) -│ ├── lingguan_new/ # 领馆新版 (黑底) -│ ├── coach/ # 教练车 (黄底, 末位XUE) -│ └── new_energy/ # 新能源 (黄绿双拼, 8位) -├── ccpd_blur/ -├── ccpd_challenge/ -├── ccpd_fn/ -├── ccpd_rotate/ -├── ccpd_tilt/ -└── ccpd_weather/ -``` - -## 文件命名规则 - -``` -{CCPD原始文件名}__{特殊车牌类型}__{车牌号拼音}.jpg -``` - -### 字段说明 - -| 段 | 示例 | 说明 | -|----|------|------| -| CCPD原始文件名 | `003402777-94_83-366&419_...-123-6` | 保留 CCPD 全部几何标注(角点/bbox/亮度/模糊度) | -| 特殊车牌类型 | `gangao` | 9 种类型之一 | -| 车牌号拼音 | `B690TGANG` | 原车牌号,中文转拼音标注 | - -### 拼音标注表 - -| 中文 | 拼音 | 含义 | -|------|------|------| -| 港 | GANG | 港澳入出境—香港 | -| 澳 | AO | 港澳入出境—澳门 | -| 警 | JING | 警车 | -| 使 | SHI | 使馆 | -| 领 | LING | 领馆 | -| 学 | XUE | 教练车 | - -### 类型对照 - -| 目录名 | 底板色 | 说明 | -|--------|--------|------| -| `police` | 白底 | 警用车牌,末位"警"红色 | -| `army` | 白底 | 军车/武警 | -| `gangao` | 黑底 | 港澳入出境,粤Z·XXXX港/澳 | -| `shiguan_old` | 黑底 | 使馆旧版,"使"在首位红色 | -| `shiguan_new` | 黑底 | 使馆新版,XXX·XXX使 | -| `lingguan_old` | 黑底 | 领馆旧版,省A·XXXX领 | -| `lingguan_new` | 黑底 | 领馆新版,省XXX·XX领 | -| `coach` | 黄底 | 教练车,末位"学" | -| `new_energy` | 黄绿双拼 | 大型新能源,8位 | - -### 使用示例 - -```python -# 按类型筛选 -gangao_files = [f for f in glob('CCPD_SPE/*/gangao/*.jpg')] - -# 按港/澳筛选 -hk_files = [f for f in gangao_files if 'GANG' in f] -mo_files = [f for f in gangao_files if 'AO' in f and 'GANG' not in f] - -# 解析CCPD角点 -# CCPD文件名格式: area_tilt_bbox_corners_chars_brightness_blurriness -# corners段: TL_x&TL_y_TR_x&TR_y_BR_x&BR_y_BL_x&BL_y -``` - -## 生成参数 - -| 参数 | 值 | -|------|-----| -| 引擎 | C++ / OpenCV 4.10 | -| 线程 | 24 | -| 速度 | ~53 张/秒 | -| 含退化模拟 | JPEG+降采样+模糊 | -| 羽化 | 内侧 erode+GaussianBlur | -| 色温匹配 | 参考原车牌白字色温 | -| 噪声匹配 | 原车牌区域噪声σ估计 | +> **以脚本为准**:文件名编码包含 `__{type}__` 双下划线标记,类型在 `dataset.py` 的 `known_markers` 中定义。 diff --git a/CCPD_SPE_GEN/cpp_pipeline/build.ps1 b/CCPD_SPE_GEN/cpp_pipeline/build.ps1 index 2990dac..dfb0a5f 100644 --- a/CCPD_SPE_GEN/cpp_pipeline/build.ps1 +++ b/CCPD_SPE_GEN/cpp_pipeline/build.ps1 @@ -39,4 +39,4 @@ Write-Host "可执行文件: $buildDir\Release\plate_swap.exe" # 使用示例 Write-Host "" Write-Host "用法示例:" -Write-Host " .\build\Release\plate_swap.exe --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type police --plate-pool ./output/plate_pool --num 500 --workers 32 --fast" +Write-Host " .\build\Release\plate_swap.exe --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type police --plate-pool ./output/plate_pool --num 500 --workers 32" diff --git a/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py b/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py index ec5702e..af928a8 100644 --- a/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py +++ b/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py @@ -49,6 +49,8 @@ from plate_number import ( # 类型配置 PLATE_TYPES = [ + ('blue', 'blue', generate_plate_number_blue), + ('green', 'green_car', lambda: generate_plate_number_blue(length=8)), ('police', 'white', generate_plate_number_white), ('army', 'white_army', generate_plate_number_army), ('gangao', 'black', generate_plate_number_black_gangao), diff --git a/CCPD_SPE_GEN/cpp_pipeline/main.cpp b/CCPD_SPE_GEN/cpp_pipeline/main.cpp index b73e3ae..672ac82 100644 --- a/CCPD_SPE_GEN/cpp_pipeline/main.cpp +++ b/CCPD_SPE_GEN/cpp_pipeline/main.cpp @@ -18,7 +18,6 @@ struct Config { string plate_pool_dir; int num_images = 500; unsigned workers = thread::hardware_concurrency(); - bool fast = false; int seed = 42; int h_shift = 0, s_scale = 100, v_scale = 100; }; @@ -50,7 +49,6 @@ Config parse_args(int argc, char* argv[]) { else if (arg == "--num" && i+1 < argc) cfg.num_images = stoi(argv[++i]); else if (arg == "--workers" && i+1 < argc) cfg.workers = stoi(argv[++i]); else if (arg == "--seed" && i+1 < argc) cfg.seed = stoi(argv[++i]); - else if (arg == "--fast") cfg.fast = true; else if (arg == "--h-shift" && i+1 < argc) cfg.h_shift = stoi(argv[++i]); else if (arg == "--s-scale" && i+1 < argc) cfg.s_scale = stoi(argv[++i]); else if (arg == "--v-scale" && i+1 < argc) cfg.v_scale = stoi(argv[++i]); @@ -103,7 +101,6 @@ int main(int argc, char* argv[]) { cerr << " --plate-pool