Developing a Complete Automated Parking Management Solution
A full-featured unmanned parking solution featuring license plate detection, automatic billing, and gate control
Project Overview and Significance
As urbanization accelerates and vehicle ownership grows, parking lot management faces challenges like low efficiency, high labor costs, and traffic congestion. Traditional manual methods are inefficient and prone to billing errors. License Plate Recognition (LPR) technology, an essential component of intelligent transportation systems, enables automated vehicle detection, identification, billing, and gate operations—addressing these issues effectively.
This tutorial guides you through building a complete unmanned parking lot system based on Raspberry Pi 4B with core features:
- ✅ Automatic vehicle detection upon approach
- ✅ Real-time license plate recognition
- ✅ Automated billing (based on parking duration)
- ✅ Gate automation control
- ✅ Real-time parking space availability
- ✅ Vehicle data storage
System Architecture and Hardware Setup
2.1 Overall System Design
2.2 Required Components
| Component | Model | Function | Estimated Cost |
|---|---|---|---|
| Raspberry Pi | 4B (4GB RAM) | Main controller | Approx. 500 CNY |
| Camera | Raspberry Pi official CSI camera | Image capture | Approx. 100 CNY |
| Ultrasonic Sensor | HC-SR04 | Vehicle proximity detection | Approx. 15 CNY |
| Servo Motor | SG90 | Gate simulation | Approx. 20 CNY |
| Buzzer | Active buzzer module | Audio alerts | Approx. 5 CNY |
| Jumper Wires | Male-to-female/Male-to-male | Circuit connections | Approx. 10 CNY |
| Breadboard | 400-pin | Prototype assembly | Approx. 10 CNY |
💡 Recommendation: Choose Raspberry Pi 4B with 4GB+ RAM for smooth image processing. Use CSI interface cameras exclusively, as USB cameras may introduce delays.
2.3 Pin Configuration Table
| Module | Pin | Raspberry Pi GPIO | Description |
|---|---|---|---|
| HC-SR04 Ultrasonic | VCC | Pin 2 (5V) | Power positive |
| GND | Pin 6 (GND) | Ground | |
| Trig | Pin 16 (GPIO23) | Trigger signal | |
| Echo | Pin 18 (GPIO24) | Echo signal | |
| SG90 Servo | Red | Pin 2 (5V) | Power positive |
| Brown | Pin 6 (GND) | Ground | |
| Orange | Pin 12 (GPIO18) | PWM control | |
| Buzzer | VCC | Pin 4 (5V) | Power positive |
| I/O | Pin 22 (GPIO25) | Control signal | |
| GND | Pin 14 (GND) | Ground |
Software Environment Setup
3.1 Raspberry Pi OS Installation
Use the official Raspberry Pi Imager tool to flash the operating system:
- Download and install Raspberry Pi Imager
- Insert SD card and select Raspberry Pi OS (64-bit)
- Click gear icon to preconfigure:
- Set WiFi credentials
- Enable SSH service
- Configure username/password
- Click write and wait for completion
3.2 System Updates and Dependencies
# Update system
sudo apt-get update && sudo apt-get upgrade -y
# Install Python3 and pip
sudo apt-get install python3 python3-pip -y
# Install OpenCV dependencies
sudo apt-get install -y \
libhdf5-dev libhdf5-serial-dev \
libqtgui4 libqtwebkit4 libqt4-test \
python3-pyqt5 \
libatlas-base-dev \
libjasper-dev
# Install OpenCV
sudo apt-get install python3-opencv -y
3.3 Install HyperLPR3 License Plate Recognition Library
This system uses HyperLPR3 as the recognition engine—a high-performance deep learning-based Chinese license plate recognition framework. It can recognize a license plate in under 100ms on Raspberry Pi with 95%-97% accuracy.
# Install HyperLPR3
pip3 install hyperlpr3
# Verify installation
python3 -c "import hyperlpr3; print('Installation successful')"
3.4 Additional Python Dependencies
# Install RPi.GPIO library
pip3 install RPi.GPIO
# Install Pillow for image handling
pip3 install pillow
# Install Tkinter (included in Raspberry Pi OS)
sudo apt-get install python3-tk -y
# Install NumPy
pip3 install numpy
3.5 Configure VNC Remote Desktop (Optional)
# Launch Raspberry Pi configuration
sudo raspi-config
# Navigate to Interface Options → VNC → Select Yes
# Reboot after completion
sudo reboot
After rebooting, connect via RealVNC Viewer using you're Raspberry Pi's IP address.
Core Module Implementation
4.1 Ultrasonic Distance Measurement (HC-SR04)
The HC-SR04 sensor works by sending a 40kHz ultrasonic pulse that reflects off obstacles. The distance is calculated using the echo time:
Distance (cm) = Echo Time (μs) / 58
import RPi.GPIO as GPIO
import time
class UltrasonicSensor:
"""HC-SR04 ultrasonic sensor driver class"""
def __init__(self, trig_pin=23, echo_pin=24):
"""
Initialize ultrasonic sensor
:param trig_pin: Trigger GPIO pin number
:param echo_pin: Echo GPIO pin number
"""
self.trig_pin = trig_pin
self.echo_pin = echo_pin
# Set GPIO mode
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
# Initialize pins
GPIO.setup(self.trig_pin, GPIO.OUT)
GPIO.setup(self.echo_pin, GPIO.IN)
# Initial state: Trig low
GPIO.output(self.trig_pin, False)
time.sleep(0.5)
def get_distance(self):
"""
Get distance measurement
:return: Distance in cm, returns -1 on failure
"""
try:
# Send 10us trigger pulse
GPIO.output(self.trig_pin, True)
time.sleep(0.00001) # 10 microseconds
GPIO.output(self.trig_pin, False)
# Wait for Echo to go high, timeout
timeout_start = time.time()
while GPIO.input(self.echo_pin) == 0:
if time.time() - timeout_start > 0.1:
return -1 # Timeout
pulse_start = time.time()
# Wait for Echo to go low
while GPIO.input(self.echo_pin) == 1:
if time.time() - pulse_start > 0.1:
return -1
pulse_end = time.time()
# Calculate pulse duration (seconds)
pulse_duration = pulse_end - pulse_start
# Calculate distance: speed of sound 340 m/s = 34000 cm/s
# Distance = time × speed / 2
distance = pulse_duration * 34000 / 2
return round(distance, 2)
except Exception as e:
print(f"Ultrasonic measurement error: {e}")
return -1
def is_vehicle_approaching(self, threshold=30):
"""
Check if vehicle is approaching
:param threshold: Distance threshold in cm
:return: True if vehicle approaching, False otherwise
"""
distance = self.get_distance()
if distance == -1:
return False
return distance < threshold
def cleanup(self):
"""Clean up GPIO resources"""
GPIO.cleanup([self.trig_pin, self.echo_pin])
4.2 Servo Motor Control (SG90)
Servos use PWM signals for angle control, with a 20ms period and 0.5ms–2.5ms pulse width corresponding to 0°–180°.
import RPi.GPIO as GPIO
import time
class ServoMotor:
"""SG90 servo motor controller class"""
def __init__(self, pwm_pin=18):
"""
Initialize servo motor
:param pwm_pin: PWM output GPIO pin number
"""
self.pwm_pin = pwm_pin
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.pwm_pin, GPIO.OUT)
# Create PWM instance at 50Hz (20ms period)
self.pwm = GPIO.PWM(self.pwm_pin, 50)
self.pwm.start(0) # Initial duty cycle 0%
time.sleep(0.5)
def set_angle(self, angle):
"""
Set servo angle
:param angle: Angle in degrees (0–180)
"""
if angle < 0:
angle = 0
elif angle > 180:
angle = 180
# Calculate duty cycle: 0.5ms = 0°, 2.5ms = 180°
# Duty cycle = (pulse width / period) × 100%
# 0°: 0.5ms → duty cycle 2.5%
# 180°: 2.5ms → duty cycle 12.5%
duty = 2.5 + (angle / 180.0) * 10.0
self.pwm.ChangeDutyCycle(duty)
time.sleep(0.3) # Allow motor to reach position
self.pwm.ChangeDutyCycle(0) # Stop signal to prevent jitter
def open_gate(self):
"""Open gate (rotate to 90 degrees)"""
self.set_angle(90)
print("Gate opened")
def close_gate(self):
"""Close gate (rotate to 0 degrees)"""
self.set_angle(0)
print("Gate closed")
def cleanup(self):
"""Clean up resources"""
self.pwm.stop()
GPIO.cleanup([self.pwm_pin])
4.3 Buzzer Module
import RPi.GPIO as GPIO
import time
class Buzzer:
"""Buzzer controller class"""
def __init__(self, pin=25):
"""
Initialize buzzer
:param pin: Control GPIO pin number
"""
self.pin = pin
GPIO.setmode(GPIO.BCM)
GPIO.setup(self.pin, GPIO.OUT)
GPIO.output(self.pin, GPIO.HIGH) # Initially off (high level)
def beep(self, duration=0.1):
"""Single short beep"""
GPIO.output(self.pin, GPIO.LOW) # Low triggers
time.sleep(duration)
GPIO.output(self.pin, GPIO.HIGH)
def double_beep(self):
"""Two short beeps"""
self.beep(0.1)
time.sleep(0.1)
self.beep(0.1)
def cleanup(self):
"""Clean up resources"""
GPIO.output(self.pin, GPIO.HIGH)
GPIO.cleanup([self.pin])
4.4 License Plate Recognition Module (HyperLPR3)
This is the core module implementing plate detection and recognition using HyperLPR3.
import cv2
import hyperlpr3 as lpr3
import numpy as np
class LicensePlateRecognizer:
"""License plate recognition class (using HyperLPR3)"""
def __init__(self):
"""Initialize plate recognizer"""
# Create HyperLPR3 recognizer instance
self.catcher = lpr3.LicensePlateCatcher()
# Recognition result cache for verification
self.last_plate = None
self.consecutive_count = 0
def recognize_from_image(self, image):
"""
Recognize license plate from image
:param image: OpenCV image (numpy array)
:return: Recognized plate number, None on failure
"""
try:
results = self.catcher(image)
if results and len(results) > 0:
# Take highest confidence result
plate_info = results[0]
plate_number = plate_info['code']
confidence = plate_info['confidence']
print(f"Recognition result: {plate_number}, confidence: {confidence:.2f}")
return plate_number
return None
except Exception as e:
print(f"Plate recognition error: {e}")
return None
def recognize_with_verification(self, image, required_matches=7):
"""
Plate recognition with consecutive validation
Only confirm if same plate detected multiple times for higher accuracy
:param image: OpenCV image
:param required_matches: Required consecutive matches
:return: Confirmed plate number, None if unconfirmed
"""
plate = self.recognize_from_image(image)
if plate is None:
self.consecutive_count = 0
self.last_plate = None
return None
# Validate plate length (Chinese plates 7-8 characters)
if len(plate) not in [7, 8]:
self.consecutive_count = 0
self.last_plate = None
return None
if plate == self.last_plate:
self.consecutive_count += 1
else:
self.last_plate = plate
self.consecutive_count = 1
if self.consecutive_count >= required_matches:
self.consecutive_count = 0
self.last_plate = None
return plate
return None
def draw_plate_box(self, image):
"""
Draw license plate bounding box on image
:param image: OpenCV image
:return: Image with drawn box
"""
results = self.catcher(image)
if results and len(results) > 0:
plate_info = results[0]
# Get plate coordinates
if 'box' in plate_info:
box = plate_info['box']
# Draw rectangle
cv2.polylines(image, [np.array(box, np.int32)], True, (0, 255, 0), 2)
# Add label
cv2.putText(image, plate_info['code'], (box[0][0], box[0][1] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
return image
4.5 Data Storage Module (CSV File)
Store vehicle data including plate number, entry time, and timestamp in CSV format.
import csv
import os
from datetime import datetime
class ParkingDataManager:
"""Parking data manager class"""
def __init__(self, filename="parking_data.csv"):
"""
Initialize data manager
:param filename: CSV file name
"""
self.filename = filename
self.vehicles = [] # List of vehicle records
self.load_data()
def load_data(self):
"""Load vehicle data from CSV file"""
self.vehicles = []
if not os.path.exists(self.filename):
# File doesn't exist, create empty file with header
self._create_empty_file()
return
try:
with open(self.filename, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
self.vehicles.append({
'plate': row['plate'],
'entry_time': row['entry_time'],
'timestamp': int(row['timestamp'])
})
print(f"Loaded {len(self.vehicles)} vehicle records")
except Exception as e:
print(f"Failed to load data: {e}")
self.vehicles = []
def _create_empty_file(self):
"""Create empty CSV file"""
with open(self.filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['plate', 'entry_time', 'timestamp'])
def save_data(self):
"""Save vehicle data to CSV file"""
with open(self.filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['plate', 'entry_time', 'timestamp'])
for v in self.vehicles:
writer.writerow([v['plate'], v['entry_time'], v['timestamp']])
def add_vehicle(self, plate):
"""
Add vehicle (entry)
:param plate: License plate number
:return: Success status
"""
# Check if already registered
if self.find_vehicle(plate):
print(f"Vehicle {plate} already in database")
return False
now = datetime.now()
entry_time = now.strftime("%Y-%m-%d %H:%M:%S")
timestamp = int(now.timestamp())
self.vehicles.append({
'plate': plate,
'entry_time': entry_time,
'timestamp': timestamp
})
self.save_data()
print(f"Vehicle {plate} entered at {entry_time}")
return True
def remove_vehicle(self, plate):
"""
Remove vehicle (exit)
:param plate: License plate number
:return: Removed vehicle record, None if not found
"""
for i, v in enumerate(self.vehicles):
if v['plate'] == plate:
removed = self.vehicles.pop(i)
self.save_data()
return removed
return None
def find_vehicle(self, plate):
"""
Find vehicle
:param plate: License plate number
:return: Vehicle record, None if not found
"""
for v in self.vehicles:
if v['plate'] == plate:
return v
return None
def get_parking_count(self):
"""Get current number of parked vehicles"""
return len(self.vehicles)
def get_available_spots(self, total_spots=10):
"""Get available parking spots"""
return total_spots - len(self.vehicles)
def calculate_fee(self, entry_timestamp, exit_timestamp=None):
"""
Calculate parking fee
:param entry_timestamp: Entry timestamp
:param exit_timestamp: Exit timestamp, default current time
:return: Fee in RMB
"""
if exit_timestamp is None:
exit_timestamp = int(datetime.now().timestamp())
# Calculate duration in minutes
duration_minutes = (exit_timestamp - entry_timestamp) / 60
# Billing rules: free first 30 minutes, then 1 RMB per 30 minutes, cap at 20 RMB/day
if duration_minutes <= 30:
return 0.0
# Charge 1 RMB every 30 minutes after 30 minutes
billable_half_hours = (duration_minutes - 30) / 30
fee = billable_half_hours * 1.0
# Daily cap at 20 RMB
if fee > 20:
fee = 20.0
return round(fee, 2)
def display_all_vehicles(self):
"""Display all vehicle records"""
print("\nCurrently parked vehicles:")
print("-" * 50)
for v in self.vehicles:
print(f"Plate: {v['plate']}, Entry time: {v['entry_time']}")
print("-" * 50)
4.6 Display Interface Module (Tkinter)
Use Tkinter to build a graphical user interface showing parking status, time, and recognition results.
import tkinter as tk
from tkinter import ttk, font
from datetime import datetime
import threading
import time
class ParkingGUI:
"""Parking management GUI interface"""
def __init__(self, data_manager, total_spots=10):
"""
Initialize GUI
:param data_manager: Data manager instance
:param total_spots: Total parking spots
"""
self.data_manager = data_manager
self.total_spots = total_spots
# Create main window
self.root = tk.Tk()
self.root.title("Smart Parking Management System")
self.root.geometry("800x600")
self.root.configure(bg='#f0f0f0')
# Set fonts
self.title_font = font.Font(family="微软雅黑", size=24, weight="bold")
self.info_font = font.Font(family="微软雅黑", size=14)
self.setup_ui()
self.update_time() # Start time update
def setup_ui(self):
"""Set up UI layout"""
# Title
title_label = tk.Label(
self.root,
text="Smart Parking Management System",
font=self.title_font,
bg='#f0f0f0',
fg='#333333'
)
title_label.pack(pady=20)
# Main info frame
info_frame = tk.Frame(self.root, bg='#f0f0f0')
info_frame.pack(pady=10)
# Parking spot info
self.spots_label = tk.Label(
info_frame,
text=self.get_spots_text(),
font=self.info_font,
bg='#f0f0f0',
fg='#2c3e50'
)
self.spots_label.pack()
# Time info
self.time_label = tk.Label(
info_frame,
text="",
font=self.info_font,
bg='#f0f0f0',
fg='#7f8c8d'
)
self.time_label.pack(pady=5)
# Prompt message
self.tip_label = tk.Label(
info_frame,
text="Slow down, auto recognition active",
font=font.Font(family="微软雅黑", size=12),
bg='#f0f0f0',
fg='#e67e22'
)
self.tip_label.pack(pady=10)
# Vehicle info display area
self.vehicle_frame = tk.Frame(
self.root,
bg='white',
relief=tk.GROOVE,
bd=2
)
self.vehicle_frame.pack(pady=20, padx=40, fill=tk.BOTH, expand=True)
self.vehicle_info = tk.Text(
self.vehicle_frame,
height=8,
font=font.Font(family="Consolas", size=12),
bg='white',
fg='#333333',
relief=tk.FLAT
)
self.vehicle_info.pack(padx=10, pady=10, fill=tk.BOTH, expand=True)
# Status bar
status_bar = tk.Label(
self.root,
text="System running...",
bd=1,
relief=tk.SUNKEN,
anchor=tk.W,
bg='#ecf0f1'
)
status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def get_spots_text(self):
"""Get parking spot status text"""
available = self.data_manager.get_available_spots(self.total_spots)
used = self.data_manager.get_parking_count()
if available > 0:
return f" Available spots: {available} / {self.total_spots} (Parked {used})"
else:
return f" Full! {used} / {self.total_spots}"
def update_time(self):
"""Update time display (refresh every second)"""
current_time = datetime.now().strftime("%Y年%m月%d日 %H:%M:%S")
self.time_label.config(text=f" {current_time}")
self.spots_label.config(text=self.get_spots_text())
# Refresh every 1 second
self.root.after(1000, self.update_time)
def show_entry_info(self, plate, entry_time):
"""Show entry information"""
available = self.data_manager.get_available_spots(self.total_spots)
info = f"""
"""
self.vehicle_info.delete(1.0, tk.END)
self.vehicle_info.insert(1.0, info)
def show_exit_info(self, plate, entry_time, exit_time, fee):
"""Show exit information"""
available = self.data_manager.get_available_spots(self.total_spots)
info = f"""
"""
self.vehicle_info.delete(1.0, tk.END)
self.vehicle_info.insert(1.0, info)
def show_full_info(self, plate, current_time):
"""Show full parking notification"""
info = f"""
"""
self.vehicle_info.delete(1.0, tk.END)
self.vehicle_info.insert(1.0, info)
def clear_info(self):
"""Clear displayed information"""
self.vehicle_info.delete(1.0, tk.END)
def start(self):
"""Start GUI main loop"""
self.root.mainloop()
4.7 Main Program Integration
Integrate all modules into a complete parking management system.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Automated parking lot license plate recognition system
Built on Raspberry Pi 4B with HyperLPR3
"""
import cv2
import time
import threading
from datetime import datetime
# Import custom modules
from ultrasonic import UltrasonicSensor
from servo import ServoMotor
from buzzer import Buzzer
from plate_recognition import LicensePlateRecognizer
from data_manager import ParkingDataManager
from parking_gui import ParkingGUI
class ParkingSystem:
"""Main parking management system class"""
def __init__(self):
"""Initialize system"""
print("Initializing parking management system...")
# Initialize components
self.ultrasonic = UltrasonicSensor(trig_pin=23, echo_pin=24)
self.servo = ServoMotor(pwm_pin=18)
self.buzzer = Buzzer(pin=25)
self.recognizer = LicensePlateRecognizer()
self.data_manager = ParkingDataManager("parking_data.csv")
# System parameters
self.total_spots = 10 # Total parking spaces
self.last_operation_time = 0 # Last operation time
self.operation_interval = 5 # Minimum operation interval (seconds)
self.distance_threshold = 30 # Detection distance threshold (cm)
# Camera initialization
self.cap = cv2.VideoCapture(0)
if not self.cap.isOpened():
print("Error: Cannot open camera")
exit(1)
# GUI interface
self.gui = ParkingGUI(self.data_manager, self.total_spots)
# Running flag
self.running = True
print("System initialization complete")
def process_entry(self, plate):
"""
Handle vehicle entry
:param plate: License plate number
:return: Success status
"""
# Check if parking is full
available = self.data_manager.get_available_spots(self.total_spots)
if available <= 0:
print(f"Parking full, rejecting vehicle {plate}")
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.gui.show_full_info(plate, current_time)
return False
# Add vehicle record
if self.data_manager.add_vehicle(plate):
# Get entry time
vehicle = self.data_manager.find_vehicle(plate)
entry_time = vehicle['entry_time']
# Display entry info
self.gui.show_entry_info(plate, entry_time)
# Open gate
self.buzzer.double_beep()
self.servo.open_gate()
time.sleep(3) # Simulate vehicle passage
self.servo.close_gate()
print(f"Vehicle {plate} entered successfully")
return True
else:
print(f"Vehicle {plate} entry failed")
return False
def process_exit(self, plate):
"""
Handle vehicle exit
:param plate: License plate number
:return: Success status
"""
# Find vehicle
vehicle = self.data_manager.find_vehicle(plate)
if vehicle is None:
print(f"No record found for vehicle {plate}")
return False
entry_time = vehicle['entry_time']
entry_timestamp = vehicle['timestamp']
# Calculate parking fee
exit_timestamp = int(datetime.now().timestamp())
fee = self.data_manager.calculate_fee(entry_timestamp, exit_timestamp)
exit_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove vehicle record
self.data_manager.remove_vehicle(plate)
# Display exit info
self.gui.show_exit_info(plate, entry_time, exit_time, fee)
# Open gate
self.buzzer.double_beep()
self.servo.open_gate()
time.sleep(3) # Simulate vehicle passage
self.servo.close_gate()
print(f"Vehicle {plate} exited successfully, fee: ¥{fee}")
return True
def camera_loop(self):
"""Camera recognition loop (separate thread)"""
consecutive_failures = 0
max_failures = 25 # Maximum consecutive failures
while self.running:
# Detect vehicle approach
if not self.ultrasonic.is_vehicle_approaching(self.distance_threshold):
time.sleep(0.1)
continue
# Check operation interval
current_time = time.time()
if current_time - self.last_operation_time < self.operation_interval:
continue
print("Vehicle detected, starting recognition...")
# Capture image
ret, frame = self.cap.read()
if not ret:
print("Camera read failed")
continue
# Plate recognition
plate = self.recognizer.recognize_with_verification(frame, required_matches=7)
if plate:
print(f"Successfully recognized plate: {plate}")
# Determine entry or exit
vehicle = self.data_manager.find_vehicle(plate)
if vehicle:
# Vehicle in database → exit
self.process_exit(plate)
else:
# Vehicle not in database → entry
self.process_entry(plate)
# Update operation time
self.last_operation_time = time.time()
consecutive_failures = 0
# Wait for vehicle to leave sensor range
while self.ultrasonic.is_vehicle_approaching(self.distance_threshold):
time.sleep(0.5)
else:
consecutive_failures += 1
print(f"No plate recognized ({consecutive_failures}/{max_failures})")
if consecutive_failures >= max_failures:
print("Continuous failure, exiting recognition")
consecutive_failures = 0
self.gui.clear_info()
def run(self):
"""Start system"""
print("Parking management system started")
# Start camera recognition thread
camera_thread = threading.Thread(target=self.camera_loop, daemon=True)
camera_thread.start()
# Start GUI main loop
try:
self.gui.start()
except KeyboardInterrupt:
print("\nShutting down system...")
finally:
self.cleanup()
def cleanup(self):
"""Cleanup resources"""
self.running = False
self.cap.release()
self.ultrasonic.cleanup()
self.servo.cleanup()
self.buzzer.cleanup()
cv2.destroyAllWindows()
print("System shutdown complete")
if __name__ == "__main__":
system = ParkingSystem()
system.run()
Deep Dive into License Plate Recognition Technology
In the previous section, we implemented a complete parking lot license plate recognition system through code. But how does the system actually "see" license plates from an image? Why is HyperLPR3 so accurate in locating and recognizing characters? This section explores the underlying principles, breaking down each stage of the recognition process.
1. Overall Recognition Process
A typical license plate recognition system involves six key steps:
Image acquisition → Preprocessing → Plate localization → Character segmentation → Character recognition → Output results
Each step builds upon the previous one, with precision at any stage affecting overall performance. Using HyperLPR3 as an example, we'll examine how it handles each phase.
2. Image Preprocessing: Prepare for Recognition
Raw images often contain noise, uneven lighting, and low contrast, making preprocessing crucial to highlight plate features.
2.1 Grayscale Conversion
Convert color images to grayscale to reduce computational load while preserving texture and edge information.
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
2.2 Histogram Equalization
Enhance image contrast to make plate characters stand out more clearly against background, especially useful in dim conditions.
equalized = cv2.equalizeHist(gray)
2.3 Gaussian Filtering
Apply Gaussian kernel smoothing to suppress noise and avoid false edges in subsequent edge detection.
blurred = cv2.GaussianBlur(equalized, (5, 5), 0)
After preprocessing, the image proceeds to the critical plate localization stage.
3. Plate Localization: Find Where the Plate Is
The goal is to extract the rectangular region containing the license plate from complex backgrounds. HyperLPR3 uses a hybrid strategy combining cascade classifiers, morphological processing, and deep learning regression.
3.1 Haar Cascade Detection: Fast Initial Plate Location
Haar cascades are machine learning classifiers trained on positive and negative samples (plates vs non-plates). They quickly identify candidate regions.
Principle: Haar features resemble convolution kernels, calculating differences between black and white regions to describe local textures. For example, license plate characters typically have alternating vertical and horizontal edges, which can be captured by specific Haar features.
Pros: Extremely fast, completes detection in milliseconds. Cons: Prone to false positives and sensitive to rotation or tilt, providing only rough regions.
Therefore, HyperLPR3 uses Haar results as initial boxes, followed by refinement with more precise techniques.
3.2 Expand Rectangle Region
Since Haar detection might only cover part of the plate (e.g., just the "冀"), we extend the rectangle by a certain ratio to ensure full coverage.
# Expand rectangle
x, y, w, h = rect
new_w = int(w * 1.2)
new_h = int(h * 1.2)
x = max(0, x - (new_w - w) // 2)
y = max(0, y - (new_h - h) // 2)
3.3 MSER + RANSAC for Boundary Fitting
MSER (Maximally Stable Extremal Regions)
MSER detects stable regions by applying various thresholds to grayscale images. Text areas tend to remain connected across thresholds due to consistent internal gray levels, enabling detection.
Mathematical Description: For grayscale image I, define binary image B_t at threshold t. If a connected component remains stable in area between thresholds t and t+Δ, it's considered maximally stable extremal region.
In license plates, character are usually dark on light backgrounds (or vice versa), MSER effectively finds character candidates, indirectly locating plates.
RANSAC (Random Sample Consensus) for Boundary Fitting
With MSER-generated character points, we fit plate boundaries. Due to noise (like screws or stains), direct least squares fails. RANSAC randomly samples, fits models, and selects best model with most inliers.
Steps:
- Randomly pick two points to form a line.
- Measure distances from all points to this line; points within threshold are "inliers".
- Record number of inliers, repeat several times, select line with maximum inliers.
3.4 CNN Regression for Left/Right Boundaries
Traditional projection methods fail when plates are tilted. HyperLPR3 uses a lightweight CNN to regress left/right boundaries directly.
Network Structure: Input is a plate candidate image, processed through convolutional and fully connected layers, outputting horizontal coordinates of left/right boundaries. Training uses labeled plate boundary samples with regression loss (Smooth L1 Loss).
This method surpasses traditional projection techniques in robustness, especially under tilts or partial occlusions.
3.5 Texture Field Based Skew Correction
Due to camera angles, plates may appear skewed, affecting subsequent character segmentation. HyperLPR3 uses texture field algorithms to compute skew angle.
Principle: Divide image into small patches, compute gradient directions using Sobel operater in each patch. Statistical histogram of gradients identifies dominant direction, smoothed via Gaussian KDE to obtain correction angle.
4. Character Segmentation: Separate Characters
Once the precise plate area is located, we need to split the continuous string into individual characters.
4.1 CNN Sliding Window Cutting
Traditional methods rely on vertical projections: compute column pixel sums, peaks indicate characters, valleys indicate gaps. However, this fails with character overlaps or interference.
HyperLPR3 employs sliding window + CNN classifier:
- Use fixed-size sliding window (e.g., 16×32 pixels), slide from left to right.
- For each window, use CNN to determine if it contains character center.
- Locate characters based on response peaks.
This end-to-end approach adapts to varying character widths and resists noise interference.
4.2 Character Normalization
Cut characters may vary in size, requiring uniform scaling (e.g., 20×20 pixels) for recognition.
5. Character Recognition: Identify Each Character
Character recognition is the final step, showcasing deep learning advantages.
5.1 CNN Character Classification
HyperLPR3 uses a lightweight CNN to classify segmented characters. Output layer has 34 nodes (31 provinces + 10 digits + 24 letters, excluding I and O), with softmax activation for probability distribution.
Training Data: Millions of real-world license plate character images with various lighting, damage, and fonts.
5.2 End-to-End Recognition (Optional)
Modern systems (HyperLPR high versions) use CRNN + CTC for end-to-end recognition. Input is full plate image, output is complete character sequence. Particularly effective for overlapping or irregular spacing, though computationally intensive—sliding window + CNN is preferred on Raspberry Pi.
6. Why Consecutive Verification Improves Accuracy?
Our code includes a parameter required_matches, requiring multiple consecutive identical recognitions before confirming. The logic is:
- Avoid transient misidentification: Vehicle just entering view may expose only part of plate.
- Leverage temporal redundancy: As vehicle approaches, image clarity improves; repeated checks filter transient issues.
- Balance speed and accuracy: With
required_matches = 7, accuracy exceeds 97%, with only ~0.5s delay—acceptable.
Despite seeming simple, license plate recognition is a classic computer vision problem. From classical Haar cascades, MSER, RANSAC to modern CNN regression and classification, HyperLPR3 elegantly combines classic and deep learning methods for high accuracy and real-time performance. Understanding these principles helps better utilize HyperLPR3 and lay groundwork for future algorithm optimization.
System Debugging and Optimization
6.1 Consecutive Recognition Validation
To improve accuracy, the system uses consecutive validation: only confirm recognition after multiple identical detections. Experimental results show optimal balance at 7 consecutive matches.
# Recognition configuration
REQUIRED_MATCHES = 7 # Required consecutive matches
MAX_RECOGNITION_ATTEMPTS = 25 # Max recognition attempts
6.2 Billing Rule Configuration
def calculate_fee(entry_timestamp, exit_timestamp=None):
"""
Billing rules:
- Free for first 30 minutes
- 1 RMB per 30 minutes after
- Cap at 20 RMB daily
"""
duration_minutes = (exit_timestamp - entry_timestamp) / 60
if duration_minutes <= 30:
return 0.0
billable_half_hours = (duration_minutes - 30) / 30
fee = billable_half_hours * 1.0
return min(fee, 20.0) # Cap at 20 RMB
6.3 Common Issues and Solutions
| Issue | Possible Cause | Solution |
|---|---|---|
| Low recognition rate | Insufficient lighting | Add illumination or adjust camera angle |
| Slow recognition | High resolution | Reduce camera resolution to 640x480 |
| Ultrasonic false triggers | Interference signals | Use average of multiple measurements |
| Servo jitter | Insufficient power supply | Use independent 5V power supply |
System Operation Demonstration
7.1 Main Interface
Upon startup, the Tkinter interface shows:
- Welcome title and logo
- Real-time parking availability
- Current time (updates every second)
- Prompt messages
- Vehicle information display area
7.2 Entry Process
- Vehicle approaches ultrasonic sensor (<30cm)
- Camera automatically captures and recognizes plate
- Confirm after 7 consecutive matches
- Buzzer sounds twice, servo opens gate
- Interface displays entry info (plate, entry time, available spots)
- Gate closes after vehicle passes
7.3 Exit Process
- Vehicle approaches exit
- Recognize plate
- Query entry time, calculate parking fee
- Buzzer alerts, gate opens
- Interface displays exit info (plate, entry time, exit time, fee)
- Remove vehicle from CSV records
7.4 Full Parking Alert
When available spots reach zero, new vehicle detection shows "Parking Full" message, gate remains closed.
Summary and Future Work
This article presents a complete Raspberry Pi-based parking lot license plate recognition system with the following features:
- ✅ Cost-effective: Uses Raspberry Pi 4B and open-source software, hardware cost ~700 CNY
- ✅ High accuracy: HyperLPR3 achieves 95%-97% accuracy in entry/exit scenarios
- ✅ Complete functionality: Covers vehicle detection, plate recognition, billing, and gate control
- ✅ Extensible design: Supports multi-entry points, cloud data synchronization
Future Optimization Areas
- Environmental adaptability: Add night illumination, rain/fog enhancement
- Multi-entry support: Coordinate multiple cameras
- Cloud sync: Upload parking data for remote monitoring
- Mobile payment: Integrate WeChat/Alipay payment options
Project source code: Interested readers can request full source code in comments or via private message.
References:
- HyperLPR3 Official Documentation
- Raspberry Pi Official Documentation
- HC-SR04 Sensor Datasheet
- OpenCV-Python Tutorial