Содержание

Доработка License Plate Reader (CodeProject.AI) для российских номеров

Описание

Стандартный модуль License Plate Reader в CodeProject.AI плохо работает с российскими автомобильными номерами по следующим причинам:

Данная инструкция описывает модификацию модуля для корректного распознавания российских номеров с автоматической конвертацией латиницы в кириллицу.

Что было сделано

  1. Создан модуль нормализации текста для конвертации латиницы → кириллица
  2. Добавлена валидация формата российских номеров
  3. Расширена область детекции на 20% для захвата региона
  4. Оптимизированы параметры OCR для лучшего распознавания
  5. Включена предобработка изображений (опционально)

Архитектура решения

Изображение → YOLO детектор номера → Расширение области (+20%) → 
→ PaddleOCR (английский) → Нормализация (латиница → кириллица) → 
→ Валидация формата → Результат (К181КК123)

Установка и настройка

Шаг 1. Структура проекта

Предполагается что CodeProject.AI установлен в Docker:

docker-compose.yml
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:

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:

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]

Замените этот блок на:

ALPR.py (фрагмент)
# 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]

Добавьте импорт модулей в начало файла (после существующих импортов):

ALPR.py (начало файла)
# 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

Добавьте сразу ПОСЛЕ этого блока:

ALPR.py (фрагмент после OCR)
        # 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):

options.py (фрагмент)
        # 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 и измените параметры:

modulesettings.json (фрагмент EnvironmentVariables)
      "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: ✓ Поддержка российских номеров активирована

Тестирование

Проверка через веб-интерфейс

  1. Откройте http://your-server:32168
  2. Перейдите в Modules → ALPR
  3. Нажмите 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:

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') }}"

Автоматическое открытие ворот

configuration.yaml
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)

Устранение неполадок

Модуль не загружается

Проверьте логи:

docker compose logs codeproject | grep -i error
docker compose logs codeproject | grep -i russian

Если нет строки «✓ Поддержка российских номеров активирована», проверьте:

  1. Наличие файлов russian_plate_patch.py и image_preprocessing.py
  2. Правильность путей в импортах

Номера распознаются неправильно

Проблема: Обрезается регион (например, «В707ОР» вместо «В707ОР77»)

Решение: Увеличьте expand_percent в ALPR.py:

expand_percent = 0.30  # Было 0.20

Проблема: Латинские буквы вместо кириллицы

Решение: Проверьте что функция enhance_plate_for_russian вызывается:

docker compose logs -f | grep "Нормализация:"

Низкая производительность

Для ускорения на CPU:

Бэкап конфигурации

Сохраните модифицированные файлы:

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/"

Дополнительные возможности

База данных всех проездов

configuration.yaml
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

Уведомления с изображением

configuration.yaml
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

Связанные статьи