Dataset: add READMEs, organize scripts, update .gitignore
- Add README.md to all 16 dataset directories with script references - Centralize generation scripts under scripts/ with full README - Update .gitignore to cover all data directories, keep only metadata - Remove redundant CCPD/.gitignore (parent covers same rules) - Remove stale scripts/test/ (254MB pipeline residue) - Fix label parser reference: scripts are authoritative source of truth
This commit is contained in:
1 parent
7867c3bfaf
commit
a64690477a
43 files changed
+2690
-204
No files matched your search
+78
-10
@@ -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/
|
||||||
@@ -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/` 子目录。
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# CCPD raw data (download from official source)
|
|
||||||
CCPD2019/
|
|
||||||
CCPD2020/
|
|
||||||
|
|
||||||
# 7z archives (optional, via git LFS)
|
|
||||||
*.7z
|
|
||||||
|
|
||||||
+13
-15
@@ -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/ # 标注文件(本地,不入库)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
test/
|
||||||
|
train/
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
test/
|
||||||
|
train/
|
||||||
@@ -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 # 全量
|
||||||
|
```
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
test/
|
||||||
|
train/
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# CCPD_19K_EXP_CUT_TRANS — ICE-COR 角点矫正 + 透视变换
|
||||||
|
|
||||||
|
> **以脚本为准**:生成逻辑以 `ICE_Dataset/scripts/build_ccpd_19k_exp_cut_trans.py` 为准。
|
||||||
|
|
||||||
|
由 CCPD_19K_EXP_CUT 经 ICE-COR 角点推理 → 中心外扩 8%(x)/5%(y) → 透视变换生成。
|
||||||
|
|
||||||
|
## 处理流程
|
||||||
|
|
||||||
|
```
|
||||||
|
CCPD_19K_EXP_CUT (~18,887 张)
|
||||||
|
→ ICE-COR best.pth 推理 (192×64)
|
||||||
|
→ 角点排序 (TL→TR→BR→BL)
|
||||||
|
→ 中心外扩: 8% (x) / 5% (y)
|
||||||
|
→ 透视变换 → 192×64
|
||||||
|
→ 文件名追加 _trans
|
||||||
|
→ CCPD_19K_EXP_CUT_TRANS
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
CCPD_19K_EXP_CUT_TRANS/
|
||||||
|
├── train/ # 与源数据集对应
|
||||||
|
│ ├── blue/
|
||||||
|
│ ├── green/
|
||||||
|
│ ├── police/
|
||||||
|
│ ├── army/
|
||||||
|
│ ├── gangao/
|
||||||
|
│ ├── shiguan_old/
|
||||||
|
│ ├── shiguan_new/
|
||||||
|
│ ├── lingguan_old/
|
||||||
|
│ ├── lingguan_new/
|
||||||
|
│ ├── coach/
|
||||||
|
│ └── new_energy/
|
||||||
|
├── test/
|
||||||
|
│ └── ... (同上 11 类)
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生成脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ICE_Dataset/CCPD_19K_EXP_CUT_TRANS
|
||||||
|
python build_ccpd_19k_exp_cut_trans.py --dry-run # 预览 (每类2张)
|
||||||
|
python build_ccpd_19k_exp_cut_trans.py --n 100 # 预览 100 张
|
||||||
|
python build_ccpd_19k_exp_cut_trans.py --full # 全量处理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 参数说明
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 外扩比例 | 8% (x) / 5% (y),从四角点中心向外径向扩展 |
|
||||||
|
| 输出尺寸 | 192×64 (对齐车牌生成器 440×140 底图比例) |
|
||||||
|
| 文件名 | 原文件名追加 `_trans`,不重映射角点 |
|
||||||
|
| 跳过策略 | ICE-COR 推理失败 / 角点越界 → 直接跳过 |
|
||||||
|
|
||||||
|
## 模型
|
||||||
|
|
||||||
|
- ICE-COR: `ICE_NNv2/ICE-COR/output/1_CCPD_19K_EXP_CUT/best.pth`
|
||||||
|
- 专为 CCPD_19K_EXP_CUT 裁剪车牌 ROI 训练
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
CCPD_19K_EXP_CUT → ICE-COR 角点推理 → 中心外扩 → 透视变换 → CCPD_19K_EXP_CUT_TRANS
|
||||||
|
|
||||||
|
基于 CCPD_19K_EXP_CUT 裁剪车牌图片,使用 ICE-COR 检测四角点,
|
||||||
|
按 8%(x)/5%(y) 中心外扩后进行透视变换矫正至 192×64,
|
||||||
|
文件名追加 _trans 后缀,保持原文件夹结构。
|
||||||
|
|
||||||
|
外扩算法参考: Linux/App/main.cc rectify_plate_pfld()
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python build_ccpd_19k_exp_cut_trans.py --dry-run # 预览 (每类2张)
|
||||||
|
python build_ccpd_19k_exp_cut_trans.py --n 100 # 预览 100 张
|
||||||
|
python build_ccpd_19k_exp_cut_trans.py --full # 全量处理 (~19K)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# ---- Unicode-safe I/O ----
|
||||||
|
def _imread_unicode(filepath, flags=cv2.IMREAD_COLOR):
|
||||||
|
try:
|
||||||
|
data = np.fromfile(filepath, dtype=np.uint8)
|
||||||
|
if len(data) == 0:
|
||||||
|
return None
|
||||||
|
return cv2.imdecode(data, flags)
|
||||||
|
except Exception:
|
||||||
|
return cv2.imread(filepath, flags) # fallback
|
||||||
|
|
||||||
|
def _imwrite_unicode(filepath, img, params=None):
|
||||||
|
"""Unicode-safe imwrite — NEVER falls back to cv2.imwrite."""
|
||||||
|
try:
|
||||||
|
ext = os.path.splitext(filepath)[1]
|
||||||
|
ok, buf = cv2.imencode(ext, img, params or [])
|
||||||
|
if ok:
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(buf.tobytes())
|
||||||
|
return True
|
||||||
|
# imencode failed — try writing via temp file
|
||||||
|
import tempfile, shutil
|
||||||
|
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
|
||||||
|
tmp.write(cv2.imencode(ext, img, params or [])[1].tobytes())
|
||||||
|
shutil.move(tmp.name, filepath)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
# Last resort: imencode to bytes, write with Python I/O
|
||||||
|
try:
|
||||||
|
ext = os.path.splitext(filepath)[1]
|
||||||
|
ok, buf = cv2.imencode(ext, img, params or [])
|
||||||
|
if ok:
|
||||||
|
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
||||||
|
with open(filepath, 'wb') as f:
|
||||||
|
f.write(buf.tobytes())
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 路径
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
SCRIPT_DIR = Path(__file__).resolve().parent # CCPD_19K_EXP_CUT_TRANS/
|
||||||
|
PROJ_ROOT = SCRIPT_DIR.parents[1] # ICEv2/
|
||||||
|
ICE_COR_DIR = PROJ_ROOT / "ICE_NNv2" / "ICE-COR"
|
||||||
|
MODEL_PATH = ICE_COR_DIR / "output" / "1_CCPD_19K_EXP_CUT" / "best.pth"
|
||||||
|
SRC_DIR = PROJ_ROOT / "ICE_Dataset" / "CCPD_19K_EXP_CUT" # 源数据集
|
||||||
|
DST_DIR = SCRIPT_DIR # 本目录
|
||||||
|
|
||||||
|
if str(ICE_COR_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ICE_COR_DIR))
|
||||||
|
|
||||||
|
from corner_rectify import load_corner_model, predict_plate_corners, order_corners
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 参数
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
OUT_W = 192
|
||||||
|
OUT_H = 64
|
||||||
|
CORNER_PAD_X = 0.08 # 从中心向外扩展 8% (x)
|
||||||
|
CORNER_PAD_Y = 0.05 # 从中心向外扩展 5% (y)
|
||||||
|
|
||||||
|
SPLITS = ["train", "test"]
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# 核心处理: 单张图片
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
def process_single(model, device, img_path: Path, out_img_path: Path,
|
||||||
|
draw_corners: bool = False):
|
||||||
|
"""
|
||||||
|
对单张裁剪车牌图片:
|
||||||
|
1. ICE-COR 推理 → 4 角点像素坐标
|
||||||
|
2. order_corners → TL, TR, BR, BL
|
||||||
|
3. 中心外扩 8%(x)/5%(y), clip 到图像边界
|
||||||
|
4. 透视变换 → 192×64
|
||||||
|
5. (可选) 在输出图上绘制角点
|
||||||
|
6. 保存, 文件名追加 _trans
|
||||||
|
|
||||||
|
返回:
|
||||||
|
"ok" — 成功
|
||||||
|
"skip" — 跳过 (推理失败/角点异常)
|
||||||
|
"""
|
||||||
|
img = _imread_unicode(str(img_path))
|
||||||
|
if img is None:
|
||||||
|
return "skip"
|
||||||
|
|
||||||
|
H, W = img.shape[:2]
|
||||||
|
|
||||||
|
# ---- 1. ICE-COR 角点推理 ----
|
||||||
|
try:
|
||||||
|
corners_raw = predict_plate_corners(img, model, device) # (4,2) 像素坐标
|
||||||
|
except Exception:
|
||||||
|
return "skip"
|
||||||
|
|
||||||
|
# ---- 2. 排序 (TL, TR, BR, BL) ----
|
||||||
|
ordered = order_corners(corners_raw) # (4,2) float32
|
||||||
|
|
||||||
|
# 校验: 角点是否有效 (非退化四边形)
|
||||||
|
if not _valid_quad(ordered, W, H):
|
||||||
|
return "skip"
|
||||||
|
|
||||||
|
# ---- 3. 中心外扩 ----
|
||||||
|
src_pts = _expand_corners(ordered, W, H, CORNER_PAD_X, CORNER_PAD_Y)
|
||||||
|
|
||||||
|
# ---- 4. 透视变换 ----
|
||||||
|
dst_pts = np.float32([[0, 0], [OUT_W - 1, 0], [OUT_W - 1, OUT_H - 1], [0, OUT_H - 1]])
|
||||||
|
M = cv2.getPerspectiveTransform(src_pts, dst_pts)
|
||||||
|
warped = cv2.warpPerspective(img, M, (OUT_W, OUT_H))
|
||||||
|
|
||||||
|
# ---- 5. 绘制角点 (预览模式) ----
|
||||||
|
if draw_corners:
|
||||||
|
# 将原始角点 (ordered) 和外扩后角点 (src_pts) 映射到 warped 上绘制
|
||||||
|
_draw_debug(warped, ordered, src_pts, M)
|
||||||
|
|
||||||
|
# ---- 6. 保存 ----
|
||||||
|
stem = img_path.stem
|
||||||
|
new_name = stem + "_trans" + img_path.suffix
|
||||||
|
out_img_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
final_out = out_img_path.parent / new_name
|
||||||
|
_imwrite_unicode(str(final_out), warped)
|
||||||
|
|
||||||
|
return "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def _valid_quad(pts, W, H):
|
||||||
|
"""检查四边形是否有效 (非退化, 不越界太离谱)"""
|
||||||
|
pts = np.asarray(pts, dtype=np.float32)
|
||||||
|
# 所有点必须在图像范围内 (允许少许越界)
|
||||||
|
if (pts[:, 0] < -0.5 * W).any() or (pts[:, 0] > 1.5 * W).any():
|
||||||
|
return False
|
||||||
|
if (pts[:, 1] < -0.5 * H).any() or (pts[:, 1] > 1.5 * H).any():
|
||||||
|
return False
|
||||||
|
# 面积不能太小 (退化)
|
||||||
|
area = cv2.contourArea(pts.reshape(-1, 1, 2).astype(np.float32))
|
||||||
|
if area < 0.01 * W * H:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_corners(pts, W, H, pad_x, pad_y):
|
||||||
|
"""
|
||||||
|
从四角点中心出发, 各点沿径向向外扩展 pad_x/pad_y 比例.
|
||||||
|
参考: Linux/App/main.cc rectify_plate_pfld()
|
||||||
|
"""
|
||||||
|
pts = np.asarray(pts, dtype=np.float32).copy()
|
||||||
|
center = pts.mean(axis=0)
|
||||||
|
|
||||||
|
for i in range(4):
|
||||||
|
pts[i, 0] = center[0] + (pts[i, 0] - center[0]) * (1.0 + pad_x)
|
||||||
|
pts[i, 1] = center[1] + (pts[i, 1] - center[1]) * (1.0 + pad_y)
|
||||||
|
|
||||||
|
# clip 到图像边界
|
||||||
|
pts[:, 0] = np.clip(pts[:, 0], 0.0, W - 1.0)
|
||||||
|
pts[:, 1] = np.clip(pts[:, 1], 0.0, H - 1.0)
|
||||||
|
return pts
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_debug(warped, ordered, src_pts, M):
|
||||||
|
"""在 warped 图上绘制 ICE-COR 原始角点和外扩后角点"""
|
||||||
|
# 原始角点 (ordered) → warped 坐标系
|
||||||
|
ordered_h = cv2.perspectiveTransform(
|
||||||
|
ordered.reshape(1, -1, 2).astype(np.float32), M
|
||||||
|
).reshape(-1, 2).astype(np.int32)
|
||||||
|
|
||||||
|
# 外扩角点 (src_pts) → warped 坐标系 (应该映射到四角)
|
||||||
|
src_h = cv2.perspectiveTransform(
|
||||||
|
src_pts.reshape(1, -1, 2).astype(np.float32), M
|
||||||
|
).reshape(-1, 2).astype(np.int32)
|
||||||
|
|
||||||
|
# 绿色: 外扩后的四边形
|
||||||
|
cv2.polylines(warped, [src_h], isClosed=True, color=(0, 255, 0), thickness=1)
|
||||||
|
# 蓝色: 原始 ICE-COR 角点
|
||||||
|
for pt in ordered_h:
|
||||||
|
cv2.circle(warped, tuple(pt), 2, (255, 0, 0), -1)
|
||||||
|
|
||||||
|
# 标签
|
||||||
|
cv2.putText(warped, "blue=ICE-COR green=expanded",
|
||||||
|
(3, 10), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (255, 255, 255), 1)
|
||||||
|
|
||||||
|
|
||||||
|
# ===================================================================
|
||||||
|
# 主流程
|
||||||
|
# ===================================================================
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="CCPD_19K_EXP_CUT → ICE-COR角点 + 外扩 + 透视变换 → CCPD_19K_EXP_CUT_TRANS")
|
||||||
|
parser.add_argument("--dry-run", action="store_true",
|
||||||
|
help="预览 (每类2张, 图上绘制角点)")
|
||||||
|
parser.add_argument("--n", type=int, default=None,
|
||||||
|
help="预览 N 张")
|
||||||
|
parser.add_argument("--full", action="store_true",
|
||||||
|
help="全量处理 (~19K)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not (args.dry_run or args.n or args.full):
|
||||||
|
print("请指定 --dry-run, --n N, 或 --full")
|
||||||
|
print(f" python {Path(__file__).name} --dry-run")
|
||||||
|
print(f" python {Path(__file__).name} --full")
|
||||||
|
return
|
||||||
|
|
||||||
|
draw = not args.full
|
||||||
|
label = ("DRY-RUN (每类2张)" if args.dry_run else
|
||||||
|
f"预览 ({args.n} 张)" if args.n else "全量处理")
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"CCPD_19K_EXP_CUT → ICE-COR 角点 + 外扩 {CORNER_PAD_X:.0%}(x)/{CORNER_PAD_Y:.0%}(y) → 透视变换 {OUT_W}×{OUT_H}")
|
||||||
|
print(f"模式: {label}")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# ---- 加载 ICE-COR 模型 ----
|
||||||
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
print(f"\n加载 ICE-COR: {MODEL_PATH}")
|
||||||
|
model, _ = load_corner_model(str(MODEL_PATH), device)
|
||||||
|
print(f"设备: {device} | 输出: {OUT_W}×{OUT_H}")
|
||||||
|
|
||||||
|
# ---- 收集图片 ----
|
||||||
|
print("\n收集图片...")
|
||||||
|
all_tasks = []
|
||||||
|
for split in SPLITS:
|
||||||
|
split_dir = SRC_DIR / split
|
||||||
|
if not split_dir.exists():
|
||||||
|
continue
|
||||||
|
for cls_dir in sorted(split_dir.iterdir()):
|
||||||
|
if not cls_dir.is_dir():
|
||||||
|
continue
|
||||||
|
for jpg in sorted(cls_dir.glob("*.jpg")):
|
||||||
|
rel = jpg.relative_to(SRC_DIR)
|
||||||
|
out = DST_DIR / rel
|
||||||
|
all_tasks.append((jpg, out))
|
||||||
|
|
||||||
|
print(f"共 {len(all_tasks)} 张")
|
||||||
|
|
||||||
|
# ---- 限制数量 ----
|
||||||
|
if args.dry_run:
|
||||||
|
by_class = defaultdict(list)
|
||||||
|
for src, dst in all_tasks:
|
||||||
|
by_class[src.parent.name].append((src, dst))
|
||||||
|
limited = []
|
||||||
|
for cls in sorted(by_class):
|
||||||
|
limited.extend(by_class[cls][:2])
|
||||||
|
all_tasks = limited
|
||||||
|
print(f"预览: 限制 {len(all_tasks)} 张 (每类 2 张)")
|
||||||
|
elif args.n:
|
||||||
|
all_tasks = all_tasks[:args.n]
|
||||||
|
print(f"预览: 限制 {len(all_tasks)} 张")
|
||||||
|
|
||||||
|
# ---- 清理旧输出 (非 full 模式) ----
|
||||||
|
if not args.full:
|
||||||
|
import shutil
|
||||||
|
for split in SPLITS:
|
||||||
|
sp = DST_DIR / split
|
||||||
|
if sp.exists():
|
||||||
|
shutil.rmtree(sp)
|
||||||
|
|
||||||
|
# ---- 处理 ----
|
||||||
|
print(f"\n开始处理...")
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
stats = {"total": 0, "ok": 0, "skip": 0}
|
||||||
|
sc = defaultdict(lambda: {"ok": 0, "skip": 0})
|
||||||
|
|
||||||
|
for idx, (src_path, out_path) in enumerate(all_tasks):
|
||||||
|
rel = src_path.relative_to(SRC_DIR)
|
||||||
|
parts = rel.parts
|
||||||
|
split_name, cls_name = parts[0], parts[1]
|
||||||
|
|
||||||
|
result = process_single(model, device, src_path, out_path,
|
||||||
|
draw_corners=draw)
|
||||||
|
|
||||||
|
stats["total"] += 1
|
||||||
|
key = f"{split_name}/{cls_name}"
|
||||||
|
if result == "ok":
|
||||||
|
stats["ok"] += 1
|
||||||
|
sc[key]["ok"] += 1
|
||||||
|
else:
|
||||||
|
stats["skip"] += 1
|
||||||
|
sc[key]["skip"] += 1
|
||||||
|
|
||||||
|
if (idx + 1) % 500 == 0 or (idx + 1) == len(all_tasks):
|
||||||
|
elapsed = time.perf_counter() - t0
|
||||||
|
rate = (idx + 1) / elapsed if elapsed > 0 else 0
|
||||||
|
eta = (len(all_tasks) - (idx + 1)) / rate if rate > 0 else 0
|
||||||
|
print(f" [{idx+1}/{len(all_tasks)}] "
|
||||||
|
f"成功={stats['ok']} 跳过={stats['skip']} "
|
||||||
|
f"速率={rate:.1f}张/s ETA={eta:.0f}s")
|
||||||
|
|
||||||
|
t1 = time.perf_counter()
|
||||||
|
|
||||||
|
# ---- 统计 ----
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
print(f"完成! {t1 - t0:.1f}s")
|
||||||
|
print(f" 总图: {stats['total']}")
|
||||||
|
print(f" 成功: {stats['ok']} ({stats['ok'] / max(stats['total'], 1) * 100:.1f}%)")
|
||||||
|
print(f" 跳过: {stats['skip']}")
|
||||||
|
|
||||||
|
print(f"\n 按类别:")
|
||||||
|
for key in sorted(sc):
|
||||||
|
s = sc[key]
|
||||||
|
print(f" {key:30s} ok={s['ok']:5d} skip={s['skip']:5d}")
|
||||||
|
|
||||||
|
print(f"\n 输出: {DST_DIR}")
|
||||||
|
for split in SPLITS:
|
||||||
|
sp = DST_DIR / split
|
||||||
|
if sp.exists():
|
||||||
|
jpg_n = sum(1 for _ in sp.rglob("*.jpg"))
|
||||||
|
cls_n = len([d for d in sp.iterdir() if d.is_dir()])
|
||||||
|
print(f" {split}/ : {jpg_n} 图, {cls_n} 类")
|
||||||
|
|
||||||
|
print(f"\n{'='*70}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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/`。
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# CCPD 322K YOLO 裁剪
|
||||||
|
|
||||||
|
| 项目 | 详情 |
|
||||||
|
|---|---|
|
||||||
|
| 来源 | `CCPD_322K/` 经过 YOLO 车牌检测裁剪(12%/18% 外扩) |
|
||||||
|
| 规模 | 321,122 张 |
|
||||||
|
| 生成脚本 | `scripts/pipeline_322k.py` 第二步 |
|
||||||
|
| 下游脚本 | 同脚本第三步 → `CCPD_322K_EXP_CUT_TRANS/` |
|
||||||
|
|
||||||
|
> **以脚本为准**:此目录为管线中间产物,不直接用于训练。
|
||||||
@@ -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`。
|
||||||
@@ -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。
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# CCPD Random 蓝 150K + 绿 100K
|
||||||
|
|
||||||
|
| 项目 | 详情 |
|
||||||
|
|---|---|
|
||||||
|
| 来源 | `gen_random_bg.py` 大规模随机背景合成 |
|
||||||
|
| 规模 | 250,002 张 |
|
||||||
|
| 训练用途 | 未在 OCR 训练中使用 |
|
||||||
|
|
||||||
|
> **以脚本为准**:此数据集为生成脚本的大规模产物,当前模型训练未引用。
|
||||||
@@ -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 步骤同上。
|
||||||
@@ -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
@@ -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 |
|
|
||||||
| 色温匹配 | 参考原车牌白字色温 |
|
|
||||||
| 噪声匹配 | 原车牌区域噪声σ估计 |
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 避免重复解析)
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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` 负责从多个源目录合并并转换标注格式。
|
||||||
@@ -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
|
||||||
@@ -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`。
|
||||||
@@ -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')))}")
|
||||||
@@ -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()
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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)")
|
||||||
@@ -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)")
|
||||||
Reference in new issue
Block a user