r/Dashcam 2d ago

Video Can anyone help me pull the license plate from this dash cam footage?

https://www.dropbox.com/scl/fo/v4bx7t0brir1d5atf1d6e/AFmGj-mvnkSOJxUA-2Vgolo?rlkey=pl1an793s7ya50thzz4onw5hd&st=08xg0emu&dl=0
Upvotes

4 comments sorted by

u/DeliriousBlues 2d ago

Run a negative filter to see if the numbers come through

u/Legitimate_Career212 2d ago

I was involved in a hit and run at a gas station in Lone Tree, CO. The other driver was in a white Toyota Camry and took off immediately after hitting me.
I managed to get the gas station's security footage, which shows the accident clearly, but the Camry didn't have a front license plate. Fortunately, my own dash cam caught the back of the car right as I was pulling into the station. The problem is the footage is slightly blurred, and I just can't quite make out the characters on the rear plate.
I've linked the raw dash cam footage and the security video below. Are there any wizards on here who can help enhance the video or decipher the plate? Any help, tips, or enhancements would be hugely appreciated!

u/Individdy 2d ago edited 2d ago

I asked Google AI Mode about Multi-Frame Super-Resolution (MF-SR) and it gave me some Python code to run on Google Colab (never used this before) and it worked. I fed it the video from 5.5 to 7.5 seconds, and had it crop down to just around the car. Not as sharp as I was hoping, but might help (I added an enhanced version with some unsharp mask to make text a little clearer): https://postimg.cc/gallery/fYmkYTg

Python code (I just went to https://colab.research.google.com/, created new notebook, pasted it in, uploaded a short clip with the folder icon where there was no major motion as dashcam_clip.mp4, and set target_box at the end of the code (left, top, width, height), clicked Run All at the top, refreshed the file area, and downloaded enhanced_plate.png):

import cv2
import numpy as np

def multi_frame_super_resolution_cropped(video_path, output_image_path, crop_box=None, scale_factor=2):
    """
    Crops video frames on-the-fly and fuses them using dense optical flow 
    sub-pixel shifts to reconstruct a higher-resolution image.

    crop_box: Tuple of (x, y, w, h) defining the region to isolate.
    """
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        print("Error: Could not open video file.")
        return

    # Check original dimensions to ensure safety boundaries
    orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

    frames = []
    while True:
        ret, frame = cap.read()
        if not ret:
            break

        # Convert to grayscale first to maximize structural/luminance detail tracking
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)

        # Apply the crop dynamically in-memory (Avoiding Transcoding)
        if crop_box is not None:
            x, y, w, h = crop_box
            # Ensure crop boundaries do not exceed the actual video dimensions
            x, y = max(0, x), max(0, y)
            w, h = min(w, orig_w - x), min(h, orig_h - y)
            gray = gray[y:y+h, x:x+w]

        frames.append(gray.astype(np.float32))

    cap.release()

    if len(frames) < 5:
        print("Error: Provide a clip with at least 5-10 frames.")
        return

    print(f"Loaded {len(frames)} frames dynamically cropped to {frames[0].shape[::-1]}. Alignment starting...")

    # Choose the middle frame as the anchor target grid
    anchor_idx = len(frames) // 2
    anchor_frame = frames[anchor_idx]

    # Scale up the base target matrix dimensions
    h, w = anchor_frame.shape
    new_h, new_w = int(h * scale_factor), int(w * scale_factor)

    # Initialize accumulators
    high_res_accumulator = cv2.resize(anchor_frame, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
    weight_matrix = np.ones_like(high_res_accumulator)

    for i, frame in enumerate(frames):
        if i == anchor_idx:
            continue

        # Calculate sub-pixel displacements using dense optical flow
        # Upgraded local tracking settings
        flow = cv2.calcOpticalFlowFarneback(
            prev=anchor_frame.astype(np.uint8), 
            next=frame.astype(np.uint8), 
            flow=None, 
            pyr_scale=0.5, 
            levels=5,       # Increased from 3 to 5 for deeper layer tracking
            winsize=21,     # Increased from 15 to 21 to capture wider sub-pixel motion
            iterations=7,   # Increased from 3 to 7 for cleaner mathematical alignment
            poly_n=7,       # Increased from 5 to 7 for smoother pixel expansion
            poly_sigma=1.5, 
            flags=0
        )

        #flow = cv2.calcOpticalFlowFarneback(
        #            prev=anchor_frame.astype(np.uint8), 
        #            next=frame.astype(np.uint8), 
        #            flow=None, pyr_scale=0.5, levels=3, winsize=15, 
        #            iterations=3, poly_n=5, poly_sigma=1.2, flags=0
        #)

        # Scale displacement map to match the target high-res resolution
        flow_scaled = cv2.resize(flow, (new_w, new_h), interpolation=cv2.INTER_LINEAR) * scale_factor

        # Map pixel sub-pixel target adjustments
        map_x, map_y = np.meshgrid(np.arange(new_w), np.arange(new_h))
        map_x = (map_x + flow_scaled[..., 0]).astype(np.float32)
        map_y = (map_y + flow_scaled[..., 1]).astype(np.float32)

        # Remap the low-res elements onto the expanded grid coordinates
        warped_frame = cv2.remap(
            cv2.resize(frame, (new_w, new_h), interpolation=cv2.INTER_CUBIC), 
            map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT
        )

        high_res_accumulator += warped_frame
        weight_matrix += 1

    # Fuse frame calculations together
    fused_output = high_res_accumulator / weight_matrix
    fused_output = np.clip(fused_output, 0, 255).astype(np.uint8)

    # Post-processing: Enhance text legibility with a crisp unsharp mask
    blurred = cv2.GaussianBlur(fused_output, (3, 3), 0)
    enhanced = cv2.addWeighted(fused_output, 1.8, blurred, -0.8, 0)

    cv2.imwrite(output_image_path, enhanced)
    print(f"Super-resolution complete! File saved: {output_image_path}")

# --- CONFIGURATION & EXECUTION ---
video_clip = "dashcam_clip.mp4" 
output_result = "enhanced_plate.png"

# Using your exact crop dimensions
target_box = (928, 429, 256, 256) 

# Changed scale_factor to 4 for deeper sub-pixel density extraction
multi_frame_super_resolution_cropped(video_clip, output_result, crop_box=target_box, scale_factor=4)

Reading through the code, it basically assembles the frames from the crop in the video, chooses an anchor frame in the middle of the video as a motion reference, then for every frame, determines a subpixel offset (the slight camera motion due to vibration etc), and adjusts the image by this offset to align with the anchor position. It mixes all these frames together. Since the adjustments are in fractional pixels, it's able to reconstruct more resolution, thanks to the camera not being perfectly still. It's a variation on the way you can see through a slotted fence as you move by, because over time you get more information, even though at any particular moment you can only see through narrow slits.

u/Legitimate_Career212 1d ago

Thank you so much!