Russian-ALPR / app /services /plate_normalizer.py
Naumeex's picture
Initial deploy: Russian ALPR
186b436
Raw
History Blame Contribute Delete
4.29 kB
"""Нормализация номеров РФ с позиционной коррекцией OCR (ГОСТ Р 50577, без региона)."""
import re
ALLOWED_CYR = "АВЕКМНОРСТУХ"
LAT2CYR = dict(zip("ABEKMHOPCTYX", ALLOWED_CYR))
# ГОСТ-формат без региона: буква + 3 цифры + 2 буквы
PLATE_RE = re.compile(rf"^[{ALLOWED_CYR}]\d{{3}}[{ALLOWED_CYR}]{{2}}$")
# Коррекция «цифра -> похожая буква» (для буквенных позиций)
DIGIT_TO_LETTER = {
"0": "О",
"8": "В",
"4": "А", # иногда 4 читается как А
"6": "В",
"1": "Т", # реже
}
# Коррекция «буква -> похожая цифра» (для цифровых позиций)
LETTER_TO_DIGIT = {
"О": "0", "O": "0", # русская и латинская О
"В": "8", "B": "8",
"З": "3",
"Ч": "4",
"Б": "6",
"Т": "1",
"А": "4",
"Е": "0",
}
def latin_to_cyrillic(text: str) -> str:
return "".join(LAT2CYR.get(ch, ch) for ch in text.upper())
def clean_ocr_output(raw: str) -> str:
"""Убираем padding, пробелы, точки, дефисы; латиницу -> кириллицу."""
s = raw.upper()
for ch in ("_", " ", ".", "-", "·", "•"):
s = s.replace(ch, "")
return latin_to_cyrillic(s)
def _fix_to_letter(ch: str) -> str | None:
"""Привести символ к букве из алфавита ГОСТ (если возможно)."""
if ch in ALLOWED_CYR:
return ch
return DIGIT_TO_LETTER.get(ch)
def _fix_to_digit(ch: str) -> str | None:
"""Привести символ к цифре (если возможно)."""
if ch.isdigit():
return ch
return LETTER_TO_DIGIT.get(ch)
def normalize_plate(raw: str) -> tuple[str, bool]:
"""
Возвращает (номер_без_региона, прошёл_ли_ГОСТ).
Стратегия:
1. Очистить строку.
2. Применить ПОЗИЦИОННУЮ МАСКУ: пройти по символам, собирая
структуру [буква][цифра][цифра][цифра][буква][буква],
корректируя путаницу 0<->О, 8<->В и т.п. по ожидаемому типу позиции.
3. Если собрали 6 валидных символов — это номер, ГОСТ = True.
"""
cleaned = clean_ocr_output(raw)
# Прямое совпадение
if PLATE_RE.match(cleaned):
return cleaned, True
# Позиционная сборка: пытаемся начать с каждой позиции, где стоит буква
# (на случай мусора в начале строки)
for start in range(len(cleaned)):
result = _try_build_plate(cleaned[start:])
if result:
return result, True
# Не собралось — вернём очищенную строку как есть, ГОСТ = False
return cleaned, False
def _try_build_plate(s: str) -> str | None:
"""
Пытается собрать номер по маске Б-Ц-Ц-Ц-Б-Б из начала строки s,
исправляя ошибки OCR по типу позиции. Возвращает 6-символьный номер или None.
"""
# Маска: True = ожидаем букву, False = ожидаем цифру
mask = [True, False, False, False, True, True]
out = []
si = 0 # индекс в строке
for expect_letter in mask:
if si >= len(s):
return None
ch = s[si]
if expect_letter:
fixed = _fix_to_letter(ch)
else:
fixed = _fix_to_digit(ch)
if fixed is None:
# Символ не подходит и не корректируется — этот старт неудачный
return None
out.append(fixed)
si += 1
plate = "".join(out)
return plate if PLATE_RE.match(plate) else None
def is_valid_gost(text: str) -> bool:
"""ГОСТ = формат «буква + 3 цифры + 2 буквы»."""
return bool(PLATE_RE.match(text))