feat: 特殊车牌生成管线 + C++/多进程/色温校正

This commit is contained in:
kirl committed 2026-06-21 10:32:37 +08:00
1 parent 3808b9f035
commit 3101992f51
29 files changed
+2336 -80

No files matched your search

+2
View File
@@ -33,3 +33,5 @@
*.yml text eol=lf *.yml text eol=lf
*.csv text eol=lf *.csv text eol=lf
*.md text eol=lf *.md text eol=lf
CCPD_SPE_GEN/cpp_pipeline/install/*.exe filter=lfs diff=lfs merge=lfs -text
+6
View File
@@ -19,3 +19,9 @@ Desktop.ini
# Jupyter # Jupyter
.ipynb_checkpoints/ .ipynb_checkpoints/
ccpd_combined_5k5k_spe/
output/
CCPD_SPE/*/
build/
+91
View File
@@ -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 |
| 色温匹配 | 参考原车牌白字色温 |
| 噪声匹配 | 原车牌区域噪声σ估计 |
+83
View File
@@ -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 位) |
+24
View File
@@ -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()
+42
View File
@@ -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"
+123
View File
@@ -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()
+145
View File
@@ -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
```
Binary file not shown.
+203
View File
@@ -0,0 +1,203 @@
#include "plate_swap.hpp"
#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <filesystem>
namespace fs = std::filesystem;
using namespace std;
using namespace ice;
// ── 配置 ──
struct Config {
vector<string> 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<IndexedImage> collect_and_index(const vector<string>& dirs, mt19937& rng) {
vector<IndexedImage> 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 <dirs> --plate-type <type> [options]" << endl;
cerr << " --plate-pool <dir> Plate pool directory (required)" << endl;
cerr << " --num <N> Number of images (default 500)" << endl;
cerr << " --workers <N> 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<double>(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<IndexedImage> images = collect_and_index(cfg.ccpd_dirs, rng);
auto t2 = chrono::steady_clock::now();
cout << " (" << images.size() << " valid, "
<< chrono::duration<double>(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<int> success{0}, failed{0};
atomic<int> idx{0};
int total = min((int)images.size(), cfg.num_images);
mutex cout_mtx;
auto t_proc = chrono::steady_clock::now();
vector<thread> 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<mutex> 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<double>(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;
}
+692
View File
@@ -0,0 +1,692 @@
#include "plate_swap.hpp"
#include <fstream>
#include <algorithm>
#include <numeric>
#include <cmath>
#include <cstring>
#include <filesystem>
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<string> 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<Point2f> 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<PlateEntry>& 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<PlateEntry>& 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<uint8_t> 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<size_t> dist(0, v.size()-1);
return v[dist(rng)];
}
}
throw runtime_error("PlatePool is empty");
}
uniform_int_distribution<size_t> 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<uint8_t> data((istreambuf_iterator<char>(f)), istreambuf_iterator<char>());
return imdecode(data, IMREAD_COLOR);
}
bool imwrite_unicode(const string& path, const Mat& img, const vector<int>& params) {
vector<uint8_t> 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<const char*>(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<uint8_t>(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<uint8_t>(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<Mat> 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<Mat, Mat> 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<Point> 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<Mat>{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<uint8_t> 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<uint8_t>(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
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <opencv2/opencv.hpp>
#include <string>
#include <vector>
#include <random>
#include <unordered_map>
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<std::string, std::vector<PlateEntry>> 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<cv::Mat, cv::Mat> 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<int>& params = {});
} // namespace ice
+295
View File
@@ -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()
File renamed without changes.
@@ -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)')
@@ -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),
}
@@ -57,8 +57,8 @@ from tqdm import tqdm
# ============================================================ # ============================================================
# 路径解析:定位 chinese_license_plate_generator # 路径解析:定位 chinese_license_plate_generator
# ============================================================ # ============================================================
_SCRIPT_DIR = Path(__file__).resolve().parent # ICE_Dataset/tools/ _SCRIPT_DIR = Path(__file__).resolve().parent # .../CCPD_SPE_GEN/cv_pipeline/
_PROJECT_DIR = _SCRIPT_DIR.parent # ICE_Dataset/ _PROJECT_DIR = _SCRIPT_DIR.parent.parent # .../ICE_Dataset/
_GENERATOR_DIR = _PROJECT_DIR / 'chinese_license_plate_generator' _GENERATOR_DIR = _PROJECT_DIR / 'chinese_license_plate_generator'
if str(_GENERATOR_DIR) not in sys.path: if str(_GENERATOR_DIR) not in sys.path:
@@ -77,6 +77,34 @@ from plate_number import (
generate_plate_number_blue, 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 result = syn * gain
return np.clip(result, 0, 255).astype(np.uint8) 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 color_cast = white_ref / ref_mean
strength_map = { 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) 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 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') 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()
@@ -700,7 +821,7 @@ class PlateSwapPipeline:
return None return None
# ---- 2. 读取原图 ---- # ---- 2. 读取原图 ----
img_orig = cv2.imread(image_path) img_orig = _imread_unicode(image_path)
if img_orig is None: if img_orig is None:
return None return None
@@ -725,6 +846,9 @@ class PlateSwapPipeline:
new_plate_number, self.bg_color, is_double=False, enhance=False 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. 色温滤镜 ---- # ---- 6. 色温滤镜 ----
matched_plate = match_plate_color(synthetic_plate, orig_plate_roi, self.bg_color) matched_plate = match_plate_color(synthetic_plate, orig_plate_roi, self.bg_color)
@@ -741,8 +865,10 @@ 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), '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) 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 = (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. 退化模拟(快速模式下跳过) ----
# JPEG 极限压缩 if not self._fast_mode:
matched_plate = jpeg_compress(matched_plate, quality=2) # 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) h_p, w_p = matched_plate.shape[:2]
matched_plate = cv2.resize(small, (w_p, h_p), interpolation=cv2.INTER_NEAREST) 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) # 较强模糊
matched_plate = cv2.GaussianBlur(matched_plate, (5, 5), 1.5)
# ---- 9. 透视变换回贴 ---- # ---- 9. 透视变换回贴 ----
warped, plate_mask = warp_plate_back( warped, plate_mask = warp_plate_back(
@@ -778,7 +905,7 @@ class PlateSwapPipeline:
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)
cv2.imwrite(out_path, final, [cv2.IMWRITE_JPEG_QUALITY, 95]) _imwrite_unicode(out_path, final, [cv2.IMWRITE_JPEG_QUALITY, 95])
# ---- 可选: 保存调试中间结果 ---- # ---- 可选: 保存调试中间结果 ----
if preview: if preview:
@@ -799,7 +926,9 @@ class PlateSwapPipeline:
output_dir: str, output_dir: str,
num_images: int = 500, num_images: int = 500,
preview: bool = False, preview: bool = False,
seed: int = 42): seed: int = 42,
workers: int = 1,
fast: bool = False):
""" """
批量处理 CCPD 图片目录。 批量处理 CCPD 图片目录。
@@ -809,6 +938,8 @@ class PlateSwapPipeline:
num_images: 生成图片总数 num_images: 生成图片总数
preview: 是否保存调试中间结果 preview: 是否保存调试中间结果
seed: 随机种子 seed: 随机种子
workers: 并行进程数 (1=单进程, >1=多进程)
fast: 快速模式 (跳过退化模拟步骤)
""" """
random.seed(seed) random.seed(seed)
np.random.seed(seed) np.random.seed(seed)
@@ -829,38 +960,88 @@ class PlateSwapPipeline:
print("[ERROR] 未找到任何 CCPD 图片") print("[ERROR] 未找到任何 CCPD 图片")
return return
# 随机排列
random.shuffle(all_images) 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.config['description']} ({self.plate_type})")
print(f"底板颜色: {self.bg_color}") print(f"底板颜色: {self.bg_color}")
print(f"CCPD 图片总数: {len(all_images)}") print(f"CCPD 图片总数: {len(all_images)}")
print(f"目标生成数: {num_images}") print(f"目标生成数: {num_images}")
print(f"输出目录: {output_dir}") print(f"输出目录: {output_dir}")
print(f"并行进程: {workers}" + (" (多核加速)" if workers > 1 else ""))
if fast:
print(f"快速模式: 跳过退化模拟")
print("-" * 60) print("-" * 60)
success = 0 import time
failed = 0 t0 = time.perf_counter()
pbar = tqdm(total=num_images, desc='生成特殊车牌', unit='张')
for img_path in all_images: if workers <= 1:
if success >= num_images: # 单进程模式
break success, failed = self._process_sequential(
image_paths, output_dir, num_images, preview
result = self.process_single( )
str(img_path), output_dir, preview=(preview and success < 10) 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: if result:
success += 1 success += 1
pbar.update(1) pbar.update(1)
pbar.set_postfix_str(f'成功:{success} 失败:{failed}') pbar.set_postfix_str(f'成功:{success} 失败:{failed}')
else: else:
failed += 1 failed += 1
pbar.close() pbar.close()
print(f"\n完成! 成功: {success}, 失败: {failed}") return success, failed
print(f"输出目录: {output_dir}")
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张的中间结果供调试') help='保存前10张的中间结果供调试')
parser.add_argument('--seed', type=int, default=42, parser.add_argument('--seed', type=int, default=42,
help='随机种子 (默认: 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() args = parser.parse_args()
@@ -902,6 +1087,8 @@ def main():
num_images=args.num, num_images=args.num,
preview=args.preview, preview=args.preview,
seed=args.seed, seed=args.seed,
workers=args.workers,
fast=args.fast,
) )
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,10 @@
#!/bin/bash #!/bin/bash
# ============================================================ # ============================================================
# GAN 数据准备:合成车牌(A) + 真实车牌(B) # GAN 数据准备:合成车牌(A) + 真实车牌(B)
# 用法: cd ICE_Dataset && bash CCPD_SPE_GEN/scripts/gan_data_prep.sh
# ============================================================ # ============================================================
source /home/huawei/rknn_env/bin/activate source /home/huawei/rknn_env/bin/activate # TODO: 替换为你的虚拟环境路径
cd /mnt/d/ICEv2/ICE_Dataset cd /mnt/d/ICEv2/ICE_Dataset # TODO: 替换为你的 ICE_Dataset 路径
OUT_A=./gan_data/domain_A_synthetic OUT_A=./gan_data/domain_A_synthetic
OUT_B=./gan_data/domain_B_real OUT_B=./gan_data/domain_B_real
@@ -14,7 +15,7 @@ echo " Phase 1: 生成合成车牌 (9类型 x 1000张)"
echo "============================================" echo "============================================"
for p in police army gangao shiguan_old shiguan_new lingguan_old lingguan_new coach new_energy; do for p in police army gangao shiguan_old shiguan_new lingguan_old lingguan_new coach new_energy; do
echo "--- $p ---" echo "--- $p ---"
python3 tools/plate_swap_pipeline.py \ python3 CCPD_SPE_GEN/cv_pipeline/plate_swap.py \
--ccpd-dir ./CCPD/CCPD2019/ccpd_base \ --ccpd-dir ./CCPD/CCPD2019/ccpd_base \
--plate-type $p \ --plate-type $p \
--num 1000 \ --num 1000 \
@@ -26,7 +27,7 @@ echo ""
echo "============================================" echo "============================================"
echo " Phase 2: 提取真实车牌ROI (1000张)" echo " Phase 2: 提取真实车牌ROI (1000张)"
echo "============================================" echo "============================================"
python3 tools/extract_real_plates.py \ python3 CCPD_SPE_GEN/cv_pipeline/extract_real_plates.py \
--ccpd-dir "$CCPD" \ --ccpd-dir "$CCPD" \
--num 1000 \ --num 1000 \
--output $OUT_B \ --output $OUT_B \
+11
View File
@@ -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"
+7
View File
@@ -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
@@ -114,6 +114,9 @@ class MultiPlateGenerator:
self.location_xys['{}_{}_{}'.format(i, j, k)] = \ self.location_xys['{}_{}_{}'.format(i, j, k)] = \
get_location_data(length=i, split_id=j, height=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): def get_location_multi(self, plate_number, height=140):
length = len(plate_number) length = len(plate_number)
@@ -227,9 +230,15 @@ class MultiPlateGenerator:
""" """
height = 220 if is_double else 140 height = 220 if is_double else 140
# print(plate_number, height, bg_color, is_double)
number_xy = self.get_location_multi(plate_number, height) 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)) img_plate_model = cv2.resize(img_plate_model, (440 if len(plate_number) == 7 else 480, height))
for i in range(len(plate_number)): for i in range(len(plate_number)):
+237
View File
@@ -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()
-5
View File
@@ -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
-9
View File
@@ -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"
-28
View File
@@ -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)')