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

+79 -11
View File
@@ -1,32 +1,100 @@
# ============================================================
# Python # Python
# ============================================================
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
# ============================================================
# IDE / Editor # IDE / Editor
# ============================================================
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
*.swo *.swo
*~ *~
# ============================================================
# OS # OS
# ============================================================
.DS_Store .DS_Store
Thumbs.db Thumbs.db
Desktop.ini Desktop.ini
# Logs # ============================================================
# Logs & Cache
# ============================================================
*.log *.log
# Jupyter
.ipynb_checkpoints/ .ipynb_checkpoints/
ccpd_combined_5k5k_spe/ # ============================================================
output/ # CCPD 原始真图 (~353K, ~23GB)
CCPD_SPE/*/ # ============================================================
build/
# CCPD raw data (keep on disk, never commit)
CCPD/CCPD2019/ CCPD/CCPD2019/
CCPD/CCPD2020/ CCPD/CCPD2020/
CCPD/*.7z 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/
+11
View File
@@ -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/` 子目录。
-7
View File
@@ -1,7 +0,0 @@
# CCPD raw data (download from official source)
CCPD2019/
CCPD2020/
# 7z archives (optional, via git LFS)
*.7z
+13 -15
View File
@@ -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/ CCPD/
├── CCPD2019/ # 原始 CCPD2019(本地,不入库) ├── CCPD2019/ # ccpd_base / blur / challenge / fn / rotate / tilt / weather / np
│ ├── ccpd_base/ # 199,996 张 └── CCPD2020/ # 同上结构
│ ├── ccpd_blur/ # 20,611 张 ```
│ ├── ccpd_challenge/ # 50,003 张
│ ├── ccpd_db/ # 10,132 张
│ ├── ccpd_fn/ # 20,967 张
│ ├── ccpd_np/ # 3,036 张
│ ├── ccpd_rotate/ # 10,053 张
│ ├── ccpd_tilt/ # 30,216 张 │ ├── ccpd_tilt/ # 30,216 张
│ ├── ccpd_weather/ # 9,999 张 │ ├── ccpd_weather/ # 9,999 张
│ └── splits/ # 标注文件(本地,不入库) │ └── splits/ # 标注文件(本地,不入库)
+2
View File
@@ -0,0 +1,2 @@
test/
train/
+75
View File
@@ -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
```
+195
View File
@@ -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)
+2
View File
@@ -0,0 +1,2 @@
test/
train/
+69
View File
@@ -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 # 全量
```
+422
View File
@@ -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()
+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()
+10
View File
@@ -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/`。
+10
View File
@@ -0,0 +1,10 @@
# CCPD 322K YOLO 裁剪
| 项目 | 详情 |
|---|---|
| 来源 | `CCPD_322K/` 经过 YOLO 车牌检测裁剪(12%/18% 外扩) |
| 规模 | 321,122 张 |
| 生成脚本 | `scripts/pipeline_322k.py` 第二步 |
| 下游脚本 | 同脚本第三步 → `CCPD_322K_EXP_CUT_TRANS/` |
> **以脚本为准**:此目录为管线中间产物,不直接用于训练。
+11
View File
@@ -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`。
+11
View File
@@ -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。
+9
View File
@@ -0,0 +1,9 @@
# CCPD Random 蓝 150K + 绿 100K
| 项目 | 详情 |
|---|---|
| 来源 | `gen_random_bg.py` 大规模随机背景合成 |
| 规模 | 250,002 张 |
| 训练用途 | 未在 OCR 训练中使用 |
> **以脚本为准**:此数据集为生成脚本的大规模产物,当前模型训练未引用。
+10
View File
@@ -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 步骤同上。
+188
View File
@@ -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()
@@ -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()
+8 -89
View File
@@ -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 类别 |
## 目录结构 > **以脚本为准**:文件名编码包含 `__{type}__` 双下划线标记,类型在 `dataset.py` 的 `known_markers` 中定义。
```
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 |
| 色温匹配 | 参考原车牌白字色温 |
| 噪声匹配 | 原车牌区域噪声σ估计 |
+1 -1
View File
@@ -39,4 +39,4 @@ Write-Host "可执行文件: $buildDir\Release\plate_swap.exe"
# 使用示例 # 使用示例
Write-Host "" Write-Host ""
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"
@@ -49,6 +49,8 @@ from plate_number import (
# 类型配置 # 类型配置
PLATE_TYPES = [ PLATE_TYPES = [
('blue', 'blue', generate_plate_number_blue),
('green', 'green_car', lambda: generate_plate_number_blue(length=8)),
('police', 'white', generate_plate_number_white), ('police', 'white', generate_plate_number_white),
('army', 'white_army', generate_plate_number_army), ('army', 'white_army', generate_plate_number_army),
('gangao', 'black', generate_plate_number_black_gangao), ('gangao', 'black', generate_plate_number_black_gangao),
-5
View File
@@ -18,7 +18,6 @@ struct Config {
string plate_pool_dir; string plate_pool_dir;
int num_images = 500; int num_images = 500;
unsigned workers = thread::hardware_concurrency(); unsigned workers = thread::hardware_concurrency();
bool fast = false;
int seed = 42; int seed = 42;
int h_shift = 0, s_scale = 100, v_scale = 100; 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 == "--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 == "--workers" && i+1 < argc) cfg.workers = stoi(argv[++i]);
else if (arg == "--seed" && i+1 < argc) cfg.seed = 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 == "--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 == "--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]); 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 <dir> Plate pool directory (required)" << endl; cerr << " --plate-pool <dir> Plate pool directory (required)" << endl;
cerr << " --num <N> Number of images (default 500)" << endl; cerr << " --num <N> Number of images (default 500)" << endl;
cerr << " --workers <N> Thread count (default all cores)" << endl; cerr << " --workers <N> Thread count (default all cores)" << endl;
cerr << " --fast Fast mode (skip degradation)" << endl;
return 1; return 1;
} }
@@ -111,7 +108,6 @@ int main(int argc, char* argv[]) {
cout << "Type: " << cfg.plate_type << endl; cout << "Type: " << cfg.plate_type << endl;
cout << "Threads: " << cfg.workers << endl; cout << "Threads: " << cfg.workers << endl;
cout << "Target: " << cfg.num_images << " images" << endl; cout << "Target: " << cfg.num_images << " images" << endl;
cout << "Fast: " << (cfg.fast ? "yes" : "no") << endl;
if (cfg.plate_pool_dir.empty()) { if (cfg.plate_pool_dir.empty()) {
cerr << "[ERROR] --plate-pool is required" << endl; cerr << "[ERROR] --plate-pool is required" << endl;
@@ -153,7 +149,6 @@ int main(int argc, char* argv[]) {
SwapConfig swap_cfg; SwapConfig swap_cfg;
swap_cfg.plate_type = cfg.plate_type; swap_cfg.plate_type = cfg.plate_type;
swap_cfg.output_dir = cfg.output_dir; swap_cfg.output_dir = cfg.output_dir;
swap_cfg.fast_mode = cfg.fast;
swap_cfg.h_shift = cfg.h_shift; swap_cfg.h_shift = cfg.h_shift;
swap_cfg.s_scale = cfg.s_scale; swap_cfg.s_scale = cfg.s_scale;
swap_cfg.v_scale = cfg.v_scale; swap_cfg.v_scale = cfg.v_scale;
+9 -28
View File
@@ -184,6 +184,8 @@ static string bg_color_of(const string& plate_type) {
if (plate_type == "lingguan_old" || plate_type == "lingguan_new") return "black"; if (plate_type == "lingguan_old" || plate_type == "lingguan_new") return "black";
if (plate_type == "coach") return "yellow"; if (plate_type == "coach") return "yellow";
if (plate_type == "new_energy") return "green_truck"; if (plate_type == "new_energy") return "green_truck";
if (plate_type == "blue") return "blue";
if (plate_type == "green") return "green_car";
return "white"; return "white";
} }
@@ -322,7 +324,7 @@ Mat match_plate_color(const Mat& synthetic, const Mat& orig_roi,
// 白/绿牌 // 白/绿牌
Scalar color_cast(white_ref[0]/ref_mean, white_ref[1]/ref_mean, white_ref[2]/ref_mean); Scalar color_cast(white_ref[0]/ref_mean, white_ref[1]/ref_mean, white_ref[2]/ref_mean);
float strength = (bg_color == "green_truck") ? 1.5f : 1.0f; float strength = (bg_color == "green_truck" || bg_color == "green_car") ? 1.5f : 1.0f;
Scalar amplified(1.0f + (color_cast[0]-1.0f)*strength, 1.0f+(color_cast[1]-1.0f)*strength, 1.0f+(color_cast[2]-1.0f)*strength); Scalar amplified(1.0f + (color_cast[0]-1.0f)*strength, 1.0f+(color_cast[1]-1.0f)*strength, 1.0f+(color_cast[2]-1.0f)*strength);
amplified[0] = std::clamp((float)amplified[0], 0.85f, 1.15f); amplified[0] = std::clamp((float)amplified[0], 0.85f, 1.15f);
amplified[1] = std::clamp((float)amplified[1], 0.85f, 1.15f); amplified[1] = std::clamp((float)amplified[1], 0.85f, 1.15f);
@@ -359,7 +361,8 @@ Mat fix_bg_color(const Mat& plate, const string& bg_color,
int h_lo = 0, h_hi = 180; int h_lo = 0, h_hi = 180;
if (bg_color == "yellow") { h_lo = 15; h_hi = 50; } if (bg_color == "yellow") { h_lo = 15; h_hi = 50; }
else if (bg_color == "green_truck") { h_lo = 40; h_hi = 85; } else if (bg_color == "green_truck" || bg_color == "green_car") { h_lo = 40; h_hi = 85; }
else if (bg_color == "blue") { h_lo = 90; h_hi = 130; }
Mat hsv; Mat hsv;
cvtColor(plate, hsv, COLOR_BGR2HSV); cvtColor(plate, hsv, COLOR_BGR2HSV);
@@ -629,7 +632,7 @@ bool process_single(const string& img_path, const SwapConfig& cfg,
if (current_w > 10 && abs(target_w - current_w) > 10) { if (current_w > 10 && abs(target_w - current_w) > 10) {
float l_ratio = target_w / current_w; float l_ratio = target_w / current_w;
float min_c = 0.5f, max_c = 1.1f; float min_c = 0.5f, max_c = 1.1f;
if (bg_color == "green_truck" || bg_color == "yellow") { min_c = 0.4f; max_c = 1.0f; } if (bg_color == "green_truck" || bg_color == "green_car" || bg_color == "yellow") { min_c = 0.4f; max_c = 1.0f; }
else if (bg_color == "black" || bg_color == "black_shi") { min_c = 0.3f; max_c = 1.2f; } else if (bg_color == "black" || bg_color == "black_shi") { min_c = 0.3f; max_c = 1.2f; }
l_ratio = std::clamp(l_ratio, min_c, max_c); l_ratio = std::clamp(l_ratio, min_c, max_c);
@@ -650,38 +653,16 @@ bool process_single(const string& img_path, const SwapConfig& cfg,
} }
// 8. Degradation // 8. Degradation
if (!cfg.fast_mode) matched = degrade_plate(matched);
matched = degrade_plate(matched);
// 9-11. Warp + blend + noise // 9-11. Warp + blend + noise
auto [warped, mask] = warp_plate_back(matched, meta.corners, Size(w_img, h_img)); auto [warped, mask] = warp_plate_back(matched, meta.corners, Size(w_img, h_img));
Mat blended = feather_blend(img_orig, warped, mask); Mat blended = feather_blend(img_orig, warped, mask);
Mat final = match_noise(blended, img_orig, mask); Mat final = match_noise(blended, img_orig, mask);
// 12. Save with pinyin tags // 12. Save with original Chinese plate number (imwrite_unicode supports UTF-8)
string stem = fp.stem().string(); string stem = fp.stem().string();
// Convert Chinese plate markers to ASCII tags string out_name = stem + "__" + cfg.plate_type + "__" + plate_number + ".jpg";
string tag = plate_number;
// Manual pinyin mapping for common plate characters (UTF-8 byte sequences)
// 港=E6B8AF 澳=E6BEB3 警=E8ADA6 使=E4BDBF 领=E9A286 学=E5ADA6
struct { const char* utf8; const char* py; } map[] = {
{"\xE6\xB8\xAF", "GANG"}, {"\xE6\xBE\xB3", "AO"},
{"\xE8\xAD\xA6", "JING"}, {"\xE4\xBD\xBF", "SHI"},
{"\xE9\xA2\x86", "LING"}, {"\xE5\xAD\xA6", "XUE"},
};
for (auto& m : map) {
size_t pos = 0;
while ((pos = tag.find(m.utf8, pos)) != string::npos) {
tag.replace(pos, 3, m.py);
pos += strlen(m.py);
}
}
// Remove any remaining non-ASCII
string clean;
for (char c : tag) {
if ((unsigned char)c < 128) clean += c;
}
string out_name = stem + "__" + cfg.plate_type + "__" + clean + ".jpg";
string ccpd_subdir = fp.parent_path().filename().string(); string ccpd_subdir = fp.parent_path().filename().string();
fs::path out_dir = fs::path(cfg.output_dir) / ccpd_subdir / cfg.plate_type; fs::path out_dir = fs::path(cfg.output_dir) / ccpd_subdir / cfg.plate_type;
fs::create_directories(out_dir); fs::create_directories(out_dir);
-1
View File
@@ -34,7 +34,6 @@ struct SwapConfig {
std::string plate_type; std::string plate_type;
std::string output_dir; std::string output_dir;
int h_shift = 0, s_scale = 100, v_scale = 100; int h_shift = 0, s_scale = 100, v_scale = 100;
bool fast_mode = false;
}; };
// 单张处理 (pre-parsed meta 避免重复解析) // 单张处理 (pre-parsed meta 避免重复解析)
+26 -15
View File
@@ -45,8 +45,10 @@ from plate_number import (
# ── 车牌类型 ── # ── 车牌类型 ──
PLATE_TYPES = [ PLATE_TYPES = [
('blue', '蓝牌', generate_plate_number_blue),
('green_car', '小新能源', lambda: generate_plate_number_blue(length=8)),
('yellow', '教练车', generate_plate_number_yellow_xue), ('yellow', '教练车', generate_plate_number_yellow_xue),
('green_truck','新能源', lambda: generate_plate_number_blue(length=8)), ('green_truck','大新能源', lambda: generate_plate_number_blue(length=8)),
('white', '警车', generate_plate_number_white), ('white', '警车', generate_plate_number_white),
('white_army', '军车', generate_plate_number_army), ('white_army', '军车', generate_plate_number_army),
('black', '港澳', generate_plate_number_black_gangao), ('black', '港澳', generate_plate_number_black_gangao),
@@ -113,7 +115,8 @@ def adjust_plate(plate_bgr, bg_color, h_shift, s_scale, v_scale):
return plate_bgr.copy() return plate_bgr.copy()
hue_ranges = { hue_ranges = {
'yellow': (15, 50), 'green_truck': (40, 85), 'yellow': (15, 50), 'green_truck': (40, 85), 'green_car': (40, 85),
'blue': (90, 130),
'white': (0, 180), 'white_army': (0, 180), 'white': (0, 180), 'white_army': (0, 180),
'black': (0, 180), 'black_shi': (0, 180), 'black': (0, 180), 'black_shi': (0, 180),
} }
@@ -182,7 +185,7 @@ def update_display(*_args):
(w + 10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 200), 1) (w + 10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 200), 1)
# 类型标签 # 类型标签
cv2.putText(display, f"[{current_type_idx+1}/9] {label}", cv2.putText(display, f"[{current_type_idx+1}/{len(PLATE_TYPES)}] {label}",
(10, display.shape[0] - 10), (10, display.shape[0] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (180, 180, 180), 1) cv2.FONT_HERSHEY_SIMPLEX, 0.5, (180, 180, 180), 1)
@@ -206,7 +209,8 @@ def save_params():
'# Format: (h_shift, s_scale_percent, v_scale_percent)', '# Format: (h_shift, s_scale_percent, v_scale_percent)',
'', '',
'PLATE_COLOR_PROFILE = {'] 'PLATE_COLOR_PROFILE = {']
for bg_color in ['yellow', 'green_truck', 'white', 'white_army', 'black', 'black_shi']: for bg_color in ['yellow', 'green_truck', 'green_car', 'blue',
'white', 'white_army', 'black', 'black_shi']:
p = params.get(bg_color, {'h_shift': 0, 's_scale': 100, 'v_scale': 100}) p = params.get(bg_color, {'h_shift': 0, 's_scale': 100, 'v_scale': 100})
lines.append(f" '{bg_color}': ({p['h_shift']}, {p['s_scale']}, {p['v_scale']}),") lines.append(f" '{bg_color}': ({p['h_shift']}, {p['s_scale']}, {p['v_scale']}),")
lines.append('}') lines.append('}')
@@ -219,19 +223,26 @@ def save_params():
def on_key(key): def on_key(key):
global current_type_idx, synthetic_plate global current_type_idx, synthetic_plate
# keys: 1-9, 0(=10), -(=11) — 切换车牌类型
idx = -1
if ord('1') <= key <= ord('9'): if ord('1') <= key <= ord('9'):
idx = key - ord('1') idx = key - ord('1')
if idx < len(PLATE_TYPES): elif key == ord('0'):
current_type_idx = idx idx = 9
# 切换类型时复位 trackbar 到该类型已保存的参数 elif key == ord('-'):
bg_color = PLATE_TYPES[current_type_idx][0] idx = 10
p = load_params(bg_color)
cv2.setTrackbarPos('H offset', 'Color Tuner', p['h_shift'] + 30) if 0 <= idx < len(PLATE_TYPES):
cv2.setTrackbarPos('S scale', 'Color Tuner', p['s_scale']) current_type_idx = idx
cv2.setTrackbarPos('V scale', 'Color Tuner', p['v_scale']) # 切换类型时复位 trackbar 到该类型已保存的参数
# 生成新车牌 bg_color = PLATE_TYPES[current_type_idx][0]
synthetic_plate, _, _ = generate_plate(current_type_idx) p = load_params(bg_color)
update_display() cv2.setTrackbarPos('H offset', 'Color Tuner', p['h_shift'] + 30)
cv2.setTrackbarPos('S scale', 'Color Tuner', p['s_scale'])
cv2.setTrackbarPos('V scale', 'Color Tuner', p['v_scale'])
# 生成新车牌
synthetic_plate, _, _ = generate_plate(current_type_idx)
update_display()
elif key == ord('r') or key == ord('R'): elif key == ord('r') or key == ord('R'):
bg_color = PLATE_TYPES[current_type_idx][0] bg_color = PLATE_TYPES[current_type_idx][0]
params[bg_color] = {'h_shift': 0, 's_scale': 100, 'v_scale': 100} params[bg_color] = {'h_shift': 0, 's_scale': 100, 'v_scale': 100}
@@ -3,8 +3,10 @@
# Format: (h_shift, s_scale_percent, v_scale_percent) # Format: (h_shift, s_scale_percent, v_scale_percent)
PLATE_COLOR_PROFILE = { PLATE_COLOR_PROFILE = {
'yellow': (-2, 150, 100), 'yellow': (0, 150, 150),
'green_truck': (0, 100, 100), 'green_truck': (0, 100, 150),
'green_car': (-4, 69, 92),
'blue': (-7, 150, 150),
'white': (0, 100, 100), 'white': (0, 100, 100),
'white_army': (0, 100, 100), 'white_army': (0, 100, 100),
'black': (0, 100, 100), 'black': (0, 100, 100),
+30 -31
View File
@@ -156,6 +156,16 @@ PLATE_TYPE_CONFIG = {
'generator': lambda: generate_plate_number_blue(length=8), 'generator': lambda: generate_plate_number_blue(length=8),
'description': '大型新能源' 'description': '大型新能源'
}, },
'blue': {
'bg_color': 'blue',
'generator': generate_plate_number_blue,
'description': '蓝牌小型汽车'
},
'green': {
'bg_color': 'green_car',
'generator': lambda: generate_plate_number_blue(length=8),
'description': '小型新能源'
},
} }
@@ -407,7 +417,7 @@ def match_plate_color(synthetic_plate: np.ndarray,
color_cast = white_ref / ref_mean color_cast = white_ref / ref_mean
strength_map = { strength_map = {
'white': 1.0, 'white_army': 1.0, 'white': 1.0, 'white_army': 1.0,
'green_truck': 1.5, 'blue': 1.0, 'green_car': 1.5, 'green_truck': 1.5,
} }
strength = strength_map.get(bg_color, 2.0) strength = strength_map.get(bg_color, 2.0)
@@ -492,7 +502,8 @@ def fix_plate_bg_color(plate_bgr: np.ndarray, bg_color: str) -> np.ndarray:
# 色相范围 # 色相范围
hue_ranges = { hue_ranges = {
'yellow': (15, 50), 'green_truck': (40, 85), 'yellow': (15, 50), 'green_truck': (40, 85), 'green_car': (40, 85),
'blue': (90, 130),
'white': (0, 180), 'white_army': (0, 180), 'white': (0, 180), 'white_army': (0, 180),
'black': (0, 180), 'black_shi': (0, 180), 'black': (0, 180), 'black_shi': (0, 180),
} }
@@ -741,11 +752,10 @@ def match_image_quality(result: np.ndarray,
_worker_pipeline = None _worker_pipeline = None
def _worker_init(plate_type, fast): def _worker_init(plate_type):
"""进程初始化:每个 worker 启动时调用一次(加载字体、底板缓存)""" """进程初始化:每个 worker 启动时调用一次(加载字体、底板缓存)"""
global _worker_pipeline global _worker_pipeline
_worker_pipeline = PlateSwapPipeline(plate_type=plate_type) _worker_pipeline = PlateSwapPipeline(plate_type=plate_type)
_worker_pipeline._fast_mode = fast
def _worker_process_single(args): def _worker_process_single(args):
@@ -791,9 +801,6 @@ class PlateSwapPipeline:
font_dir = str(_GENERATOR_DIR / 'font_model') font_dir = str(_GENERATOR_DIR / 'font_model')
self.generator = MultiPlateGenerator(plate_model_dir, font_dir) self.generator = MultiPlateGenerator(plate_model_dir, font_dir)
# 快速模式标志(多进程时由 _worker_process_single 设置)
self._fast_mode = False
def generate_plate_number(self) -> str: def generate_plate_number(self) -> str:
"""生成随机特殊车牌号码""" """生成随机特殊车牌号码"""
return self.number_generator() return self.number_generator()
@@ -864,7 +871,8 @@ class PlateSwapPipeline:
# 不同底板色用不同下限,防止白底变灰、绿牌变亮 # 不同底板色用不同下限,防止白底变灰、绿牌变亮
clip_cfg = { clip_cfg = {
'white': (0.5, 1.1), 'white_army': (0.5, 1.1), 'white': (0.5, 1.1), 'white_army': (0.5, 1.1),
'green_truck': (0.4, 1.0), 'blue': (0.5, 1.1),
'green_truck': (0.4, 1.0), 'green_car': (0.4, 1.0),
'yellow': (0.4, 1.0), 'yellow': (0.4, 1.0),
'black': (0.3, 1.2), 'black_shi': (0.3, 1.2), 'black': (0.3, 1.2), 'black_shi': (0.3, 1.2),
} }
@@ -879,16 +887,15 @@ class PlateSwapPipeline:
matched_plate = (matched_plate.astype(np.float32) * effective_ratio) matched_plate = (matched_plate.astype(np.float32) * effective_ratio)
matched_plate = np.clip(matched_plate, 0, 255).astype(np.uint8) matched_plate = np.clip(matched_plate, 0, 255).astype(np.uint8)
# ---- 8. 退化模拟(快速模式下跳过) ---- # ---- 8. 退化模拟 ----
if not self._fast_mode: # JPEG 极限压缩
# JPEG 极限压缩 matched_plate = jpeg_compress(matched_plate, quality=2)
matched_plate = jpeg_compress(matched_plate, quality=2) # 降采样→升采样 模拟马赛克/像素化
# 降采样→升采样 模拟马赛克/像素化 h_p, w_p = matched_plate.shape[:2]
h_p, w_p = matched_plate.shape[:2] small = cv2.resize(matched_plate, (w_p // 4, h_p // 4), interpolation=cv2.INTER_LINEAR)
small = cv2.resize(matched_plate, (w_p // 4, h_p // 4), interpolation=cv2.INTER_LINEAR) matched_plate = cv2.resize(small, (w_p, h_p), interpolation=cv2.INTER_NEAREST)
matched_plate = cv2.resize(small, (w_p, h_p), interpolation=cv2.INTER_NEAREST) # 较强模糊
# 较强模糊 matched_plate = cv2.GaussianBlur(matched_plate, (5, 5), 1.5)
matched_plate = cv2.GaussianBlur(matched_plate, (5, 5), 1.5)
# ---- 9. 透视变换回贴 ---- # ---- 9. 透视变换回贴 ----
warped, plate_mask = warp_plate_back( warped, plate_mask = warp_plate_back(
@@ -903,7 +910,7 @@ class PlateSwapPipeline:
# ---- 12. 保存 ---- # ---- 12. 保存 ----
basename = Path(filename).stem basename = Path(filename).stem
out_name = f"{basename}_{self.plate_type}_{new_plate_number}.jpg" out_name = f"{basename}__{self.plate_type}__{new_plate_number}.jpg"
out_path = os.path.join(output_dir, out_name) out_path = os.path.join(output_dir, out_name)
_imwrite_unicode(out_path, final, [cv2.IMWRITE_JPEG_QUALITY, 95]) _imwrite_unicode(out_path, final, [cv2.IMWRITE_JPEG_QUALITY, 95])
@@ -927,8 +934,7 @@ class PlateSwapPipeline:
num_images: int = 500, num_images: int = 500,
preview: bool = False, preview: bool = False,
seed: int = 42, seed: int = 42,
workers: int = 1, workers: int = 1):
fast: bool = False):
""" """
批量处理 CCPD 图片目录。 批量处理 CCPD 图片目录。
@@ -939,7 +945,6 @@ class PlateSwapPipeline:
preview: 是否保存调试中间结果 preview: 是否保存调试中间结果
seed: 随机种子 seed: 随机种子
workers: 并行进程数 (1=单进程, >1=多进程) workers: 并行进程数 (1=单进程, >1=多进程)
fast: 快速模式 (跳过退化模拟步骤)
""" """
random.seed(seed) random.seed(seed)
np.random.seed(seed) np.random.seed(seed)
@@ -969,8 +974,6 @@ class PlateSwapPipeline:
print(f"目标生成数: {num_images}") print(f"目标生成数: {num_images}")
print(f"输出目录: {output_dir}") print(f"输出目录: {output_dir}")
print(f"并行进程: {workers}" + (" (多核加速)" if workers > 1 else "")) print(f"并行进程: {workers}" + (" (多核加速)" if workers > 1 else ""))
if fast:
print(f"快速模式: 跳过退化模拟")
print("-" * 60) print("-" * 60)
import time import time
@@ -984,7 +987,7 @@ class PlateSwapPipeline:
else: else:
# 多进程模式 # 多进程模式
success, failed = self._process_parallel( success, failed = self._process_parallel(
image_paths, output_dir, num_images, preview, workers, fast image_paths, output_dir, num_images, preview, workers
) )
elapsed = time.perf_counter() - t0 elapsed = time.perf_counter() - t0
@@ -1013,7 +1016,7 @@ class PlateSwapPipeline:
return success, failed return success, failed
def _process_parallel(self, image_paths, output_dir, num_images, def _process_parallel(self, image_paths, output_dir, num_images,
preview, workers, fast): preview, workers):
"""多进程并行处理(worker 复用 Pipeline,仅初始化一次)""" """多进程并行处理(worker 复用 Pipeline,仅初始化一次)"""
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
n_procs = min(workers, cpu_count()) n_procs = min(workers, cpu_count())
@@ -1022,7 +1025,7 @@ class PlateSwapPipeline:
# 每个 worker 启动时加载一次 Pipeline(含字体、底板缓存) # 每个 worker 启动时加载一次 Pipeline(含字体、底板缓存)
with Pool(n_procs, initializer=_worker_init, with Pool(n_procs, initializer=_worker_init,
initargs=(self.plate_type, fast)) as pool: initargs=(self.plate_type,)) as pool:
# 参数精简: (img_path, output_dir, preview_bool) # 参数精简: (img_path, output_dir, preview_bool)
args = [ args = [
(p, output_dir, (preview and i < 10)) (p, output_dir, (preview and i < 10))
@@ -1074,9 +1077,6 @@ def main():
help='随机种子 (默认: 42)') help='随机种子 (默认: 42)')
parser.add_argument('--workers', type=int, default=1, parser.add_argument('--workers', type=int, default=1,
help='并行进程数 (默认: 1, 建议设为CPU核心数)') help='并行进程数 (默认: 1, 建议设为CPU核心数)')
parser.add_argument('--fast', action='store_true',
help='快速模式:跳过退化模拟,速度提升约40%%')
args = parser.parse_args() args = parser.parse_args()
ccpd_dirs = [d.strip() for d in args.ccpd_dir.split(',') if d.strip()] ccpd_dirs = [d.strip() for d in args.ccpd_dir.split(',') if d.strip()]
@@ -1088,7 +1088,6 @@ def main():
preview=args.preview, preview=args.preview,
seed=args.seed, seed=args.seed,
workers=args.workers, workers=args.workers,
fast=args.fast,
) )
+10
View File
@@ -0,0 +1,10 @@
# CCPD Combined 5K+5K+SPE
| 项目 | 详情 |
|---|---|
| 来源 | 混合 Blue 5K + Green 5K + SPE 特殊车牌 |
| 规模 | 37,907 张 |
| 生成脚本 | `scripts/build_ccpd_combined.py` |
| 训练用途 | YOLO 检测模型训练 |
> **以脚本为准**:`build_ccpd_combined.py` 负责从多个源目录合并并转换标注格式。
+10
View File
@@ -0,0 +1,10 @@
# CCPD Combined Dataset (single class: license_plate)
# CCPD2019 5k + CCPD2020 5k + CCPD_SPE all = 19000 images
path: C:/Users/Kirl/Desktop/ICEv2/ICE_Dataset/ccpd_combined_5k5k_spe
train: images/train
val: images/val
nc: 1
names:
0: license_plate
+26
View File
@@ -0,0 +1,26 @@
# 数据生成脚本
此目录包含所有数据集生成脚本和少量测试样本。
## 脚本清单
| 脚本 | 输入 | 输出 | 用途 |
|------|------|------|------|
| `pipeline_322k.py` | `CCPD_322K/` | `CCPD_322K_EXP_CUT_TRANS/` | 322K 合成全管线(3 步) |
| `sample_ccpd_full.py` | `CCPD/` | `CCPD_REAL_TRANS/` | CCPD 真图全量 GT-warp |
| `sample_ccpd_real.py` | `CCPD/` | `CCPD_REAL_TRANS/` | CCPD 真图采样(实验用) |
| `build_cblprd_trans.py` | CBLPRD zip | `CBLPRD_TRANS/` | CBLPRD COR 矫正 TRANS |
| `build_ccpd_combined.py` | 多源目录 | `ccpd_combined_5k5k_spe/` | 合并数据集 |
| `rebuild_spe.py` | — | `CCPD_SPE/` | SPE 特殊车牌生成 |
| `gen_random_bg.py` | — | `CCPD_Random_*` | 随机背景蓝绿合成 |
## 目录结构
```
scripts/
├── *.py, *.ps1 ← 生成脚本(权威来源)
├── train/ ← 空(数据已移至 CCPD_322K_EXP_CUT_TRANS/)
└── test/ ← 34004 张测试样本
```
> **以脚本为准**:所有数据集的生成逻辑、标签格式、预处理参数均以本目录中的脚本为唯一权威来源。文件名后缀标签的解析规则见 `ICE_NNv2/ICE-OCR/script/tools/dataset.py`。
+138
View File
@@ -0,0 +1,138 @@
"""
CBLPRD → CCPD_TRANS 数据集构建 (ICE-COR 矫正)
输出: ICE_Dataset/CBLPRD_TRANS/train/
"""
import sys, os, cv2, numpy as np, torch
from pathlib import Path
import zipfile
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock
ICEv2 = Path(r"C:\Users\Kirl\Desktop\ICEv2")
COR_DIR = ICEv2 / "ICE_NNv2/ICE-COR"
MODEL_COR = COR_DIR / "output/1_CCPD_19K_EXP_CUT/best.pth"
ZIP_PATH = r"C:\Users\Kirl\Downloads\CBLPRD-330k_v1.zip"
OUT_DIR = ICEv2 / "ICE_Dataset/CBLPRD_TRANS/train"
sys.path.insert(0, str(COR_DIR))
from corner_rectify import load_corner_model, predict_plate_corners, order_corners
# 类型→文件名标记
TYPE_MARKER = {
'普通蓝牌': 'blue',
'新能源小型车': 'green',
'新能源大型车': 'new_energy',
'单层黄牌': 'yellow',
'黑色车牌': 'black',
}
SUPPORTED = set(TYPE_MARKER.keys())
print(f"COR: {MODEL_COR}")
dev = torch.device("cuda" if torch.cuda.is_available() else "cpu")
cor_model, _ = load_corner_model(str(MODEL_COR), dev)
print(f"Output: {OUT_DIR}")
OUT_DIR.mkdir(parents=True, exist_ok=True)
# 读取 CBLPRD train.txt
zf = zipfile.ZipFile(ZIP_PATH, 'r')
labels = []
for line in zf.read('train.txt').decode('utf-8').strip().split('\n'):
parts = line.strip().split(' ')
if len(parts) < 3 or parts[2] not in SUPPORTED:
continue
fname = parts[0].split('/')[-1]
labels.append((fname, parts[1], parts[2]))
# 同时加入 val.txt (合并训练)
for line in zf.read('val.txt').decode('utf-8').strip().split('\n'):
parts = line.strip().split(' ')
if len(parts) < 3 or parts[2] not in SUPPORTED:
continue
fname = parts[0].split('/')[-1]
labels.append((fname, parts[1], parts[2]))
print(f"Total labels: {len(labels)}")
from collections import Counter
for t, c in Counter(l[2] for l in labels).most_common():
print(f" {t}: {c}")
# Build name→zip_path index
name_idx = {}
for n in zf.namelist():
if n.endswith('.jpg'):
name_idx[n.split('/')[-1]] = n
print(f"Zip index: {len(name_idx)} images")
# 统计已完成的
existing = set(f.stem.split('_trans')[0] if '_trans' in f.stem else '' for f in OUT_DIR.glob('*.jpg'))
print(f"Already done: {len(existing)}")
# 过滤已完成 (按原始文件名前缀)
pending = []
for fname, plate, ptype in labels:
if fname in name_idx and fname.rsplit('.',1)[0] not in existing:
pending.append((fname, plate, ptype, name_idx[fname]))
print(f"Pending: {len(pending)}")
lock = Lock()
count = [0]
total = len(pending)
def process_one(item):
fname, plate, ptype, zip_path = item
try:
data = zf.read(zip_path)
img = cv2.imdecode(np.frombuffer(data, np.uint8), cv2.IMREAD_COLOR)
if img is None:
return f"READ_ERR:{fname}"
# ICE-COR
corners = predict_plate_corners(img, cor_model, dev)
ordered = order_corners(corners)
OW, OH = 192, 64
cx, cy = ordered.mean(axis=0)
exp = ordered + (ordered - np.array([[cx, cy]])) * np.array([0.08, 0.05])
exp[:, 0] = np.clip(exp[:, 0], 0, img.shape[1] - 1)
exp[:, 1] = np.clip(exp[:, 1], 0, img.shape[0] - 1)
dst = np.float32([[0, 0], [OW - 1, 0], [OW - 1, OH - 1], [0, OH - 1]])
M = cv2.getPerspectiveTransform(exp.astype(np.float32), dst)
warped = cv2.warpPerspective(img, M, (OW, OH))
except Exception as e:
return f"COR_ERR:{fname}:{e}"
# 文件名: {原始名}_{marker}_{plate}_trans.jpg
marker = TYPE_MARKER[ptype]
stem = fname.rsplit('.', 1)[0]
out_name = f"{stem}_{marker}_{plate}_trans.jpg"
out_path = OUT_DIR / out_name
# Unicode safe write
ret, enc = cv2.imencode('.jpg', warped, [cv2.IMWRITE_JPEG_QUALITY, 95])
if ret:
with open(str(out_path), 'wb') as f:
f.write(enc.tobytes())
else:
return f"WRITE_ERR:{fname}"
with lock:
count[0] += 1
if count[0] % 500 == 0:
print(f" [{count[0]}/{total}] {fname} -> {marker}:{plate}")
return f"OK:{fname}"
# 多线程处理
WORKERS = 8
print(f"\nProcessing {total} images with {WORKERS} workers...")
with ThreadPoolExecutor(max_workers=WORKERS) as executor:
futures = {executor.submit(process_one, item): item for item in pending}
for future in as_completed(futures):
result = future.result()
if result.startswith("READ_ERR") or result.startswith("WRITE_ERR"):
print(f" [!] {result}")
zf.close()
print(f"\nDone! Total saved: {count[0]}/{total}")
print(f"Output: {OUT_DIR}")
print(f"Files: {len(list(OUT_DIR.glob('*.jpg')))}")
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""
全量生成 Random 蓝绿车牌
blue 150K + green 100K, 覆盖全部 CCPD 背景
"""
import os, sys, time
from pathlib import Path
ICE_DATASET = Path(r'C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset')
os.chdir(str(ICE_DATASET))
sys.path.insert(0, str(ICE_DATASET / 'CCPD_SPE_GEN' / 'cv_pipeline'))
from plate_swap import PlateSwapPipeline
CCPD2019 = ICE_DATASET / 'CCPD' / 'CCPD2019'
CCPD2020 = ICE_DATASET / 'CCPD' / 'CCPD2020'
OUT = ICE_DATASET / 'CCPD_Random_Blue150K_Green100K'
# 全部可用 CCPD 子集 (含全量, 不限于2019)
CCPD_DIRS = [
str(CCPD2019 / 'ccpd_base'),
str(CCPD2019 / 'ccpd_blur'),
str(CCPD2019 / 'ccpd_challenge'),
str(CCPD2019 / 'ccpd_fn'),
str(CCPD2019 / 'ccpd_rotate'),
str(CCPD2019 / 'ccpd_tilt'),
str(CCPD2019 / 'ccpd_weather'),
]
# 只保留实际存在的
CCPD_DIRS = [d for d in CCPD_DIRS if Path(d).is_dir()]
print(f"CCPD 背景目录: {len(CCPD_DIRS)}")
SEED = 42
WORKERS = 24
def main():
import shutil
# 断点续传: 不删除已有数据
total_start = time.time()
for plate_type, target in [('blue', 150000), ('green', 100000)]:
out_dir = OUT / plate_type
out_dir.mkdir(parents=True, exist_ok=True)
existing = len(list(out_dir.glob('*.jpg')))
if existing >= target:
print(f"\n[{plate_type}] 已有 {existing} >= {target}, 跳过")
continue
need = target - existing
print(f"\n{'='*60}")
print(f"[{plate_type}] 已有 {existing}, 需补 {need} → 目标 {target}")
print(f"{'='*60}")
pipeline = PlateSwapPipeline(plate_type=plate_type)
print(f" 生成中...", end=' ', flush=True)
t0 = time.time()
pipeline.process_batch(
ccpd_dirs=CCPD_DIRS,
output_dir=str(out_dir),
num_images=need,
preview=False,
seed=SEED,
workers=WORKERS,
)
elapsed = time.time() - t0
print(f"完成 ({elapsed:.0f}s, {need/elapsed:.0f} 张/s)")
total_t = time.time() - total_start
blue_final = len(list((OUT / 'blue').glob('*.jpg')))
green_final = len(list((OUT / 'green').glob('*.jpg')))
print(f"\nDONE! blue={blue_final} green={green_final}, {total_t/60:.1f}min")
if __name__ == '__main__':
main()
+38
View File
@@ -0,0 +1,38 @@
# 322K 全自动: 等生成完 → 构建 → EXP_CUT → TRANS → 训练
# 用法: 生成进程跑完后执行此脚本
$ErrorActionPreference = "Continue"
$PY = "C:/Program Files/Python313/python.exe"
$ICE = "C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset"
Write-Host "===== 322K Pipeline =====" -ForegroundColor Cyan
Write-Host "Waiting for SPE + Random generation to complete..."
Write-Host "SPE dir: $ICE\CCPD_SPE\ccpd_base"
Write-Host "Random dir: $ICE\CCPD_Random_Blue150K_Green100K"
# Wait for SPE (9 types x 8000)
$spe_types = @('police','army','gangao','shiguan_old','shiguan_new','lingguan_old','lingguan_new','coach','new_energy')
do {
$done = $true
foreach ($t in $spe_types) {
$c = @(Get-ChildItem "$ICE\CCPD_SPE\ccpd_base\$t" -Filter *.jpg -ErrorAction SilentlyContinue).Count
if ($c -lt 8000) { $done = $false; break }
}
if (-not $done) { Write-Host " SPE not ready, waiting 60s..."; Start-Sleep 60 }
} while (-not $done)
Write-Host "SPE: OK" -ForegroundColor Green
# Wait for Random
do {
$b = @(Get-ChildItem "$ICE\CCPD_Random_Blue150K_Green100K\blue" -Filter *.jpg -ErrorAction SilentlyContinue).Count
$g = @(Get-ChildItem "$ICE\CCPD_Random_Blue150K_Green100K\green" -Filter *.jpg -ErrorAction SilentlyContinue).Count
if ($b -lt 150000 -or $g -lt 100000) {
Write-Host " Random: blue=$b green=$g, waiting 60s..."
Start-Sleep 60
} else { break }
} while ($true)
Write-Host "Random: blue=$b green=$g OK" -ForegroundColor Green
# Now run the pipeline
Write-Host "`nAll data ready. Starting pipeline..." -ForegroundColor Cyan
& "C:/Program Files/Python313/python.exe" "$PSScriptRoot\pipeline_322k.py"
+85
View File
@@ -0,0 +1,85 @@
# 322K 全自动流水线: 构建数据集 → EXP_CUT → TRANS → 训练
$ErrorActionPreference = "Continue"
$PY = "C:/Program Files/Python313/python.exe"
$ICE = "C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset"
$NN = "C:\Users\Kirl\Desktop\ICEv2\ICE_NNv2"
# ===== Step 1: Build CCPD_322K =====
Write-Host "`n===== 1/4 构建 CCPD_322K =====" -ForegroundColor Cyan
$SRC_SPE = "$ICE\CCPD_SPE\ccpd_base"
$SRC_RND = "$ICE\CCPD_Random_Blue150K_Green100K"
$DST = "$ICE\CCPD_322K"
if (Test-Path $DST) { Remove-Item -Recurse -Force $DST }
New-Item -ItemType Directory "$DST\train" -Force | Out-Null
New-Item -ItemType Directory "$DST\test" -Force | Out-Null
# SPE 9类 (9:1 split)
$types = @('police','army','gangao','shiguan_old','shiguan_new','lingguan_old','lingguan_new','coach','new_energy')
foreach ($t in $types) {
$src = "$SRC_SPE\$t"
if (-not (Test-Path $src)) { Write-Host " SKIP $t"; continue }
$files = Get-ChildItem $src -Filter *.jpg | Sort-Object Name
$n = $files.Count
$nTrain = [math]::Floor($n * 0.9)
for ($i = 0; $i -lt $n; $i++) {
$split = if ($i -lt $nTrain) { "train" } else { "test" }
$d = "$DST\$split\$t"; New-Item -ItemType Directory $d -Force | Out-Null
Copy-Item $files[$i].FullName "$d\" -Force
}
Write-Host " $t : $n (train=$nTrain test=$($n-$nTrain))"
}
# Random蓝绿 (9:1 split)
foreach ($t in @('blue','green')) {
$src = "$SRC_RND\$t"
if (-not (Test-Path $src)) { Write-Host " SKIP $t"; continue }
$files = Get-ChildItem $src -Filter *.jpg | Sort-Object Name
$n = $files.Count
$nTrain = [math]::Floor($n * 0.9)
for ($i = 0; $i -lt $n; $i++) {
$split = if ($i -lt $nTrain) { "train" } else { "test" }
$d = "$DST\$split\$t"; New-Item -ItemType Directory $d -Force | Out-Null
Copy-Item $files[$i].FullName "$d\" -Force
}
Write-Host " $t : $n (train=$nTrain test=$($n-$nTrain))"
}
$total = (Get-ChildItem $DST -Recurse -Filter *.jpg).Count
Write-Host " TOTAL: $total" -ForegroundColor Green
# ===== Step 2: EXP_CUT =====
Write-Host "`n===== 2/4 YOLO EXP_CUT =====" -ForegroundColor Cyan
$EXP_CUT = "$ICE\CCPD_322K_EXP_CUT"
if (Test-Path $EXP_CUT) { Remove-Item -Recurse -Force $EXP_CUT }
# Patch the build script with our paths
$script = Get-Content "$ICE\CCPD_19K_EXP_CUT\build_ccpd_19k_exp_cut.py" -Raw -Encoding UTF8
$script = $script -replace 'SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K"', "SRC_DIR = Path(r'$DST')"
$script = $script -replace 'DST_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT"', "DST_DIR = Path(r'$EXP_CUT')"
$tmp = "$ICE\scripts\_exp_cut_322k.py"
[System.IO.File]::WriteAllText($tmp, $script, [System.Text.UTF8Encoding]::new($false))
& $PY $tmp --full
Remove-Item $tmp
# ===== Step 3: TRANS =====
Write-Host "`n===== 3/4 ICE-COR TRANS =====" -ForegroundColor Cyan
$TRANS = "$ICE\CCPD_322K_EXP_CUT_TRANS"
if (Test-Path $TRANS) { Remove-Item -Recurse -Force $TRANS }
$script = Get-Content "$ICE\CCPD_19K_EXP_CUT_TRANS\build_ccpd_19k_exp_cut_trans.py" -Raw -Encoding UTF8
$script = $script -replace 'SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT"', "SRC_DIR = Path(r'$EXP_CUT')"
$script = $script -replace 'DST_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT_TRANS"', "DST_DIR = Path(r'$TRANS')"
$tmp = "$ICE\scripts\_trans_322k.py"
[System.IO.File]::WriteAllText($tmp, $script, [System.Text.UTF8Encoding]::new($false))
& $PY $tmp --full
Remove-Item $tmp
# ===== Step 4: Training =====
Write-Host "`n===== 4/4 ICE-OCR Training =====" -ForegroundColor Cyan
Write-Host " Data: $TRANS"
Write-Host " Launching..."
& $PY "$NN\ICE-OCR\script\train.py" --dataset ccpd_trans --epochs 100 --batch 128 --output ccpd_322k_best.pth --workers 2 --data-dir $TRANS
Write-Host "`n===== DONE =====" -ForegroundColor Green
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
"""322K Pipeline Step 2-4: EXP_CUT -> TRANS -> Train"""
import os, sys, shutil, subprocess
from pathlib import Path
ICE = Path(r'C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset')
PY = r'C:/Program Files/Python313/python.exe'
NN = Path(r'C:\Users\Kirl\Desktop\ICEv2\ICE_NNv2')
DATASET = ICE / 'CCPD_322K'
EXP_CUT = ICE / 'CCPD_322K_EXP_CUT'
TRANS = ICE / 'CCPD_322K_EXP_CUT_TRANS'
def run(cmd):
print(f" {str(cmd)[:150]}...")
if isinstance(cmd, list):
r = subprocess.run(cmd)
else:
r = subprocess.run(cmd, shell=True)
if r.returncode != 0: print(f" FAILED ({r.returncode})"); sys.exit(1)
# ===== Step 2: EXP_CUT =====
print("===== 2/4 EXP_CUT =====")
shutil.rmtree(EXP_CUT, ignore_errors=True)
EXP_CUT.mkdir(parents=True)
exp_py = ICE / 'CCPD_19K_EXP_CUT' / 'build_ccpd_19k_exp_cut.py'
code = exp_py.read_text(encoding='utf-8')
code = code.replace(
'SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K" # 源数据集',
f'SRC_DIR = Path(r"{DATASET}")')
code = code.replace(
'DST_DIR = SCRIPT_DIR # 本目录 (CCPD_19K_EXP_CUT)',
f'DST_DIR = Path(r"{EXP_CUT}")')
tmp = ICE / 'scripts' / '_e.py'
tmp.write_text(code, encoding='utf-8')
run([PY, str(tmp), '--full'])
tmp.unlink()
print(f" Images: {sum(1 for _ in EXP_CUT.rglob('*.jpg'))}")
# ===== Step 3: TRANS =====
print("===== 3/4 TRANS =====")
shutil.rmtree(TRANS, ignore_errors=True)
TRANS.mkdir(parents=True)
trans_py = ICE / 'CCPD_19K_EXP_CUT_TRANS' / 'build_ccpd_19k_exp_cut_trans.py'
code = trans_py.read_text(encoding='utf-8')
code = code.replace(
'SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT" # 源数据集',
f'SRC_DIR = Path(r"{EXP_CUT}")')
code = code.replace(
'DST_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT_TRANS"',
f'DST_DIR = Path(r"{TRANS}")')
tmp = ICE / 'scripts' / '_t.py'
tmp.write_text(code, encoding='utf-8')
run([PY, str(tmp), '--full'])
tmp.unlink()
print(f" Images: {sum(1 for _ in TRANS.rglob('*.jpg'))}")
# ===== Step 4: Training =====
print("===== 4/4 Training =====")
os.chdir(str(NN / 'ICE-OCR'))
run([PY, 'script/train.py', '--dataset', 'ccpd_trans', '--epochs', '100', '--batch', '128', '--output', 'ccpd_322k_best.pth', '--workers', '2', '--data-dir', str(TRANS)])
print("DONE")
+54
View File
@@ -0,0 +1,54 @@
# C++ 管线批量重生 CCPD_SPE (24核)
$ErrorActionPreference = "Continue"
$env:Path = "C:\opencv\build\x64\vc16\bin;$env:Path"
$EXE = "C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset\CCPD_SPE_GEN\cpp_pipeline\build\Release\plate_swap.exe"
$POOL = "C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset\cpp_pipeline\output\plate_pool"
$CCPD = "C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset\CCPD\CCPD2019"
$SPE = "C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset\CCPD_SPE"
# 清旧
if (Test-Path $SPE) {
Write-Host "删除旧 CCPD_SPE..."
Remove-Item -Recurse -Force $SPE
}
$DIST = @{
'ccpd_base' = 588
'ccpd_blur' = 55
'ccpd_challenge' = 159
'ccpd_fn' = 58
'ccpd_rotate' = 30
'ccpd_tilt' = 82
'ccpd_weather' = 28
}
$TYPES = @('police','army','gangao','shiguan_old','shiguan_new',
'lingguan_old','lingguan_new','coach','new_energy')
$total = 0
$sw = [System.Diagnostics.Stopwatch]::StartNew()
foreach ($type in $TYPES) {
Write-Host "`n=== $type ===" -ForegroundColor Cyan
foreach ($subdir in $DIST.Keys) {
$count = $DIST[$subdir]
$ccpdDir = Join-Path $CCPD $subdir
$outDir = Join-Path $SPE "$subdir\$type"
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
Write-Host " $subdir : $count 张... " -NoNewline
& $EXE --ccpd-dir $ccpdDir --plate-type $type --plate-pool $POOL `
--output $outDir --num $count --workers 24 --seed 42 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) {
Write-Host "OK" -ForegroundColor Green
$total += $count
} else {
Write-Host "FAIL" -ForegroundColor Red
}
}
}
$sw.Stop()
Write-Host "`nDONE! $total 张, $([math]::Round($sw.Elapsed.TotalMinutes,1))min" -ForegroundColor Green
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
批量重新生成 CCPD_SPE 数据集 — 全量版
SPE 9类 × 8000 (ccpd_base) = 72,000 张
蓝绿由独立脚本生成 150K+100K
"""
import os
import sys
import time
from pathlib import Path
ICE_DATASET = Path(r'C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset')
os.chdir(str(ICE_DATASET))
sys.path.insert(0, str(ICE_DATASET / 'CCPD_SPE_GEN' / 'cv_pipeline'))
from plate_swap import PlateSwapPipeline
# 只生成 9 类特殊车牌 (蓝绿由独立脚本处理)
SPE_TYPES = ['police','army','gangao','shiguan_old','shiguan_new',
'lingguan_old','lingguan_new','coach','new_energy']
COUNT_PER_TYPE = 8000 # 每类 8000, 全部用 ccpd_base 背景
CCPD2019_DIR = ICE_DATASET / 'CCPD' / 'CCPD2019'
SPE_OUTPUT = ICE_DATASET / 'CCPD_SPE'
SEED = 42
WORKERS = 24
def main():
total_start = time.time()
total_images = 0
import shutil
if SPE_OUTPUT.exists():
print(f"删除旧 CCPD_SPE: {SPE_OUTPUT}")
shutil.rmtree(str(SPE_OUTPUT))
SPE_OUTPUT.mkdir(parents=True, exist_ok=True)
for plate_type in SPE_TYPES:
print(f"\n{'='*60}")
print(f"[{plate_type}] {COUNT_PER_TYPE} 张")
print(f"{'='*60}")
pipeline = PlateSwapPipeline(plate_type=plate_type)
out_dir = SPE_OUTPUT / 'ccpd_base' / plate_type
out_dir.mkdir(parents=True, exist_ok=True)
print(f" 生成中...", end=' ', flush=True)
t0 = time.time()
pipeline.process_batch(
ccpd_dirs=[str(CCPD2019_DIR / 'ccpd_base')],
output_dir=str(out_dir),
num_images=COUNT_PER_TYPE,
preview=False,
seed=SEED,
workers=WORKERS,
)
elapsed = time.time() - t0
total_images += COUNT_PER_TYPE
print(f"完成 ({elapsed:.0f}s, {COUNT_PER_TYPE/elapsed:.0f} 张/s)")
total_t = time.time() - total_start
print(f"\n{'='*60}")
print(f"SPE 完成! {total_images} 张, {total_t/60:.1f}min")
print(f"{'='*60}")
if __name__ == '__main__':
main()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
C++ 管线批量重生 CCPD_SPE (9 类 x 7 子集 = 9,000 张)
比 Python 版快 ~5x,24 线程饱和
"""
import os
import sys
import time
import shutil
import subprocess
from pathlib import Path
ICE_DATASET = Path(r'C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset')
CPP_EXE = ICE_DATASET / 'CCPD_SPE_GEN' / 'cpp_pipeline' / 'build' / 'Release' / 'plate_swap.exe'
PLATE_POOL = ICE_DATASET / 'cpp_pipeline' / 'output' / 'plate_pool'
CCPD2019 = ICE_DATASET / 'CCPD' / 'CCPD2019'
SPE_OUT = ICE_DATASET / 'CCPD_SPE'
# 原始分布
DIST = {
'ccpd_base': 588,
'ccpd_blur': 55,
'ccpd_challenge': 159,
'ccpd_fn': 58,
'ccpd_rotate': 30,
'ccpd_tilt': 82,
'ccpd_weather': 28,
}
TYPES = ['police','army','gangao','shiguan_old','shiguan_new',
'lingguan_old','lingguan_new','coach','new_energy']
WORKERS = 24 # 物理核心数
SEED = 42
def main():
if not CPP_EXE.exists():
print(f"[FATAL] C++ exe not found: {CPP_EXE}")
return 1
if not PLATE_POOL.is_dir():
print(f"[FATAL] Plate pool not found: {PLATE_POOL}")
return 1
# 清空旧数据
if SPE_OUT.exists():
print(f"删除旧 CCPD_SPE: {SPE_OUT}")
shutil.rmtree(str(SPE_OUT))
total_start = time.time()
total = 0
for plate_type in TYPES:
t0 = time.time()
type_count = 0
print(f"\n{'='*50}")
print(f"[{plate_type}]")
print(f"{'='*50}")
for subdir, count in DIST.items():
ccpd_dir = CCPD2019 / subdir
if not ccpd_dir.is_dir():
continue
out_dir = SPE_OUT / subdir / plate_type
out_dir.mkdir(parents=True, exist_ok=True)
cmd = [
str(CPP_EXE),
'--ccpd-dir', str(ccpd_dir),
'--plate-type', plate_type,
'--plate-pool', str(PLATE_POOL),
'--output', str(out_dir),
'--num', str(count),
'--workers', str(WORKERS),
'--seed', str(SEED + hash(plate_type) % 10000),
]
print(f" {subdir}: {count} 张...", end=' ', flush=True)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
print(f"OK")
type_count += count
else:
print(f"FAIL")
print(f" stderr: {result.stderr[:200]}")
elapsed = time.time() - t0
print(f" [{plate_type}] {type_count} 张, {elapsed:.0f}s")
total += type_count
total_t = time.time() - total_start
print(f"\n{'='*50}")
print(f"DONE! {total} 张, {total_t/60:.1f}min")
print(f"{'='*50}")
if __name__ == '__main__':
os.chdir(str(ICE_DATASET))
main()
+126
View File
@@ -0,0 +1,126 @@
"""
CCPD原始真图 全量 TRANS (GT顶点外接框 + 12%/18%外扩)
输出: ICE_Dataset/CCPD_REAL_TRANS/train/
全量 ~338K, 文件名 _mask_real_ 标记 (训练时屏蔽省位+城市位)
"""
import os, sys, cv2, shutil
import numpy as np
from pathlib import Path
ICEv2 = Path(r"C:\Users\Kirl\Desktop\ICEv2")
sys.path.insert(0, str(ICEv2 / "ICE_NNv2/ICE-OCR/script/tools"))
from dataset import parse_ccpd_filename, imread_unicode
CCPD_DIRS = [
ICEv2 / "ICE_Dataset/CCPD/CCPD2019",
ICEv2 / "ICE_Dataset/CCPD/CCPD2020",
]
OUT_DIR = ICEv2 / "ICE_Dataset/CCPD_REAL_TRANS/train"
EXPAND_NORMAL = 1.12
EXPAND_SMALL = 1.18
SMALL_BOX_W = SMALL_BOX_H = 16
MIN_PAD = 2
def process_one(full_path, stem):
"""GT顶点→外接框→12/18%外扩→裁切→顶点重映射→warp"""
img = imread_unicode(str(full_path))
if img is None:
return None
h, w = img.shape[:2]
parts = stem.split("-")
if len(parts) < 4:
return None
# GT顶点 (CCPD: br, bl, tl, tr)
verts = []
for v in parts[3].split("_"):
x, y = v.split("&")
verts.append([float(x), float(y)])
verts = np.float32(verts)
# 外接框
x1, y1 = verts[:, 0].min(), verts[:, 1].min()
x2, y2 = verts[:, 0].max(), verts[:, 1].max()
bw, bh = x2 - x1, y2 - y1
# 外扩
ef = EXPAND_SMALL if (bw < SMALL_BOX_W or bh < SMALL_BOX_H) else EXPAND_NORMAL
px = max(MIN_PAD, int(round(bw * (ef - 1.0) / 2.0)))
py = max(MIN_PAD, int(round(bh * (ef - 1.0) / 2.0)))
cx1 = max(0, int(x1) - px); cy1 = max(0, int(y1) - py)
cx2 = min(w, int(x2) + px); cy2 = min(h, int(y2) + py)
if cx2 <= cx1 or cy2 <= cy1:
return None
cropped = img[cy1:cy2, cx1:cx2]
# 顶点重映射到裁切坐标 + OpenCV顺序 (CCPD: br,bl,tl,tr → tl,tr,br,bl)
verts_crop = verts[[2, 3, 0, 1], :] - np.array([cx1, cy1])
verts_crop = verts_crop.astype(np.float32)
OW, OH = 192, 64
dst = np.float32([[0, 0], [OW - 1, 0], [OW - 1, OH - 1], [0, OH - 1]])
M = cv2.getPerspectiveTransform(verts_crop, dst)
return cv2.warpPerspective(cropped, M, (OW, OH))
if __name__ == "__main__":
# Step 0: 清空
if OUT_DIR.exists():
shutil.rmtree(str(OUT_DIR))
OUT_DIR.mkdir(parents=True, exist_ok=True)
# Step 1: 收集
print("[1/2] Scanning all CCPD...")
all_files = []
total = skipped = 0
for d in CCPD_DIRS:
for root, dirs, files in os.walk(str(d)):
for f in files:
if not f.lower().endswith(".jpg"):
continue
indices, plate = parse_ccpd_filename(f)
if indices is None:
skipped += 1
continue
total += 1
all_files.append((os.path.join(root, f), Path(f).stem, plate))
print(f" Valid: {total}, Skipped: {skipped}")
for prov in sorted(set(p[2][0] for p in all_files),
key=lambda p: -sum(1 for x in all_files if x[2][0] == p)):
cnt = sum(1 for x in all_files if x[2][0] == prov)
print(f" {prov}: {cnt}")
# Step 2: GT顶点 warp
print(f"\n[2/2] GT-vertex warp {len(all_files)} -> {OUT_DIR}...")
done = err = 0
for i, (full_path, stem, plate) in enumerate(all_files):
try:
warped = process_one(full_path, stem)
if warped is None:
err += 1
continue
# _mask_real_ 标记: 训练时屏蔽 pos0(省)+pos1(城市码)
out_name = f"{stem}_mask_real_{plate}_trans.jpg"
out_path = OUT_DIR / out_name
ret, enc = cv2.imencode(".jpg", warped, [cv2.IMWRITE_JPEG_QUALITY, 95])
if ret:
with open(str(out_path), "wb") as wf:
wf.write(enc.tobytes())
done += 1
else:
err += 1
except Exception as e:
err += 1
if err <= 3:
print(f" [!] {Path(full_path).name}: {e}")
if (i + 1) % 10000 == 0:
print(f" [{i+1}/{len(all_files)}] done={done}, err={err}")
print(f"\nDone! Saved: {done}, Errors: {err}")
print(f"Output: {OUT_DIR} ({len(list(OUT_DIR.glob('*.jpg')))} files)")
+156
View File
@@ -0,0 +1,156 @@
"""
CCPD原始真图 去偏采样 + YOLO EXP_CUT + 角点TRANS处理
输出: ICE_Dataset/CCPD_REAL_TRANS/train/
省份均衡: 每省最多 MAX_PER_PROVINCE 张
管线: YOLO检出→12%/18%外扩→裁切→角点重映射→warp (与322K EXP_CUT一致)
"""
import os, sys, cv2, random, shutil
import numpy as np
from pathlib import Path
from collections import defaultdict
ICEv2 = Path(r"C:\Users\Kirl\Desktop\ICEv2")
OCR_TOOLS = ICEv2 / "ICE_NNv2/ICE-OCR/script/tools"
sys.path.insert(0, str(OCR_TOOLS))
from dataset import parse_ccpd_filename, imread_unicode
CCPD_DIRS = [
ICEv2 / "ICE_Dataset/CCPD/CCPD2019",
ICEv2 / "ICE_Dataset/CCPD/CCPD2020",
]
OUT_DIR = ICEv2 / "ICE_Dataset/CCPD_REAL_TRANS/train"
MAX_PER_PROVINCE = 300
SEED = 42
YOLO_PATH = str(ICEv2 / "ICE_NNv2/yolov5t/output/1_CCPD_19K/weights/best.pt")
EXPAND_NORMAL = 1.12 # 常规外扩 12%
EXPAND_SMALL = 1.18 # 小车牌外扩 18%
SMALL_BOX_W = SMALL_BOX_H = 16
MIN_PAD = 2
random.seed(SEED)
def process_one(img_path, plate, model, img_size=640):
"""YOLO→外扩→裁切→角点重映射→warp (仿 EXP_CUT 管线)"""
img = imread_unicode(str(img_path))
if img is None:
return None
h, w = img.shape[:2]
results = model(img_path, imgsz=img_size, verbose=False)
if len(results) == 0 or results[0].boxes is None or len(results[0].boxes) == 0:
return None
boxes = results[0].boxes
# 取置信度最高的框 (YOLO 输出 xyxy 格式)
best_idx = boxes.conf.argmax()
x1, y1, x2, y2 = boxes.xyxy[best_idx].cpu().numpy().astype(int)
bw, bh = x2 - x1, y2 - y1
# 外扩 (与 EXP_CUT 完全一致)
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)))
cx1 = max(0, int(x1) - pad_x)
cy1 = max(0, int(y1) - pad_y)
cx2 = min(w, int(x2) + pad_x)
cy2 = min(h, int(y2) + pad_y)
if cx2 <= cx1 or cy2 <= cy1:
return None
cropped = img[cy1:cy2, cx1:cx2]
# 角点: 用 YOLO 检出框的四角 (原始图坐标), 重映射到裁切图坐标
corners_orig = np.float32([[x1, y1], [x2, y1], [x2, y2], [x1, y2]]) # TL, TR, BR, BL
corners_crop = (corners_orig - np.array([cx1, cy1])).astype(np.float32)
# 透视变换 → 192x64
OW, OH = 192, 64
dst = np.float32([[0, 0], [OW - 1, 0], [OW - 1, OH - 1], [0, OH - 1]])
M = cv2.getPerspectiveTransform(corners_crop, dst)
warped = cv2.warpPerspective(cropped, M, (OW, OH))
return warped
# ---- Step 0: 清空旧输出 ----
if OUT_DIR.exists():
shutil.rmtree(str(OUT_DIR))
OUT_DIR.mkdir(parents=True, exist_ok=True)
# ---- Step 1: 收集CCPD真图, 按省份分组 ----
print("[1/3] Scanning CCPD directories...")
prov_files = defaultdict(list) # province -> [(full_path, stem, plate)]
total = skipped = 0
for ccpd_dir in CCPD_DIRS:
for root, dirs, files in os.walk(str(ccpd_dir)):
for f in files:
if not f.lower().endswith(".jpg"):
continue
indices, plate = parse_ccpd_filename(f)
if indices is None:
skipped += 1
continue
total += 1
prov = plate[0]
full_path = os.path.join(root, f)
stem = Path(f).stem
prov_files[prov].append((full_path, stem, plate))
print(f" Valid: {total}, Skipped: {skipped}, Provinces: {len(prov_files)}")
for prov in sorted(prov_files, key=lambda p: -len(prov_files[p])):
print(f" {prov}: {len(prov_files[prov])}")
# ---- Step 2: 去偏采样 ----
print(f"\n[2/3] Sampling max {MAX_PER_PROVINCE} per province...")
sampled = []
for prov, items in prov_files.items():
if len(items) <= MAX_PER_PROVINCE:
sampled.extend(items)
else:
sampled.extend(random.sample(items, MAX_PER_PROVINCE))
random.shuffle(sampled)
print(f" Sampled: {len(sampled)}")
prov_dist = defaultdict(int)
for _, _, plate in sampled:
prov_dist[plate[0]] += 1
for prov in sorted(prov_dist):
print(f" {prov}: {prov_dist[prov]}")
# ---- Step 3: YOLO EXP_CUT + warp + 保存 ----
print(f"\n[3/3] YOLO EXP_CUT (12%/18%) + warp {len(sampled)} images -> {OUT_DIR}...")
print(f" YOLO: {YOLO_PATH}")
from ultralytics import YOLO
model = YOLO(YOLO_PATH)
done = err = 0
for i, (full_path, stem, plate) in enumerate(sampled):
try:
warped = process_one(full_path, plate, model)
if warped is None:
err += 1; continue
out_name = f"{stem}_real_blue_{plate}_trans.jpg"
out_path = OUT_DIR / out_name
ret, enc = cv2.imencode(".jpg", warped, [cv2.IMWRITE_JPEG_QUALITY, 95])
if ret:
with open(str(out_path), "wb") as wf:
wf.write(enc.tobytes())
done += 1
else:
err += 1
except Exception as e:
err += 1
if err <= 5:
print(f" [!] {Path(full_path).name}: {e}")
if (i + 1) % 200 == 0:
print(f" [{i+1}/{len(sampled)}] done={done}, err={err}")
print(f"\nDone! Saved: {done}, Errors: {err}")
print(f"Output: {OUT_DIR} ({len(list(OUT_DIR.glob('*.jpg')))} files)")