# Модул за разпознаване на регистрационни номера

## 1. Въведение

Модулът за разпознаване на регистрационни номера е ключов компонент на системата, който осигурява автоматично разпознаване на автомобилни номера от видео потоци. Този документ описва различните опции за интеграция на такъв модул, препоръчителни библиотеки и технологии, примерен код за имплементация и добри практики за оптимизация и сигурност.

## 2. Опции за разпознаване на регистрационни номера

Съществуват два основни подхода за интегриране на функционалност за разпознаване на регистрационни номера:

### 2.1. Локално разпознаване

При този подход, алгоритмите за разпознаване се изпълняват директно на сървъра, където е инсталирана системата.

**Предимства:**
- Не изисква постоянна интернет връзка
- По-ниска латентност при разпознаване
- Няма месечни такси за API услуги
- Пълен контрол върху данните и процеса на разпознаване
- Възможност за персонализация на алгоритмите

**Недостатъци:**
- По-високи изисквания към хардуера на сървъра
- Необходимост от поддръжка и актуализация на библиотеките
- Потенциално по-ниска точност в сравнение с някои облачни решения
- По-сложна инсталация и конфигурация

### 2.2. Облачно разпознаване (чрез API)

При този подход, кадрите от видео потоците се изпращат към външна услуга, която извършва разпознаването и връща резултатите.

**Предимства:**
- По-ниски изисквания към хардуера на сървъра
- Постоянно актуализирани алгоритми без необходимост от ръчна поддръжка
- Често по-висока точност на разпознаване
- По-лесна интеграция и настройка

**Недостатъци:**
- Изисква постоянна интернет връзка
- Месечни такси за API услуги
- Потенциални проблеми с поверителността на данните
- Зависимост от външен доставчик
- По-висока латентност при разпознаване

## 3. Препоръчителни библиотеки и услуги

### 3.1. Библиотеки за локално разпознаване

#### 3.1.1. OpenALPR

[OpenALPR](https://github.com/openalpr/openalpr) е библиотека с отворен код за автоматично разпознаване на регистрационни номера, която поддържа множество формати на номера от различни държави.

**Характеристики:**
- Поддръжка на множество формати на номера (включително европейски)
- Висока точност на разпознаване
- Възможност за обучение за специфични формати
- Налична е комерсиална версия с допълнителни функции
- Интеграция с различни програмни езици, включително PHP чрез обвивка

#### 3.1.2. Tesseract OCR с предварителна обработка

[Tesseract OCR](https://github.com/tesseract-ocr/tesseract) е библиотека за оптично разпознаване на символи, която може да бъде използвана за разпознаване на регистрационни номера след подходяща предварителна обработка на изображенията.

**Характеристики:**
- Безплатна библиотека с отворен код
- Добра точност при правилна предварителна обработка
- Поддръжка на множество езици и символи
- Интеграция с PHP чрез различни обвивки

#### 3.1.3. EasyOCR

[EasyOCR](https://github.com/JaidedAI/EasyOCR) е Python библиотека, базирана на дълбоко обучение, която може да бъде използвана за разпознаване на текст, включително регистрационни номера.

**Характеристики:**
- Базирана на съвременни невронни мрежи
- Поддръжка на множество езици
- Добра точност без специфично обучение
- Може да бъде интегрирана с PHP чрез изпълнение на Python скриптове

### 3.2. Облачни услуги за разпознаване

#### 3.2.1. Plate Recognizer

[Plate Recognizer](https://platerecognizer.com/) е облачна услуга, специализирана в разпознаването на регистрационни номера от изображения и видео.

**Характеристики:**
- Висока точност на разпознаване
- Поддръжка на номера от цял свят
- Лесна интеграция чрез REST API
- Опция за локално разгръщане на модела
- Различни ценови планове според нуждите

#### 3.2.2. OpenALPR Cloud API

[OpenALPR Cloud API](https://www.openalpr.com/cloud-api.html) е облачната версия на OpenALPR, предлагаща разпознаване на регистрационни номера като услуга.

**Характеристики:**
- Висока точност и скорост на разпознаване
- Поддръжка на номера от различни държави
- Лесна интеграция чрез REST API
- Различни ценови планове според обема на заявките

#### 3.2.3. Amazon Rekognition

[Amazon Rekognition](https://aws.amazon.com/rekognition/) е услуга за компютърно зрение, която включва функционалност за разпознаване на текст, която може да бъде използвана за разпознаване на регистрационни номера.

**Характеристики:**
- Част от екосистемата на AWS
- Добра точност за общо разпознаване на текст
- Лесна интеграция с други AWS услуги
- Ценообразуване според използването

## 4. Интеграция с PHP

### 4.1. Локална интеграция с OpenALPR

#### 4.1.1. Инсталация на OpenALPR

```bash
# Инсталация на OpenALPR и зависимости на Ubuntu
sudo apt-get update
sudo apt-get install -y openalpr openalpr-daemon openalpr-utils libopenalpr-dev

# Инсталация на PHP обвивка за OpenALPR
sudo apt-get install -y php-dev
git clone https://github.com/openalpr/openalpr.git
cd openalpr/src/bindings/php
phpize
./configure
make
sudo make install

# Добавяне на разширението в php.ini
echo "extension=openalpr.so" | sudo tee -a /etc/php/8.0/apache2/php.ini
echo "extension=openalpr.so" | sudo tee -a /etc/php/8.0/cli/php.ini

# Рестартиране на Apache
sudo systemctl restart apache2
```

#### 4.1.2. Примерен PHP код за разпознаване на номера от изображение

```php
<?php
// Проверка дали разширението е заредено
if (!extension_loaded('openalpr')) {
    die('OpenALPR PHP extension is not loaded');
}

/**
 * Функция за разпознаване на регистрационен номер от изображение
 * 
 * @param string $imagePath Път до изображението
 * @param string $region Регион/държава на номера (например 'eu' за Европа)
 * @param int $topN Брой на най-вероятните резултати
 * @return array Масив с резултати от разпознаването
 */
function recognizeLicensePlate($imagePath, $region = 'eu', $topN = 3) {
    try {
        // Създаване на инстанция на OpenALPR
        $openalpr = new OpenALPR($region);
        
        // Задаване на конфигурационни параметри
        $openalpr->setTopN($topN);
        $openalpr->setDetectRegion(true);
        
        // Разпознаване на номера от изображението
        $results = $openalpr->recognize($imagePath);
        
        // Преобразуване на резултата в масив
        $results = json_decode($results, true);
        
        return $results;
    } catch (Exception $e) {
        error_log('Error recognizing license plate: ' . $e->getMessage());
        return [
            'error' => true,
            'message' => $e->getMessage()
        ];
    }
}

/**
 * Функция за запис на разпознат номер в базата данни
 * 
 * @param array $plateData Данни от разпознаването
 * @param int $cameraId ID на камерата
 * @param int $streamId ID на потока
 * @param string $imagePath Път до изображението
 * @param PDO $pdo PDO връзка към базата данни
 * @return int|bool ID на записа или false при грешка
 */
function savePlateRecognition($plateData, $cameraId, $streamId, $imagePath, $pdo) {
    try {
        // Проверка за грешки в разпознаването
        if (isset($plateData['error']) && $plateData['error']) {
            error_log('Error in plate data: ' . $plateData['message']);
            return false;
        }
        
        // Проверка дали има разпознати номера
        if (empty($plateData['results'])) {
            error_log('No license plates detected');
            return false;
        }
        
        // Вземане на най-вероятния резултат
        $bestResult = $plateData['results'][0];
        $licensePlate = $bestResult['plate'];
        $confidence = $bestResult['confidence'];
        
        // Създаване на изрязано изображение на номера
        $plateImagePath = createPlateImage($imagePath, $bestResult['coordinates']);
        
        // Проверка дали номерът е в бял или черен списък
        $isInWhitelist = checkIfInWhitelist($licensePlate, $pdo);
        $isInBlacklist = checkIfInBlacklist($licensePlate, $pdo);
        
        // Запис в базата данни
        $stmt = $pdo->prepare("
            INSERT INTO recognitions 
            (license_plate, confidence, camera_id, stream_id, capture_time, image_path, plate_image_path, is_in_whitelist, is_in_blacklist)
            VALUES 
            (:license_plate, :confidence, :camera_id, :stream_id, NOW(), :image_path, :plate_image_path, :is_in_whitelist, :is_in_blacklist)
        ");
        
        $stmt->execute([
            ':license_plate' => $licensePlate,
            ':confidence' => $confidence,
            ':camera_id' => $cameraId,
            ':stream_id' => $streamId,
            ':image_path' => $imagePath,
            ':plate_image_path' => $plateImagePath,
            ':is_in_whitelist' => $isInWhitelist ? 1 : 0,
            ':is_in_blacklist' => $isInBlacklist ? 1 : 0
        ]);
        
        $recognitionId = $pdo->lastInsertId();
        
        // Ако номерът е в черния списък, създаваме известие
        if ($isInBlacklist) {
            createBlacklistAlert($licensePlate, $recognitionId, $cameraId, $pdo);
        }
        
        return $recognitionId;
    } catch (PDOException $e) {
        error_log('Database error: ' . $e->getMessage());
        return false;
    } catch (Exception $e) {
        error_log('Error saving recognition: ' . $e->getMessage());
        return false;
    }
}

/**
 * Функция за създаване на изрязано изображение на номера
 * 
 * @param string $imagePath Път до оригиналното изображение
 * @param array $coordinates Координати на номера в изображението
 * @return string Път до изрязаното изображение
 */
function createPlateImage($imagePath, $coordinates) {
    // Генериране на име за изрязаното изображение
    $plateImagePath = 'uploads/plates/' . pathinfo($imagePath, PATHINFO_FILENAME) . '_plate.jpg';
    
    // Създаване на директорията, ако не съществува
    if (!file_exists(dirname($plateImagePath))) {
        mkdir(dirname($plateImagePath), 0755, true);
    }
    
    // Зареждане на оригиналното изображение
    $image = imagecreatefromjpeg($imagePath);
    
    // Изчисляване на координатите на правоъгълника
    $x1 = min(array_column($coordinates, 'x'));
    $y1 = min(array_column($coordinates, 'y'));
    $x2 = max(array_column($coordinates, 'x'));
    $y2 = max(array_column($coordinates, 'y'));
    
    // Добавяне на малко допълнително пространство около номера
    $padding = 10;
    $x1 = max(0, $x1 - $padding);
    $y1 = max(0, $y1 - $padding);
    $x2 = min(imagesx($image), $x2 + $padding);
    $y2 = min(imagesy($image), $y2 + $padding);
    
    // Изрязване на изображението
    $width = $x2 - $x1;
    $height = $y2 - $y1;
    $plateImage = imagecreatetruecolor($width, $height);
    imagecopy($plateImage, $image, 0, 0, $x1, $y1, $width, $height);
    
    // Запазване на изрязаното изображение
    imagejpeg($plateImage, $plateImagePath, 90);
    
    // Освобождаване на паметта
    imagedestroy($image);
    imagedestroy($plateImage);
    
    return $plateImagePath;
}

/**
 * Функция за проверка дали номер е в белия списък
 * 
 * @param string $licensePlate Регистрационен номер
 * @param PDO $pdo PDO връзка към базата данни
 * @return bool True ако номерът е в белия списък
 */
function checkIfInWhitelist($licensePlate, $pdo) {
    $stmt = $pdo->prepare("
        SELECT COUNT(*) FROM whitelist 
        WHERE license_plate = :license_plate 
        AND (valid_to IS NULL OR valid_to >= NOW()) 
        AND valid_from <= NOW()
    ");
    $stmt->execute([':license_plate' => $licensePlate]);
    return $stmt->fetchColumn() > 0;
}

/**
 * Функция за проверка дали номер е в черния списък
 * 
 * @param string $licensePlate Регистрационен номер
 * @param PDO $pdo PDO връзка към базата данни
 * @return bool True ако номерът е в черния списък
 */
function checkIfInBlacklist($licensePlate, $pdo) {
    $stmt = $pdo->prepare("
        SELECT COUNT(*) FROM blacklist 
        WHERE license_plate = :license_plate 
        AND (valid_to IS NULL OR valid_to >= NOW()) 
        AND valid_from <= NOW()
    ");
    $stmt->execute([':license_plate' => $licensePlate]);
    return $stmt->fetchColumn() > 0;
}

/**
 * Функция за създаване на известие при засичане на номер от черния списък
 * 
 * @param string $licensePlate Регистрационен номер
 * @param int $recognitionId ID на разпознаването
 * @param int $cameraId ID на камерата
 * @param PDO $pdo PDO връзка към базата данни
 * @return int|bool ID на известието или false при грешка
 */
function createBlacklistAlert($licensePlate, $recognitionId, $cameraId, $pdo) {
    try {
        // Вземане на информация за номера от черния списък
        $stmt = $pdo->prepare("
            SELECT reason, alert_level FROM blacklist 
            WHERE license_plate = :license_plate 
            AND (valid_to IS NULL OR valid_to >= NOW()) 
            AND valid_from <= NOW()
        ");
        $stmt->execute([':license_plate' => $licensePlate]);
        $blacklistInfo = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if (!$blacklistInfo) {
            return false;
        }
        
        // Вземане на информация за камерата
        $stmt = $pdo->prepare("SELECT name, location FROM cameras WHERE id = :camera_id");
        $stmt->execute([':camera_id' => $cameraId]);
        $cameraInfo = $stmt->fetch(PDO::FETCH_ASSOC);
        
        // Създаване на заглавие и съобщение за известието
        $title = "Засечен номер от черния списък: {$licensePlate}";
        $message = "Регистрационен номер {$licensePlate} от черния списък е засечен от камера {$cameraInfo['name']} ({$cameraInfo['location']}).\n";
        $message .= "Причина за включване в черния списък: {$blacklistInfo['reason']}\n";
        $message .= "Ниво на алерта: {$blacklistInfo['alert_level']}";
        
        // Запис на известието в базата данни
        $stmt = $pdo->prepare("
            INSERT INTO notifications 
            (type, title, message, recognition_id, created_at)
            VALUES 
            ('BLACKLIST_ALERT', :title, :message, :recognition_id, NOW())
        ");
        
        $stmt->execute([
            ':title' => $title,
            ':message' => $message,
            ':recognition_id' => $recognitionId
        ]);
        
        $notificationId = $pdo->lastInsertId();
        
        // Изпращане на известието до всички потребители с активирани известия за черен списък
        $stmt = $pdo->prepare("
            INSERT INTO user_notifications (notification_id, user_id)
            SELECT :notification_id, user_id FROM notification_settings
            WHERE blacklist_alerts = 1
        ");
        $stmt->execute([':notification_id' => $notificationId]);
        
        // Тук може да се добави код за изпращане на имейл или SMS известия
        
        return $notificationId;
    } catch (PDOException $e) {
        error_log('Database error: ' . $e->getMessage());
        return false;
    } catch (Exception $e) {
        error_log('Error creating alert: ' . $e->getMessage());
        return false;
    }
}

// Пример за използване
try {
    // Връзка към базата данни
    $pdo = new PDO('mysql:host=localhost;dbname=license_plate_system;charset=utf8mb4', 'username', 'password');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    // Път до изображението
    $imagePath = 'uploads/captures/capture_1.jpg';
    
    // Разпознаване на номера
    $plateData = recognizeLicensePlate($imagePath);
    
    // Запис в базата данни
    $recognitionId = savePlateRecognition($plateData, 1, 1, $imagePath, $pdo);
    
    if ($recognitionId) {
        echo "License plate recognition saved with ID: {$recognitionId}";
    } else {
        echo "Failed to save license plate recognition";
    }
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>
```

### 4.2. Интеграция с облачна услуга (Plate Recognizer API)

```php
<?php
/**
 * Функция за разпознаване на регистрационен номер чрез Plate Recognizer API
 * 
 * @param string $imagePath Път до изображението
 * @param string $apiKey API ключ за Plate Recognizer
 * @param array $options Допълнителни опции за API заявката
 * @return array Масив с резултати от разпознаването
 */
function recognizeLicensePlateCloud($imagePath, $apiKey, $options = []) {
    try {
        // URL на API
        $url = 'https://api.platerecognizer.com/v1/plate-reader/';
        
        // Подготовка на заявката
        $curl = curl_init();
        
        // Задаване на параметри за заявката
        $postFields = [
            'upload' => new CURLFile($imagePath),
            'regions' => $options['regions'] ?? '',
            'camera_id' => $options['camera_id'] ?? '',
            'timestamp' => $options['timestamp'] ?? date('c')
        ];
        
        curl_setopt_array($curl, [
            CURLOPT_URL => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_ENCODING => '',
            CURLOPT_MAXREDIRS => 10,
            CURLOPT_TIMEOUT => 30,
            CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
            CURLOPT_CUSTOMREQUEST => 'POST',
            CURLOPT_POSTFIELDS => $postFields,
            CURLOPT_HTTPHEADER => [
                'Authorization: Token ' . $apiKey
            ],
        ]);
        
        // Изпълнение на заявката
        $response = curl_exec($curl);
        $err = curl_error($curl);
        
        curl_close($curl);
        
        if ($err) {
            throw new Exception('cURL Error: ' . $err);
        }
        
        // Преобразуване на отговора в масив
        $results = json_decode($response, true);
        
        return $results;
    } catch (Exception $e) {
        error_log('Error recognizing license plate: ' . $e->getMessage());
        return [
            'error' => true,
            'message' => $e->getMessage()
        ];
    }
}

/**
 * Функция за обработка на резултатите от Plate Recognizer API
 * 
 * @param array $plateData Данни от разпознаването
 * @param int $cameraId ID на камерата
 * @param int $streamId ID на потока
 * @param string $imagePath Път до изображението
 * @param PDO $pdo PDO връзка към базата данни
 * @return int|bool ID на записа или false при грешка
 */
function processCloudRecognition($plateData, $cameraId, $streamId, $imagePath, $pdo) {
    try {
        // Проверка за грешки в разпознаването
        if (isset($plateData['error']) && $plateData['error']) {
            error_log('Error in plate data: ' . $plateData['message']);
            return false;
        }
        
        // Проверка дали има разпознати номера
        if (empty($plateData['results'])) {
            error_log('No license plates detected');
            return false;
        }
        
        // Обработка на всички разпознати номера
        foreach ($plateData['results'] as $result) {
            $licensePlate = $result['plate'];
            $confidence = $result['score'] * 100; // Преобразуване в проценти
            
            // Създаване на изрязано изображение на номера
            $plateImagePath = createPlateImageFromCloud($imagePath, $result['box']);
            
            // Проверка дали номерът е в бял или черен списък
            $isInWhitelist = checkIfInWhitelist($licensePlate, $pdo);
            $isInBlacklist = checkIfInBlacklist($licensePlate, $pdo);
            
            // Запис в базата данни
            $stmt = $pdo->prepare("
                INSERT INTO recognitions 
                (license_plate, confidence, camera_id, stream_id, capture_time, image_path, plate_image_path, is_in_whitelist, is_in_blacklist)
                VALUES 
                (:license_plate, :confidence, :camera_id, :stream_id, NOW(), :image_path, :plate_image_path, :is_in_whitelist, :is_in_blacklist)
            ");
            
            $stmt->execute([
                ':license_plate' => $licensePlate,
                ':confidence' => $confidence,
                ':camera_id' => $cameraId,
                ':stream_id' => $streamId,
                ':image_path' => $imagePath,
                ':plate_image_path' => $plateImagePath,
                ':is_in_whitelist' => $isInWhitelist ? 1 : 0,
                ':is_in_blacklist' => $isInBlacklist ? 1 : 0
            ]);
            
            $recognitionId = $pdo->lastInsertId();
            
            // Ако номерът е в черния списък, създаваме известие
            if ($isInBlacklist) {
                createBlacklistAlert($licensePlate, $recognitionId, $cameraId, $pdo);
            }
            
            return $recognitionId;
        }
        
        return false;
    } catch (PDOException $e) {
        error_log('Database error: ' . $e->getMessage());
        return false;
    } catch (Exception $e) {
        error_log('Error processing recognition: ' . $e->getMessage());
        return false;
    }
}

/**
 * Функция за създаване на изрязано изображение на номера от облачно разпознаване
 * 
 * @param string $imagePath Път до оригиналното изображение
 * @param array $box Координати на номера в изображението
 * @return string Път до изрязаното изображение
 */
function createPlateImageFromCloud($imagePath, $box) {
    // Генериране на име за изрязаното изображение
    $plateImagePath = 'uploads/plates/' . pathinfo($imagePath, PATHINFO_FILENAME) . '_plate.jpg';
    
    // Създаване на директорията, ако не съществува
    if (!file_exists(dirname($plateImagePath))) {
        mkdir(dirname($plateImagePath), 0755, true);
    }
    
    // Зареждане на оригиналното изображение
    $image = imagecreatefromjpeg($imagePath);
    
    // Изчисляване на координатите на правоъгълника
    $x1 = $box['xmin'];
    $y1 = $box['ymin'];
    $x2 = $box['xmax'];
    $y2 = $box['ymax'];
    
    // Добавяне на малко допълнително пространство около номера
    $padding = 10;
    $x1 = max(0, $x1 - $padding);
    $y1 = max(0, $y1 - $padding);
    $x2 = min(imagesx($image), $x2 + $padding);
    $y2 = min(imagesy($image), $y2 + $padding);
    
    // Изрязване на изображението
    $width = $x2 - $x1;
    $height = $y2 - $y1;
    $plateImage = imagecreatetruecolor($width, $height);
    imagecopy($plateImage, $image, 0, 0, $x1, $y1, $width, $height);
    
    // Запазване на изрязаното изображение
    imagejpeg($plateImage, $plateImagePath, 90);
    
    // Освобождаване на паметта
    imagedestroy($image);
    imagedestroy($plateImage);
    
    return $plateImagePath;
}

// Пример за използване на облачно разпознаване
try {
    // Връзка към базата данни
    $pdo = new PDO('mysql:host=localhost;dbname=license_plate_system;charset=utf8mb4', 'username', 'password');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    
    // Път до изображението
    $imagePath = 'uploads/captures/capture_1.jpg';
    
    // API ключ за Plate Recognizer
    $apiKey = 'YOUR_API_KEY';
    
    // Опции за API заявката
    $options = [
        'regions' => 'eu', // Регион за разпознаване (eu за Европа)
        'camera_id' => 'camera_1' // Идентификатор на камерата
    ];
    
    // Разпознаване на номера
    $plateData = recognizeLicensePlateCloud($imagePath, $apiKey, $options);
    
    // Обработка на резултатите
    $recognitionId = processCloudRecognition($plateData, 1, 1, $imagePath, $pdo);
    
    if ($recognitionId) {
        echo "License plate recognition saved with ID: {$recognitionId}";
    } else {
        echo "Failed to save license plate recognition";
    }
} catch (Exception $e) {
    echo "Error: " . $e->getMessage();
}
?>
```

## 5. Обработка на видео потоци

### 5.1. Архитектура за обработка на видео потоци

За ефективна обработка на видео потоци от множество камери, препоръчваме следната архитектура:

1. **Модул за получаване на видео потоци** - отговаря за свързване към камерите и получаване на видео потоците
2. **Модул за извличане на кадри** - извлича кадри от видео потоците на определен интервал
3. **Модул за разпознаване** - обработва извлечените кадри и разпознава регистрационните номера
4. **Модул за съхранение** - записва разпознатите номера и изображения в базата данни
5. **Модул за известяване** - генерира известия при засичане на номера от черния списък

### 5.2. Примерен код за обработка на видео поток с FFmpeg и PHP

```php
<?php
/**
 * Скрипт за обработка на видео поток и разпознаване на регистрационни номера
 * 
 * Този скрипт се изпълнява като демон процес и обработва видео потоци от камери
 */

// Конфигурация
$config = [
    'db_host' => 'localhost',
    'db_name' => 'license_plate_system',
    'db_user' => 'username',
    'db_pass' => 'password',
    'frame_interval' => 2, // Интервал между кадрите в секунди
    'recognition_method' => 'local', // 'local' или 'cloud'
    'api_key' => 'YOUR_API_KEY', // API ключ за облачно разпознаване
    'temp_dir' => '/tmp/license_plate_frames',
    'upload_dir' => 'uploads/captures',
    'log_file' => '/var/log/license_plate_system.log'
];

// Създаване на директории, ако не съществуват
if (!file_exists($config['temp_dir'])) {
    mkdir($config['temp_dir'], 0755, true);
}
if (!file_exists($config['upload_dir'])) {
    mkdir($config['upload_dir'], 0755, true);
}

// Функция за записване на съобщения в лог файла
function logMessage($message) {
    global $config;
    $timestamp = date('Y-m-d H:i:s');
    file_put_contents($config['log_file'], "[{$timestamp}] {$message}" . PHP_EOL, FILE_APPEND);
}

// Връзка към базата данни
try {
    $pdo = new PDO(
        "mysql:host={$config['db_host']};dbname={$config['db_name']};charset=utf8mb4",
        $config['db_user'],
        $config['db_pass']
    );
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    logMessage("Connected to database successfully");
} catch (PDOException $e) {
    logMessage("Database connection failed: " . $e->getMessage());
    exit(1);
}

// Функция за вземане на активните камери и потоци
function getActiveStreams($pdo) {
    $stmt = $pdo->query("
        SELECT s.id as stream_id, s.camera_id, s.url, s.protocol, c.name as camera_name, c.location
        FROM streams s
        JOIN cameras c ON s.camera_id = c.id
        WHERE s.is_active = 1 AND c.is_active = 1
    ");
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

// Функция за извличане на кадър от видео поток
function captureFrame($streamUrl, $outputPath) {
    $command = "ffmpeg -y -i \"{$streamUrl}\" -vframes 1 \"{$outputPath}\" 2>&1";
    exec($command, $output, $returnCode);
    
    if ($returnCode !== 0) {
        logMessage("Error capturing frame: " . implode("\n", $output));
        return false;
    }
    
    return true;
}

// Функция за обработка на поток
function processStream($stream, $pdo, $config) {
    $streamId = $stream['stream_id'];
    $cameraId = $stream['camera_id'];
    $streamUrl = $stream['url'];
    $cameraName = $stream['camera_name'];
    $location = $stream['location'];
    
    logMessage("Processing stream {$streamId} from camera {$cameraName} ({$location})");
    
    // Генериране на име за файла с кадъра
    $timestamp = time();
    $frameFilename = "camera_{$cameraId}_stream_{$streamId}_{$timestamp}.jpg";
    $tempFramePath = "{$config['temp_dir']}/{$frameFilename}";
    $finalFramePath = "{$config['upload_dir']}/{$frameFilename}";
    
    // Извличане на кадър от потока
    if (!captureFrame($streamUrl, $tempFramePath)) {
        logMessage("Failed to capture frame from stream {$streamId}");
        return false;
    }
    
    // Копиране на кадъра в постоянната директория
    if (!copy($tempFramePath, $finalFramePath)) {
        logMessage("Failed to copy frame to uploads directory");
        unlink($tempFramePath);
        return false;
    }
    
    // Разпознаване на регистрационни номера
    if ($config['recognition_method'] === 'local') {
        // Локално разпознаване с OpenALPR
        $plateData = recognizeLicensePlate($finalFramePath);
        $recognitionId = savePlateRecognition($plateData, $cameraId, $streamId, $finalFramePath, $pdo);
    } else {
        // Облачно разпознаване
        $options = [
            'regions' => 'eu',
            'camera_id' => "camera_{$cameraId}"
        ];
        $plateData = recognizeLicensePlateCloud($finalFramePath, $config['api_key'], $options);
        $recognitionId = processCloudRecognition($plateData, $cameraId, $streamId, $finalFramePath, $pdo);
    }
    
    // Изтриване на временния файл
    unlink($tempFramePath);
    
    if ($recognitionId) {
        logMessage("Successfully recognized license plate with ID: {$recognitionId}");
        return true;
    } else {
        logMessage("No license plates recognized in frame");
        return false;
    }
}

// Основен цикъл за обработка на потоци
logMessage("Starting license plate recognition service");

while (true) {
    try {
        // Вземане на активните потоци
        $streams = getActiveStreams($pdo);
        logMessage("Found " . count($streams) . " active streams");
        
        // Обработка на всеки поток
        foreach ($streams as $stream) {
            processStream($stream, $pdo, $config);
        }
        
        // Изчакване преди следващата итерация
        sleep($config['frame_interval']);
    } catch (Exception $e) {
        logMessage("Error: " . $e->getMessage());
        sleep(10); // Изчакване при грешка
    }
}
?>
```

### 5.3. Стартиране на скрипта като системна услуга

За да стартирате скрипта за обработка на видео потоци като системна услуга, можете да използвате systemd:

1. Създайте файл `/etc/systemd/system/license-plate-recognition.service`:

```ini
[Unit]
Description=License Plate Recognition Service
After=network.target mysql.service

[Service]
User=www-data
Group=www-data
ExecStart=/usr/bin/php /path/to/your/script.php
Restart=always
RestartSec=5
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=license-plate-recognition

[Install]
WantedBy=multi-user.target
```

2. Активирайте и стартирайте услугата:

```bash
sudo systemctl daemon-reload
sudo systemctl enable license-plate-recognition
sudo systemctl start license-plate-recognition
```

3. Проверете статуса на услугата:

```bash
sudo systemctl status license-plate-recognition
```

## 6. Добри практики и оптимизации

### 6.1. Оптимизация на производителността

1. **Интервал между кадрите** - Настройте подходящ интервал между обработваните кадри според нуждите и ресурсите на системата. По-малък интервал означава по-голяма вероятност за засичане, но и по-голямо натоварване.

2. **Паралелна обработка** - Използвайте многонишкова обработка за едновременна работа с множество потоци:

```php
// Пример за паралелна обработка с PHP-FPM
$processes = [];
$maxProcesses = 4; // Максимален брой паралелни процеси

foreach ($streams as $stream) {
    // Изчакване, ако достигнем максималния брой процеси
    while (count($processes) >= $maxProcesses) {
        foreach ($processes as $pid => $process) {
            if (!posix_kill($pid, 0)) {
                unset($processes[$pid]);
            }
        }
        usleep(100000); // 100ms
    }
    
    // Стартиране на нов процес
    $pid = pcntl_fork();
    
    if ($pid == -1) {
        // Грешка при създаване на процес
        logMessage("Error forking process");
    } else if ($pid) {
        // Родителски процес
        $processes[$pid] = true;
    } else {
        // Дъщерен процес
        processStream($stream, $pdo, $config);
        exit(0);
    }
}

// Изчакване на всички процеси да приключат
foreach ($processes as $pid => $process) {
    pcntl_waitpid($pid, $status);
}
```

3. **Кеширане на резултати** - Кеширайте резултатите от разпознаването за кратки периоди, за да избегнете дублиране на записи за един и същ автомобил:

```php
function isRecentlyRecognized($licensePlate, $cameraId, $timeWindow, $pdo) {
    $stmt = $pdo->prepare("
        SELECT COUNT(*) FROM recognitions 
        WHERE license_plate = :license_plate 
        AND camera_id = :camera_id 
        AND capture_time >= DATE_SUB(NOW(), INTERVAL :time_window SECOND)
    ");
    
    $stmt->execute([
        ':license_plate' => $licensePlate,
        ':camera_id' => $cameraId,
        ':time_window' => $timeWindow
    ]);
    
    return $stmt->fetchColumn() > 0;
}
```

4. **Оптимизация на изображения** - Преоразмерете и оптимизирайте изображенията преди разпознаване:

```php
function optimizeImage($imagePath, $maxWidth = 1280, $maxHeight = 720) {
    $image = imagecreatefromjpeg($imagePath);
    $width = imagesx($image);
    $height = imagesy($image);
    
    // Преоразмеряване, ако е необходимо
    if ($width > $maxWidth || $height > $maxHeight) {
        $ratio = min($maxWidth / $width, $maxHeight / $height);
        $newWidth = round($width * $ratio);
        $newHeight = round($height * $ratio);
        
        $resized = imagecreatetruecolor($newWidth, $newHeight);
        imagecopyresampled($resized, $image, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
        
        imagejpeg($resized, $imagePath, 85); // Запазване с 85% качество
        imagedestroy($resized);
    } else {
        // Само оптимизиране на качеството
        imagejpeg($image, $imagePath, 85);
    }
    
    imagedestroy($image);
    return $imagePath;
}
```

### 6.2. Управление на съхранението

1. **Ротация на изображения** - Автоматично изтривайте стари изображения след определен период:

```php
function cleanupOldImages($pdo, $daysToKeep = 30) {
    // Вземане на стари записи
    $stmt = $pdo->prepare("
        SELECT image_path, plate_image_path FROM recognitions
        WHERE capture_time < DATE_SUB(NOW(), INTERVAL :days DAY)
    ");
    $stmt->execute([':days' => $daysToKeep]);
    
    $oldRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    // Изтриване на изображенията
    foreach ($oldRecords as $record) {
        if (file_exists($record['image_path'])) {
            unlink($record['image_path']);
        }
        if (file_exists($record['plate_image_path'])) {
            unlink($record['plate_image_path']);
        }
    }
    
    // Изтриване на записите от базата данни
    $stmt = $pdo->prepare("
        DELETE FROM recognitions
        WHERE capture_time < DATE_SUB(NOW(), INTERVAL :days DAY)
    ");
    $stmt->execute([':days' => $daysToKeep]);
    
    return $stmt->rowCount();
}
```

2. **Компресиране на архивни данни** - Компресирайте и архивирайте стари данни:

```php
function archiveOldData($pdo, $archivePath, $monthsToArchive = 3) {
    // Създаване на архивна директория
    if (!file_exists($archivePath)) {
        mkdir($archivePath, 0755, true);
    }
    
    // Експортиране на стари данни в CSV
    $stmt = $pdo->prepare("
        SELECT * FROM recognitions
        WHERE capture_time < DATE_SUB(NOW(), INTERVAL :months MONTH)
        AND capture_time >= DATE_SUB(NOW(), INTERVAL :next_month MONTH)
    ");
    
    $archiveDate = date('Y-m', strtotime("-{$monthsToArchive} months"));
    $nextMonth = $monthsToArchive + 1;
    
    $stmt->execute([
        ':months' => $monthsToArchive,
        ':next_month' => $nextMonth
    ]);
    
    $records = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    if (count($records) > 0) {
        $csvFile = "{$archivePath}/recognitions_{$archiveDate}.csv";
        $fp = fopen($csvFile, 'w');
        
        // Запис на хедъри
        fputcsv($fp, array_keys($records[0]));
        
        // Запис на данни
        foreach ($records as $record) {
            fputcsv($fp, $record);
        }
        
        fclose($fp);
        
        // Компресиране на CSV файла
        exec("gzip {$csvFile}");
        
        // Изтриване на архивираните данни от базата
        $stmt = $pdo->prepare("
            DELETE FROM recognitions
            WHERE capture_time < DATE_SUB(NOW(), INTERVAL :months MONTH)
            AND capture_time >= DATE_SUB(NOW(), INTERVAL :next_month MONTH)
        ");
        
        $stmt->execute([
            ':months' => $monthsToArchive,
            ':next_month' => $nextMonth
        ]);
        
        return $stmt->rowCount();
    }
    
    return 0;
}
```

### 6.3. Сигурност

1. **Защита на API ключове** - Съхранявайте API ключове и чувствителни данни в защитени конфигурационни файлове:

```php
// Зареждане на конфигурация от защитен файл
function loadSecureConfig($configFile) {
    if (!file_exists($configFile)) {
        throw new Exception("Configuration file not found: {$configFile}");
    }
    
    $config = parse_ini_file($configFile, true);
    
    if (!$config) {
        throw new Exception("Failed to parse configuration file");
    }
    
    return $config;
}

// Използване
try {
    $secureConfig = loadSecureConfig('/etc/license_plate_system/config.ini');
    $apiKey = $secureConfig['api']['key'];
} catch (Exception $e) {
    logMessage("Error loading configuration: " . $e->getMessage());
    exit(1);
}
```

2. **Валидация на входните данни** - Винаги валидирайте и санитизирайте входните данни:

```php
function validateLicensePlate($licensePlate) {
    // Премахване на специални символи и интервали
    $cleaned = preg_replace('/[^a-zA-Z0-9]/', '', $licensePlate);
    
    // Проверка за минимална дължина
    if (strlen($cleaned) < 2 || strlen($cleaned) > 10) {
        return false;
    }
    
    return $cleaned;
}
```

3. **Защита на изображенията** - Ограничете достъпа до изображенията чрез .htaccess или подобни механизми:

```apache
# .htaccess в директорията с изображения
<FilesMatch "\.(jpg|jpeg|png)$">
    Order Deny,Allow
    Deny from all
    # Разрешаване на достъп само за автентикирани потребители
    Require valid-user
</FilesMatch>
```

4. **Логване на действията** - Поддържайте подробен лог на всички действия:

```php
function logAction($action, $details, $userId = null, $pdo) {
    $stmt = $pdo->prepare("
        INSERT INTO system_log (user_id, action, description, ip_address, user_agent)
        VALUES (:user_id, :action, :description, :ip_address, :user_agent)
    ");
    
    $stmt->execute([
        ':user_id' => $userId,
        ':action' => $action,
        ':description' => json_encode($details),
        ':ip_address' => $_SERVER['REMOTE_ADDR'] ?? null,
        ':user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? null
    ]);
    
    return $pdo->lastInsertId();
}
```

### 6.4. Мониторинг и поддръжка

1. **Мониторинг на системата** - Следете производителността и състоянието на системата:

```php
function checkSystemStatus($pdo) {
    $status = [
        'database' => false,
        'disk_space' => false,
        'camera_connections' => [],
        'recognition_service' => false
    ];
    
    // Проверка на базата данни
    try {
        $stmt = $pdo->query("SELECT 1");
        $status['database'] = ($stmt->fetchColumn() === 1);
    } catch (PDOException $e) {
        $status['database_error'] = $e->getMessage();
    }
    
    // Проверка на дисковото пространство
    $diskFree = disk_free_space('/');
    $diskTotal = disk_total_space('/');
    $status['disk_space'] = [
        'free' => $diskFree,
        'total' => $diskTotal,
        'percent_free' => ($diskFree / $diskTotal) * 100
    ];
    
    // Проверка на връзките с камерите
    $stmt = $pdo->query("
        SELECT s.id, s.url, c.name
        FROM streams s
        JOIN cameras c ON s.camera_id = c.id
        WHERE s.is_active = 1 AND c.is_active = 1
        LIMIT 10
    ");
    
    $streams = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    foreach ($streams as $stream) {
        $ch = curl_init($stream['url']);
        curl_setopt($ch, CURLOPT_NOBODY, true);
        curl_setopt($ch, CURLOPT_TIMEOUT, 5);
        curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        $status['camera_connections'][$stream['id']] = [
            'name' => $stream['name'],
            'status' => ($httpCode >= 200 && $httpCode < 300)
        ];
    }
    
    // Проверка на услугата за разпознаване
    $serviceStatus = shell_exec('systemctl is-active license-plate-recognition.service');
    $status['recognition_service'] = (trim($serviceStatus) === 'active');
    
    return $status;
}
```

2. **Автоматично възстановяване** - Имплементирайте механизми за автоматично възстановяване при проблеми:

```php
function restartRecognitionService() {
    exec('sudo systemctl restart license-plate-recognition.service', $output, $returnCode);
    return $returnCode === 0;
}

function reconnectCamera($streamId, $pdo) {
    // Вземане на информация за потока
    $stmt = $pdo->prepare("SELECT url, protocol FROM streams WHERE id = :stream_id");
    $stmt->execute([':stream_id' => $streamId]);
    $stream = $stmt->fetch(PDO::FETCH_ASSOC);
    
    if (!$stream) {
        return false;
    }
    
    // Опит за възстановяване на връзката
    $ch = curl_init($stream['url']);
    curl_setopt($ch, CURLOPT_NOBODY, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    // Ако връзката е успешна, актуализираме статуса
    if ($httpCode >= 200 && $httpCode < 300) {
        $stmt = $pdo->prepare("UPDATE streams SET is_active = 1 WHERE id = :stream_id");
        $stmt->execute([':stream_id' => $streamId]);
        return true;
    }
    
    return false;
}
```

## 7. Заключение

Интеграцията на модул за разпознаване на регистрационни номера е ключов компонент на системата. В този документ представихме различни опции за имплементация, примерен код и добри практики за оптимизация и сигурност.

При избора между локално и облачно разпознаване, трябва да се вземат предвид специфичните изисквания на проекта, наличните ресурси и бюджет. Локалното разпознаване предлага по-голям контрол и по-ниска латентност, но изисква по-мощен хардуер и повече поддръжка. Облачното разпознаване е по-лесно за имплементация и поддръжка, но изисква постоянна интернет връзка и включва месечни такси.

Независимо от избрания подход, важно е да се следват добрите практики за оптимизация на производителността, управление на съхранението, сигурност и мониторинг, за да се осигури надеждна и ефективна работа на системата.
