====== Доработка License Plate Reader (CodeProject.AI) для российских номеров ====== ===== Описание ===== Стандартный модуль **License Plate Reader** в CodeProject.AI плохо работает с российскими автомобильными номерами по следующим причинам: * Использует английский OCR, который выдает латинские буквы вместо кириллицы * Обрезает область номера слишком сильно, теряя цифры региона * Не учитывает специфику формата российских номеров (А123ВС777) Данная инструкция описывает модификацию модуля для корректного распознавания российских номеров с автоматической конвертацией латиницы в кириллицу. ===== Что было сделано ===== - Создан модуль нормализации текста для конвертации латиницы → кириллица - Добавлена валидация формата российских номеров - Расширена область детекции на 20% для захвата региона - Оптимизированы параметры OCR для лучшего распознавания - Включена предобработка изображений (опционально) ===== Архитектура решения ===== Изображение → YOLO детектор номера → Расширение области (+20%) → → PaddleOCR (английский) → Нормализация (латиница → кириллица) → → Валидация формата → Результат (К181КК123) ===== Установка и настройка ===== ==== Шаг 1. Структура проекта ==== Предполагается что 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/'' ==== Шаг 2. Создание модуля нормализации russian_plate_patch.py ==== Создайте файл ''./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 ==== Шаг 3. Создание модуля предобработки image_preprocessing.py ==== Создайте файл ''./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 ==== Шаг 4. Модификация ALPR.py ==== **Критически важное изменение:** расширение области детекции для захвата региона. Откройте ''./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})") ==== Шаг 5. Модификация options.py ==== Откройте ''./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)) ==== Шаг 6. Модификация modulesettings.json ==== Откройте ''./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.", ==== Шаг 7. Перезапуск контейнера ==== 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:32168'' - Перейдите в **Modules → ALPR** - Нажмите **Test** и загрузите изображение с российским номером ==== Проверка через API ==== curl -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 ===== ==== Настройка детекции номера ==== В 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" } ===== Интеграция с Home Assistant ===== ==== Автоматизация распознавания ==== Добавьте в ''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 | ==== Типичное время обработки (CPU) ==== * Детекция номера (YOLO): 200-450ms * OCR распознавание (PaddleOCR): 300-600ms * Нормализация и валидация: <10ms * **Итого:** 500-1100ms (~0.5-1 секунда) ===== Устранение неполадок ===== ==== Модуль не загружается ==== Проверьте логи: 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'' до 3 * Отключите ''OCR_OPTIMIZATION'' (установите ''false'') * Уменьшите разрешение изображений в AgentDVR до 1280x720 ===== Бэкап конфигурации ===== Сохраните модифицированные файлы: 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') }} ===== Заключение ===== После выполнения всех шагов система будет: * ✅ Корректно распознавать российские номера формата А123ВС777 * ✅ Автоматически конвертировать латиницу в кириллицу * ✅ Захватывать регион (2-3 цифры после букв серии) * ✅ Валидировать формат номера * ✅ Интегрироваться с Home Assistant для автоматизации **Типичная точность распознавания:** 95-100% при хорошем освещении и качестве изображения. **Автор:** Nick (CIO, 20 лет опыта в IT)\\ **Дата:** Январь 2025\\ **Версия:** 1.0 ===== Связанные статьи ===== * [[agentdvr|Настройка AgentDVR]] * [[home_assistant|Интеграция Home Assistant]] * [[codeproject_ai|CodeProject.AI установка]]