- generate_synthetic.py: Type 1/2 (one-line) and Type 3/4 (two-line) supported - Hangul char map covers all 37 glyphs available in the asset bundle - Two-line plates emit two separate PGNet polygons (top region+digits, bottom char+digits) - REGION_MAP is a best-effort guess and flagged for visual verification - Optional --dict flag prints coverage diagnostics Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>main
parent
ffdc77dec0
commit
d314415503
@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
한국 LP 합성 데이터 생성기 (PGNet 학습용 Step1 pretrain).
|
||||
|
||||
자산 출처: qjadud1994/Korean-license-plate-Generator (MIT)
|
||||
출력 라벨 포맷: PaddleOCR PGNet (PGDataSet 호환)
|
||||
<상대경로>\t[{"transcription": "12가3456", "points": [[x1,y1],...,[x4,y4]]}, ...]
|
||||
|
||||
지원 plate 종류:
|
||||
Type 1: 신형 승용 가로 1줄 (520x110, 흰)
|
||||
Type 2: 구형 승용 가로 1줄 (355x155, 흰)
|
||||
Type 3: 영업용 두 줄 (336x170, 노랑) — 지역명 + 숫자2 / 한글 + 숫자4
|
||||
Type 4: 친환경/전기 두 줄 (336x170, 파랑·녹색)
|
||||
|
||||
주의 — REGION_MAP은 추정 매핑입니다. 합성 결과 PNG를 시각 확인 후 정정하세요.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
# 한영 자판 매핑 (자음·모음 → 영문 두 글자 코드 → 한글 글자)
|
||||
# qjadud1994 자산의 char1/char1_g/char1_y 폴더 파일명 규칙과 일치 (37자).
|
||||
HANGUL_CHAR_MAP = {
|
||||
'ah': '모', 'aj': '머', 'ak': '마', 'an': '무',
|
||||
'dh': '오', 'dj': '어', 'dk': '아', 'dn': '우',
|
||||
'eh': '도', 'ej': '더', 'ek': '다', 'en': '두',
|
||||
'fh': '로', 'fj': '러', 'fk': '라', 'fn': '루',
|
||||
'gj': '허',
|
||||
'qh': '보', 'qj': '버', 'qk': '바', 'qn': '부',
|
||||
'rh': '고', 'rj': '거', 'rk': '가', 'rn': '구',
|
||||
'sh': '노', 'sj': '너', 'sk': '나', 'sn': '누',
|
||||
'th': '소', 'tj': '서', 'tk': '사', 'tn': '수',
|
||||
'wh': '조', 'wj': '저', 'wk': '자', 'wn': '주',
|
||||
}
|
||||
|
||||
# 자산 region_y / region_g 의 알파벳 코드 → 한국 광역지자체 (16종, 세종 제외 추정).
|
||||
# 정확한 매핑은 PNG 시각 확인 필요. 학습 노이즈 방지 위해 합성 후 검증할 것.
|
||||
REGION_MAP = {
|
||||
'A': '서울', 'B': '경기', 'C': '인천', 'D': '강원',
|
||||
'E': '충남', 'F': '대전', 'G': '충북', 'H': '부산',
|
||||
'I': '울산', 'J': '대구', 'K': '경북', 'L': '경남',
|
||||
'M': '전남', 'N': '광주', 'O': '전북', 'P': '제주',
|
||||
}
|
||||
|
||||
|
||||
class LPGenerator:
|
||||
def __init__(self, asset_dir: Path):
|
||||
self.asset = Path(asset_dir)
|
||||
if not self.asset.is_dir():
|
||||
raise FileNotFoundError(f"asset dir not found: {self.asset}. Run data_gen/setup_assets.sh first.")
|
||||
|
||||
self.plate_w = cv2.imread(str(self.asset / "plate.jpg"))
|
||||
self.plate_y = cv2.imread(str(self.asset / "plate_y.jpg"))
|
||||
self.plate_g = cv2.imread(str(self.asset / "plate_g.jpg"))
|
||||
|
||||
self.num_w = self._load("num")
|
||||
self.num_y = self._load("num_y")
|
||||
self.num_g = self._load("num_g")
|
||||
self.char_w = self._load("char1")
|
||||
self.char_y = self._load("char1_y")
|
||||
self.char_g = self._load("char1_g")
|
||||
self.region_y_imgs = self._load("region_y")
|
||||
self.region_g_imgs = self._load("region_g")
|
||||
|
||||
def _load(self, sub: str) -> dict:
|
||||
out = {}
|
||||
for fp in sorted((self.asset / sub).iterdir()):
|
||||
if fp.suffix.lower() in {'.jpg', '.png'}:
|
||||
out[fp.stem] = cv2.imread(str(fp))
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _resize_dict(d: dict, w: int, h: int) -> dict:
|
||||
return {k: cv2.resize(v, (w, h)) for k, v in d.items()}
|
||||
|
||||
def gen_type1(self):
|
||||
plate = cv2.resize(self.plate_w, (520, 110))
|
||||
num = self._resize_dict(self.num_w, 56, 83)
|
||||
char = self._resize_dict(self.char_w, 60, 83)
|
||||
|
||||
d = [random.choice('0123456789') for _ in range(2)]
|
||||
ch = random.choice(list(HANGUL_CHAR_MAP))
|
||||
e = [random.choice('0123456789') for _ in range(4)]
|
||||
|
||||
row, col = 13, 35
|
||||
for x in d:
|
||||
plate[row:row+83, col:col+56] = num[x]; col += 56
|
||||
plate[row:row+83, col:col+60] = char[ch]; col += 60 + 36
|
||||
for x in e:
|
||||
plate[row:row+83, col:col+56] = num[x]; col += 56
|
||||
|
||||
text = ''.join(d) + HANGUL_CHAR_MAP[ch] + ''.join(e)
|
||||
return plate, text
|
||||
|
||||
def gen_type2(self):
|
||||
plate = cv2.resize(self.plate_w, (355, 155))
|
||||
num = self._resize_dict(self.num_w, 45, 83)
|
||||
char = self._resize_dict(self.char_w, 49, 70)
|
||||
|
||||
d = [random.choice('0123456789') for _ in range(2)]
|
||||
ch = random.choice(list(HANGUL_CHAR_MAP))
|
||||
e = [random.choice('0123456789') for _ in range(4)]
|
||||
|
||||
row, col = 46, 10
|
||||
plate[row:row+83, col:col+45] = num[d[0]]; col += 45
|
||||
plate[row:row+83, col:col+45] = num[d[1]]; col += 45
|
||||
plate[row+12:row+82, col+2:col+51] = char[ch]; col += 51
|
||||
plate[row:row+83, col+2:col+47] = num[e[0]]; col += 47
|
||||
for x in e[1:]:
|
||||
plate[row:row+83, col:col+45] = num[x]; col += 45
|
||||
|
||||
text = ''.join(d) + HANGUL_CHAR_MAP[ch] + ''.join(e)
|
||||
return plate, text
|
||||
|
||||
def _gen_two_line(self, plate_bg, num_src, char_src, region_src):
|
||||
plate = cv2.resize(plate_bg, (336, 170))
|
||||
num1 = self._resize_dict(num_src, 44, 60)
|
||||
num2 = self._resize_dict(num_src, 64, 90)
|
||||
region = self._resize_dict(region_src, 88, 60)
|
||||
char = self._resize_dict(char_src, 64, 62)
|
||||
|
||||
rkey = random.choice(list(region))
|
||||
d = [random.choice('0123456789') for _ in range(2)]
|
||||
ch = random.choice(list(HANGUL_CHAR_MAP))
|
||||
e = [random.choice('0123456789') for _ in range(4)]
|
||||
|
||||
row, col = 8, 76
|
||||
plate[row:row+60, col:col+88] = region[rkey]; col += 88 + 8
|
||||
for x in d:
|
||||
plate[row:row+60, col:col+44] = num1[x]; col += 44
|
||||
|
||||
row, col = 72, 8
|
||||
plate[row:row+62, col:col+64] = char[ch]; col += 64
|
||||
for x in e:
|
||||
plate[row:row+90, col:col+64] = num2[x]; col += 64
|
||||
|
||||
# transcription: 줄 단위로 분리 (PGNet은 두 polygon으로 라벨링하는 게 정석)
|
||||
top = REGION_MAP.get(rkey, '?') + ''.join(d)
|
||||
bot = HANGUL_CHAR_MAP[ch] + ''.join(e)
|
||||
return plate, top, bot
|
||||
|
||||
def gen_type3(self):
|
||||
plate, top, bot = self._gen_two_line(self.plate_y, self.num_y, self.char_y, self.region_y_imgs)
|
||||
return plate, top, bot
|
||||
|
||||
def gen_type4(self):
|
||||
plate, top, bot = self._gen_two_line(self.plate_g, self.num_g, self.char_g, self.region_g_imgs)
|
||||
return plate, top, bot
|
||||
|
||||
|
||||
def make_label_one_line(plate, text):
|
||||
"""가로 한 줄 LP — polygon은 plate 전체."""
|
||||
h, w = plate.shape[:2]
|
||||
poly = [[0, 0], [w, 0], [w, h], [0, h]]
|
||||
return [{"transcription": text, "points": poly}]
|
||||
|
||||
|
||||
def make_label_two_line(plate, top, bot):
|
||||
"""두 줄 LP — 위·아래 두 polygon 으로 분리.
|
||||
위 줄 0~50% (region+num), 아래 줄 50~100% (char+num*4) — 단순 분할."""
|
||||
h, w = plate.shape[:2]
|
||||
mid = h // 2
|
||||
up_poly = [[0, 0], [w, 0], [w, mid], [0, mid]]
|
||||
dn_poly = [[0, mid], [w, mid], [w, h], [0, h]]
|
||||
return [
|
||||
{"transcription": top, "points": up_poly},
|
||||
{"transcription": bot, "points": dn_poly},
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--asset_dir", default=str(Path(__file__).parent / "Korean-license-plate-Generator"))
|
||||
p.add_argument("--out_dir", required=True, help="합성 데이터셋 출력 루트")
|
||||
p.add_argument("--num", type=int, default=200, help="총 이미지 개수")
|
||||
p.add_argument("--test_ratio", type=float, default=0.05)
|
||||
p.add_argument("--types", default="1,2,3,4", help="포함할 type (콤마 구분)")
|
||||
p.add_argument("--dict", default=None, help="검증용 dict 경로 (선택)")
|
||||
p.add_argument("--seed", type=int, default=42)
|
||||
args = p.parse_args()
|
||||
|
||||
random.seed(args.seed)
|
||||
np.random.seed(args.seed)
|
||||
|
||||
gen = LPGenerator(Path(args.asset_dir))
|
||||
type_funcs = {
|
||||
'1': ('one', gen.gen_type1),
|
||||
'2': ('one', gen.gen_type2),
|
||||
'3': ('two', gen.gen_type3),
|
||||
'4': ('two', gen.gen_type4),
|
||||
}
|
||||
chosen = [type_funcs[t.strip()] for t in args.types.split(',') if t.strip() in type_funcs]
|
||||
if not chosen:
|
||||
sys.exit("No valid types selected.")
|
||||
|
||||
seen_chars = set()
|
||||
out = Path(args.out_dir)
|
||||
n_test = max(1, int(args.num * args.test_ratio))
|
||||
n_train = args.num - n_test
|
||||
|
||||
for split, count in [("train", n_train), ("test", n_test)]:
|
||||
img_dir = out / split / "images"
|
||||
img_dir.mkdir(parents=True, exist_ok=True)
|
||||
records = []
|
||||
for i in range(count):
|
||||
kind, fn = random.choice(chosen)
|
||||
if kind == 'one':
|
||||
plate, text = fn()
|
||||
label = make_label_one_line(plate, text)
|
||||
seen_chars.update(text)
|
||||
else:
|
||||
plate, top, bot = fn()
|
||||
label = make_label_two_line(plate, top, bot)
|
||||
seen_chars.update(top); seen_chars.update(bot)
|
||||
fname = f"{i:06d}.jpg"
|
||||
cv2.imwrite(str(img_dir / fname), plate)
|
||||
records.append((f"images/{fname}", json.dumps(label, ensure_ascii=False)))
|
||||
|
||||
with open(out / split / f"{split}.txt", "w", encoding="utf-8") as f:
|
||||
for path, lab in records:
|
||||
f.write(f"{path}\t{lab}\n")
|
||||
print(f" {split}: {len(records)} images → {out / split}")
|
||||
|
||||
# dict 검증
|
||||
if args.dict:
|
||||
with open(args.dict, encoding="utf-8") as f:
|
||||
dict_chars = {ln.strip() for ln in f if ln.strip()}
|
||||
missing = seen_chars - dict_chars - {'?'}
|
||||
unused = dict_chars - seen_chars
|
||||
print()
|
||||
print(f"dict 검증 (in {args.dict}):")
|
||||
print(f" 생성에 등장한 글자 {len(seen_chars)}자")
|
||||
print(f" dict 누락 (라벨에 있는데 dict에 없음): {sorted(missing) or 'none'}")
|
||||
print(f" 미등장 (dict에 있는데 합성 데이터에 없음): {sorted(unused) or 'none'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in new issue