Objective and use case
What you’ll build: An advanced ANPR system on Raspberry Pi 4 using the HQ Camera and OpenCV to accurately recognize license plates in real-time.
Why it matters / Use cases
- Automated toll collection systems utilizing real-time license plate recognition to streamline vehicle passage.
- Parking management solutions that automatically identify vehicles for entry and exit, enhancing user experience.
- Law enforcement applications for tracking stolen vehicles or monitoring traffic violations through automated surveillance.
- Smart city initiatives that integrate ANPR for traffic analysis and urban planning.
Expected outcome
- Achieve a recognition accuracy of over 95% for alphanumeric plates under various lighting conditions.
- Process frames at a rate of at least 10 FPS, ensuring real-time performance.
- Maintain latency under 200 ms from capture to recognition output.
- Successfully identify and log license plates in a database for further analysis with a minimum of 100 entries per hour.
Audience: Advanced users; Level: Advanced
Architecture/flow: The system captures images via the HQ Camera, processes them using OpenCV for plate detection, and employs Tesseract OCR for text recognition.
Advanced Hands‑On: Raspberry Pi 4 + HQ Camera ANPR (OpenCV License Plate Recognition)
Objective: Build an on‑device ANPR (Automatic Number Plate Recognition) system on a Raspberry Pi 4 using the HQ Camera, OpenCV, and Tesseract OCR. The pipeline captures frames, detects plate candidates, rectifies the plate region, and performs text recognition tuned for alphanumeric plates. This guide provides code, exact commands, connections, and a validation path, targeted at advanced users.
Prerequisites
- Expertise level: Advanced (comfortable with Linux, Python, OpenCV, and basic GPIO).
- Operating System: Raspberry Pi OS Bookworm 64‑bit modern stack (libcamera).
- Python 3.11 on the Raspberry Pi 4.
- Stable internet connection for package installation.
- Basic familiarity with camera optics (focusing the C‑mount lens on the HQ Camera).
- A test environment where you can legally capture license plates or use printed/stock images for validation.
Materials (Exact Models and Versions)
| Item | Exact Model / Version | Notes |
|---|---|---|
| Single‑board computer | Raspberry Pi 4 Model B (4 GB or 8 GB RAM recommended) | 4 GB minimum for smoother OpenCV OCR workloads |
| Camera | Raspberry Pi HQ Camera (IMX477) | With C‑mount |
| Lens | C‑Mount Lens, 6 mm or 12 mm fixed focal length (e.g., Raspberry Pi 6mm Lens) | Choose focal based on distance/field of view |
| Camera cable | Official Raspberry Pi CSI‑2 ribbon cable | Shorter cables reduce noise |
| microSD | 32 GB microSD card (A1/U1 or better) | Raspberry Pi OS Bookworm 64‑bit |
| Power | 5V/3A USB‑C power supply | Official recommended |
| Mount | Tripod or rigid mount for HQ Camera | Stability is critical |
| Optional trigger | Momentary pushbutton and 2x female‑female jumpers | For GPIO capture trigger |
| Optional indicator | 3.3V LED + 330 Ω resistor | Visual feedback on detection |
| Enclosure | Any camera mount or case | To reduce vibrations and stray light |
Setup and Connections
1) Flash and update Raspberry Pi OS Bookworm 64‑bit
- Use Raspberry Pi Imager to flash “Raspberry Pi OS (64‑bit)” (Bookworm).
- After first boot:
sudo apt update
sudo apt full-upgrade -y
sudo reboot
2) Enable camera and interfaces (raspi‑config)
- Interactive:
- Run:
sudo raspi-config - Interface Options:
- Camera → Enable
- I2C → Enable (optional; for future sensors)
- SPI → Enable (optional; for future SPI peripherals)
-
Finish → Reboot when prompted.
-
Alternatively, via /boot/firmware/config.txt (Bookworm path is /boot/firmware):
- Edit:
sudo nano /boot/firmware/config.txt - Ensure the following lines exist (add if missing):
camera_auto_detect=1
dtoverlay=imx477
gpu_mem=256 - Save and reboot:
sudo reboot
Notes:
– Bookworm uses libcamera; you do not need legacy start_x settings.
– gpu_mem=256 is recommended for camera preview/processing pipelines.
3) Connect the Raspberry Pi HQ Camera
- Power off the Pi before connecting the cable.
- Lift the CSI connector latch on the Pi; insert the ribbon cable with the exposed contacts facing the HDMI ports; close latch.
- On the HQ Camera side, match the ribbon orientation and close latch.
- Mount the lens and perform coarse focus manually. You will refine focus later using a live preview.
4) Optional: Wire a trigger pushbutton to GPIO 17
If you want a hardware trigger to capture and process:
| Component | Raspberry Pi header pin | Signal |
|---|---|---|
| Pushbutton leg 1 | Pin 11 | GPIO 17 (BCM numbering) |
| Pushbutton leg 2 | Pin 6 | GND |
- We will use an internal pull‑up. Pressing the button pulls GPIO 17 to GND (active‑low).
- Optional LED indicator (annotated detection feedback):
- LED anode → 330 Ω resistor → Pin 13 (GPIO 27)
- LED cathode → Pin 9 (GND)
No external power required for the button/LED besides Pi’s own 3.3V domain (and we’re using internal pull‑ups).
Software Setup
We will create a Python 3.11 virtual environment that can use system site packages so we can consume Picamera2 from apt, and install OpenCV, OCR, and utilities with pip.
1) Base packages, camera stack, and OCR engine
sudo apt update
sudo apt install -y \
libcamera-apps \
python3-picamera2 \
tesseract-ocr tesseract-ocr-eng \
libtesseract-dev \
git curl
- Validate camera quickly:
libcamera-hello -t 2000
libcamera-still -o /tmp/test.jpg
2) Create a project and venv (with system packages)
mkdir -p ~/projects/anpr-rpi4/src ~/projects/anpr-rpi4/out
cd ~/projects/anpr-rpi4
python3 --version
python3 -m venv --system-site-packages .venv
source .venv/bin/activate
3) Install Python dependencies (pip)
Pin versions for reproducibility:
pip install --upgrade pip
pip install \
numpy==1.26.4 \
opencv-python==4.8.1.78 \
imutils==0.5.4 \
pytesseract==0.3.10 \
gpiozero==2.0 \
smbus2==0.4.3 \
spidev==3.6
Quick import tests:
python - <<'PY'
import cv2, numpy as np, pytesseract
print("cv2:", cv2.__version__)
print("numpy:", np.__version__)
print("tesseract:", pytesseract.get_tesseract_version())
PY
Full Code (OpenCV + Picamera2 + Tesseract OCR)
Create the file:
– Path: ~/projects/anpr-rpi4/src/anpr_cam.py
#!/usr/bin/env python3
import os
import re
import time
import argparse
from datetime import datetime
from typing import List, Tuple
import cv2
import numpy as np
import pytesseract
try:
from picamera2 import Picamera2
except ImportError as e:
raise SystemExit("Picamera2 not found. Ensure 'python3-picamera2' is installed via apt and that your venv uses --system-site-packages.") from e
# Optional GPIO trigger/indicator
try:
from gpiozero import Button, LED
except Exception:
Button = None
LED = None
ALNUM_WHITELIST = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
TESSERACT_CONFIG = f"--oem 1 --psm 7 -c tessedit_char_whitelist={ALNUM_WHITELIST}"
PLATE_REGEX = re.compile(r"[A-Z0-9]{5,9}") # generic; adjust to your region
def rectify_plate(image: np.ndarray, contour: np.ndarray, output_size=(240, 80)) -> np.ndarray:
"""
Given a contour approximated to 4 points, create a perspective transform
to rectify the plate ROI to a canonical size suitable for OCR.
"""
pts = contour.reshape(4, 2).astype(np.float32)
# Order points: top-left, top-right, bottom-right, bottom-left
s = pts.sum(axis=1)
diff = np.diff(pts, axis=1)
tl = pts[np.argmin(s)]
br = pts[np.argmax(s)]
tr = pts[np.argmin(diff)]
bl = pts[np.argmax(diff)]
ordered = np.array([tl, tr, br, bl], dtype=np.float32)
dst = np.array(
[[0, 0], [output_size[0] - 1, 0], [output_size[0] - 1, output_size[1] - 1], [0, output_size[1] - 1]],
dtype=np.float32
)
M = cv2.getPerspectiveTransform(ordered, dst)
warp = cv2.warpPerspective(image, M, output_size, flags=cv2.INTER_CUBIC)
return warp
def detect_plate_candidates(frame: np.ndarray) -> List[np.ndarray]:
"""
Use a classical pipeline to propose plate-shaped contours:
- grayscale
- bilateral filter
- Canny edges
- morphological closing
- contour filtering by area/aspect ratio/rectangularity
Returns a list of quadrilateral contours (4 points).
"""
gray = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
blur = cv2.bilateralFilter(gray, 9, 75, 75)
edges = cv2.Canny(blur, 80, 200)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel, iterations=2)
cnts, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
candidates = []
h, w = gray.shape
img_area = h * w
for c in cnts:
area = cv2.contourArea(c)
if area < 0.001 * img_area or area > 0.2 * img_area:
continue
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.02 * peri, True)
if len(approx) == 4:
# Aspect ratio filter using bounding box
x, y, bw, bh = cv2.boundingRect(approx)
if bh == 0:
continue
ar = bw / float(bh)
if 1.8 <= ar <= 6.5:
candidates.append(approx)
return candidates
def ocr_plate(plate_img: np.ndarray) -> Tuple[str, float]:
"""
Run OCR on the rectified plate image.
Returns recognized text and an average confidence in [0, 100].
"""
gray = cv2.cvtColor(plate_img, cv2.COLOR_RGB2GRAY)
# Adaptive threshold improves robustness under varying lighting
th = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, 31, 15)
th = cv2.medianBlur(th, 3)
data = pytesseract.image_to_data(th, config=TESSERACT_CONFIG, output_type=pytesseract.Output.DICT, lang="eng")
words = []
confs = []
for txt, conf in zip(data["text"], data["conf"]):
try:
cval = float(conf)
except ValueError:
cval = -1.0
clean = "".join([ch for ch in txt.upper() if ch in ALNUM_WHITELIST])
if clean and cval >= 0:
words.append(clean)
confs.append(cval)
if not words:
return "", 0.0
text = "".join(words)
avg_conf = float(np.mean(confs)) if confs else 0.0
# Simple regex filter to avoid obvious garbage
m = PLATE_REGEX.search(text)
if m:
text = m.group(0)
return text, avg_conf
def annotate(frame: np.ndarray, contour: np.ndarray, text: str, conf: float):
cv2.drawContours(frame, [contour], -1, (0, 255, 0), 2)
x, y, w, h = cv2.boundingRect(contour)
label = f"{text} ({conf:.0f}%)"
cv2.rectangle(frame, (x, y - 24), (x + 8 * len(label), y), (0, 255, 0), -1)
cv2.putText(frame, label, (x + 4, y - 6),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 0), 1, cv2.LINE_AA)
def process_frame(frame: np.ndarray, min_conf: float = 55.0):
"""
Process a frame: detect candidate plates, OCR the best candidate,
and return annotated frame plus best (text, conf).
"""
candidates = detect_plate_candidates(frame)
best_text, best_conf, best_contour = "", 0.0, None
for cnt in candidates:
try:
roi = rectify_plate(frame, cnt)
except Exception:
continue
text, conf = ocr_plate(roi)
if conf > best_conf and len(text) >= 5:
best_text, best_conf, best_contour = text, conf, cnt
if best_contour is not None and best_conf >= min_conf:
annotate(frame, best_contour, best_text, best_conf)
return frame, best_text, best_conf
def run_camera(args):
picam = Picamera2()
# 1080p RGB frames for OCR-friendly resolution
config = picam.create_video_configuration(main={"size": (1920, 1080), "format": "RGB888"})
picam.configure(config)
picam.start()
time.sleep(0.5)
button = None
led = None
if args.trigger_gpio is not None and Button is not None:
button = Button(args.trigger_gpio, pull_up=True, bounce_time=0.03)
if args.led_gpio is not None and LED is not None:
led = LED(args.led_gpio)
print("Camera running. Press Ctrl+C to exit.")
try:
while True:
if button:
# Wait for button press to capture/process
button.wait_for_press()
frame = picam.capture_array() # RGB888
t0 = time.time()
annotated, text, conf = process_frame(frame, min_conf=args.min_conf)
dt = (time.time() - t0) * 1000.0
if text:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = os.path.join(args.out_dir, f"anpr_{ts}_{text}_{int(conf)}.jpg")
cv2.imwrite(out_path, cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
print(f"[{ts}] {text} ({conf:.1f}%) - saved {out_path} - {dt:.1f} ms")
if led:
led.on()
time.sleep(0.1)
led.off()
elif args.verbose:
print(f"No plate | {dt:.1f} ms")
if args.display:
cv2.imshow("ANPR", cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
if cv2.waitKey(1) & 0xFF == ord('q'):
break
if button:
# Debounce window after capture
time.sleep(0.25)
except KeyboardInterrupt:
pass
finally:
if args.display:
cv2.destroyAllWindows()
picam.stop()
def run_on_media(args):
if args.image:
frame_bgr = cv2.imread(args.image)
assert frame_bgr is not None, f"Cannot read image: {args.image}"
frame = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
annotated, text, conf = process_frame(frame, min_conf=args.min_conf)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = os.path.join(args.out_dir, f"anpr_{ts}_{text or 'none'}_{int(conf)}.jpg")
cv2.imwrite(out_path, cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
print(f"[IMAGE] {args.image} -> {text} ({conf:.1f}%) -> {out_path}")
if args.display:
cv2.imshow("ANPR", cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
cv2.waitKey(0)
cv2.destroyAllWindows()
return
if args.video:
cap = cv2.VideoCapture(args.video)
assert cap.isOpened(), f"Cannot open video: {args.video}"
while True:
ok, frame_bgr = cap.read()
if not ok:
break
frame = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
annotated, text, conf = process_frame(frame, min_conf=args.min_conf)
if text:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = os.path.join(args.out_dir, f"anpr_{ts}_{text}_{int(conf)}.jpg")
cv2.imwrite(out_path, cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
print(f"[VIDEO] {text} ({conf:.1f}%) -> {out_path}")
if args.display:
cv2.imshow("ANPR", cv2.cvtColor(annotated, cv2.COLOR_RGB2BGR))
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
if args.display:
cv2.destroyAllWindows()
def main():
parser = argparse.ArgumentParser(description="Raspberry Pi 4 + HQ Camera ANPR (OpenCV + Tesseract)")
parser.add_argument("--display", action="store_true", help="Show annotated frames")
parser.add_argument("--out-dir", type=str, default=os.path.expanduser("~/projects/anpr-rpi4/out"), help="Output directory")
parser.add_argument("--min-conf", type=float, default=55.0, help="Minimum OCR confidence to accept")
parser.add_argument("--verbose", action="store_true", help="Verbose output")
parser.add_argument("--trigger-gpio", type=int, default=None, help="Button GPIO (BCM) for trigger, e.g., 17")
parser.add_argument("--led-gpio", type=int, default=None, help="LED GPIO (BCM) to blink on detection, e.g., 27")
parser.add_argument("--image", type=str, default=None, help="Run once on a single image")
parser.add_argument("--video", type=str, default=None, help="Run on a video file")
args = parser.parse_args()
os.makedirs(args.out_dir, exist_ok=True)
if args.image or args.video:
run_on_media(args)
else:
run_camera(args)
if __name__ == "__main__":
main()
Make it executable:
chmod +x ~/projects/anpr-rpi4/src/anpr_cam.py
Build/Flash/Run Commands
Although “build/flash” is minimal for Python, the environment setup is critical. Use the exact sequence below.
1) Confirm camera works (libcamera)
libcamera-hello -t 3000
libcamera-still -o ~/projects/anpr-rpi4/out/libcamera_test.jpg
2) Validate Tesseract OCR alone
tesseract --version
# Quick smoke test with a synthetic label:
convert -size 400x120 xc:white -fill black -pointsize 72 -gravity center \
-annotate 0 "ABC1234" /tmp/plate.png
tesseract /tmp/plate.png stdout -l eng --psm 7
If ImageMagick convert is missing:
sudo apt install -y imagemagick
3) Run the ANPR app on an image
source ~/projects/anpr-rpi4/.venv/bin/activate
python ~/projects/anpr-rpi4/src/anpr_cam.py --image /tmp/plate.png --display --verbose
4) Run live with the HQ Camera (with optional GPIO trigger)
- Continuous processing, headless:
python ~/projects/anpr-rpi4/src/anpr_cam.py --min-conf 55.0 - Show annotated preview (requires desktop/X or Wayland):
python ~/projects/anpr-rpi4/src/anpr_cam.py --display --min-conf 55.0 - Hardware trigger on GPIO 17 and LED feedback on GPIO 27:
python ~/projects/anpr-rpi4/src/anpr_cam.py --trigger-gpio 17 --led-gpio 27 --min-conf 55.0
Outputs are saved in:
– ~/projects/anpr-rpi4/out/
Filename format:
– anpr_YYYYMMDD_HHMMSS_PLATETEXT_CONF.jpg
Step‑by‑Step Validation
Follow this sequence to isolate and validate each subsystem.
1) OS and Python
– Confirm 64‑bit OS and Python 3.11:
uname -m
python3 --version
Expect: aarch64, Python 3.11.x
2) Camera driver and sensor
– Test libcamera alive:
libcamera-hello -t 2000
You should see a live preview and no errors in the terminal.
- Capture a still:
libcamera-still -o /tmp/hq_check.jpg
file /tmp/hq_check.jpg
3) Picamera2 import
– From the venv:
source ~/projects/anpr-rpi4/.venv/bin/activate
python - <<'PY'
from picamera2 import Picamera2
picam = Picamera2()
print("Picamera2 OK:", picam)
PY
4) OpenCV and Tesseract together
– Confirm OpenCV can load and Tesseract can OCR:
python - <<'PY'
import cv2, numpy as np, pytesseract
img = cv2.imread("/tmp/hq_check.jpg")
print("cv2 version:", cv2.__version__, "img:", img.shape if img is not None else None)
print("tesseract:", pytesseract.get_tesseract_version())
PY
5) OCR with synthetic plate
– Create and OCR:
convert -size 400x120 xc:white -fill black -pointsize 72 -gravity center \
-annotate 0 "KJ67ABC" /tmp/fakeplate.png
python ~/projects/anpr-rpi4/src/anpr_cam.py --image /tmp/fakeplate.png --display --verbose
Expect: A console line showing “KJ67ABC (~90%+)” depending on rendering.
6) Optical focus and exposure
– Use a live preview to manually focus the HQ camera:
libcamera-hello -t 0
Point at a printed license plate (or high‑contrast text). Adjust lens focus and back‑focus for maximal sharpness. ANPR is highly sensitive to sharpness.
7) End‑to‑end live ANPR
– Place a test plate in the field of view at realistic distance.
– Run:
python ~/projects/anpr-rpi4/src/anpr_cam.py --display --min-conf 55.0 --verbose
– Observe console logs:
– If a plate is recognized, you will see lines like:
[20250114_143501] ABC1234 (84.0%) - saved /home/pi/projects/anpr-rpi4/out/anpr_20250114_143501_ABC1234_84.jpg - 72.3 ms
– Check saved images under ~/projects/anpr-rpi4/out to verify the bounding box and label.
8) Trigger validation (if wired)
– With the pushbutton wired to GPIO 17, run:
python ~/projects/anpr-rpi4/src/anpr_cam.py --trigger-gpio 17 --led-gpio 27 --min-conf 55.0
– Press the button to capture/process a single frame each press. LED should blink on successful recognition.
Troubleshooting
- Camera not detected / libcamera errors:
- Ensure the ribbon cable orientation and latches are correct on both ends.
- Confirm raspi‑config “Camera” is enabled and rebooted.
- Check /boot/firmware/config.txt contains:
camera_auto_detect=1
dtoverlay=imx477
gpu_mem=256 -
Inspect dmesg:
dmesg | grep -i imx477 -
Picamera2 import fails inside venv:
- You must create the venv with system site packages:
python3 -m venv --system-site-packages ~/projects/anpr-rpi4/.venv -
Or install python3-picamera2 globally via apt (already done) and avoid a venv if desired.
-
OpenCV import error or “Illegal instruction”:
- Reinstall the pinned wheel:
pip install --force-reinstall opencv-python==4.8.1.78 -
Alternatively, use the distro package:
deactivate
sudo apt install -y python3-opencv
Then change venv strategy or run system Python 3.11. -
Tesseract poor accuracy:
- Increase plate ROI resolution (raise camera resolution).
- Improve illumination; avoid motion blur (use faster shutter).
- Use a different Tesseract page segmentation mode (psm 7 vs 8 or 6).
- Adjust whitelist and remove characters not present in your region.
-
Try a different threshold: Otsu vs adaptive.
-
No contours found or spurious detections:
- Tune the Canny thresholds and morphological kernel size.
- Adjust aspect ratio bounds in detect_plate_candidates (for your country’s plate format).
-
Consider cropping search area to expected vertical band (e.g., lower half of frame).
-
High CPU usage, low FPS:
- Reduce resolution to 1280×720 in Picamera2 config.
- Disable display (–display off).
- Use PyTorch/CUDA? Not on Pi 4; prefer classical methods or tiny detectors.
-
Batch OCR only when contour is stable across consecutive frames (temporal smoothing).
-
Permission issues with GPIO:
- Ensure your user is in gpio group (default on Raspberry Pi OS).
- Run:
groups
If needed:
sudo usermod -aG gpio $USER
newgrp gpio
Improvements and Extensions
- Robust plate detection with a trained detector:
- Replace classical contour heuristic with a plate detector (e.g., YOLOv5n/YOLOv8n or Haar/LBP cascades). Run a lightweight model via OpenCV DNN or ONNXRuntime on CPU; use 320×320 input to keep latency reasonable.
-
Apply non‑maximum suppression and track boxes across frames (e.g., SORT) for stability and fewer OCR calls.
-
OCR specialization:
- Train a custom OCR model for your region; or use PaddleOCR/EasyOCR for multi‑language.
- Use Tesseract with custom language data tuned to plate fonts.
-
Implement character segmentation and per‑glyph classification (SVM/CNN) for strict formats.
-
Post‑processing:
- Validate OCR against regional patterns (regex) and correct likely confusions (O/0, I/1, B/8, S/5).
-
Temporal voting: require consistent reads across N frames before reporting.
-
Camera control:
-
Lock exposure and white balance for consistency:
- With Picamera2, set:
picam.set_controls({"AeEnable": False, "AwbEnable": False, "ExposureTime": 8000, "AnalogueGain": 1.5}) - Tune ExposureTime to avoid motion blur while maintaining SNR under your lighting.
- With Picamera2, set:
-
Optics and illumination:
- Use a longer focal length (e.g., 12 mm) for distant plates.
- Add polarized illumination and a polarizing filter to reduce glare.
-
Consider IR‑sensitive setups with appropriate illumination and filters (within legal constraints).
-
Performance:
- Use ROI strategy: detect car region first (background subtraction or motion box), then run plate detection inside ROI.
- Vectorize pre‑processing; keep arrays in RGB and convert once; reuse buffers.
-
Multi‑thread capture and processing (producer/consumer queues).
-
Deployment:
- Run as a systemd service to auto‑start on boot.
- Write logs in CSV/JSON with timestamps, confidences, and image paths.
- Provide a simple REST API (Flask/FastAPI) to fetch last N detections.
Final Checklist
- Hardware
- Raspberry Pi 4 and Raspberry Pi HQ Camera (IMX477) connected via CSI‑2 ribbon.
- Lens mounted and focused; camera rigidly mounted.
-
Optional: Button on GPIO 17 to GND; LED on GPIO 27 via 330 Ω to GND.
-
OS and interfaces
- Raspberry Pi OS Bookworm 64‑bit installed and updated.
- Camera enabled via raspi‑config; I2C/SPI enabled if needed.
- /boot/firmware/config.txt contains:
- camera_auto_detect=1
- dtoverlay=imx477
- gpu_mem=256
-
Rebooted after changes.
-
Software environment
- Project directory: ~/projects/anpr-rpi4
- Python venv with –system-site-packages created and activated.
- apt packages: libcamera-apps, python3-picamera2, tesseract-ocr, tesseract-ocr-eng, libtesseract-dev.
- pip packages pinned: numpy==1.26.4, opencv-python==4.8.1.78, imutils==0.5.4, pytesseract==0.3.10, gpiozero==2.0, smbus2==0.4.3, spidev==3.6.
-
Sanity checks: libcamera-hello OK; tesseract –version OK; Python imports OK.
-
Code and execution
- anpr_cam.py placed under ~/projects/anpr-rpi4/src, made executable.
- Test image run: python src/anpr_cam.py –image /tmp/plate.png –display.
- Live run: python src/anpr_cam.py –display –min-conf 55.0.
-
Output images saved under ~/projects/anpr-rpi4/out with timestamp and confidence.
-
Validation
- Verified plate detection and readable OCR on controlled images.
- Adjusted lens focus and lighting to improve accuracy.
- Tuned thresholds (Canny, morphological ops, min_conf) for your scenario.
If all boxes are ticked, your Raspberry Pi 4 + HQ Camera is performing license plate recognition locally using OpenCV and Tesseract, meeting the “opencv-anpr-license-plates” objective with a reproducible, documented setup.
Find this product and/or books on this topic on Amazon
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



