feat: 特殊车牌生成管线 + C++/多进程/色温校正
This commit is contained in:
1 parent
3808b9f035
commit
3101992f51
29 files changed
+2336
-80
No files matched your search
@@ -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()
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
CCPD 真实车牌 ROI 提取工具
|
||||
==========================
|
||||
从 CCPD 图片中透视校正并裁剪车牌区域,保存为统一尺寸的 ROI 图片。
|
||||
用于 CycleGAN/CUT 风格对齐训练的真实车牌域数据。
|
||||
|
||||
用法:
|
||||
python extract_real_plates.py --ccpd-dir ./CCPD/CCPD2019/ccpd_base --num 500 --output ./gan_data/real_plates
|
||||
|
||||
输出:
|
||||
440×140 的校正车牌 ROI (BGR .jpg)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def parse_ccpd_corners(filename: str) -> Optional[np.ndarray]:
|
||||
"""
|
||||
从 CCPD 文件名解析4角点,几何排序为 [TL, TR, BR, BL]。
|
||||
"""
|
||||
base = Path(filename).stem
|
||||
parts = base.split('-')
|
||||
if len(parts) < 5:
|
||||
return None
|
||||
|
||||
try:
|
||||
coords_str = parts[3]
|
||||
pts = []
|
||||
for pt in coords_str.split('_'):
|
||||
x, y = pt.split('&')
|
||||
pts.append((float(x), float(y)))
|
||||
|
||||
if len(pts) != 4:
|
||||
return None
|
||||
|
||||
a = np.array(pts)
|
||||
sums = a[:, 0] + a[:, 1]
|
||||
diffs = a[:, 0] - a[:, 1]
|
||||
|
||||
tl = a[np.argmin(sums)]
|
||||
tr = a[np.argmax(diffs)]
|
||||
br = a[np.argmax(sums)]
|
||||
bl = a[np.argmin(diffs)]
|
||||
|
||||
return np.float32([tl, tr, br, bl])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def extract_plate_roi(image_bgr: np.ndarray,
|
||||
corners: np.ndarray,
|
||||
output_size: Tuple[int, int] = (440, 140)) -> Optional[np.ndarray]:
|
||||
"""
|
||||
透视校正提取车牌 ROI。
|
||||
"""
|
||||
h, w = image_bgr.shape[:2]
|
||||
# 校验角点在图像范围内
|
||||
if corners.min() < -10 or corners[:, 0].max() > w + 10 or corners[:, 1].max() > h + 10:
|
||||
return None
|
||||
|
||||
dst = np.float32([
|
||||
[0, 0],
|
||||
[output_size[0] - 1, 0],
|
||||
[output_size[0] - 1, output_size[1] - 1],
|
||||
[0, output_size[1] - 1],
|
||||
])
|
||||
|
||||
M = cv2.getPerspectiveTransform(corners, dst)
|
||||
roi = cv2.warpPerspective(image_bgr, M, output_size,
|
||||
flags=cv2.INTER_CUBIC,
|
||||
borderMode=cv2.BORDER_REPLICATE)
|
||||
return roi
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='CCPD 真实车牌 ROI 提取')
|
||||
parser.add_argument('--ccpd-dir', type=str, required=True,
|
||||
help='CCPD 图片目录(多个用逗号分隔)')
|
||||
parser.add_argument('--num', type=int, default=500,
|
||||
help='提取数量 (默认: 500)')
|
||||
parser.add_argument('--output', type=str, default='./gan_data/real_plates',
|
||||
help='输出目录')
|
||||
parser.add_argument('--seed', type=int, default=42)
|
||||
|
||||
args = parser.parse_args()
|
||||
random.seed(args.seed)
|
||||
np.random.seed(args.seed)
|
||||
|
||||
# 收集图片
|
||||
ccpd_dirs = [d.strip() for d in args.ccpd_dir.split(',') if d.strip()]
|
||||
all_images = []
|
||||
for d in ccpd_dirs:
|
||||
dd = Path(d)
|
||||
if not dd.is_dir():
|
||||
print(f"[WARN] 目录不存在: {d}")
|
||||
continue
|
||||
for ext in ('*.jpg', '*.jpeg', '*.png'):
|
||||
all_images.extend(dd.glob(ext))
|
||||
|
||||
random.shuffle(all_images)
|
||||
print(f"CCPD 图片总数: {len(all_images)}")
|
||||
print(f"目标提取数: {args.num}")
|
||||
print(f"输出目录: {args.output}")
|
||||
print("-" * 50)
|
||||
|
||||
os.makedirs(args.output, exist_ok=True)
|
||||
|
||||
success = 0
|
||||
for img_path in tqdm(all_images, desc='提取真实车牌ROI', unit='张'):
|
||||
if success >= args.num:
|
||||
break
|
||||
|
||||
corners = parse_ccpd_corners(img_path.name)
|
||||
if corners is None:
|
||||
continue
|
||||
|
||||
img = cv2.imread(str(img_path))
|
||||
if img is None:
|
||||
continue
|
||||
|
||||
roi = extract_plate_roi(img, corners)
|
||||
if roi is None:
|
||||
continue
|
||||
|
||||
out_name = f"real_{img_path.stem}.jpg"
|
||||
cv2.imwrite(os.path.join(args.output, out_name), roi,
|
||||
[cv2.IMWRITE_JPEG_QUALITY, 95])
|
||||
success += 1
|
||||
|
||||
print(f"\n完成! 提取: {success} 张")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -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),
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user