---
# System prepended metadata

title: LPR Documentation

---

---
title: LPR Documentation

---

# LPR Documentation
## Collecting Data
- Collecting data from github, no copyright Youtube footage, and user daily footage.
- https://youtu.be/UpleTeyFPJ0?si=M0P-mkP80Ivcljfo
- https://youtu.be/FOo0AbigryE?si=cTwvDfNwbS_7Gbsx
- https://youtu.be/jek6V2UlMUo?si=QmznOYTNyBPLCxsX
- https://github.com/mendez-luisjose/License-Plate-Detection-with-YoloV8-and-EasyOCR
- https://github.com/ablanco1950/LicensePlate_Yolov8_Filters_PaddleOCR

## Labeling Data
- We're using website to annotate the data we've collected. In this case we using Roboflow to label our data.
![Screenshot 2025-05-03 003226](https://hackmd.io/_uploads/SJKWMFGglx.png)
- The interface and the usage is as shown above. ctrl+b to mark the object we want to annotate, and define the class (In this case it's 88888888 (License Plate)).

## Training Data
- After annotating all of the data we've collected next we need to train the dataset.
```python=
from ultralytics import YOLO

# Load a YOLOv8m model (pre-trained weights)
model = YOLO("yolov8m.pt")

# Train the model on your dataset
model.train(
    data="dataset/data.yaml",  # path to the YAML config
    epochs=50,
    imgsz=640,
    batch=16,
    workers=4,
    project="my_yolo_project",
    name="yolov8m_custom",
    exist_ok=True  # overwrite if the project folder exists
)
```
Above shown is the **example** of a training scripts using Yolov8
- After extracting the zip file or the Roboflow script, we just need to import the path to the data.yaml file into the training script.
- The output will be a model that's ready to be used for implementation on video.

## Inference
- Next step is to make the inference script to implement the model.
```python=
%%writefile app.py
import streamlit as st
import cv2
import numpy as np
import torch
import os
import warnings
from datetime import datetime
from ultralytics import YOLO
from ByteTrack.yolox.tracker.byte_tracker import BYTETracker
from configs.tracker_cfg import args
from sam2.build_sam import build_sam2
from sam2.sam2_image_predictor import SAM2ImagePredictor
import tempfile
import shutil

# Disable warnings for cleaner output
warnings.filterwarnings("ignore")

# ===== Debug Configuration =====
DEBUG = True
DEBUG_DIR = tempfile.mkdtemp()
os.makedirs(DEBUG_DIR, exist_ok=True)

# ===== Step 1: Initialize Models =====
try:
    yolo_model = YOLO("V5.pt")
    checkpoint = "./sam2/checkpoints/sam2.1_hiera_small.pt"
    model_cfg = "./configs/sam2.1/sam2.1_hiera_s.yaml"
    sam2_model = build_sam2(model_cfg, checkpoint)
    predictor = SAM2ImagePredictor(sam2_model)
    tracker = BYTETracker(args=args, frame_rate=args.fps)
except Exception as e:
    st.error(f"Error loading models: {str(e)}")
    st.stop()

if not torch.cuda.is_available():
    st.warning("GPU is not available. Processing may be slow on CPU.")

# ===== Step 2: Utility Functions =====
def log(message, image=None, prefix=""):
    if DEBUG:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        print(f"[{timestamp}] {message}")
        if image is not None:
            filename = f"{DEBUG_DIR}/{prefix}_{timestamp}.png"
            cv2.imwrite(filename, image)

def apply_mask(frame, mask, x1, y1, x2, y2):
    try:
        current_height = y2 - y1
        current_width = x2 - x1
        resized_mask = cv2.resize(
            (mask * 255).astype(np.uint8),
            (current_width, current_height),
            interpolation=cv2.INTER_NEAREST
        )
        frame[y1:y2, x1:x2][resized_mask > 0] = 255
    except Exception as e:
        print(f"Error applying mask: {str(e)}")

def segment_plate(cropped_plate):
    try:
        cropped_plate_rgb = cv2.cvtColor(cropped_plate, cv2.COLOR_BGR2RGB)
        h, w = cropped_plate_rgb.shape[:2]
        scale = 0.9
        new_w = int(w * scale)
        new_h = int(h * scale)
        x1 = (w - new_w) // 2
        y1 = (h - new_h) // 2
        x2 = x1 + new_w
        y2 = y1 + new_h
        prompt_box = np.array([[x1, y1, x2, y2]])
        center_point = np.array([[w // 2, h // 2]])
        point_labels = np.array([1])
        with torch.inference_mode(), torch.autocast("cuda", dtype=torch.bfloat16):
            predictor.set_image(cropped_plate_rgb)
            masks, scores, _ = predictor.predict(
                box=prompt_box,
                point_coords=center_point,
                point_labels=point_labels,
                multimask_output=False
            )
        if masks is not None and len(masks) > 0:
            best_mask = masks[np.argmax(scores)]
            return best_mask
        return None
    except Exception as e:
        print(f"Error during segmentation: {str(e)}")
        return None

# ===== Step 3: Process Video =====
def process_video(input_path, output_path, temp_crop_folder):
    try:
        cap = cv2.VideoCapture(input_path)
        width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
        fps = int(cap.get(cv2.CAP_PROP_FPS))

        out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mp4v'), fps, (width, height))
        tracker = BYTETracker(args=args, frame_rate=fps)

        mask_memory = {}
        frame_count = 0
        os.makedirs(temp_crop_folder, exist_ok=True)

        track_id_map = {}
        next_track_id = 1

        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
            frame_count += 1

            results = yolo_model(frame)[0]
            detections = [box.xyxy[0].cpu().numpy().tolist() + [box.conf.item()] for box in results.boxes]
            tracks = tracker.update(np.array(detections), (height, width), (height, width)) if detections else []

            for track in tracks:
                tracker_track_id = int(track.track_id)
                if tracker_track_id not in track_id_map:
                    track_id_map[tracker_track_id] = next_track_id
                    next_track_id += 1
                my_track_id = track_id_map[tracker_track_id]

                x1, y1, x2, y2 = track.tlbr.astype(int)

                # Clamp coordinates to frame boundaries
                x1 = max(0, x1)
                y1 = max(0, y1)
                x2 = min(width, x2)
                y2 = min(height, y2)

                # Check if the crop region is valid
                if x1 < x2 and y1 < y2:
                    cropped_plate = frame[y1:y2, x1:x2]

                    temp_file = os.path.join(temp_crop_folder, f"track_{my_track_id}_frame_{frame_count}.jpg")
                    cv2.imwrite(temp_file, cropped_plate)

                    if my_track_id not in mask_memory:
                        best_mask = segment_plate(cropped_plate)
                        if best_mask is not None:
                            mask_memory[my_track_id] = best_mask
                            log(f"Segmentation mask for track ID {my_track_id} created",
                                (best_mask * 255).astype(np.uint8), f"mask_{my_track_id}")
                        else:
                            print(f"Segmentation failed for track ID {my_track_id} on frame {frame_count}")
                            continue

                    if my_track_id in mask_memory:
                        apply_mask(frame, mask_memory[my_track_id], x1, y1, x2, y2)

                    cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2)
                    cv2.putText(frame, str(my_track_id), (x1 + 5, y1 - 10),
                                cv2.FONT_HERSHEY_SIMPLEX, 0.8, (255, 255, 255), 2)
                else:
                    print(f"Invalid crop coordinates for track ID {my_track_id}: ({x1}, {y1}, {x2}, {y2})")

            out.write(frame)
            log("Frame processed", frame, "frame_output")

        cap.release()
        out.release()
    except Exception as e:
        st.error(f"Error processing video: {str(e)}")
        raise e

# ===== Step 4: Streamlit App =====
def main():
    st.title("License Plate Detection and Segmentation")
    uploaded_file = st.file_uploader("Upload a video", type=["mp4", "avi", "mov"])

    if uploaded_file is not None:
        with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_input:
            temp_input.write(uploaded_file.read())
            input_path = temp_input.name

        output_path = os.path.join(tempfile.gettempdir(), "processed_video.mp4")
        temp_crop_folder = tempfile.mkdtemp()

        try:
            st.write("Processing video...")
            process_video(input_path, output_path, temp_crop_folder)
            st.write("Video processed successfully!")

            st.video(output_path)

            with open(output_path, "rb") as file:
                st.download_button(
                    label="Download Processed Video",
                    data=file,
                    file_name="processed_video.mp4",
                    mime="video/mp4"
                )
        finally:
            # Clean up temporary files and directories
            if os.path.exists(input_path):
                os.remove(input_path)
            if os.path.exists(output_path):
                os.remove(output_path)
            if os.path.exists(temp_crop_folder):
                shutil.rmtree(temp_crop_folder)
            if os.path.exists(DEBUG_DIR):
                shutil.rmtree(DEBUG_DIR)

if __name__ == "__main__":
    main()
```
- In this case, we're using colab to run the inference code. we convert the script shown above to app.py.
- as we're integrating Streamlit as a UI for inference purpose, we need to run a Streamlit web application in a local environment, and make it publicly accessible on the internet using LocalTunnel as shown below.
```python=
print("Dependencies installed. Now running the app...")
!echo "Put this IP as password:"
!wget -q -O - ipv4.icanhazip.com
!streamlit run app.py --server.fileWatcherType none & npx localtunnel --port 8501
```
![image](https://hackmd.io/_uploads/r1PedKGegx.png)
This is the looks after running the code above.
- Simply tap the url and copy the IP address as a password.

![Screenshot 2025-03-18 212903](https://hackmd.io/_uploads/HJg3uYMxxg.png)
This is how the UI looks like. Simply drag or upload the videos, then download the processed videos as shown in the image.