diff --git a/.gitattributes b/.gitattributes index 063d6c5..75a37e6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,5 @@ *.yml text eol=lf *.csv text eol=lf *.md text eol=lf + +CCPD_SPE_GEN/cpp_pipeline/install/*.exe filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index 7ebca97..c86fc2f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ Desktop.ini # Jupyter .ipynb_checkpoints/ + +ccpd_combined_5k5k_spe/ +output/ +CCPD_SPE/*/ + +build/ \ No newline at end of file diff --git a/CCPD_SPE/README.md b/CCPD_SPE/README.md new file mode 100644 index 0000000..e93d05a --- /dev/null +++ b/CCPD_SPE/README.md @@ -0,0 +1,91 @@ +# CCPD_SPE — 特殊车牌数据集 + +基于 CCPD 真实街景照片,将普通蓝牌替换为 9 种特殊车牌。 + +## 目录结构 + +``` +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 | +| 色温匹配 | 参考原车牌白字色温 | +| 噪声匹配 | 原车牌区域噪声σ估计 | diff --git a/CCPD_SPE_GEN/README.md b/CCPD_SPE_GEN/README.md new file mode 100644 index 0000000..4667a98 --- /dev/null +++ b/CCPD_SPE_GEN/README.md @@ -0,0 +1,83 @@ +# CCPD_SPE_GEN — 特殊车牌数据集生成工具 + +基于 CCPD 真实车辆照片,将普通蓝牌替换为 9 种特殊车牌(警车/军车/港澳/使馆/领馆/教练/新能源)。 + +--- + +## 目录结构 + +``` +CCPD_SPE_GEN/ +├── README.md +│ +├── cv_pipeline/ # 传统 CV 管线(无需 GPU/训练) +│ ├── plate_swap.py # 主体:车牌替换全流程 +│ ├── extract_real_plates.py # 从 CCPD 提取真实车牌 ROI +│ └── gen_pure_plates.py # 生成纯合成车牌(无背景) +│ +├── gan_pipeline/ # GAN 管线(CUT 纹理迁移) +│ ├── dataset.py # L 通道 DataLoader + 推理封装 +│ ├── models.py # Generator + PatchGAN + PatchNCE Loss +│ ├── train.py # 训练脚本 +│ └── requirements.txt # PyTorch 依赖 +│ +└── scripts/ # 一键运行脚本 + ├── quick_test.sh # 快速测试 20 张警车牌 + ├── preview_all.sh # 预览全部 9 种类型 + └── gan_data_prep.sh # GAN 训练数据准备 +``` + +## 两套算法 + +### CV 管线 (`cv_pipeline/`) +**经典计算机视觉,零训练成本,即开即用。** + +流程:CCPD 原图 → 解析文件名角点 → 生成特殊车牌 → Lab 色彩对齐 → 透视回贴 → 距离变换羽化 → 噪声/模糊匹配 → JPEG 退化模拟 → 输出 + +```bash +cd ICE_Dataset +python CCPD_SPE_GEN/cv_pipeline/plate_swap.py \ + --ccpd-dir ./CCPD/CCPD2019/ccpd_base \ + --plate-type police \ + --num 500 \ + --output ./output/police_dataset +``` + +### GAN 管线 (`gan_pipeline/`) +**基于 CUT (Contrastive Unpaired Translation),需 PyTorch + GPU 训练。** + +仅对车牌 L (亮度) 通道做纹理迁移,保留 AB 颜色通道不变。训练后将 GAN 推理接入 CV 管线,替换硬编码的退化模拟步骤。 + +```bash +# 训练 +cd CCPD_SPE_GEN/gan_pipeline +python train.py \ + --domain-a ../../gan_data/domain_A_synthetic/police \ + --domain-b ../../gan_data/domain_B_real \ + --epochs 100 --batch 4 \ + --output ../../output/cut_police + +# 推理 (在 CV 管线中调用) +# from CCPD_SPE_GEN.gan_pipeline.dataset import LChannelInference +``` + +## 依赖 + +| 管线 | 依赖 | +|------|------| +| CV | `numpy`, `opencv-python`, `tqdm`, `chinese_license_plate_generator/` | +| GAN | 以上 + `torch>=1.12`, `torchvision`, `tensorboard` | + +## 支持的特殊车牌类型 + +| 类型 | 底板 | 说明 | +|------|------|------| +| `police` | 白底 | 警用车牌 (末位"警"红字) | +| `army` | 白底 | 军车/武警 | +| `gangao` | 黑底 | 港澳入出境 | +| `shiguan_old` | 黑底 | 使馆旧版 ("使"红字) | +| `shiguan_new` | 黑底 | 使馆新版 (白字) | +| `lingguan_old` | 黑底 | 领馆旧版 ("领"红字) | +| `lingguan_new` | 黑底 | 领馆新版 (白字) | +| `coach` | 黄底 | 教练车 (末位"学") | +| `new_energy` | 黄绿双拼 | 大型新能源 (8 位) | diff --git a/CCPD_SPE_GEN/cpp_pipeline/CMakeLists.txt b/CCPD_SPE_GEN/cpp_pipeline/CMakeLists.txt new file mode 100644 index 0000000..7751a1e --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.16) +project(ice_plate_swap LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# OpenCV +find_package(OpenCV REQUIRED) + +# 源文件 +add_executable(plate_swap + main.cpp + plate_swap.cpp + plate_swap.hpp +) + +target_link_libraries(plate_swap ${OpenCV_LIBS}) + +# 优化选项 +if(MSVC) + target_compile_options(plate_swap PRIVATE /O2 /arch:AVX2 /utf-8) +else() + target_compile_options(plate_swap PRIVATE -O3 -march=native -fopenmp) +endif() diff --git a/CCPD_SPE_GEN/cpp_pipeline/build.ps1 b/CCPD_SPE_GEN/cpp_pipeline/build.ps1 new file mode 100644 index 0000000..2990dac --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/build.ps1 @@ -0,0 +1,42 @@ +# C++ 管线编译脚本 +# 前置条件: Visual Studio 2022 + C++ workload + OpenCV +# OpenCV 安装: 下载 https://opencv.org/releases/ 解压到 C:\opencv + +param( + [string]$OpenCVDir = "C:\opencv\build" +) + +$ErrorActionPreference = "Stop" + +Write-Host "=== ICE C++ Plate Swap 编译 ===" -ForegroundColor Cyan + +# 检查 OpenCV +if (-not (Test-Path "$OpenCVDir\OpenCVConfig.cmake")) { + Write-Host "[ERROR] OpenCV 未找到: $OpenCVDir" -ForegroundColor Red + Write-Host " 请从 https://opencv.org/releases/ 下载 Windows 版" + Write-Host " 解压到 C:\opencv 或使用 -OpenCVDir 指定路径" + exit 1 +} + +# 创建 build 目录 +$buildDir = "build" +if (Test-Path $buildDir) { Remove-Item -Recurse -Force $buildDir } +New-Item -ItemType Directory $buildDir | Out-Null + +# CMake 配置 (使用 VS 2022 生成器) +Write-Host "CMake 配置..." +cmake -B $buildDir -G "Visual Studio 17 2022" -A x64 ` + -DOpenCV_DIR="$OpenCVDir" + +# 编译 Release +Write-Host "编译 Release..." +cmake --build $buildDir --config Release --parallel + +Write-Host "" +Write-Host "=== 编译完成 ===" -ForegroundColor Green +Write-Host "可执行文件: $buildDir\Release\plate_swap.exe" + +# 使用示例 +Write-Host "" +Write-Host "用法示例:" +Write-Host " .\build\Release\plate_swap.exe --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type police --plate-pool ./output/plate_pool --num 500 --workers 32 --fast" diff --git a/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py b/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py new file mode 100644 index 0000000..ec5702e --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/gen_plate_pool.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +预生成合成车牌池 (供 C++ 管线使用) +=================================== +为每种特殊车牌类型批量生成纯车牌图,C++ 运行时直接读内存池, +避免每张 CCPD 原图都重新渲染车牌。 + +用法: + python gen_plate_pool.py --num 5000 --output ./output/plate_pool + +输出目录结构: + plate_pool/ + yellow/ 教练车牌 (440x140) + green_truck/ 新能源车牌 (480x140) + white/ 警车车牌 + white_army/ 军车车牌 + black/ 港澳/领馆车牌 + black_shi/ 使馆车牌 +""" + +import os +import sys +import argparse +from pathlib import Path + +import cv2 +import numpy as np +from tqdm import tqdm + +# 路径 +_SCRIPT_DIR = Path(__file__).resolve().parent +_PROJECT_DIR = _SCRIPT_DIR.parent.parent +_GENERATOR_DIR = _PROJECT_DIR / 'chinese_license_plate_generator' +sys.path.insert(0, str(_GENERATOR_DIR)) + +from generate_multi_plate import MultiPlateGenerator +from plate_number import ( + generate_plate_number_white, + generate_plate_number_army, + generate_plate_number_black_gangao, + generate_plate_number_black_shi_old, + generate_plate_number_black_shi_new, + generate_plate_number_black_ling_old, + generate_plate_number_black_ling_new, + generate_plate_number_yellow_xue, + generate_plate_number_blue, +) + +# 类型配置 +PLATE_TYPES = [ + ('police', 'white', generate_plate_number_white), + ('army', 'white_army', generate_plate_number_army), + ('gangao', 'black', generate_plate_number_black_gangao), + ('shiguan_old', 'black_shi', generate_plate_number_black_shi_old), + ('shiguan_new', 'black_shi', generate_plate_number_black_shi_new), + ('lingguan_old','black', generate_plate_number_black_ling_old), + ('lingguan_new','black', generate_plate_number_black_ling_new), + ('coach', 'yellow', generate_plate_number_yellow_xue), + ('new_energy', 'green_truck', lambda: generate_plate_number_blue(length=8)), +] + + +def main(): + parser = argparse.ArgumentParser(description='预生成合成车牌池') + parser.add_argument('--num', type=int, default=5000, + help='每种类型生成数量 (默认: 5000)') + parser.add_argument('--output', type=str, default='./output/plate_pool', + help='输出根目录') + args = parser.parse_args() + + # 初始化生成器 + pm = str(_GENERATOR_DIR / 'plate_model') + fm = str(_GENERATOR_DIR / 'font_model') + gen = MultiPlateGenerator(pm, fm) + + for plate_type, bg_color, num_func in PLATE_TYPES: + out_dir = Path(args.output) / bg_color + out_dir.mkdir(parents=True, exist_ok=True) + + existing = len(list(out_dir.glob('*.jpg'))) + if existing >= args.num: + print(f"[SKIP] {plate_type:12s} ({bg_color:12s}): 已有 {existing} 张") + continue + + need = args.num - existing + print(f"[GEN] {plate_type:12s} ({bg_color:12s}): {need} 张...", end=' ', flush=True) + + success = 0 + pbar = tqdm(total=need, desc=f'{plate_type}', unit='张', leave=False) + attempts = 0 + while success < need and attempts < need * 3: + attempts += 1 + try: + number = num_func() + plate = gen.generate_plate_special( + number, bg_color, is_double=False, enhance=False + ) + # 用 imencode 支持中文文件名 + fname = f"{number}.jpg" + ok, buf = cv2.imencode('.jpg', plate, [cv2.IMWRITE_JPEG_QUALITY, 95]) + if ok: + with open(str(out_dir / fname), 'wb') as f: + f.write(buf.tobytes()) + success += 1 + pbar.update(1) + except Exception: + pass + pbar.close() + print(f"OK ({success} 张)") + + print(f"\n完成! 输出: {args.output}") + # 统计 + for bg_color in ['yellow','green_truck','white','white_army','black','black_shi']: + d = Path(args.output) / bg_color + count = len(list(d.glob('*.jpg'))) if d.is_dir() else 0 + print(f" {bg_color:15s}: {count} 张") + + +if __name__ == '__main__': + import os + os.chdir(str(_PROJECT_DIR)) + main() diff --git a/CCPD_SPE_GEN/cpp_pipeline/install/README.md b/CCPD_SPE_GEN/cpp_pipeline/install/README.md new file mode 100644 index 0000000..e983f4d --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/install/README.md @@ -0,0 +1,145 @@ +# ICE Plate Swap — C++ 高性能特殊车牌替换引擎 + +基于 CCPD 真实街景照片,将普通蓝牌替换为 9 种特殊车牌。 +C++ / OpenCV 实现,32 线程满血,~53 张/秒。 + +--- + +## 快速开始 + +### 1. 环境要求 + +| 组件 | 说明 | +|------|------| +| Windows 10/11 x64 | — | +| [Visual Studio 2022 BuildTools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022) | 含 C++ 桌面开发 workload | +| [OpenCV 4.10.0](https://github.com/opencv/opencv/releases/tag/4.10.0) | 解压到 `C:\opencv` | + +### 2. 预编译版 (推荐) + +直接下载 `plate_swap.exe`(已通过 Git LFS 管理): + +```powershell +# 设置 OpenCV DLL 路径 +$env:PATH = "C:\opencv\build\x64\vc16\bin;$env:PATH" + +# 运行 +.\install\plate_swap.exe --help +``` + +### 3. 从源码编译 + +```powershell +# 打开 VS Developer Command Prompt +# 然后: +cd CCPD_SPE_GEN\cpp_pipeline +cmake -B build -G "Visual Studio 17 2022" -A x64 -DOpenCV_DIR=C:\opencv\build +cmake --build build --config Release --parallel +``` + +--- + +## 用法 + +```powershell +$env:PATH = "C:\opencv\build\x64\vc16\bin;$env:PATH" + +.\plate_swap.exe ` + --ccpd-dir ./CCPD/CCPD2019/ccpd_base,./CCPD/CCPD2019/ccpd_tilt ` + --plate-type police ` + --plate-pool ./output/plate_pool_v2 ` + --output ./CCPD_SPE ` + --num 1000 ` + --workers 24 +``` + +### 参数 + +| 参数 | 必需 | 默认 | 说明 | +|------|------|------|------| +| `--ccpd-dir` | ✅ | — | CCPD 图片目录,多个用逗号分隔 | +| `--plate-type` | ✅ | — | 特殊车牌类型 | +| `--plate-pool` | ✅ | — | 预生成车牌池路径(含 `pool.bin`) | +| `--output` | ✅ | — | 输出根目录 | +| `--num` | | 500 | 生成图片数量 | +| `--workers` | | 全部核心 | 并行线程数 | +| `--seed` | | 42 | 随机种子 | +| `--fast` | | false | 快速模式(跳过退化模拟,不建议) | +| `--h-shift` | | 0 | 底板色相偏移 (GUI调色) | +| `--s-scale` | | 100 | 底板饱和度缩放% | +| `--v-scale` | | 100 | 底板明度缩放% | + +### 支持的类型 + +``` +police 警用车牌 (白底, JING) +army 军车/武警 (白底) +gangao 港澳入出境 (黑底, GANG/AO) +shiguan_old 使馆旧版 (黑底, SHI) +shiguan_new 使馆新版 (黑底) +lingguan_old 领馆旧版 (黑底, LING) +lingguan_new 领馆新版 (黑底) +coach 教练车 (黄底, XUE) +new_energy 大型新能源 (黄绿双拼, 8位) +``` + +--- + +## 预生成车牌池 + +```powershell +cd ICE_Dataset +python CCPD_SPE_GEN\cpp_pipeline\gen_plate_pool.py --num 1000 --output ./output/plate_pool_v2 +``` + +生成 `pool.bin` (~1.6GB),含 9 种类型各 1000 张 + 车牌号元数据。 + +--- + +## 输出结构 + +``` +CCPD_SPE/ +├── ccpd_base/ +│ ├── police/ +│ ├── gangao/ +│ └── ... +├── ccpd_tilt/ +└── ... +``` + +文件名: `{CCPD原名}__{类型}__{车牌号拼音}.jpg` + +--- + +## 性能 + +| 指标 | 值 | +|------|-----| +| 单线程 | ~7 张/秒 | +| 24 线程 | ~53 张/秒 | +| 35.5 万张全量 | ~1.9 小时 | + +--- + +## 调色 GUI + +```powershell +python CCPD_SPE_GEN\cv_pipeline\color_tuner.py +``` + +H/S/V 滑块实时预览 → 按 `S` 保存 → `plate_color_profile.py` → C++ 管线 `--h-shift` `--s-scale` `--v-scale` 读取。 + +--- + +## 构建 + +```powershell +# 安装 OpenCV +Invoke-WebRequest "https://github.com/opencv/opencv/releases/download/4.10.0/opencv-4.10.0-windows.exe" -OutFile "$env:TEMP\opencv.exe" +Start-Process "$env:TEMP\opencv.exe" -Args "-oC:\opencv -y" -Wait + +# 编译 +cd CCPD_SPE_GEN\cpp_pipeline +.\build.ps1 +``` diff --git a/CCPD_SPE_GEN/cpp_pipeline/install/plate_swap.exe b/CCPD_SPE_GEN/cpp_pipeline/install/plate_swap.exe new file mode 100644 index 0000000..d3480d9 --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/install/plate_swap.exe @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1610b01c6935e48be223eba29509a7de6cb5b2a08edfc11ca08c73042762003c +size 151552 diff --git a/CCPD_SPE_GEN/cpp_pipeline/main.cpp b/CCPD_SPE_GEN/cpp_pipeline/main.cpp new file mode 100644 index 0000000..b73e3ae --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/main.cpp @@ -0,0 +1,203 @@ +#include "plate_swap.hpp" +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace std; +using namespace ice; + +// ── 配置 ── +struct Config { + vector ccpd_dirs; + string plate_type; + string output_dir; + string plate_pool_dir; + int num_images = 500; + unsigned workers = thread::hardware_concurrency(); + bool fast = false; + int seed = 42; + int h_shift = 0, s_scale = 100, v_scale = 100; +}; + +// ── 参数解析 ── +Config parse_args(int argc, char* argv[]) { + Config cfg; + cfg.workers = thread::hardware_concurrency(); + + for (int i = 1; i < argc; i++) { + string arg = argv[i]; + if (arg == "--ccpd-dir" && i+1 < argc) { + string dirs = argv[++i]; + size_t pos = 0; + while (pos < dirs.size()) { + size_t comma = dirs.find(',', pos); + string d = (comma == string::npos) ? dirs.substr(pos) : dirs.substr(pos, comma-pos); + // trim + while (!d.empty() && d.front() == ' ') d.erase(0,1); + while (!d.empty() && d.back() == ' ') d.pop_back(); + if (!d.empty()) cfg.ccpd_dirs.push_back(d); + if (comma == string::npos) break; + pos = comma + 1; + } + } + else if (arg == "--plate-type" && i+1 < argc) cfg.plate_type = argv[++i]; + else if (arg == "--output" && i+1 < argc) cfg.output_dir = argv[++i]; + else if (arg == "--plate-pool" && i+1 < argc) cfg.plate_pool_dir = 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 == "--seed" && i+1 < argc) cfg.seed = stoi(argv[++i]); + else if (arg == "--fast") cfg.fast = true; + else if (arg == "--h-shift" && i+1 < argc) cfg.h_shift = stoi(argv[++i]); + else if (arg == "--s-scale" && i+1 < argc) cfg.s_scale = stoi(argv[++i]); + else if (arg == "--v-scale" && i+1 < argc) cfg.v_scale = stoi(argv[++i]); + } + + return cfg; +} + +// ── 收集并预解析 CCPD 图片(过滤无效,预存角点) ── +struct IndexedImage { + string path; + CCPDMeta meta; +}; + +vector collect_and_index(const vector& dirs, mt19937& rng) { + vector result; + for (auto& d : dirs) { + error_code ec; + if (!fs::is_directory(d, ec)) { + cerr << "[WARN] Dir not found: " << d << endl; + continue; + } + for (auto& entry : fs::directory_iterator(d, ec)) { + if (ec) break; + if (!entry.is_regular_file()) continue; + string ext = entry.path().extension().string(); + for (auto& c : ext) c = (char)tolower(c); + if (ext != ".jpg" && ext != ".jpeg" && ext != ".png") continue; + + string fname = entry.path().filename().string(); + CCPDMeta meta = parse_ccpd_filename(fname); + if (!meta.valid) continue; + + IndexedImage ii; + ii.path = entry.path().string(); + ii.meta = meta; + result.push_back(ii); + } + } + shuffle(result.begin(), result.end(), rng); + return result; +} + +// ── 主函数 ── +int main(int argc, char* argv[]) { + Config cfg = parse_args(argc, argv); + + if (cfg.ccpd_dirs.empty() || cfg.plate_type.empty()) { + cerr << "Usage: plate_swap --ccpd-dir --plate-type [options]" << endl; + cerr << " --plate-pool Plate pool directory (required)" << endl; + cerr << " --num Number of images (default 500)" << endl; + cerr << " --workers Thread count (default all cores)" << endl; + cerr << " --fast Fast mode (skip degradation)" << endl; + return 1; + } + + cout << "=== ICE C++ Plate Swap ===" << endl; + cout << "Type: " << cfg.plate_type << endl; + cout << "Threads: " << cfg.workers << endl; + cout << "Target: " << cfg.num_images << " images" << endl; + cout << "Fast: " << (cfg.fast ? "yes" : "no") << endl; + + if (cfg.plate_pool_dir.empty()) { + cerr << "[ERROR] --plate-pool is required" << endl; + return 1; + } + + // Load plate pool + auto t0 = chrono::steady_clock::now(); + cout << "Loading plate pool..." << flush; + PlatePool pool; + string bin_path = cfg.plate_pool_dir + "/pool.bin"; + if (fs::exists(bin_path)) + pool.load_bin(bin_path); + else + pool.load(cfg.plate_pool_dir); + size_t pool_count = pool.count(cfg.plate_type); + auto t1 = chrono::steady_clock::now(); + cout << " (" << pool_count << " available, " + << chrono::duration(t1-t0).count() << "s)" << endl; + if (pool_count == 0) { + cerr << "[ERROR] No plates of type " << cfg.plate_type << " in pool" << endl; + return 1; + } + + // Collect & pre-parse CCPD (filter invalid, store corners) + mt19937 rng(cfg.seed); + cout << "Scanning & indexing CCPD..." << flush; + vector images = collect_and_index(cfg.ccpd_dirs, rng); + auto t2 = chrono::steady_clock::now(); + cout << " (" << images.size() << " valid, " + << chrono::duration(t2-t1).count() << "s)" << endl; + if (images.empty()) { + cerr << "[ERROR] No valid CCPD images found" << endl; + return 1; + } + + fs::create_directories(cfg.output_dir); + + SwapConfig swap_cfg; + swap_cfg.plate_type = cfg.plate_type; + swap_cfg.output_dir = cfg.output_dir; + swap_cfg.fast_mode = cfg.fast; + swap_cfg.h_shift = cfg.h_shift; + swap_cfg.s_scale = cfg.s_scale; + swap_cfg.v_scale = cfg.v_scale; + + // Parallel processing with pre-parsed metadata + atomic success{0}, failed{0}; + atomic idx{0}; + int total = min((int)images.size(), cfg.num_images); + mutex cout_mtx; + + auto t_proc = chrono::steady_clock::now(); + + vector threads; + for (unsigned t = 0; t < cfg.workers; t++) { + threads.emplace_back([&](unsigned seed_offset) { + mt19937 local_rng(cfg.seed + seed_offset * 10007); + + while (true) { + int i = idx.fetch_add(1); + if (i >= total) break; + + bool ok = process_single(images[i].path, swap_cfg, pool, + local_rng, &images[i].meta); + if (ok) { + int s = success.fetch_add(1) + 1; + if (s % 100 == 0 || s == total) { + lock_guard lk(cout_mtx); + cout << "\rProgress: " << s << "/" << total << flush; + } + } else { + failed.fetch_add(1); + } + } + }, t); + } + + for (auto& th : threads) th.join(); + + auto t_done = chrono::steady_clock::now(); + double elapsed = chrono::duration(t_done - t_proc).count(); + + cout << "\rDone! Success: " << success << ", Failed: " << failed << endl; + cout << "Process time: " << elapsed << "s (" << (success/max(elapsed,0.01)) << " img/s)" << endl; + cout << "Output: " << cfg.output_dir << endl; + + return 0; +} diff --git a/CCPD_SPE_GEN/cpp_pipeline/plate_swap.cpp b/CCPD_SPE_GEN/cpp_pipeline/plate_swap.cpp new file mode 100644 index 0000000..18676d7 --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/plate_swap.cpp @@ -0,0 +1,692 @@ +#include "plate_swap.hpp" +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace cv; +using namespace std; + +namespace ice { + +// ═══════════════════════════════════════════════════════════ +// CCPD 文件名解析 +// ═══════════════════════════════════════════════════════════ + +CCPDMeta parse_ccpd_filename(const string& filename) { + CCPDMeta meta; + meta.valid = false; + + // 提取不带扩展名的部分 + fs::path p(filename); + string stem = p.stem().string(); + + // 按 '-' 分割 + vector parts; + size_t pos = 0; + while (pos < stem.size()) { + size_t dash = stem.find('-', pos); + if (dash == string::npos) { + parts.push_back(stem.substr(pos)); + break; + } + parts.push_back(stem.substr(pos, dash - pos)); + pos = dash + 1; + } + + if (parts.size() < 5) return meta; + + try { + // parts[3] = 4角点 + string& coords = parts[3]; + vector pts; + size_t start = 0; + while (start < coords.size()) { + size_t us = coords.find('_', start); + string pt = (us == string::npos) ? coords.substr(start) : coords.substr(start, us - start); + size_t amp = pt.find('&'); + if (amp == string::npos) break; + float x = stof(pt.substr(0, amp)); + float y = stof(pt.substr(amp + 1)); + pts.push_back({x, y}); + if (us == string::npos) break; + start = us + 1; + } + + if (pts.size() != 4) return meta; + + // 几何排序: TL=min(x+y), TR=max(x-y), BR=max(x+y), BL=min(x-y) + float sums[4], diffs[4]; + for (int i = 0; i < 4; i++) { + sums[i] = pts[i].x + pts[i].y; + diffs[i] = pts[i].x - pts[i].y; + } + int tl = (int)(min_element(sums, sums+4) - sums); + int tr = (int)(max_element(diffs, diffs+4) - diffs); + int br = (int)(max_element(sums, sums+4) - sums); + int bl = (int)(min_element(diffs, diffs+4) - diffs); + + meta.corners[0] = pts[tl]; + meta.corners[1] = pts[tr]; + meta.corners[2] = pts[br]; + meta.corners[3] = pts[bl]; + meta.valid = true; + } catch (...) { + meta.valid = false; + } + + return meta; +} + + +// ═══════════════════════════════════════════════════════════ +// 合成车牌池 +// ═══════════════════════════════════════════════════════════ + +void PlatePool::load(const string& dir) { + // Fallback: load from directory + for (auto& entry : fs::directory_iterator(dir)) { + if (!entry.is_directory()) continue; + string bg_color = entry.path().filename().string(); + vector& vec = pools_[bg_color]; + + for (auto& f : fs::directory_iterator(entry.path())) { + if (!f.is_regular_file()) continue; + string ext = f.path().extension().string(); + if (ext != ".jpg" && ext != ".png" && ext != ".PNG") continue; + + Mat img = imread_unicode(f.path().string()); + if (img.empty()) continue; + + PlateEntry pe; + pe.image = img; + vec.push_back(pe); + } + } +} + +void PlatePool::load_bin(const string& bin_path) { + ifstream f(bin_path, ios::binary); + if (!f) { cerr << "[WARN] pool.bin not found" << endl; return; } + + char magic[4]; + f.read(magic, 4); + if (memcmp(magic, "IPPL", 4) != 0) { cerr << "[WARN] bad magic" << endl; return; } + + uint32_t num_types; + f.read((char*)&num_types, 4); + + for (uint32_t ti = 0; ti < num_types; ti++) { + uint32_t name_len; + f.read((char*)&name_len, 4); + string plate_type(name_len, 0); + f.read(&plate_type[0], name_len); + + uint32_t count; + f.read((char*)&count, 4); + + vector& vec = pools_[plate_type]; + vec.reserve(count); + + for (uint32_t pi = 0; pi < count; pi++) { + uint32_t w, h, data_len; + f.read((char*)&w, 4); + f.read((char*)&h, 4); + f.read((char*)&data_len, 4); + + vector buf(data_len); + f.read((char*)buf.data(), data_len); + + PlateEntry pe; + pe.image = Mat(h, w, CV_8UC3, buf.data()).clone(); + + // Read plate number metadata + uint32_t num_len; + f.read((char*)&num_len, 4); + pe.plate_number.resize(num_len); + f.read(&pe.plate_number[0], num_len); + + vec.push_back(pe); + } + } +} + +const PlateEntry& PlatePool::random(const string& plate_type, mt19937& rng) { + auto& vec = pools_[plate_type]; + if (vec.empty()) { + for (auto& [k, v] : pools_) { + if (!v.empty()) { + uniform_int_distribution dist(0, v.size()-1); + return v[dist(rng)]; + } + } + throw runtime_error("PlatePool is empty"); + } + uniform_int_distribution dist(0, vec.size()-1); + return vec[dist(rng)]; +} + +size_t PlatePool::count(const string& plate_type) const { + auto it = pools_.find(plate_type); + return (it != pools_.end()) ? it->second.size() : 0; +} + + +// Helper: plate_type → bg_color for color matching +static string bg_color_of(const string& plate_type) { + if (plate_type == "police") return "white"; + if (plate_type == "army") return "white_army"; + if (plate_type == "gangao") return "black"; + if (plate_type == "shiguan_old" || plate_type == "shiguan_new") return "black_shi"; + if (plate_type == "lingguan_old" || plate_type == "lingguan_new") return "black"; + if (plate_type == "coach") return "yellow"; + if (plate_type == "new_energy") return "green_truck"; + return "white"; +} + + +// ═══════════════════════════════════════════════════════════ +// Unicode 文件读写 +// ═══════════════════════════════════════════════════════════ + +Mat imread_unicode(const string& path) { + ifstream f(fs::path(path), ios::binary); + if (!f) return Mat(); + vector data((istreambuf_iterator(f)), istreambuf_iterator()); + return imdecode(data, IMREAD_COLOR); +} + +bool imwrite_unicode(const string& path, const Mat& img, const vector& params) { + vector buf; + fs::path p(path); + string ext = p.extension().string(); + if (!imencode(ext, img, buf, params)) return false; + // Use fs::path directly for proper Unicode support on Windows + ofstream f(p, ios::binary); + if (!f) return false; + f.write(reinterpret_cast(buf.data()), buf.size()); + return f.good(); +} + + +// ═══════════════════════════════════════════════════════════ +// 透视提取车牌 ROI +// ═══════════════════════════════════════════════════════════ + +Mat extract_plate_roi(const Mat& img, const Point2f corners[4], + int plate_w, int plate_h) { + Point2f dst[4] = { + {0, 0}, + {(float)(plate_w - 1), 0}, + {(float)(plate_w - 1), (float)(plate_h - 1)}, + {0, (float)(plate_h - 1)} + }; + Mat M = getPerspectiveTransform(corners, dst); + Mat roi; + warpPerspective(img, roi, M, Size(plate_w, plate_h), + INTER_LINEAR, BORDER_REPLICATE); + return roi; +} + + +// ═══════════════════════════════════════════════════════════ +// 色温匹配 +// ═══════════════════════════════════════════════════════════ + +Mat match_plate_color(const Mat& synthetic, const Mat& orig_roi, + const string& bg_color) { + Mat syn, orig; + synthetic.convertTo(syn, CV_32F); + orig_roi.convertTo(orig, CV_32F); + + // 尺寸对齐 + if (syn.size() != orig.size()) + resize(syn, syn, orig.size(), 0, 0, INTER_LINEAR); + + if (bg_color == "black" || bg_color == "black_shi") { + Mat orig_gray; + cvtColor(orig_roi, orig_gray, COLOR_BGR2GRAY); + double thresh_val; + { + Mat sorted; + orig_gray.reshape(1, 1).copyTo(sorted); + cv::sort(sorted, sorted, SORT_EVERY_ROW + SORT_ASCENDING); + int idx = (int)(sorted.cols * 0.90); + thresh_val = sorted.at(0, idx); + } + Mat white_mask = orig_gray > thresh_val; + if (countNonZero(white_mask) < 50) return synthetic; + + Scalar white_ref = mean(orig, white_mask); + float ref_mean = (float)(white_ref[0] + white_ref[1] + white_ref[2]) / 3.0f; + if (ref_mean < 10) return synthetic; + + Scalar color_cast(white_ref[0]/ref_mean, white_ref[1]/ref_mean, white_ref[2]/ref_mean); + Scalar amplified(1.0f + (color_cast[0]-1.0f), 1.0f+(color_cast[1]-1.0f), 1.0f+(color_cast[2]-1.0f)); + amplified[0] = std::clamp((double)amplified[0], 0.85, 1.15); + amplified[1] = std::clamp((double)amplified[1], 0.85, 1.15); + amplified[2] = std::clamp((double)amplified[2], 0.85, 1.15); + + Mat syn_gray; + cvtColor(synthetic, syn_gray, COLOR_BGR2GRAY); + Mat alpha; + syn_gray.convertTo(alpha, CV_32F, 1.0/128.0); + alpha = max(0.0f, min(1.0f, alpha)); + + Mat result = syn.clone(); + for (int c = 0; c < 3; c++) { + Mat ch_syn, ch_res; + extractChannel(syn, ch_syn, c); + ch_res = ch_syn.mul(1.0f + ((float)amplified[c] - 1.0f) * alpha); + ch_res.setTo(0, ch_res < 0); + ch_res.setTo(255, ch_res > 255); + insertChannel(ch_res, result, c); + } + result.convertTo(result, CV_8U); + return result; + } + + // 亮底牌:提取白参考 + Mat orig_gray; + cvtColor(orig_roi, orig_gray, COLOR_BGR2GRAY); + double thresh_val; + { + Mat sorted; + orig_gray.reshape(1, 1).copyTo(sorted); + cv::sort(sorted, sorted, SORT_EVERY_ROW + SORT_ASCENDING); + int idx = (int)(sorted.cols * 0.90); + thresh_val = sorted.at(0, idx); + } + Mat white_mask = orig_gray > thresh_val; + if (countNonZero(white_mask) < 50) return synthetic; + + Scalar white_ref = mean(orig, white_mask); + float ref_mean = (float)(white_ref[0] + white_ref[1] + white_ref[2]) / 3.0f; + if (ref_mean < 10) return synthetic; + + if (bg_color == "yellow") { + Scalar gain = white_ref / 255.0; + gain[0] = std::clamp((float)gain[0], 0.6f, 1.3f); + gain[1] = std::clamp((float)gain[1], 0.6f, 1.3f); + gain[2] = std::clamp((float)gain[2], 0.6f, 1.3f); + Mat res; + multiply(syn, gain, res); + res.setTo(0, res < 0); + res.setTo(255, res > 255); + res.convertTo(res, CV_8U); + return res; + } + + // 白/绿牌 + 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; + 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[1] = std::clamp((float)amplified[1], 0.85f, 1.15f); + amplified[2] = std::clamp((float)amplified[2], 0.85f, 1.15f); + + Mat syn_gray; + cvtColor(synthetic, syn_gray, COLOR_BGR2GRAY); + Mat alpha; + syn_gray.convertTo(alpha, CV_32F, 1.0/128.0); + alpha = max(0.0f, min(1.0f, alpha)); + + Mat result = syn.clone(); + for (int c = 0; c < 3; c++) { + Mat ch_syn, ch_res; + extractChannel(syn, ch_syn, c); + ch_res = ch_syn.mul(1.0f + (amplified[c] - 1.0f) * alpha); + ch_res.setTo(0, ch_res < 0); + ch_res.setTo(255, ch_res > 255); + insertChannel(ch_res, result, c); + } + result.convertTo(result, CV_8U); + return result; +} + + +// ═══════════════════════════════════════════════════════════ +// 底板色校正 +// ═══════════════════════════════════════════════════════════ + +Mat fix_bg_color(const Mat& plate, const string& bg_color, + int h_shift, int s_scale, int v_scale) { + if (h_shift == 0 && s_scale == 100 && v_scale == 100) + return plate; + + int h_lo = 0, h_hi = 180; + if (bg_color == "yellow") { h_lo = 15; h_hi = 50; } + else if (bg_color == "green_truck") { h_lo = 40; h_hi = 85; } + + Mat hsv; + cvtColor(plate, hsv, COLOR_BGR2HSV); + hsv.convertTo(hsv, CV_32F); + + vector ch(3); + split(hsv, ch); + Mat &H = ch[0], &S = ch[1], &V = ch[2]; + + Mat bg_mask = (H > h_lo) & (H < h_hi) & (S > 80) & (V > 100); + if (countNonZero(bg_mask) < 200) return plate; + + // 距离变换羽化 + Mat bg_u8, dist; + bg_mask.convertTo(bg_u8, CV_8U, 255); + distanceTransform(bg_u8, dist, DIST_L2, DIST_MASK_PRECISE); + Mat soft; + dist.convertTo(soft, CV_32F, 1.0/4.0); + soft = max(0.0f, min(1.0f, soft)); + + H = H + h_shift * soft; + S = S.mul(1.0f + (s_scale / 100.0f - 1.0f) * soft); + V = V.mul(1.0f + (v_scale / 100.0f - 1.0f) * soft); + + H.setTo(0, H < 0); H.setTo(180, H > 180); + S.setTo(0, S < 0); S.setTo(255, S > 255); + V.setTo(0, V < 0); V.setTo(255, V > 255); + + Mat result; + merge(ch, hsv); + hsv.convertTo(hsv, CV_8U); + cvtColor(hsv, result, COLOR_HSV2BGR); + return result; +} + + +// ═══════════════════════════════════════════════════════════ +// 透视回贴 +// ═══════════════════════════════════════════════════════════ + +pair warp_plate_back(const Mat& plate, const Point2f dst_corners[4], + Size output_size) { + int h = plate.rows, w = plate.cols; + Point2f src[4] = { + {0, 0}, {(float)(w-1), 0}, {(float)(w-1), (float)(h-1)}, {0, (float)(h-1)} + }; + + Mat M = getPerspectiveTransform(src, dst_corners); + Mat warped, mask_warped; + warpPerspective(plate, warped, M, output_size, INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0,0)); + + Mat mask(h, w, CV_8UC1, Scalar(255)); + warpPerspective(mask, mask_warped, M, output_size, INTER_LINEAR, BORDER_CONSTANT, Scalar(0)); + + return {warped, mask_warped}; +} + + +// ═══════════════════════════════════════════════════════════ +// 羽化贴合 +// ═══════════════════════════════════════════════════════════ + +Mat feather_blend(const Mat& original, const Mat& warped, + const Mat& mask, int edge_width) { + Mat mask_u8; + mask.convertTo(mask_u8, CV_8U); + + if (countNonZero(mask_u8) < 100) { + Mat result = original.clone(); + warped.copyTo(result, mask_u8); + return result; + } + + // Fast blend: erode mask slightly for edge transition, no expensive distanceTransform + Mat eroded; + int ksize = max(3, edge_width) | 1; // ensure odd + Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(ksize, ksize)); + erode(mask_u8, eroded, kernel); + + Mat alpha; + eroded.convertTo(alpha, CV_32F, 1.0/255.0); + // Soften edges with a small blur + GaussianBlur(alpha, alpha, Size(ksize, ksize), ksize/3.0); + + Mat result(original.size(), CV_32FC3); + Mat warped_f, orig_f; + warped.convertTo(warped_f, CV_32F); + original.convertTo(orig_f, CV_32F); + + for (int c = 0; c < 3; c++) { + Mat wc, oc; + extractChannel(warped_f, wc, c); + extractChannel(orig_f, oc, c); + Mat rc = wc.mul(alpha) + oc.mul(1.0f - alpha); + rc.setTo(0, rc < 0); + rc.setTo(255, rc > 255); + insertChannel(rc, result, c); + } + result.convertTo(result, CV_8U); + return result; +} + + +// ═══════════════════════════════════════════════════════════ +// 噪声匹配 +// ═══════════════════════════════════════════════════════════ + +Mat match_noise(const Mat& blended, const Mat& original, const Mat& mask) { + Mat mask_u8; + mask.convertTo(mask_u8, CV_8U); + if (countNonZero(mask_u8) < 100) return blended; + + // Crop plate region for noise estimation (like Python's plate_crop) + vector pts; + findNonZero(mask_u8, pts); + if (pts.size() < 200) return blended; + + int bw = blended.cols, bh = blended.rows; + int xmin = bw, xmax = 0, ymin = bh, ymax = 0; + for (auto& p : pts) { + if (p.x < xmin) xmin = p.x; + if (p.x > xmax) xmax = p.x; + if (p.y < ymin) ymin = p.y; + if (p.y > ymax) ymax = p.y; + } + int x1 = max(0, xmin - 5), x2 = min(bw, xmax + 5); + int y1 = max(0, ymin - 5), y2 = min(bh, ymax + 5); + Mat plate_crop = original(Rect(x1, y1, x2-x1, y2-y1)); + + // 1) Noise sigma from original plate crop + Mat crop_blurred, crop_residual; + GaussianBlur(plate_crop, crop_blurred, Size(5,5), 1.0); + Mat crop_f32, blur_f32; + plate_crop.convertTo(crop_f32, CV_32F); + crop_blurred.convertTo(blur_f32, CV_32F); + crop_residual = crop_f32 - blur_f32; + Scalar stddev; + meanStdDev(crop_residual, Scalar(), stddev); + float noise_sigma = (float)max(stddev[0], max(stddev[1], max(stddev[2], 1.5))); + + // 2) Blur sigma from original plate crop + Mat crop_gray; + cvtColor(plate_crop, crop_gray, COLOR_BGR2GRAY); + Mat lap; + Laplacian(crop_gray, lap, CV_64F); + Scalar lap_m, lap_s; + meanStdDev(lap, lap_m, lap_s); + float lap_var = (float)(lap_s[0] * lap_s[0]); + float blur_sigma = 0; + if (lap_var < 50) blur_sigma = 2.0f; + else if (lap_var < 200) blur_sigma = 1.2f; + else if (lap_var < 500) blur_sigma = 0.5f; + + // Apply blur to plate area + Mat result = blended.clone(); + if (blur_sigma > 0.1f) { + Mat blurred_full; + GaussianBlur(result, blurred_full, Size(0,0), blur_sigma); + blurred_full.copyTo(result, mask_u8); + } + + // Add noise to plate area only + Mat result_f; + result.convertTo(result_f, CV_32F); + Mat noise(result.size(), CV_32FC3); + randn(noise, Scalar(0,0,0), Scalar(noise_sigma, noise_sigma, noise_sigma)); + Mat noisy = result_f + noise; + + // Alpha blend: noise in plate area, clean outside + Mat alpha; + mask_u8.convertTo(alpha, CV_32F, 1.0/255.0); + Mat alpha_3; + merge(vector{alpha, alpha, alpha}, alpha_3); + Mat one_minus_a; + subtract(Scalar::all(1.0), alpha_3, one_minus_a); + noisy = noisy.mul(alpha_3) + result_f.mul(one_minus_a); + + // Clip AFTER blend (matching Python np.clip order) + noisy.setTo(0, noisy < 0); + noisy.setTo(255, noisy > 255); + noisy.convertTo(result, CV_8U); + return result; +} + +Mat degrade_plate(const Mat& plate) { + Mat result = plate.clone(); + + // JPEG 极限压缩 + vector buf; + imencode(".jpg", result, buf, {IMWRITE_JPEG_QUALITY, 2}); + result = imdecode(buf, IMREAD_COLOR); + + // 降采样升采样 + int w = result.cols, h = result.rows; + Mat small; + resize(result, small, Size(w/4, h/4), 0, 0, INTER_LINEAR); + resize(small, result, Size(w, h), 0, 0, INTER_NEAREST); + + // 模糊 + GaussianBlur(result, result, Size(5,5), 1.5); + + return result; +} + + +// ═══════════════════════════════════════════════════════════ +// 单张处理 +// ═══════════════════════════════════════════════════════════ + +bool process_single(const string& img_path, const SwapConfig& cfg, + PlatePool& pool, mt19937& rng, const CCPDMeta* pre_meta) { + // 1. 解析文件名 (use pre-parsed if available) + fs::path fp(img_path); + CCPDMeta meta; + if (pre_meta && pre_meta->valid) { + meta = *pre_meta; + } else { + meta = parse_ccpd_filename(fp.filename().string()); + if (!meta.valid) return false; + } + + // 2. 读原图 + Mat img_orig = imread_unicode(img_path); + if (img_orig.empty()) return false; + + int h_img = img_orig.rows, w_img = img_orig.cols; + + // 校验角点 + for (int i = 0; i < 4; i++) { + auto& c = meta.corners[i]; + if (c.x < 0 || c.y < 0 || c.x > w_img*1.5 || c.y > h_img*1.5) + return false; + } + + // 3. Pick from pool + string bg_color = bg_color_of(cfg.plate_type); + const PlateEntry& entry = pool.random(cfg.plate_type, rng); + Mat synthetic = entry.image.clone(); + int plate_w = synthetic.cols, plate_h = synthetic.rows; + string plate_number = entry.plate_number; + + // 4. Extract original ROI + Mat orig_roi = extract_plate_roi(img_orig, meta.corners, plate_w, plate_h); + + + // 5. Fix bg color + synthetic = fix_bg_color(synthetic, bg_color, + cfg.h_shift, cfg.s_scale, cfg.v_scale); + + // 6. Color match + Mat matched = match_plate_color(synthetic, orig_roi, bg_color); + + // 7. Brightness match + Mat orig_gray, matched_gray; + cvtColor(orig_roi, orig_gray, COLOR_BGR2GRAY); + cvtColor(matched, matched_gray, COLOR_BGR2GRAY); + + auto p95 = [](const Mat& m) -> float { + Mat sorted; + m.reshape(1,1).copyTo(sorted); + cv::sort(sorted, sorted, SORT_EVERY_ROW + SORT_ASCENDING); + return sorted.at(0, (int)(sorted.cols * 0.95)); + }; + float target_w = p95(orig_gray); + float current_w = p95(matched_gray); + + if (current_w > 10 && abs(target_w - current_w) > 10) { + float l_ratio = target_w / current_w; + 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; } + 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); + + Mat alpha; + matched_gray.convertTo(alpha, CV_32F, 1.0/128.0); + alpha = max(0.3f, min(1.0f, alpha)); + + Mat matched_f; + matched.convertTo(matched_f, CV_32F); + for (int c = 0; c < 3; c++) { + Mat ch; + extractChannel(matched_f, ch, c); + ch = ch.mul(1.0f + (l_ratio - 1.0f) * alpha); + ch.setTo(0, ch < 0); ch.setTo(255, ch > 255); + insertChannel(ch, matched_f, c); + } + matched_f.convertTo(matched, CV_8U); + } + + // 8. Degradation + if (!cfg.fast_mode) + matched = degrade_plate(matched); + + // 9-11. Warp + blend + noise + auto [warped, mask] = warp_plate_back(matched, meta.corners, Size(w_img, h_img)); + Mat blended = feather_blend(img_orig, warped, mask); + Mat final = match_noise(blended, img_orig, mask); + + // 12. Save with pinyin tags + string stem = fp.stem().string(); + // Convert Chinese plate markers to ASCII tags + 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(); + fs::path out_dir = fs::path(cfg.output_dir) / ccpd_subdir / cfg.plate_type; + fs::create_directories(out_dir); + + return imwrite_unicode((out_dir / out_name).string(), final, {IMWRITE_JPEG_QUALITY, 75}); +} + +} // namespace ice diff --git a/CCPD_SPE_GEN/cpp_pipeline/plate_swap.hpp b/CCPD_SPE_GEN/cpp_pipeline/plate_swap.hpp new file mode 100644 index 0000000..b5d9f64 --- /dev/null +++ b/CCPD_SPE_GEN/cpp_pipeline/plate_swap.hpp @@ -0,0 +1,80 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace ice { + +// ── CCPD 角点解析 ── +struct CCPDMeta { + cv::Point2f corners[4]; // TL, TR, BR, BL + bool valid = false; +}; +CCPDMeta parse_ccpd_filename(const std::string& filename); + +// ── 合成车牌池 ── +struct PlateEntry { + cv::Mat image; // BGR + std::string plate_number; // 车牌号 (e.g. "粤Z1234港") +}; +class PlatePool { +public: + void load(const std::string& dir); + void load_bin(const std::string& bin_path); + const PlateEntry& random(const std::string& plate_type, std::mt19937& rng); + size_t count(const std::string& plate_type) const; +private: + std::unordered_map> pools_; +}; + +// ── 管线核心 ── +struct SwapConfig { + std::string plate_type; + std::string output_dir; + int h_shift = 0, s_scale = 100, v_scale = 100; + bool fast_mode = false; +}; + +// 单张处理 (pre-parsed meta 避免重复解析) +bool process_single(const std::string& img_path, + const SwapConfig& cfg, + PlatePool& pool, + std::mt19937& rng, + const CCPDMeta* pre_meta = nullptr); + +// 透视提取车牌ROI +cv::Mat extract_plate_roi(const cv::Mat& img, const cv::Point2f corners[4], + int plate_w, int plate_h); + +// 色温匹配 +cv::Mat match_plate_color(const cv::Mat& synthetic, const cv::Mat& orig_roi, + const std::string& bg_color); + +// 底板色校正 +cv::Mat fix_bg_color(const cv::Mat& plate, const std::string& bg_color, + int h_shift, int s_scale, int v_scale); + +// 透视回贴: 返回 (warped, mask) +std::pair warp_plate_back(const cv::Mat& plate, + const cv::Point2f dst_corners[4], + cv::Size output_size); + +// 羽化贴合 +cv::Mat feather_blend(const cv::Mat& original, const cv::Mat& warped, + const cv::Mat& mask, int edge_width = 8); + +// 噪声匹配 +cv::Mat match_noise(const cv::Mat& blended, const cv::Mat& original, + const cv::Mat& mask); + +// 退化模拟 +cv::Mat degrade_plate(const cv::Mat& plate); + +// ── 工具 ── +cv::Mat imread_unicode(const std::string& path); +bool imwrite_unicode(const std::string& path, const cv::Mat& img, + const std::vector& params = {}); + +} // namespace ice diff --git a/CCPD_SPE_GEN/cv_pipeline/color_tuner.py b/CCPD_SPE_GEN/cv_pipeline/color_tuner.py new file mode 100644 index 0000000..a781601 --- /dev/null +++ b/CCPD_SPE_GEN/cv_pipeline/color_tuner.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +车牌底板色手动调色 GUI +====================== +用 HSV 滑块实时调整合成车牌底板色,预览效果,保存参数供管线调用。 + +用法: + python color_tuner.py + +控件: + H 偏移 — 色相偏移量 (-30 ~ +30) + S 缩放%% — 饱和度缩放 (10% ~ 150%) + V 缩放%% — 明度缩放 (10% ~ 150%) + [1-9] — 切换车牌类型 + [R] — 复位 + [S] — 保存当前参数到 plate_color_profile.py + [ESC] — 退出 +""" + +import sys +from pathlib import Path + +import cv2 +import numpy as np + +# ── 路径 ── +_SCRIPT_DIR = Path(__file__).resolve().parent +_PROJECT_DIR = _SCRIPT_DIR.parent.parent +_GENERATOR_DIR = _PROJECT_DIR / 'chinese_license_plate_generator' +sys.path.insert(0, str(_GENERATOR_DIR)) + +from generate_multi_plate import MultiPlateGenerator +from plate_number import ( + generate_plate_number_white, + generate_plate_number_army, + generate_plate_number_black_gangao, + generate_plate_number_black_shi_old, + generate_plate_number_black_shi_new, + generate_plate_number_black_ling_old, + generate_plate_number_black_ling_new, + generate_plate_number_yellow_xue, + generate_plate_number_blue, +) + +# ── 车牌类型 ── +PLATE_TYPES = [ + ('yellow', '教练车', generate_plate_number_yellow_xue), + ('green_truck','新能源', lambda: generate_plate_number_blue(length=8)), + ('white', '警车', generate_plate_number_white), + ('white_army', '军车', generate_plate_number_army), + ('black', '港澳', generate_plate_number_black_gangao), + ('black_shi', '使馆旧', generate_plate_number_black_shi_old), + ('black_shi', '使馆新', generate_plate_number_black_shi_new), + ('black', '领馆旧', generate_plate_number_black_ling_old), + ('black', '领馆新', generate_plate_number_black_ling_new), +] + +# ── 参考图 ── +REF_PATH = Path(r"C:\Users\Kirl\Downloads\R-C (3).jpg") + +# ── 全局状态 ── +generator = None +current_type_idx = 0 +synthetic_plate = None +ref_image = None + +# 各类型的默认参数 (H偏移, S缩放%, V缩放%) +params = {} + + +def init_generator(): + global generator + pm = str(_GENERATOR_DIR / 'plate_model') + fm = str(_GENERATOR_DIR / 'font_model') + generator = MultiPlateGenerator(pm, fm) + + +def load_ref(): + global ref_image + if REF_PATH.is_file(): + ref = cv2.imread(str(REF_PATH)) + if ref is not None: + h, w = ref.shape[:2] + ref = cv2.resize(ref, (int(w * 0.6), int(h * 0.6))) + ref_image = ref + + +def generate_plate(type_idx): + global synthetic_plate + bg_color, label, gen_func = PLATE_TYPES[type_idx] + number = gen_func() + plate_w = 480 if len(number) == 8 else 440 + plate = generator.generate_plate_special(number, bg_color, is_double=False, enhance=False) + synthetic_plate = plate + return plate, number, label + + +def load_params(bg_color): + """从已保存的 profile 加载参数""" + key = bg_color + if key not in params: + params[key] = {'h_shift': 0, 's_scale': 100, 'v_scale': 100} + return params[key] + + +def adjust_plate(plate_bgr, bg_color, h_shift, s_scale, v_scale): + """ + 在 HSV 空间调整底板色。 + 仅作用于底板区域(高S + 对应色相范围 + 高V),字符不受影响。 + """ + if h_shift == 0 and s_scale == 100 and v_scale == 100: + return plate_bgr.copy() + + hue_ranges = { + 'yellow': (15, 50), 'green_truck': (40, 85), + 'white': (0, 180), 'white_army': (0, 180), + 'black': (0, 180), 'black_shi': (0, 180), + } + h_lo, h_hi = hue_ranges.get(bg_color, (0, 180)) + + hsv = cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) + h, s, v = hsv[:, :, 0], hsv[:, :, 1], hsv[:, :, 2] + + # 底板区域 mask + hue_in = (h > h_lo) & (h < h_hi) + bg_mask = hue_in & (s > 80) & (v > 100) + + if bg_mask.sum() < 200: + return plate_bgr.copy() + + # 羽化 + bg_u8 = bg_mask.astype(np.uint8) * 255 + dist = cv2.distanceTransform(bg_u8, cv2.DIST_L2, cv2.DIST_MASK_PRECISE) + soft = np.clip(dist / 4.0, 0.0, 1.0) + + h_new = h + h_shift * soft + s_new = s * (1.0 + (s_scale / 100.0 - 1.0) * soft) + v_new = v * (1.0 + (v_scale / 100.0 - 1.0) * soft) + + hsv[:, :, 0] = np.clip(h_new, 0, 180) + hsv[:, :, 1] = np.clip(s_new, 0, 255) + hsv[:, :, 2] = np.clip(v_new, 0, 255) + + return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) + + +def refresh(): + global synthetic_plate + _, number, label = generate_plate(current_type_idx) + update_display() + + +def update_display(*_args): + bg_color, label, _ = PLATE_TYPES[current_type_idx] + p = load_params(bg_color) + + # trackbar 可能在创建过程中尚未就绪,容错读取 + try: + h_shift = cv2.getTrackbarPos('H offset', 'Color Tuner') - 30 + s_scale = cv2.getTrackbarPos('S scale', 'Color Tuner') + v_scale = cv2.getTrackbarPos('V scale', 'Color Tuner') + except cv2.error: + h_shift = p.get('h_shift', 0) + s_scale = p.get('s_scale', 100) + v_scale = p.get('v_scale', 100) + + p['h_shift'] = h_shift + p['s_scale'] = s_scale + p['v_scale'] = v_scale + + adjusted = adjust_plate(synthetic_plate, bg_color, h_shift, s_scale, v_scale) + + # 并排显示:原始 | 调整后 + h, w = synthetic_plate.shape[:2] + display = np.hstack([synthetic_plate, adjusted]) + + # 加标签 + cv2.putText(display, f"Original ({bg_color})", (10, 25), + cv2.FONT_HERSHEY_SIMPLEX, 0.55, (200, 200, 200), 1) + cv2.putText(display, f"Adjusted H:{h_shift:+d} S:{s_scale} V:{v_scale}", + (w + 10, 25), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 200), 1) + + # 类型标签 + cv2.putText(display, f"[{current_type_idx+1}/9] {label}", + (10, display.shape[0] - 10), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (180, 180, 180), 1) + + # 如果参考图存在,放在下方 + if ref_image is not None: + rw = display.shape[1] + rh = ref_image.shape[0] * rw // ref_image.shape[1] + ref_rs = cv2.resize(ref_image, (rw, rh)) + display = np.vstack([display, np.ones((4, rw, 3), dtype=np.uint8) * 30, ref_rs]) + cv2.putText(display, "Reference", (10, display.shape[0] - rh + 25), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1) + + cv2.imshow('Color Tuner', display) + + +def save_params(): + """保存当前所有类型的参数到 plate_color_profile.py""" + profile_path = _SCRIPT_DIR / 'plate_color_profile.py' + lines = ['# Auto-generated by color_tuner.py', + '# Plate background color correction profiles', + '# Format: (h_shift, s_scale_percent, v_scale_percent)', + '', + 'PLATE_COLOR_PROFILE = {'] + for bg_color in ['yellow', 'green_truck', 'white', 'white_army', 'black', 'black_shi']: + 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('}') + lines.append('') + + profile_path.write_text('\n'.join(lines), encoding='utf-8') + print(f"\n[SAVED] {profile_path}") + print(f" Params: { {k: v for k, v in params.items()} }") + + +def on_key(key): + global current_type_idx, synthetic_plate + if ord('1') <= key <= ord('9'): + idx = key - ord('1') + if idx < len(PLATE_TYPES): + current_type_idx = idx + # 切换类型时复位 trackbar 到该类型已保存的参数 + bg_color = PLATE_TYPES[current_type_idx][0] + p = load_params(bg_color) + 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'): + bg_color = PLATE_TYPES[current_type_idx][0] + params[bg_color] = {'h_shift': 0, 's_scale': 100, 'v_scale': 100} + # Reset trackbars + cv2.setTrackbarPos('H offset', 'Color Tuner', 30) + cv2.setTrackbarPos('S scale', 'Color Tuner', 100) + cv2.setTrackbarPos('V scale', 'Color Tuner', 100) + update_display() + elif key == ord('s') or key == ord('S'): + save_params() + elif key == ord('n') or key == ord('N'): + refresh() + + +def main(): + global ref_image + init_generator() + load_ref() + + # 初始化第一张车牌 + _, number, label = generate_plate(current_type_idx) + + # 先建窗口和滑动条,再刷新显示 + cv2.namedWindow('Color Tuner', cv2.WINDOW_NORMAL) + cv2.resizeWindow('Color Tuner', 1000, 500) + + bg_color = PLATE_TYPES[current_type_idx][0] + p = load_params(bg_color) + cv2.createTrackbar('H offset', 'Color Tuner', p['h_shift'] + 30, 60, update_display) + cv2.createTrackbar('S scale', 'Color Tuner', p['s_scale'], 150, update_display) + cv2.createTrackbar('V scale', 'Color Tuner', p['v_scale'], 150, update_display) + + update_display() + + print("=" * 60) + print(" 车牌底板色手动调色 GUI") + print("=" * 60) + print(" 按键:") + print(" 1-9 切换车牌类型") + print(" R 复位当前类型参数") + print(" N 生成新随机车牌") + print(" S 保存参数到 plate_color_profile.py") + print(" ESC 退出") + print("=" * 60) + print(f" 当前类型: {PLATE_TYPES[current_type_idx][1]}") + print("=" * 60) + + while True: + key = cv2.waitKey(50) & 0xFF + if key == 27: # ESC + break + if key != 255: + on_key(key) + + cv2.destroyAllWindows() + + +if __name__ == '__main__': + import os + os.chdir(str(_PROJECT_DIR)) + main() diff --git a/tools/extract_real_plates.py b/CCPD_SPE_GEN/cv_pipeline/extract_real_plates.py similarity index 100% rename from tools/extract_real_plates.py rename to CCPD_SPE_GEN/cv_pipeline/extract_real_plates.py diff --git a/CCPD_SPE_GEN/cv_pipeline/gen_pure_plates.py b/CCPD_SPE_GEN/cv_pipeline/gen_pure_plates.py new file mode 100644 index 0000000..460c4cf --- /dev/null +++ b/CCPD_SPE_GEN/cv_pipeline/gen_pure_plates.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +生成纯合成车牌(无场景背景)作为 CUT Domain A +============================================ +独立于 CCPD 原图,直接调用车牌生成器渲染纯车牌图像。 +""" +import sys +import os +import random +from pathlib import Path + +import cv2 +import numpy as np + +# 路径解析:定位 chinese_license_plate_generator +_SCRIPT_DIR = Path(__file__).resolve().parent # .../CCPD_SPE_GEN/cv_pipeline/ +_PROJECT_DIR = _SCRIPT_DIR.parent.parent # .../ICE_Dataset/ +_GENERATOR_DIR = _PROJECT_DIR / 'chinese_license_plate_generator' + +if str(_GENERATOR_DIR) not in sys.path: + sys.path.insert(0, str(_GENERATOR_DIR)) + +from generate_multi_plate import MultiPlateGenerator + +random.seed(42) +np.random.seed(42) + +g = MultiPlateGenerator( + str(_GENERATOR_DIR / 'plate_model'), + str(_GENERATOR_DIR / 'font_model') +) + +out = 'gan_data/domain_A_synthetic/police_pure' +os.makedirs(out, exist_ok=True) + +for i in range(500): + num, bg, _ = g.generate_plate_number() + # 强制police类型 + if '警' not in num: + num = num[:6] + '警' + img = g.generate_plate_special(num, 'white', False) + cv2.imwrite(f'{out}/synth_{i:04d}.jpg', img) + +print(f'Done: {out} ({len(os.listdir(out))} images)') diff --git a/CCPD_SPE_GEN/cv_pipeline/plate_color_profile.py b/CCPD_SPE_GEN/cv_pipeline/plate_color_profile.py new file mode 100644 index 0000000..17fe241 --- /dev/null +++ b/CCPD_SPE_GEN/cv_pipeline/plate_color_profile.py @@ -0,0 +1,12 @@ +# Auto-generated by color_tuner.py +# Plate background color correction profiles +# Format: (h_shift, s_scale_percent, v_scale_percent) + +PLATE_COLOR_PROFILE = { + 'yellow': (-2, 150, 100), + 'green_truck': (0, 100, 100), + 'white': (0, 100, 100), + 'white_army': (0, 100, 100), + 'black': (0, 100, 100), + 'black_shi': (0, 100, 100), +} diff --git a/tools/plate_swap_pipeline.py b/CCPD_SPE_GEN/cv_pipeline/plate_swap.py similarity index 79% rename from tools/plate_swap_pipeline.py rename to CCPD_SPE_GEN/cv_pipeline/plate_swap.py index 01343c4..40f024b 100644 --- a/tools/plate_swap_pipeline.py +++ b/CCPD_SPE_GEN/cv_pipeline/plate_swap.py @@ -57,8 +57,8 @@ from tqdm import tqdm # ============================================================ # 路径解析:定位 chinese_license_plate_generator # ============================================================ -_SCRIPT_DIR = Path(__file__).resolve().parent # ICE_Dataset/tools/ -_PROJECT_DIR = _SCRIPT_DIR.parent # ICE_Dataset/ +_SCRIPT_DIR = Path(__file__).resolve().parent # .../CCPD_SPE_GEN/cv_pipeline/ +_PROJECT_DIR = _SCRIPT_DIR.parent.parent # .../ICE_Dataset/ _GENERATOR_DIR = _PROJECT_DIR / 'chinese_license_plate_generator' if str(_GENERATOR_DIR) not in sys.path: @@ -77,6 +77,34 @@ from plate_number import ( generate_plate_number_blue, ) + +# ============================================================ +# Windows Unicode 文件读写 (cv2.imread/imwrite 不支持中文路径) +# ============================================================ + +def _imread_unicode(path: str, flags: int = cv2.IMREAD_COLOR) -> Optional[np.ndarray]: + """cv2.imread 的 Unicode 安全替代""" + try: + with open(path, 'rb') as f: + data = f.read() + return cv2.imdecode(np.frombuffer(data, np.uint8), flags) + except Exception: + return None + + +def _imwrite_unicode(path: str, img: np.ndarray, params=None) -> bool: + """cv2.imwrite 的 Unicode 安全替代""" + try: + 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 True + return False + except Exception: + return False + # ============================================================ # 特殊车牌类型配置 # ============================================================ @@ -375,10 +403,6 @@ def match_plate_color(synthetic_plate: np.ndarray, result = syn * gain return np.clip(result, 0, 255).astype(np.uint8) - # 白牌 / 绿牌:亮度加权色温 - result = syn * gain - return np.clip(result, 0, 255).astype(np.uint8) - # 白牌 / 绿牌:亮度加权色温 color_cast = white_ref / ref_mean strength_map = { @@ -430,6 +454,76 @@ def _estimate_plate_bg_mask(plate_gray: np.ndarray, bg_color: str) -> np.ndarray return bg_mask.astype(bool) +def fix_plate_bg_color(plate_bgr: np.ndarray, bg_color: str) -> np.ndarray: + """ + 校正合成车牌底板色。 + + 优先从 plate_color_profile.py 读取手动调色参数; + 若无配置文件则跳过(原样返回)。 + + Args: + plate_bgr: 合成车牌 (BGR) + bg_color: 底板颜色标识 + + Returns: + 底板色校正后的车牌 (BGR) + """ + # 尝试加载调色 profile + try: + profile_path = _SCRIPT_DIR / 'plate_color_profile.py' + if not profile_path.is_file(): + return plate_bgr + + import importlib.util + spec = importlib.util.spec_from_file_location('plate_color_profile', str(profile_path)) + profile_mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(profile_mod) + profile = profile_mod.PLATE_COLOR_PROFILE + except Exception: + return plate_bgr + + cfg = profile.get(bg_color) + if cfg is None: + return plate_bgr + + h_shift, s_scale, v_scale = cfg + if h_shift == 0 and s_scale == 100 and v_scale == 100: + return plate_bgr + + # 色相范围 + hue_ranges = { + 'yellow': (15, 50), 'green_truck': (40, 85), + 'white': (0, 180), 'white_army': (0, 180), + 'black': (0, 180), 'black_shi': (0, 180), + } + h_lo, h_hi = hue_ranges.get(bg_color, (0, 180)) + + hsv = cv2.cvtColor(plate_bgr, cv2.COLOR_BGR2HSV).astype(np.float32) + h, s, v = hsv[:, :, 0], hsv[:, :, 1], hsv[:, :, 2] + + # 底板区域 mask + hue_in = (h > h_lo) & (h < h_hi) + bg_mask = hue_in & (s > 80) & (v > 100) + + if bg_mask.sum() < 200: + return plate_bgr + + # 羽化 + bg_u8 = bg_mask.astype(np.uint8) * 255 + dist = cv2.distanceTransform(bg_u8, cv2.DIST_L2, cv2.DIST_MASK_PRECISE) + soft = np.clip(dist / 4.0, 0.0, 1.0) + + h_new = h + h_shift * soft + s_new = s * (1.0 + (s_scale / 100.0 - 1.0) * soft) + v_new = v * (1.0 + (v_scale / 100.0 - 1.0) * soft) + + hsv[:, :, 0] = np.clip(h_new, 0, 180) + hsv[:, :, 1] = np.clip(s_new, 0, 255) + hsv[:, :, 2] = np.clip(v_new, 0, 255) + + return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR) + + # ============================================================ # 透视变换回贴 # ============================================================ @@ -641,6 +735,30 @@ def match_image_quality(result: np.ndarray, return result +# ============================================================ +# 多进程 Worker(模块级,每个进程初始化一次 Pipeline) +# ============================================================ + +_worker_pipeline = None + +def _worker_init(plate_type, fast): + """进程初始化:每个 worker 启动时调用一次(加载字体、底板缓存)""" + global _worker_pipeline + _worker_pipeline = PlateSwapPipeline(plate_type=plate_type) + _worker_pipeline._fast_mode = fast + + +def _worker_process_single(args): + """处理单张图片(复用进程级 Pipeline,无需重复加载)""" + global _worker_pipeline + img_path, output_dir, preview = args + try: + return _worker_pipeline.process_single( + img_path, output_dir, preview=preview) is not None + except Exception: + return False + + # ============================================================ # 主管线 # ============================================================ @@ -673,6 +791,9 @@ class PlateSwapPipeline: font_dir = str(_GENERATOR_DIR / 'font_model') self.generator = MultiPlateGenerator(plate_model_dir, font_dir) + # 快速模式标志(多进程时由 _worker_process_single 设置) + self._fast_mode = False + def generate_plate_number(self) -> str: """生成随机特殊车牌号码""" return self.number_generator() @@ -700,7 +821,7 @@ class PlateSwapPipeline: return None # ---- 2. 读取原图 ---- - img_orig = cv2.imread(image_path) + img_orig = _imread_unicode(image_path) if img_orig is None: return None @@ -725,6 +846,9 @@ class PlateSwapPipeline: new_plate_number, self.bg_color, is_double=False, enhance=False ) + # ---- 5b. 底板色校正(若 plate_color_profile.py 存在则生效) ---- + synthetic_plate = fix_plate_bg_color(synthetic_plate, self.bg_color) + # ---- 6. 色温滤镜 ---- matched_plate = match_plate_color(synthetic_plate, orig_plate_roi, self.bg_color) @@ -741,8 +865,10 @@ class PlateSwapPipeline: clip_cfg = { 'white': (0.5, 1.1), 'white_army': (0.5, 1.1), 'green_truck': (0.4, 1.0), + 'yellow': (0.4, 1.0), + 'black': (0.3, 1.2), 'black_shi': (0.3, 1.2), } - min_c, max_c = clip_cfg.get(self.bg_color, (0.15, 1.5)) + min_c, max_c = clip_cfg.get(self.bg_color, (0.5, 1.1)) l_ratio = np.clip(l_ratio, min_c, max_c) # 仅对亮像素(白字)全量叠加,底色部分减半以保护本色 @@ -753,15 +879,16 @@ class PlateSwapPipeline: matched_plate = (matched_plate.astype(np.float32) * effective_ratio) matched_plate = np.clip(matched_plate, 0, 255).astype(np.uint8) - # ---- 8. 强退化模拟(原图模糊+马赛克) ---- - # JPEG 极限压缩 - matched_plate = jpeg_compress(matched_plate, quality=2) - # 降采样→升采样 模拟马赛克/像素化 - h_p, w_p = matched_plate.shape[:2] - 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.GaussianBlur(matched_plate, (5, 5), 1.5) + # ---- 8. 退化模拟(快速模式下跳过) ---- + if not self._fast_mode: + # JPEG 极限压缩 + matched_plate = jpeg_compress(matched_plate, quality=2) + # 降采样→升采样 模拟马赛克/像素化 + h_p, w_p = matched_plate.shape[:2] + 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.GaussianBlur(matched_plate, (5, 5), 1.5) # ---- 9. 透视变换回贴 ---- warped, plate_mask = warp_plate_back( @@ -778,7 +905,7 @@ class PlateSwapPipeline: basename = Path(filename).stem out_name = f"{basename}_{self.plate_type}_{new_plate_number}.jpg" out_path = os.path.join(output_dir, out_name) - cv2.imwrite(out_path, final, [cv2.IMWRITE_JPEG_QUALITY, 95]) + _imwrite_unicode(out_path, final, [cv2.IMWRITE_JPEG_QUALITY, 95]) # ---- 可选: 保存调试中间结果 ---- if preview: @@ -799,7 +926,9 @@ class PlateSwapPipeline: output_dir: str, num_images: int = 500, preview: bool = False, - seed: int = 42): + seed: int = 42, + workers: int = 1, + fast: bool = False): """ 批量处理 CCPD 图片目录。 @@ -809,6 +938,8 @@ class PlateSwapPipeline: num_images: 生成图片总数 preview: 是否保存调试中间结果 seed: 随机种子 + workers: 并行进程数 (1=单进程, >1=多进程) + fast: 快速模式 (跳过退化模拟步骤) """ random.seed(seed) np.random.seed(seed) @@ -829,38 +960,88 @@ class PlateSwapPipeline: print("[ERROR] 未找到任何 CCPD 图片") return - # 随机排列 random.shuffle(all_images) + image_paths = [str(p) for p in all_images[:num_images * 3]] # 多取一些备用 print(f"特殊车牌类型: {self.config['description']} ({self.plate_type})") print(f"底板颜色: {self.bg_color}") print(f"CCPD 图片总数: {len(all_images)}") print(f"目标生成数: {num_images}") print(f"输出目录: {output_dir}") + print(f"并行进程: {workers}" + (" (多核加速)" if workers > 1 else "")) + if fast: + print(f"快速模式: 跳过退化模拟") print("-" * 60) - success = 0 - failed = 0 - pbar = tqdm(total=num_images, desc='生成特殊车牌', unit='张') + import time + t0 = time.perf_counter() - for img_path in all_images: - if success >= num_images: - break - - result = self.process_single( - str(img_path), output_dir, preview=(preview and success < 10) + if workers <= 1: + # 单进程模式 + success, failed = self._process_sequential( + image_paths, output_dir, num_images, preview + ) + else: + # 多进程模式 + success, failed = self._process_parallel( + image_paths, output_dir, num_images, preview, workers, fast ) + elapsed = time.perf_counter() - t0 + speed = success / elapsed if elapsed > 0 else 0 + print(f"\n完成! 成功: {success}, 失败: {failed}") + print(f"耗时: {elapsed:.1f}s ({speed:.1f} 张/秒)") + print(f"输出目录: {output_dir}") + + def _process_sequential(self, image_paths, output_dir, num_images, preview): + """单进程顺序处理""" + success, failed = 0, 0 + pbar = tqdm(total=num_images, desc='生成特殊车牌', unit='张') + for img_path in image_paths: + if success >= num_images: + break + result = self.process_single( + img_path, output_dir, preview=(preview and success < 10) + ) if result: success += 1 pbar.update(1) pbar.set_postfix_str(f'成功:{success} 失败:{failed}') else: failed += 1 - pbar.close() - print(f"\n完成! 成功: {success}, 失败: {failed}") - print(f"输出目录: {output_dir}") + return success, failed + + def _process_parallel(self, image_paths, output_dir, num_images, + preview, workers, fast): + """多进程并行处理(worker 复用 Pipeline,仅初始化一次)""" + from multiprocessing import Pool, cpu_count + n_procs = min(workers, cpu_count()) + + tasks = image_paths[:num_images * 3] + + # 每个 worker 启动时加载一次 Pipeline(含字体、底板缓存) + with Pool(n_procs, initializer=_worker_init, + initargs=(self.plate_type, fast)) as pool: + # 参数精简: (img_path, output_dir, preview_bool) + args = [ + (p, output_dir, (preview and i < 10)) + for i, p in enumerate(tasks) + ] + success, failed = 0, 0 + pbar = tqdm(total=num_images, desc=f'生成({n_procs}进程)', unit='张') + for ok in pool.imap_unordered(_worker_process_single, args): + if ok: + success += 1 + pbar.update(1) + pbar.set_postfix_str(f'{success}/{failed}') + else: + failed += 1 + if success >= num_images: + break + pbar.close() + pool.terminate() + return success, failed # ============================================================ @@ -891,6 +1072,10 @@ def main(): help='保存前10张的中间结果供调试') parser.add_argument('--seed', type=int, default=42, help='随机种子 (默认: 42)') + parser.add_argument('--workers', type=int, default=1, + help='并行进程数 (默认: 1, 建议设为CPU核心数)') + parser.add_argument('--fast', action='store_true', + help='快速模式:跳过退化模拟,速度提升约40%%') args = parser.parse_args() @@ -902,6 +1087,8 @@ def main(): num_images=args.num, preview=args.preview, seed=args.seed, + workers=args.workers, + fast=args.fast, ) diff --git a/tools/cut_lchannel/dataset.py b/CCPD_SPE_GEN/gan_pipeline/dataset.py similarity index 100% rename from tools/cut_lchannel/dataset.py rename to CCPD_SPE_GEN/gan_pipeline/dataset.py diff --git a/tools/cut_lchannel/models.py b/CCPD_SPE_GEN/gan_pipeline/models.py similarity index 100% rename from tools/cut_lchannel/models.py rename to CCPD_SPE_GEN/gan_pipeline/models.py diff --git a/tools/cut_lchannel/requirements.txt b/CCPD_SPE_GEN/gan_pipeline/requirements.txt similarity index 100% rename from tools/cut_lchannel/requirements.txt rename to CCPD_SPE_GEN/gan_pipeline/requirements.txt diff --git a/tools/cut_lchannel/train.py b/CCPD_SPE_GEN/gan_pipeline/train.py similarity index 100% rename from tools/cut_lchannel/train.py rename to CCPD_SPE_GEN/gan_pipeline/train.py diff --git a/tools/_gan_prep.sh b/CCPD_SPE_GEN/scripts/gan_data_prep.sh similarity index 78% rename from tools/_gan_prep.sh rename to CCPD_SPE_GEN/scripts/gan_data_prep.sh index ebdb6d1..e64f1e7 100644 --- a/tools/_gan_prep.sh +++ b/CCPD_SPE_GEN/scripts/gan_data_prep.sh @@ -1,9 +1,10 @@ #!/bin/bash # ============================================================ # GAN 数据准备:合成车牌(A) + 真实车牌(B) +# 用法: cd ICE_Dataset && bash CCPD_SPE_GEN/scripts/gan_data_prep.sh # ============================================================ -source /home/huawei/rknn_env/bin/activate -cd /mnt/d/ICEv2/ICE_Dataset +source /home/huawei/rknn_env/bin/activate # TODO: 替换为你的虚拟环境路径 +cd /mnt/d/ICEv2/ICE_Dataset # TODO: 替换为你的 ICE_Dataset 路径 OUT_A=./gan_data/domain_A_synthetic OUT_B=./gan_data/domain_B_real @@ -14,7 +15,7 @@ echo " Phase 1: 生成合成车牌 (9类型 x 1000张)" echo "============================================" for p in police army gangao shiguan_old shiguan_new lingguan_old lingguan_new coach new_energy; do echo "--- $p ---" - python3 tools/plate_swap_pipeline.py \ + python3 CCPD_SPE_GEN/cv_pipeline/plate_swap.py \ --ccpd-dir ./CCPD/CCPD2019/ccpd_base \ --plate-type $p \ --num 1000 \ @@ -26,7 +27,7 @@ echo "" echo "============================================" echo " Phase 2: 提取真实车牌ROI (1000张)" echo "============================================" -python3 tools/extract_real_plates.py \ +python3 CCPD_SPE_GEN/cv_pipeline/extract_real_plates.py \ --ccpd-dir "$CCPD" \ --num 1000 \ --output $OUT_B \ diff --git a/CCPD_SPE_GEN/scripts/preview_all.sh b/CCPD_SPE_GEN/scripts/preview_all.sh new file mode 100644 index 0000000..6e26589 --- /dev/null +++ b/CCPD_SPE_GEN/scripts/preview_all.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# 一键预览全部 9 种特殊车牌类型 (每种 2 张,保存中间结果) +# 用法: cd ICE_Dataset && bash CCPD_SPE_GEN/scripts/preview_all.sh +source /home/huawei/rknn_env/bin/activate # TODO: 替换为你的虚拟环境 +cd /mnt/d/ICEv2/ICE_Dataset # TODO: 替换为你的 ICE_Dataset 路径 +for p in police army gangao shiguan_old shiguan_new lingguan_old lingguan_new coach new_energy; do + echo "=== $p ===" + python3 CCPD_SPE_GEN/cv_pipeline/plate_swap.py --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type $p --num 2 --output ./output/preview/$p --preview --seed 42 2>&1 | tail -3 + echo "" +done +echo "DONE" \ No newline at end of file diff --git a/CCPD_SPE_GEN/scripts/quick_test.sh b/CCPD_SPE_GEN/scripts/quick_test.sh new file mode 100644 index 0000000..fb979bf --- /dev/null +++ b/CCPD_SPE_GEN/scripts/quick_test.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# 快速测试:生成 20 张警车特殊车牌 +# 用法: cd ICE_Dataset && bash CCPD_SPE_GEN/scripts/quick_test.sh +source /home/huawei/rknn_env/bin/activate # TODO: 替换为你的虚拟环境 +cd /mnt/d/ICEv2/ICE_Dataset # TODO: 替换为你的 ICE_Dataset 路径 +python3 CCPD_SPE_GEN/cv_pipeline/plate_swap.py --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type police --num 20 --output ./output/preview/police --seed 42 2>&1 | tail -2 +echo DONE diff --git a/chinese_license_plate_generator/generate_multi_plate.py b/chinese_license_plate_generator/generate_multi_plate.py index 281d7f6..84d50a7 100644 --- a/chinese_license_plate_generator/generate_multi_plate.py +++ b/chinese_license_plate_generator/generate_multi_plate.py @@ -114,6 +114,9 @@ class MultiPlateGenerator: self.location_xys['{}_{}_{}'.format(i, j, k)] = \ get_location_data(length=i, split_id=j, height=k) + # 底板背景缓存(避免每次 generate_plate_special 读盘) + self._plate_bg_cache = {} + # 获取字符位置 def get_location_multi(self, plate_number, height=140): length = len(plate_number) @@ -227,9 +230,15 @@ class MultiPlateGenerator: """ height = 220 if is_double else 140 - # print(plate_number, height, bg_color, is_double) number_xy = self.get_location_multi(plate_number, height) - img_plate_model = cv2.imread(os.path.join(self.adr_plate_model, '{}_{}.PNG'.format(bg_color, height))) + + # 底板背景(缓存读取,避免每张图读盘) + cache_key = '{}_{}'.format(bg_color, height) + if cache_key not in self._plate_bg_cache: + bg_path = os.path.join(self.adr_plate_model, '{}.PNG'.format(cache_key)) + self._plate_bg_cache[cache_key] = cv2.imdecode( + np.fromfile(bg_path, dtype=np.uint8), cv2.IMREAD_COLOR) + img_plate_model = self._plate_bg_cache[cache_key].copy() img_plate_model = cv2.resize(img_plate_model, (440 if len(plate_number) == 7 else 480, height)) for i in range(len(plate_number)): diff --git a/scripts/build_ccpd_combined.py b/scripts/build_ccpd_combined.py new file mode 100644 index 0000000..313bfed --- /dev/null +++ b/scripts/build_ccpd_combined.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CCPD Combined Dataset Builder +============================== +从 CCPD2019(5k) + CCPD2020(5k) + CCPD_SPE(全量) 构建 YOLO 单类车牌数据集。 + +CCPD 文件名格式: + 025-95_113-154&383_386&473-386&473_177&454_154&383_363&402-... + 第4段(0-indexed=3): 4个角点 x&y_x&y_x&y_x&y + 顺序: 左下, 右下, 左上, 右上 + +输出: + ICE_Dataset/ccpd_combined_5k5k_spe/ + ├── images/train/ + ├── images/val/ + ├── labels/train/ + ├── labels/val/ + └── dataset.yaml +""" + +import os +import sys +import random +import shutil +from pathlib import Path +from PIL import Image + +# ── 路径配置 ─────────────────────────────────────────── +ICE_DATASET = Path(r'C:\Users\Kirl\Desktop\ICEv2\ICE_Dataset') +CCPD2019_DIR = ICE_DATASET / 'CCPD' / 'CCPD2019' +CCPD2020_DIR = ICE_DATASET / 'CCPD' / 'CCPD2020' +CCPD_SPE_DIR = ICE_DATASET / 'CCPD_SPE' +OUT_DIR = ICE_DATASET / 'ccpd_combined_5k5k_spe' + +SEED = 42 +TRAIN_RATIO = 0.9 + + +# ── CCPD 文件名解析 ──────────────────────────────────── +def parse_ccpd_corners(filename): + """ + 从 CCPD 文件名解析车牌 4 角点 (像素坐标) + 返回: [(x1,y1), (x2,y2), (x3,y3), (x4,y4)] 或 None + 顺序: 左下, 右下, 左上, 右上 + """ + base = os.path.splitext(filename)[0] + try: + parts = base.split('-') + if len(parts) < 5: + return None + coords_str = parts[3] + coords = [] + for pt in coords_str.split('_'): + x, y = pt.split('&') + coords.append((float(x), float(y))) + if len(coords) == 4: + return coords + except (ValueError, IndexError): + pass + return None + + +def corners_to_yolo_bbox(corners, img_w, img_h): + """ + 4 角点 → 外接矩形 → YOLO 归一化 xywh_center + 返回: (x_center, y_center, w, h) 归一化值 或 None + """ + xs = [c[0] for c in corners] + ys = [c[1] for c in corners] + xmin, xmax = min(xs), max(xs) + ymin, ymax = min(ys), max(ys) + + bw = xmax - xmin + bh = ymax - ymin + if bw <= 0 or bh <= 0: + return None + if xmin < 0 or ymin < 0 or xmax > img_w or ymax > img_h: + return None # bbox 越界,跳过 + + cx = (xmin + xmax) / 2.0 / img_w + cy = (ymin + ymax) / 2.0 / img_h + w = bw / img_w + h = bh / img_h + return (cx, cy, w, h) + + +# ── 图片收集 ─────────────────────────────────────────── +def collect_images(root_dir, limit=None): + """递归收集目录下所有 .jpg 文件,返回绝对路径列表""" + files = [] + for dirpath, _, filenames in os.walk(root_dir): + for fn in filenames: + if fn.lower().endswith('.jpg'): + files.append(os.path.join(dirpath, fn)) + random.seed(SEED) + random.shuffle(files) + if limit and len(files) > limit: + files = files[:limit] + return files + + +# ── 主流程 ───────────────────────────────────────────── +def main(): + print("=" * 60) + print("CCPD Combined Dataset Builder") + print("=" * 60) + + # 1. 收集图片 + print("\n[1/4] Collecting images...") + + print(f" CCPD2019: scanning {CCPD2019_DIR} ...") + ccpd2019_files = collect_images(str(CCPD2019_DIR), limit=5000) + print(f" → sampled {len(ccpd2019_files)}") + + print(f" CCPD2020: scanning {CCPD2020_DIR} ...") + ccpd2020_files = collect_images(str(CCPD2020_DIR), limit=5000) + print(f" → sampled {len(ccpd2020_files)}") + + print(f" CCPD_SPE: scanning {CCPD_SPE_DIR} ...") + ccpd_spe_files = collect_images(str(CCPD_SPE_DIR), limit=None) + print(f" → all {len(ccpd_spe_files)}") + + all_files = ccpd2019_files + ccpd2020_files + ccpd_spe_files + random.seed(SEED) + random.shuffle(all_files) + total = len(all_files) + print(f"\n Total images: {total}") + + # 2. 解析 & 过滤 + print("\n[2/4] Parsing bboxes & validating...") + valid_samples = [] # (src_path, yolo_bbox_str) + skipped_parse = 0 + skipped_bbox = 0 + skipped_read = 0 + + for src_path in all_files: + filename = os.path.basename(src_path) + corners = parse_ccpd_corners(filename) + if corners is None: + skipped_parse += 1 + continue + + # 获取图片尺寸 + try: + with Image.open(src_path) as img: + img_w, img_h = img.size + except Exception: + skipped_read += 1 + continue + + bbox = corners_to_yolo_bbox(corners, img_w, img_h) + if bbox is None: + skipped_bbox += 1 + continue + + # YOLO 标签: class_id cx cy w h + label_line = f"0 {bbox[0]:.6f} {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f}" + valid_samples.append((src_path, filename, label_line)) + + print(f" Valid: {len(valid_samples)}") + print(f" Skipped (parse): {skipped_parse}") + print(f" Skipped (bbox): {skipped_bbox}") + print(f" Skipped (read): {skipped_read}") + + # 3. Train/Val 分割 + print("\n[3/4] Splitting train/val (90/10)...") + random.seed(SEED) + random.shuffle(valid_samples) + n_train = int(len(valid_samples) * TRAIN_RATIO) + train_samples = valid_samples[:n_train] + val_samples = valid_samples[n_train:] + print(f" Train: {len(train_samples)}, Val: {len(val_samples)}") + + # 4. 输出 + print(f"\n[4/4] Writing to {OUT_DIR} ...") + + # 清理旧目录 + for sub in ['images/train', 'images/val', 'labels/train', 'labels/val']: + (OUT_DIR / sub).mkdir(parents=True, exist_ok=True) + + def write_split(samples, split_name): + img_dir = OUT_DIR / 'images' / split_name + lbl_dir = OUT_DIR / 'labels' / split_name + copied = 0 + for src_path, filename, label_line in samples: + # 处理重名 (不同源可能有同名文件) + dst_name = filename + counter = 0 + while (img_dir / dst_name).exists(): + counter += 1 + base, ext = os.path.splitext(filename) + dst_name = f"{base}_{counter:04d}{ext}" + label_name = os.path.splitext(dst_name)[0] + '.txt' + + try: + shutil.copy2(src_path, img_dir / dst_name) + except Exception as e: + print(f" WARNING: copy failed {src_path}: {e}") + continue + with open(lbl_dir / label_name, 'w') as f: + f.write(label_line + '\n') + copied += 1 + return copied + + n_copied_train = write_split(train_samples, 'train') + n_copied_val = write_split(val_samples, 'val') + print(f" Train images written: {n_copied_train}") + print(f" Val images written: {n_copied_val}") + + # 5. 生成 dataset.yaml + yaml_path = OUT_DIR / 'dataset.yaml' + yaml_content = f"""# CCPD Combined Dataset (single class: license_plate) +# CCPD2019 5k + CCPD2020 5k + CCPD_SPE all = {total} images + +path: {OUT_DIR.as_posix()} +train: images/train +val: images/val + +nc: 1 +names: + 0: license_plate +""" + with open(yaml_path, 'w') as f: + f.write(yaml_content) + print(f"\n dataset.yaml → {yaml_path}") + + print("\n" + "=" * 60) + print("DONE!") + print(f" Dataset: {OUT_DIR}") + print(f" Images: {n_copied_train + n_copied_val}") + print(f" YAML: {yaml_path}") + print("=" * 60) + + +if __name__ == '__main__': + main() diff --git a/tools/_p20.sh b/tools/_p20.sh deleted file mode 100644 index f60c3d7..0000000 --- a/tools/_p20.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -source /home/huawei/rknn_env/bin/activate -cd /mnt/d/ICEv2/ICE_Dataset -python3 tools/plate_swap_pipeline.py --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type police --num 20 --output ./output/preview/police --seed 42 2>&1 | tail -2 -echo DONE diff --git a/tools/_r3.sh b/tools/_r3.sh deleted file mode 100644 index 258d938..0000000 --- a/tools/_r3.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -source /home/huawei/rknn_env/bin/activate -cd /mnt/d/ICEv2/ICE_Dataset -for p in police army gangao shiguan_old shiguan_new lingguan_old lingguan_new coach new_energy; do - echo "=== $p ===" - python3 tools/plate_swap_pipeline.py --ccpd-dir ./CCPD/CCPD2019/ccpd_base --plate-type $p --num 2 --output ./output/preview/$p --preview --seed 42 2>&1 | tail -3 - echo "" -done -echo "DONE" \ No newline at end of file diff --git a/tools/gen_pure_plates.py b/tools/gen_pure_plates.py deleted file mode 100644 index 3090abd..0000000 --- a/tools/gen_pure_plates.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -"""生成纯合成车牌(无场景背景)作为 CUT Domain A""" -import sys, os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'chinese_license_plate_generator')) -from generate_multi_plate import MultiPlateGenerator -import cv2, random -import numpy as np - -random.seed(42) -np.random.seed(42) - -g = MultiPlateGenerator( - 'chinese_license_plate_generator/plate_model', - 'chinese_license_plate_generator/font_model' -) - -out = 'gan_data/domain_A_synthetic/police_pure' -os.makedirs(out, exist_ok=True) - -for i in range(500): - num, bg, _ = g.generate_plate_number() - # 强制police类型 - if '警' not in num: - num = num[:6] + '警' - img = g.generate_plate_special(num, 'white', False) - cv2.imwrite(f'{out}/synth_{i:04d}.jpg', img) - -print(f'Done: {out} ({len(os.listdir(out))} images)')