====== Доработка 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 установка]]