Стандартный модуль License Plate Reader в CodeProject.AI плохо работает с российскими автомобильными номерами по следующим причинам:
Данная инструкция описывает модификацию модуля для корректного распознавания российских номеров с автоматической конвертацией латиницы в кириллицу.
Изображение → YOLO детектор номера → Расширение области (+20%) → → PaddleOCR (английский) → Нормализация (латиница → кириллица) → → Валидация формата → Результат (К181КК123)
Предполагается что CodeProject.AI установлен в Docker:
services: codeproject: image: codeproject/ai-server:latest container_name: codeproject restart: unless-stopped environment: - TZ=Europe/Moscow - DISABLE_MODEL_SOURCE_CHECK=True volumes: - ./codeproject/data:/etc/codeproject/ai - ./codeproject/models:/app/models - ./codeproject/modules:/app/modules - ./codeproject/wwwroot:/app/Server/wwwroot - ./codeproject/paddleocr:/root/.paddleocr - ./codeproject/paddlex:/root/.paddlex networks: - webproxy networks: webproxy: external: true
Путь к модулю ALPR: ./codeproject/modules/ALPR/
Создайте файл ./codeproject/modules/ALPR/russian_plate_patch.py:
import re # Маппинг латиница → кириллица для российских номеров LATIN_TO_CYRILLIC = { 'A': 'А', 'B': 'В', 'E': 'Е', 'K': 'К', 'M': 'М', 'H': 'Н', 'O': 'О', 'P': 'Р', 'C': 'С', 'T': 'Т', 'Y': 'У', 'X': 'Х' } # Исправление ошибок OCR OCR_CORRECTIONS = { '0': 'О', 'Q': 'О', 'D': 'О', # Нули → О 'I': '1', 'l': '1', '|': '1', # I → 1 'S': '5', 'Z': '2', 'B': '8', # Похожие } def normalize_russian_plate(text): """ Нормализация российского номера Формат: К181КК123 (1 буква, 3 цифры, 2 буквы, 2-3 цифры региона) """ text = text.upper().replace(' ', '').replace('-', '').replace('_', '') result = [] for i, char in enumerate(text): # Позиция 0: буква if i == 0: if char.isdigit() and char in OCR_CORRECTIONS: char = OCR_CORRECTIONS[char] if char in LATIN_TO_CYRILLIC: char = LATIN_TO_CYRILLIC[char] # Позиции 1-3: цифры elif 1 <= i <= 3: if not char.isdigit(): for ocr_char, correction in OCR_CORRECTIONS.items(): if char == correction and ocr_char.isdigit(): char = ocr_char break # Позиции 4-5: буквы серии elif 4 <= i <= 5: if char.isdigit() and char in OCR_CORRECTIONS: char = OCR_CORRECTIONS[char] if char in LATIN_TO_CYRILLIC: char = LATIN_TO_CYRILLIC[char] result.append(char) return ''.join(result) def is_valid_russian_plate(text): """Проверка формата российского номера""" patterns = [ r'^[АВЕКМНОРСТУХ]\d{3}[АВЕКМНОРСТУХ]{2}\d{2,3}$', # К181КК123 r'^[АВЕКМНОРСТУХ]{2}\d{5,6}$', # Такси r'^\d{4}[АВЕКМНОРСТУХ]{2}\d{2,3}$', # Прицепы ] for pattern in patterns: if re.match(pattern, text): return True if 8 <= len(text) <= 9: has_letters = any(c.isalpha() for c in text) has_digits = any(c.isdigit() for c in text) return has_letters and has_digits return False def enhance_plate_for_russian(label, confidence): """ Улучшение результата для российского номера Возвращает (normalized_label, adjusted_confidence, is_russian) """ if not label: return label, confidence, False # Логируем исходный текст print(f"🔍 ALPR: OCR вернул: '{label}' (confidence: {confidence:.3f})") # Нормализация normalized = normalize_russian_plate(label) print(f"🔄 ALPR: После нормализации: '{normalized}'") # Проверка валидности is_russian = is_valid_russian_plate(normalized) print(f"✓ ALPR: Валидный формат: {is_russian}") # Корректировка уверенности if is_russian: adjusted_conf = min(confidence * 1.1, 1.0) print(f"📈 ALPR: Confidence: {confidence:.3f} → {adjusted_conf:.3f}") else: adjusted_conf = confidence * 0.7 print(f"📉 ALPR: Confidence понижен: {confidence:.3f} → {adjusted_conf:.3f}") return normalized, adjusted_conf, is_russian
Создайте файл ./codeproject/modules/ALPR/image_preprocessing.py:
import cv2 import numpy as np def preprocess_russian_plate(image): """ Улучшенная предобработка для российских номеров ВНИМАНИЕ: Отключена по умолчанию, т.к. может ухудшить качество """ # Конвертация в grayscale если цветное if len(image.shape) == 3: gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) else: gray = image # Увеличение резкости kernel_sharpen = np.array([ [-1, -1, -1], [-1, 9, -1], [-1, -1, -1] ]) sharpened = cv2.filter2D(gray, -1, kernel_sharpen) # CLAHE для выравнивания гистограммы clahe = cv2.createCLAHE(clipLimit=2.5, tileGridSize=(8, 8)) enhanced = clahe.apply(sharpened) # Увеличение контраста и яркости alpha = 1.3 # Контраст beta = 10 # Яркость adjusted = cv2.convertScaleAbs(enhanced, alpha=alpha, beta=beta) # Бинаризация Otsu _, binary = cv2.threshold(adjusted, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) # Морфологическая операция для очистки шума kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2)) morph = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # Инверсия если нужно (фон должен быть темным) if np.mean(morph) > 127: morph = cv2.bitwise_not(morph) return morph
Критически важное изменение: расширение области детекции для захвата региона.
Откройте ./codeproject/modules/ALPR/ALPR.py и найдите блок (примерно строка 60-65):
# If a plate is found we'll pass this onto OCR for plate_detection in detect_plate_response["predictions"]: # Pull out just the detected plate. # The coordinates... (relative to the original image) plate_rect = Rect(plate_detection["x_min"], plate_detection["y_min"], plate_detection["x_max"], plate_detection["y_max"]) # The image itself... (Its coordinates are now relative to itself) numpy_plate = numpy_image[plate_rect.top:plate_rect.bottom, plate_rect.left:plate_rect.right]
Замените этот блок на:
# If a plate is found we'll pass this onto OCR for plate_detection in detect_plate_response["predictions"]: # Расширяем область детекции на 20% со всех сторон для захвата региона expand_percent = 0.20 x_expand = int((plate_detection["x_max"] - plate_detection["x_min"]) * expand_percent) y_expand = int((plate_detection["y_max"] - plate_detection["y_min"]) * expand_percent) expanded_x_min = max(0, plate_detection["x_min"] - x_expand) expanded_y_min = max(0, plate_detection["y_min"] - y_expand) expanded_x_max = min(numpy_image.shape[1], plate_detection["x_max"] + x_expand) expanded_y_max = min(numpy_image.shape[0], plate_detection["y_max"] + y_expand) print(f"🔍 ALPR: Оригинальная область: {plate_detection['x_min']},{plate_detection['y_min']} - {plate_detection['x_max']},{plate_detection['y_max']}") print(f"📐 ALPR: Расширенная область: {expanded_x_min},{expanded_y_min} - {expanded_x_max},{expanded_y_max}") # Pull out just the detected plate with EXPANDED coordinates plate_rect = Rect(expanded_x_min, expanded_y_min, expanded_x_max, expanded_y_max) # The image itself... (Its coordinates are now relative to itself) numpy_plate = numpy_image[plate_rect.top:plate_rect.bottom, plate_rect.left:plate_rect.right]
Добавьте импорт модулей в начало файла (после существующих импортов):
# Import our general libraries import io import math import time from typing import Tuple import utils.tools as tool from utils.cartesian import * from codeproject_ai_sdk import LogVerbosity, ModuleRunner, JSON from PIL import Image import cv2 import numpy as np from options import Options from paddleocr import PaddleOCR # Russian plate support try: from russian_plate_patch import enhance_plate_for_russian, normalize_russian_plate from image_preprocessing import preprocess_russian_plate RUSSIAN_SUPPORT_ENABLED = True print("✓ Поддержка российских номеров активирована") except Exception as e: RUSSIAN_SUPPORT_ENABLED = False print(f"⚠ Поддержка российских номеров не загружена: {e}") # Constants debug_log = False no_plate_found = 'Characters Not Found'
Найдите блок после OCR распознавания (примерно строка 280-290):
# Read plate (label, confidence, avg_char_width, avg_char_height, plateInferenceMs) = \ await read_plate_chars_PaddleOCR(module_runner, numpy_plate) inferenceMs += plateInferenceMs
Добавьте сразу ПОСЛЕ этого блока:
# Read plate (label, confidence, avg_char_width, avg_char_height, plateInferenceMs) = \ await read_plate_chars_PaddleOCR(module_runner, numpy_plate) inferenceMs += plateInferenceMs # Обработка для российских номеров if RUSSIAN_SUPPORT_ENABLED and label and label != no_plate_found: original_label = label label, confidence, is_russian = enhance_plate_for_russian(label, confidence) # Логируем если есть изменения if label != original_label: print(f"ALPR: Нормализация: {original_label} → {label} (российский: {is_russian})")
Откройте ./codeproject/modules/ALPR/options.py и внесите изменения:
Язык OCR (строка ~35):
# БЫЛО: self.language = ModuleOptions.getEnvVariable('OCR_LANGUAGE', 'ru') # СТАЛО: self.language = ModuleOptions.getEnvVariable('OCR_LANGUAGE', 'en')
Параметры детекции (строка ~45-50):
# PaddleOCR settings self.use_gpu = ModuleOptions.enable_GPU self.box_detect_threshold = 0.40 # Уверенность детекции текстового блока self.char_detect_threshold = 0.40 # Уверенность распознавания символов self.det_db_unclip_ratio = 2.0 # Расширение bounding box self.language = ModuleOptions.getEnvVariable('OCR_LANGUAGE', 'en') self.algorithm = ModuleOptions.getEnvVariable('ALGORITHM', 'CRNN')
Масштабирование (строка ~20):
# БЫЛО: self.OCR_rescale_factor = float(ModuleOptions.getEnvVariable("PLATE_RESCALE_FACTOR", 2.0)) # СТАЛО: self.OCR_rescale_factor = float(ModuleOptions.getEnvVariable("PLATE_RESCALE_FACTOR", 4.0))
Откройте ./codeproject/modules/ALPR/modulesettings.json и измените параметры:
"EnvironmentVariables": {
"MIN_COMPUTE_CAPABILITY": "6",
"MIN_CUDNN_VERSION": "7",
"PLATE_CONFIDENCE": 0.50,
"PLATE_ROTATE_DEG": 0,
"AUTO_PLATE_ROTATE": true,
"PLATE_RESCALE_FACTOR": 4,
"OCR_OPTIMIZATION": true,
"OCR_OPTIMAL_CHARACTER_HEIGHT": 70,
"OCR_OPTIMAL_CHARACTER_WIDTH": 35,
"REMOVE_SPACES": false,
"SAVE_CROPPED_PLATE": true,
"ROOT_PATH": "%ROOT_PATH%",
"CROPPED_PLATE_DIR": "%ROOT_PATH%/Server/wwwroot",
"OCR_LANGUAGE": "en"
},
Также обновите описание модуля:
"PublishingInfo" : {
"Description": "Detects and readers single-line and multi-line license plates using YOLO object detection and the PaddleOCR toolkit. Optimized for Russian plates.",
cd /opt/codeproject docker compose down docker compose up -d # Следите за логами docker compose logs -f codeproject | grep -E "(российских|ALPR:|Plate:)"
В логах должна появиться строка:
Infor ALPR_adapter.py: ✓ Поддержка российских номеров активирована
http://your-server:32168curl -X POST http://localhost:32168/v1/vision/alpr \ -F "upload=@/path/to/plate.jpg" | jq .
Ожидаемый результат:
{
"success": true,
"predictions": [
{
"plate": "К181КК123",
"confidence": 0.89,
"label": "Plate: К181КК123",
"x_min": 100,
"y_min": 200,
"x_max": 300,
"y_max": 250
}
],
"inferenceMs": 850
}
При успешном распознавании в логах будет:
🔍 ALPR: Оригинальная область: 150,220 - 280,260 📐 ALPR: Расширенная область: 124,196 - 306,284 🔍 ALPR: OCR вернул: 'K181KK123' (confidence: 0.870) 🔄 ALPR: После нормализации: 'К181КК123' ✓ ALPR: Валидный формат: True 📈 ALPR: Confidence: 0.870 → 0.957 ALPR: Нормализация: K181KK123 → К181КК123 (российский: True) Response rec'd from License Plate Reader command 'alpr' ['Found Plate: К181КК123'] took 850ms
В AgentDVR создайте два последовательных действия:
Действие 1: License Plate Recognition
Name: License Plate Recognition
Trigger: On Motion Detection
URL: http://codeproject:32168/v1/vision/alpr
Method: POST
Body Type: multipart/form-data
Field: upload = {snapshot}
Действие 2: Send to Home Assistant
Name: Send Plate to Home Assistant
Trigger: After previous action
URL: http://home-assistant:8123/api/webhook/alpr_gate
Method: POST
Headers: Content-Type: application/json
Body:
{
"plate": "{response.predictions[0].plate}",
"confidence": "{response.predictions[0].confidence}",
"timestamp": "{timestamp}",
"camera": "{camera_name}",
"location": "main_gate"
}
Добавьте в configuration.yaml:
automation: - alias: "Въезд в ворота - Распознан номер" trigger: - platform: webhook webhook_id: alpr_gate action: - service: notify.telegram data: title: "🚗 Въезд в ворота" message: | Номер: {{ trigger.json.plate }} Уверенность: {{ (trigger.json.confidence * 100) | round(1) }}% Время: {{ trigger.json.timestamp }} Камера: {{ trigger.json.camera }} - service: logbook.log data: name: "ALPR Detection" message: "Распознан номер {{ trigger.json.plate }}" sensor: - platform: template sensors: gate_last_plate: friendly_name: "Последний номер у ворот" value_template: "{{ state_attr('automation.vezd_v_vorota_raspoznan_nomer', 'last_triggered') }}"
input_text: allowed_plates: name: Разрешенные номера initial: "К181КК123,А777АА777,В888ВВ777" automation: - alias: "Автоматическое открытие ворот" trigger: - platform: webhook webhook_id: alpr_gate condition: - condition: template value_template: > {{ trigger.json.plate in states('input_text.allowed_plates').split(',') }} - condition: numeric_state entity_id: sensor.confidence above: 0.8 action: - service: switch.turn_on target: entity_id: switch.gate_opener - service: notify.telegram data: message: "✅ Ворота открыты для {{ trigger.json.plate }}"
| Условие | PLATE_CONFIDENCE | PLATE_RESCALE_FACTOR | expand_percent |
|---|---|---|---|
| Хорошее освещение, близко | 0.60 | 3 | 0.15 |
| Среднее освещение, средне | 0.50 | 4 | 0.20 |
| Плохое освещение, далеко | 0.40 | 5 | 0.30 |
Проверьте логи:
docker compose logs codeproject | grep -i error docker compose logs codeproject | grep -i russian
Если нет строки «✓ Поддержка российских номеров активирована», проверьте:
russian_plate_patch.py и image_preprocessing.pyПроблема: Обрезается регион (например, «В707ОР» вместо «В707ОР77»)
Решение: Увеличьте expand_percent в ALPR.py:
expand_percent = 0.30 # Было 0.20
Проблема: Латинские буквы вместо кириллицы
Решение: Проверьте что функция enhance_plate_for_russian вызывается:
docker compose logs -f | grep "Нормализация:"
Для ускорения на CPU:
PLATE_RESCALE_FACTOR до 3OCR_OPTIMIZATION (установите false)Сохраните модифицированные файлы:
mkdir -p ~/codeproject_backup cp ./codeproject/modules/ALPR/ALPR.py ~/codeproject_backup/ cp ./codeproject/modules/ALPR/options.py ~/codeproject_backup/ cp ./codeproject/modules/ALPR/modulesettings.json ~/codeproject_backup/ cp ./codeproject/modules/ALPR/russian_plate_patch.py ~/codeproject_backup/ cp ./codeproject/modules/ALPR/image_preprocessing.py ~/codeproject_backup/ echo "Бэкап создан в ~/codeproject_backup/"
recorder: db_url: postgresql://user:pass@localhost/homeassistant include: entity_globs: - sensor.gate_* automation: - alias: "Запись всех проездов в базу" trigger: - platform: webhook webhook_id: alpr_gate action: - service: logbook.log data: name: "Проезд транспорта" message: > {{ trigger.json.plate }} ({{ (trigger.json.confidence * 100) | round(1) }}%) entity_id: sensor.gate_traffic_log
automation: - alias: "Уведомление с фото номера" trigger: - platform: webhook webhook_id: alpr_gate action: - service: telegram_bot.send_photo data: url: "http://codeproject:32168/Server/wwwroot/alpr.jpg" caption: | 🚗 Распознан номер: {{ trigger.json.plate }} 📊 Уверенность: {{ (trigger.json.confidence * 100) | round(1) }}% 📅 Время: {{ now().strftime('%d.%m.%Y %H:%M:%S') }}
После выполнения всех шагов система будет:
Типичная точность распознавания: 95-100% при хорошем освещении и качестве изображения.
Автор: Nick (CIO, 20 лет опыта в IT)
Дата: Январь 2025
Версия: 1.0