Friday, October 3, 2025

 Counting parking spots

Just like analytics of objects detected in aerial drone images is done just-in-time of query prompting and not for every drone image extracted from a drone video, unoccupied parking spots qualify as reasonable detections to be asked in a query. The corresponding analytics could take one of the following approaches:

1. Using color segmentation, contour detection and simple heuristics assuming that parking spots are regions in the parking lot not covered by cars.

import cv2

import numpy as np

def count_empty_parking_spots(image_path, debug=False):

    image = cv2.imread(image_path)

    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)

    # Segment pavement (brightness, not car colors)

    # Pavement is usually light gray under sunlight in aerial images

    lower_pavement = np.array([0, 0, 180]) # min hue/sat, high V (brightness)

    upper_pavement = np.array([180, 35, 255]) # low saturation to ignore cars

    mask_pavement = cv2.inRange(hsv, lower_pavement, upper_pavement)

    # Remove small noise

    kernel = np.ones((7, 7), np.uint8)

    mask_clean = cv2.morphologyEx(mask_pavement, cv2.MORPH_OPEN, kernel)

    mask_clean = cv2.morphologyEx(mask_clean, cv2.MORPH_CLOSE, kernel)

    # Adaptive thresholding as backup (if pavement color varies)

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    adaptive = cv2.adaptiveThreshold(

        gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 25, -14)

    combined = cv2.bitwise_and(mask_clean, adaptive)

    # Find contours (Connected bright regions interpreted as parking spots)

    contours, _ = cv2.findContours(combined, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    empty_count = 0

    display = image.copy()

    # Filter contours: parking spots should be rectangular and large enough

    for contour in contours:

        area = cv2.contourArea(contour)

        x, y, w, h = cv2.boundingRect(contour)

        aspect = float(w) / float(h) if h > 0 else 0

        # Tune these heuristics to ignore stray bright patches & small noise

        if 400 < area < 2500 and 1.3 < aspect < 2.7 and w > 20 and h > 11:

            empty_count += 1

            cv2.rectangle(display, (x, y), (x+w, y+h), (0,255,0), 2)

    print(f"Detected {empty_count} empty parking spots")

    if debug:

        cv2.imshow("Parking Detection", display)

        cv2.imshow("Mask", mask_clean)

        cv2.imshow("Adaptive", adaptive)

        cv2.imshow("Combined", combined)

        cv2.imwrite(image_path.replace(".jpg","")+"-detectedspots.jpg", display)

        cv2.waitKey(0)

        cv2.destroyAllWindows()

    return empty_count

# Example usage for your two frames:

count1 = count_empty_parking_spots("parking1.jpg", debug=True)

count2 = count_empty_parking_spots("parking2.jpg", debug=True)

print(f"parking1.jpg: {count1} empty spots (should be about 2)")

print(f"parking2.jpg: {count2} empty spot (should be about 1)")

"""

Results: Detected 1 empty parking spots

Detected 1 empty parking spots

parking1.jpg: 1 empty spots (should be about 2)

parking2.jpg: 1 empty spot (should be about 1)

"""

2. Using YOLOv5 for speed, precision and adaptability to aerial views:

import torch

import cv2

import numpy as np

from matplotlib import pyplot as plt

# Load YOLOv5 model (you can use 'yolov5s', 'yolov5m', or a custom-trained model)

model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True)

# Load image

image_path = 'parking2.jpg'

img = cv2.imread(image_path)

img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

# Run inference

results = model(img_rgb)

# Parse results

detections = results.pandas().xyxy[0]

cars = detections[detections['name'] == 'car']

# Optional: If you have predefined parking spot coordinates

# Define parking spots manually or load from a JSON file

parking_spots = [

     {'id': 1, 'bbox': [815, 184, 992, 387]},

     {'id': 2, 'bbox': [230, 502, 273, 619]},

     {'id': 3, 'bbox': [427, 8, 468, 691]},

    # Add more spots...

]

# Check overlap between cars and parking spots

def is_occupied(spot_bbox, car_bboxes):

    x1, y1, x2, y2 = spot_bbox

    spot_area = (x2 - x1) * (y2 - y1)

    for _, car in car_bboxes.iterrows():

        cx1, cy1, cx2, cy2 = car[['xmin', 'ymin', 'xmax', 'ymax']]

        ix1, iy1 = max(x1, cx1), max(y1, cy1)

        ix2, iy2 = min(x2, cx2), min(y2, cy2)

        iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1)

        intersection = iw * ih

        if intersection / spot_area > 0.3: # Threshold for overlap

            return True

    return False

# Count unoccupied spots

unoccupied_count = 0

for spot in parking_spots:

    if not is_occupied(spot['bbox'], cars):

        unoccupied_count += 1

print(f"Unoccupied parking spots: {unoccupied_count}")

The results of the second option appear as

YOLOv5 2025-7-21 Python-3.13.1 torch-2.7.1+cpu CPU

Fusing layers...

YOLOv5s summary: 213 layers, 7225885 parameters, 0 gradients, 16.4 GFLOPs

Adding AutoShape...

C:\Users\ravib/.cache\torch\hub\ultralytics_yolov5_master\models\common.py:906: FutureWarning: `torch.cuda.amp.autocast(args...)` is deprecated. Please use `torch.amp.autocast('cuda', args...)` instead.

  with amp.autocast(autocast):

Unoccupied parking spots: 3

#codingexercise: https://1drv.ms/w/c/d609fb70e39b65c8/EcmsFKLaR-hJqkh7Kz7vaHIB_SkOxADjDoskhScjPYa9kA?e=HwP9gp 

No comments:

Post a Comment