AI License Plate Recognition ANPR System Development

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1 servicesAll 1566 services
AI License Plate Recognition ANPR System Development
Medium
~1-2 weeks
FAQ
AI Development Areas
AI Solution Development Stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823
  • image_logo-aider_0.jpg
    AIDER company logo development
    762
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    848

Разработка AI для распознавания автомобильных номеров (ANPR/LPR)

ANPR (Automatic Number Plate Recognition) — одна из наиболее зрелых задач CV: системы работают на скоростях до 200 км/ч, в ночных условиях (ИК-подсветка), при различных углах. Applications: системы въезда/выезда на парковки, мониторинг дорожного трафика, розыск угнанных автомобилей, toll collection, транспортная аналитика. Двухэтапный pipeline: детекция номерного знака + OCR символов.

Детекция и нормализация номерного знака

import cv2
import numpy as np
from ultralytics import YOLO

class LicensePlateDetector:
    """
    Stage 1: детекция местоположения номерного знака.
    YOLOv8n — оптимальный выбор: быстро (60+ FPS) и точно.
    """
    def __init__(self, model_path: str, device: str = 'cuda'):
        self.model = YOLO(model_path)
        self.device = device

    def detect(self, image: np.ndarray,
                conf_threshold: float = 0.5) -> list[dict]:
        """Возвращает список детектированных номерных знаков"""
        results = self.model(image, conf=conf_threshold)
        plates = []

        for box in results[0].boxes:
            x1, y1, x2, y2 = map(int, box.xyxy[0])
            conf = float(box.conf)
            plate_crop = image[y1:y2, x1:x2]
            # Перспективная нормализация для наклонных номеров
            normalized = self._normalize_plate(plate_crop)

            plates.append({
                'bbox': [x1, y1, x2, y2],
                'confidence': conf,
                'crop': plate_crop,
                'normalized': normalized,
                'area': (x2-x1) * (y2-y1)
            })

        # Сортируем по площади (наиболее крупный = основной)
        return sorted(plates, key=lambda p: p['area'], reverse=True)

    def _normalize_plate(self, plate: np.ndarray,
                          target_size: tuple = (440, 140)) -> np.ndarray:
        """
        Нормализация номерного знака до стандартного размера.
        Соотношение сторон EU номера: 520×110 мм ≈ 4.7:1
        """
        h, w = plate.shape[:2]

        # Дополнительная коррекция перспективы через Hough
        gray = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY)
        edges = cv2.Canny(gray, 50, 150)
        lines = cv2.HoughLinesP(edges, 1, np.pi/180, 30, minLineLength=30, maxLineGap=10)

        if lines is not None and len(lines) > 2:
            angles = []
            for line in lines[:10]:
                x1, y1, x2, y2 = line[0]
                angle = np.degrees(np.arctan2(y2-y1, x2-x1))
                if abs(angle) < 20:
                    angles.append(angle)
            if angles:
                avg_angle = np.median(angles)
                if abs(avg_angle) > 1.0:
                    M = cv2.getRotationMatrix2D((w//2, h//2), avg_angle, 1.0)
                    plate = cv2.warpAffine(plate, M, (w, h))

        return cv2.resize(plate, target_size, interpolation=cv2.INTER_CUBIC)

OCR для номерных знаков

from paddleocr import PaddleOCR
import re

class LicensePlateOCR:
    """
    Stage 2: распознавание символов номерного знака.
    Поддержка: кириллица (UA, RU, BY), латиница (EU, US), арабский, китайский.
    """
    PLATE_PATTERNS = {
        'UA': r'^[АВЕКМНРСТУХ]{2}\d{4}[АВЕКМНРСТУХ]{2}$',         # АА1234ВВ
        'UA_custom': r'^\d{4}[АВЕКМНРСТУХ]{2}\d{2}$',              # Киев
        'EU': r'^[A-Z]{1,3}[0-9]{1,4}[A-Z]{1,3}$',
        'US_standard': r'^[A-Z0-9]{5,8}$',
        'RU': r'^[АВЕКМНРСТУХ]\d{3}[АВЕКМНРСТУХ]{2}\d{2,3}$',
    }

    def __init__(self, lang: str = 'en', use_gpu: bool = True):
        # PaddleOCR: лучший баланс для коротких текстов (номера)
        self.ocr = PaddleOCR(
            use_angle_cls=False,  # номера не перевёрнуты
            lang=lang,
            use_gpu=use_gpu,
            rec_algorithm='SVTR_LCNet',  # быстрее для коротких текстов
            show_log=False
        )

    def recognize(self, plate_image: np.ndarray) -> dict:
        """OCR нормализованного номерного знака"""
        result = self.ocr.ocr(plate_image, cls=False)
        if not result or not result[0]:
            return {'text': None, 'confidence': 0}

        # Объединяем все найденные строки текста
        texts = []
        confidences = []
        for line in result[0]:
            _, (text, conf) = line
            texts.append(text.strip())
            confidences.append(conf)

        raw_text = ' '.join(texts)
        cleaned = self._clean_plate_text(raw_text)
        plate_format = self._detect_format(cleaned)

        return {
            'raw_text': raw_text,
            'text': cleaned,
            'confidence': float(np.mean(confidences)),
            'plate_format': plate_format,
            'valid_format': plate_format is not None
        }

    def _clean_plate_text(self, text: str) -> str:
        """Нормализация: убрать пробелы, спецсимволы, привести к верхнему регистру"""
        # Замена похожих символов (OCR-ошибки)
        replacements = {'O': '0', 'I': '1', 'l': '1', 'Q': '0', 'D': '0'}
        cleaned = re.sub(r'[^A-ZА-Я0-9АВЕКМНРСТУХ]', '', text.upper())
        return cleaned

    def _detect_format(self, text: str) -> str | None:
        for format_name, pattern in self.PLATE_PATTERNS.items():
            if re.match(pattern, text):
                return format_name
        return None

Полный ANPR pipeline

import sqlite3
from datetime import datetime

class ANPRSystem:
    def __init__(self, detector: LicensePlateDetector,
                  ocr: LicensePlateOCR,
                  watchlist_db: str = None):
        self.detector = detector
        self.ocr = ocr
        self.watchlist = self._load_watchlist(watchlist_db)

    def process_frame(self, frame: np.ndarray,
                       camera_id: str) -> dict:
        plates = self.detector.detect(frame)
        results = []

        for plate in plates[:3]:  # не более 3 номеров в кадре
            ocr_result = self.ocr.recognize(plate['normalized'])
            if not ocr_result['text']:
                continue

            plate_text = ocr_result['text']
            on_watchlist = plate_text in self.watchlist

            results.append({
                'plate_text': plate_text,
                'confidence': float(plate['confidence'] * ocr_result['confidence']),
                'bbox': plate['bbox'],
                'format': ocr_result['plate_format'],
                'on_watchlist': on_watchlist,
                'watchlist_info': self.watchlist.get(plate_text) if on_watchlist else None
            })

        return {
            'camera_id': camera_id,
            'timestamp': datetime.now().isoformat(),
            'plates_detected': len(results),
            'results': results,
            'alert': any(r['on_watchlist'] for r in results)
        }

    def _load_watchlist(self, db_path: str) -> dict:
        if not db_path:
            return {}
        conn = sqlite3.connect(db_path)
        rows = conn.execute('SELECT plate, reason FROM watchlist').fetchall()
        conn.close()
        return {row[0]: row[1] for row in rows}
Условие Accuracy (UA/EU номера)
День, 0° угол, <80 км/ч 98–99.5%
День, 30° угол, <120 км/ч 94–97%
Ночь, ИК-подсветка 92–96%
Плохая погода (дождь, туман) 85–92%
Грязный номер 78–88%
Задача Срок
Базовая система ANPR для парковки 3–5 недель
Highspeed ANPR (трасса, 130+ км/ч) с ИК 8–14 недель
Федеральная платформа мониторинга транспорта 20–36 недель