Wednesday, October 8, 2025

 VideoIndexing alleviates redundant processing of each aerial drone video frame. This can be delegated to public cloud video indexing apis or local opencv PySceneDetect. Here is an example: 

from scenedetect import VideoManager, SceneManager 

from scenedetect.detectors import ContentDetector 

from scenedetect.frame_timecode import FrameTimecode 

 

# Path to your aerial drone video 

video_path = 'video01.mp4' 

 

# Initialize video and scene managers 

video_manager = VideoManager([video_path]) 

scene_manager = SceneManager() 

scene_manager.add_detector(ContentDetector(threshold=30.0))  # Adjust threshold for sensitivity 

 

# Start processing 

video_manager.set_downscale_factor()  # Optional: speeds up processing 

video_manager.start() 

scene_manager.detect_scenes(frame_source=video_manager) 

 

# Get list of detected scenes 

scene_list = scene_manager.get_scene_list() 

 

# Print scene boundaries with timestamps and frame indices 

print(f"Detected {len(scene_list)} scenes:") 

for i, (start, end) in enumerate(scene_list): 

    start_frame = start.get_frames() 

    end_frame = end.get_frames() 

    start_time = start.get_timecode() 

    end_time = end.get_timecode() 

    print(f"Scene {i+1}:") 

    print(f"  Start Frame: {start_frame}, Timestamp: {start_time}") 

    print(f"  End Frame:   {end_frame}, Timestamp: {end_time}") 

 

with results as: 

Detected 53 scenes: 

Scene 1: 

  Start Frame: 0, Timestamp: 00:00:00.000 

  End Frame:   303, Timestamp: 00:00:10.107 

Scene 2: 

  Start Frame: 303, Timestamp: 00:00:10.107 

  End Frame:   334, Timestamp: 00:00:11.141 

Scene 3: 

  Start Frame: 334, Timestamp: 00:00:11.141 

  End Frame:   965, Timestamp: 00:00:32.190 

Scene 4: 

  Start Frame: 965, Timestamp: 00:00:32.190 

  End Frame:   1446, Timestamp: 00:00:48.234 

Scene 5: 

  Start Frame: 1446, Timestamp: 00:00:48.234 

  End Frame:   1600, Timestamp: 00:00:53.371 

Scene 6: 

  Start Frame: 1600, Timestamp: 00:00:53.371 

  End Frame:   1631, Timestamp: 00:00:54.405 

Scene 7: 

  Start Frame: 1631, Timestamp: 00:00:54.405 

  End Frame:   1693, Timestamp: 00:00:56.473 

Scene 8: 

  Start Frame: 1693, Timestamp: 00:00:56.473 

  End Frame:   1724, Timestamp: 00:00:57.508 

Scene 9: 

  Start Frame: 1724, Timestamp: 00:00:57.508 

  End Frame:   1785, Timestamp: 00:00:59.542 

Scene 10: 

  Start Frame: 1785, Timestamp: 00:00:59.542 

  End Frame:   1877, Timestamp: 00:01:02.611 

Scene 11: 

  Start Frame: 1877, Timestamp: 00:01:02.611 

  End Frame:   2208, Timestamp: 00:01:13.652 

Scene 12: 

  Start Frame: 2208, Timestamp: 00:01:13.652 

  End Frame:   2269, Timestamp: 00:01:15.687 

Scene 13: 

  Start Frame: 2269, Timestamp: 00:01:15.687 

  End Frame:   2303, Timestamp: 00:01:16.821 

Scene 14: 

  Start Frame: 2303, Timestamp: 00:01:16.821 

  End Frame:   2567, Timestamp: 00:01:25.627 

Scene 15: 

  Start Frame: 2567, Timestamp: 00:01:25.627 

  End Frame:   2601, Timestamp: 00:01:26.762 

Scene 16: 

  Start Frame: 2601, Timestamp: 00:01:26.762 

  End Frame:   2784, Timestamp: 00:01:32.866 

Scene 17: 

  Start Frame: 2784, Timestamp: 00:01:32.866 

  End Frame:   2845, Timestamp: 00:01:34.901 

Scene 18: 

  Start Frame: 2845, Timestamp: 00:01:34.901 

  End Frame:   2883, Timestamp: 00:01:36.168 

Scene 19: 

  Start Frame: 2883, Timestamp: 00:01:36.168 

  End Frame:   3034, Timestamp: 00:01:41.205 

Scene 20: 

  Start Frame: 3034, Timestamp: 00:01:41.205 

  End Frame:   3079, Timestamp: 00:01:42.706 

Scene 21: 

  Start Frame: 3079, Timestamp: 00:01:42.706 

  End Frame:   3276, Timestamp: 00:01:49.278 

Scene 22: 

  Start Frame: 3276, Timestamp: 00:01:49.278 

  End Frame:   3307, Timestamp: 00:01:50.312 

Scene 23: 

  Start Frame: 3307, Timestamp: 00:01:50.312 

  End Frame:   3339, Timestamp: 00:01:51.379 

Scene 24: 

  Start Frame: 3339, Timestamp: 00:01:51.379 

  End Frame:   3370, Timestamp: 00:01:52.413 

Scene 25: 

  Start Frame: 3370, Timestamp: 00:01:52.413 

  End Frame:   3492, Timestamp: 00:01:56.483 

Scene 26: 

  Start Frame: 3492, Timestamp: 00:01:56.483 

  End Frame:   3613, Timestamp: 00:02:00.519 

Scene 27: 

  Start Frame: 3613, Timestamp: 00:02:00.519 

  End Frame:   3644, Timestamp: 00:02:01.553 

Scene 28: 

  Start Frame: 3644, Timestamp: 00:02:01.553 

  End Frame:   3735, Timestamp: 00:02:04.588 

Scene 29: 

  Start Frame: 3735, Timestamp: 00:02:04.588 

  End Frame:   3796, Timestamp: 00:02:06.623 

Scene 30: 

  Start Frame: 3796, Timestamp: 00:02:06.623 

  End Frame:   4221, Timestamp: 00:02:20.800 

Scene 31: 

  Start Frame: 4221, Timestamp: 00:02:20.800 

  End Frame:   4252, Timestamp: 00:02:21.834 

Scene 32: 

  Start Frame: 4252, Timestamp: 00:02:21.834 

  End Frame:   4463, Timestamp: 00:02:28.872 

Scene 33: 

  Start Frame: 4463, Timestamp: 00:02:28.872 

  End Frame:   4764, Timestamp: 00:02:38.913 

Scene 34: 

  Start Frame: 4764, Timestamp: 00:02:38.913 

  End Frame:   5038, Timestamp: 00:02:48.053 

Scene 35: 

  Start Frame: 5038, Timestamp: 00:02:48.053 

  End Frame:   5069, Timestamp: 00:02:49.087 

Scene 36: 

  Start Frame: 5069, Timestamp: 00:02:49.087 

  End Frame:   5101, Timestamp: 00:02:50.154 

Scene 37: 

  Start Frame: 5101, Timestamp: 00:02:50.154 

  End Frame:   5312, Timestamp: 00:02:57.193 

Scene 38: 

  Start Frame: 5312, Timestamp: 00:02:57.193 

  End Frame:   5373, Timestamp: 00:02:59.227 

Scene 39: 

  Start Frame: 5373, Timestamp: 00:02:59.227 

  End Frame:   6004, Timestamp: 00:03:20.276 

Scene 40: 

  Start Frame: 6004, Timestamp: 00:03:20.276 

  End Frame:   6035, Timestamp: 00:03:21.310 

Scene 41: 

  Start Frame: 6035, Timestamp: 00:03:21.310 

  End Frame:   6133, Timestamp: 00:03:24.579 

Scene 42: 

  Start Frame: 6133, Timestamp: 00:03:24.579 

  End Frame:   6184, Timestamp: 00:03:26.280 

Scene 43: 

  Start Frame: 6184, Timestamp: 00:03:26.280 

  End Frame:   6437, Timestamp: 00:03:34.719 

Scene 44: 

  Start Frame: 6437, Timestamp: 00:03:34.719 

  End Frame:   6498, Timestamp: 00:03:36.754 

Scene 45: 

  Start Frame: 6498, Timestamp: 00:03:36.754 

  End Frame:   6529, Timestamp: 00:03:37.788 

Scene 46: 

  Start Frame: 6529, Timestamp: 00:03:37.788 

  End Frame:   6710, Timestamp: 00:03:43.826 

Scene 47: 

  Start Frame: 6710, Timestamp: 00:03:43.826 

  End Frame:   6863, Timestamp: 00:03:48.929 

Scene 48: 

  Start Frame: 6863, Timestamp: 00:03:48.929 

  End Frame:   6894, Timestamp: 00:03:49.963 

Scene 49: 

  Start Frame: 6894, Timestamp: 00:03:49.963 

  End Frame:   7465, Timestamp: 00:04:09.010 

Scene 50: 

  Start Frame: 7465, Timestamp: 00:04:09.010 

  End Frame:   7982, Timestamp: 00:04:26.256 

Scene 51: 

  Start Frame: 7982, Timestamp: 00:04:26.256 

  End Frame:   8104, Timestamp: 00:04:30.325 

Scene 52: 

  Start Frame: 8104, Timestamp: 00:04:30.325 

  End Frame:   8675, Timestamp: 00:04:49.372 

Scene 53: 

  Start Frame: 8675, Timestamp: 00:04:49.372 

  End Frame:   9216, Timestamp: 00:05:07.418 

 

These can be stitched back into a new video file and re-indexed to further shorten the extracted video. 
Preserving actual timestamps on re-indexing is as simple as: 

from datetime import datetime, timedelta 
 
def parse_timecode(timecode): 
    # Parse HH:mm:ss.SSS string to timedelta from zero time 
    return datetime.strptime(timecode, "%H:%M:%S.%f") - datetime.strptime("00:00:00.000", "%H:%M:%S.%f") 
 
def map_offset_to_timestamp_with_validation(intervals, offset): 
    total_duration = timedelta(0) 

    actual_time = -1 
    for start, end in intervals: 
        start_td = parse_timecode(start) 
        end_td = parse_timecode(end) 
        interval_duration = end_td - start_td 
        if offset < total_duration.total_seconds() + interval_duration.total_seconds(): 
            offset_in_interval = offset - total_duration.total_seconds() 
            actual_time = start_td + timedelta(seconds=offset_in_interval) 
        total_duration += interval_duration 
    for start, end in intervals: 
        if start_td <= actual_time <= end_td: 
                return actual_time 

    return -1 
# Example usage: 
intervals = [("00:00:00.000", "00:00:10.000"), ("00:02:00.000", "00:02:05.000")] 
print(map_offset_to_timestamp_with_validation(intervals, 12))  # Returns timedelta at 00:02:02 
print(map_offset_to_timestamp_with_validation(intervals, 20))  # Returns -1 since offset out of range 

Cost Comparison: 

Prompt: Are there dedicated bicycle crossings in green color at street intersections in the attached set of aerial drone images? (+ 26 images extracted from re/indexed drone video) 

Answer: Yes, there are **dedicated bicycle crossings in green color at street intersections** visible in the attached set of aerial drone images. 

- In several of the images (notably the 12th and 15th images), you can clearly see a **bright green marking** extending across a lane at a street intersection. This matches the standard implementation of green pavement marking treatments used to highlight and designate bike crossings at intersection conflict points[1][2][3][5]. 

- Such markings are a dedicated safety feature, making bicycle paths and crossings more visible to both cyclists and motorists. They are typically installed at intersections or approach lanes to indicate the preferred bicycle route and reduce the likelihood of conflicts[1][2][3][5]. 

All other intersections depicted in these images lack such green bicycle crossings, but the presence of these markings in some images directly confirms their implementation in this area. 

Type 

Count 

Total_Tokens 

Total_cost 

Indexed video/extracted images  

26 

49316 (99% input-side, 1% output-side) 

0.156 

Original aerial drone video 

17776 

35,500,000 

$270 (including vector index and search) 

 

Tuesday, October 7, 2025

 As a continuation of previous articles on parking spots detection from aerial drone videos on urban scenes, we explore the transformative role of computer vision as sourced from online blog posts and case studies such as from Unitlab and Ultralytics.

In bustling cities, the daily hunt for parking is a familiar frustration. Traditional methods—manual checks, static signage, or rudimentary sensors—often fall short in managing the growing volume of vehicles. Enter smart parking: a fusion of cameras, computer vision, and AI that enables real-time monitoring and guidance. These systems work by capturing video footage from strategically placed cameras, which is then processed by AI models to detect vehicles, assess space occupancy, and update dashboards or mobile apps that guide drivers to available spots.

The core workflow comprises of systems for: video capture, data processing, real-time analysis, and user interaction. At the heart of this process is computer vision, which interprets visual data to identify vehicles and determine whether parking spaces are occupied or vacant. This intelligence allows for dynamic updates and alerts, improving both operational efficiency and user experience.

Key applications include occupancy detection, violation monitoring (e.g., cars parked across multiple spaces), and traffic flow optimization. These capabilities not only reduce congestion but also support better decision-making for parking lot operators.

Training models to better recognize parking spots require annotations by humans which emphasizes the importance of high-quality datasets for training robust computer vision models. Most annotations are done with the help of bounding box for vehicle detection. Annotators then label images to teach the model how to recognize vehicles and empty spots. While auto-labeling tools can accelerate this process, human oversight remains essential to ensure accuracy.

Once annotated, the dataset is versioned and released—highlighting the iterative nature of model training. As new data is collected, further annotation and version control help refine model performance. This disciplined approach to dataset management ensures that AI models remain adaptable and reliable over time.

Many case studies generalize benefits and challenges of implementing smart parking systems. For operators, these systems optimize space usage, reduce costs through automation, and enable data-driven decisions. For drivers, they offer convenience and time savings. However, challenges persist: environmental factors like lighting and weather can affect model accuracy, and scaling such systems requires significant investment in infrastructure and training.

Real-world examples underscore the technology’s impact. Cities like Barcelona have adopted AI-driven parking systems using computer vision and IoT sensors. Private operators such as ParkMobile offer real-time availability through mobile apps, while retail complexes use similar setups to enhance customer experience.

In conclusion, smart parking exemplifies how computer vision, when paired with the right tools and strategy, can solve tangible urban problems. Though implementation demands careful planning and resources, the payoff—streamlined operations, improved user satisfaction, and scalable innovation—is well worth the effort.

#codingexercise: https://1drv.ms/w/c/d609fb70e39b65c8/Ef7sSW3UHRFOvoQn8tkR2iUBv_oQ0OCUNJ1cROrOWfAOtg?e=nZ8kmu 

Monday, October 6, 2025

 The idea behind offloading drone video sensing analytics is that commodity LLMs available online can step in to do even regular counting tasks that otherwise would have required custom model or image processing techniques and analytics. For example, to count the number of unoccupied spots in a point-of-time frame from an aerial drone video, can now be requested as follows:

import requests

import base64

import os

# Read and encode image as base64

image_path = "parking2.jpg"

perplexity_api_key = os.getenv("PERPLEXITY_API_KEY")

try:

    with open(image_path, "rb") as image_file:

        base64_image = base64.b64encode(image_file.read()).decode("utf-8")

    image_data_uri = f"data:image/jpg;base64,{base64_image}" # Ensure correct MIME type

except FileNotFoundError:

    print("Error: Image file not found.")

    exit()

# API request payload

url = "https://api.perplexity.ai/chat/completions"

headers = {

    "Authorization": f"Bearer {perplexity_api_key}",

    "accept": "application/json",

    "content-type": "application/json"

}

payload = {

    "model": "sonar-pro",

    "messages": [

        {

            "role": "user",

            "content": [

                {"type": "text", "text": "How many unoccupied parking spots are there in this image?"},

                {"type": "image_url", "image_url": {"url": image_data_uri}}

            ]

        }

    ],

    "stream": False

}

try:

    response = requests.post(url, headers=headers, json=payload)

    response.raise_for_status() # Raise an exception for bad status codes

    result = response.json()

    print(result)

    print(result["choices"][0]["message"]["content"]

except requests.exceptions.RequestException as e:

    print(f"API Request failed: {e}")

With input as:

And with results as:

There are 2 unoccupied parking spots visible in the parking lot at the upper right of the image. All other parking spots are occupied by cars.

And with the full json response as:

{'id': 'ca0ca941-5daa-4308-a80b-8db31fc86ab0', 'model': 'sonar-pro', 'created': 1757643820, 'usage': {'prompt_tokens': 1240, 'completion_tokens': 31, 'total_tokens': 1271, 'search_context_size': 'low', 'cost': {'input_tokens_cost': 0.004, 'output_tokens_cost': 0.0, 'request_cost': 0.006, 'total_cost': 0.01}}, 'citations': ['https://www.youtube.com/watch?v=MeSeuzBhq2E', 'https://www.youtube.com/watch?v=caKnQlCMIYI', 'https://blog.roboflow.com/build-a-parking-lot-monitoring-system/', 'https://cseweb.ucsd.edu/classes/wi07/cse190-a/reports/ntrue.pdf', 'https://www.kaggle.com/datasets/iasadpanwhar/parking-lot-detection-counter', 'https://www.shutterstock.com/search/vacant-car-parking-space', 'https://parkinglogix.com/multi-lots/', 'https://www.istockphoto.com/photos/empty-parking-space', 'https://www.arcgis.com/home/item.html?id=5b8e047f11324775832550c6bab19d2b', 'https://www.shutterstock.com/search/empty-parking-slot'], 'search_results': [{'title': 'Parking Space detection using OpenCV Python', 'url': 'https://www.youtube.com/watch?v=MeSeuzBhq2E', 'date': '2022-11-20', 'last_updated': '2025-08-22', 'snippet': 'This project finds outs the count of empty and occupied parking spaces in a car parking lot using digital image processing techniques from ...'}, {'title': 'Parking Space Counter using OpenCV Python', 'url': 'https://www.youtube.com/watch?v=caKnQlCMIYI', 'date': '2021-12-26', 'last_updated': '2025-07-31', 'snippet': 'In this tutorial, we are going to create a Parking Space Counter. We will find how many total cars are present and how many spaces are ...'}, {'title': 'How to Build a Parking Lot Monitoring System with ...', 'url': 'https://blog.roboflow.com/build-a-parking-lot-monitoring-system/', 'date': '2024-01-17', 'last_updated': '2025-09-11', 'snippet': "We'll go through the steps of creating a system that allows users to identify the number of empty and occupied spots in a parking space, and visualize where ..."}, {'title': 'Vacant Parking Space Detection in Static Images', 'url': 'https://cseweb.ucsd.edu/classes/wi07/cse190-a/reports/ntrue.pdf', 'date': None, 'last_updated': '2025-04-27', 'snippet': 'by N True · Cited by 133 — Current parking space vacancy detection systems use simple trip sensors at the entry and exit points of parking lots. Unfortunately, this type of system ...'}, {'title': 'Parking_Lot_Detection_Counter', 'url': 'https://www.kaggle.com/datasets/iasadpanwhar/parking-lot-detection-counter', 'date': None, 'last_updated': None, 'snippet': 'This dataset is designed for the task of detecting and counting empty and occupied parking spots in a parking area. It includes images, a mask image, a video, ...'}, {'title': 'Vacant Car Parking Space royalty-free images', 'url': 'https://www.shutterstock.com/search/vacant-car-parking-space', 'date': None, 'last_updated': '2024-10-06', 'snippet': 'Find 8+ Thousand Vacant Car Parking Space stock images in HD and millions of other royalty-free stock photos, 3D objects, illustrations and vectors in the ...'}, {'title': 'Parking Counter System | Multi-Lots', 'url': 'https://parkinglogix.com/multi-lots/', 'date': '2023-03-15', 'last_updated': '2025-09-09', 'snippet': 'With our parking space counter systems, you can keep track of how many spots are left in multiple lots from a single interface.'}, {'title': 'Empty Parking Space Pictures, Images and Stock Photos', 'url': 'https://www.istockphoto.com/photos/empty-parking-space', 'date': '2020-10-13', 'last_updated': '2025-04-30', 'snippet': 'Browse 5,900+ empty parking space stock photos and images available, or search for parking lot or stolen car to find more great stock photos and pictures.'}, {'title': 'Parking Spot Detection - USA - Overview', 'url': 'https://www.arcgis.com/home/item.html?id=5b8e047f11324775832550c6bab19d2b', 'date': None, 'last_updated': None, 'snippet': 'This deep learning model is used to detect and classify parking spots in high resolution drone or aerial imagery.'}, {'title': 'Empty Parking Slot royalty-free images', 'url': 'https://www.shutterstock.com/search/empty-parking-slot', 'date': None, 'last_updated': '2024-10-19', 'snippet': 'Find 6+ Thousand Empty Parking Slot stock images in HD and millions of other royalty-free stock photos, 3D objects, illustrations and vectors in the ...'}], 'object': 'chat.completion', 'choices': [{'index': 0, 'finish_reason': 'stop', 'message': {'role': 'assistant', 'content': 'There are 2 unoccupied parking spots visible in the parking lot at the upper right of the image. All other parking spots are occupied by cars.'}, 'delta': {'role': 'assistant', 'content': ''}}]}


Sunday, October 5, 2025

 Among the objects detected in an urban landscape, parking spots and streets play a vital role in mapping the drone world to the real world. With or without the gps information from the drone, it is possible to establish this mapping afterwards and not by processing each and every frame of the drone video. To help with this analysis at selected times, deep learning models can be used for parking spot and street detection. Below are approaches are to do that:

1. YOLOv8 + SAM Hybrid (GitHub: julienborderon/parking_detection) for parking spot detection

import cv2

from ultralytics import YOLO

from segment_anything import SamPredictor, sam_model_registry

import numpy as np

import matplotlib.pyplot as plt

# --- Load YOLOv8 model ---

yolo_model = YOLO("weights/yolov8n.pt") # Replace with the correct path to pretrained weights

# --- Load SAM model ---

sam = sam_model_registry["vit_b"](checkpoint="weights/sam_vit_b.pth") # Replace with correct SAM checkpoint

predictor = SamPredictor(sam)

# --- Load aerial image ---

image_path = "drone_parking_lot.jpg"

image = cv2.imread(image_path)

image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# --- Run YOLOv8 detection ---

results = yolo_model(image_rgb)

boxes = results[0].boxes.xyxy.cpu().numpy()

classes = results[0].boxes.cls.cpu().numpy()

# --- Filter for parking spot class (assume class index for 'parking spot' is known) ---

parking_spot_class_id = 0 # Replace with correct class ID if custom-trained

parking_boxes = boxes[classes == parking_spot_class_id]

# --- Run SAM segmentation ---

predictor.set_image(image_rgb)

unoccupied_count = 0

for box in parking_boxes:

    x1, y1, x2, y2 = map(int, box)

    input_box = np.array([x1, y1, x2, y2])

    masks, scores, _ = predictor.predict(box=input_box, multimask_output=True)

    # Heuristic: if segmentation mask is mostly empty, assume unoccupied

    mask = masks[np.argmax(scores)]

    occupancy_ratio = np.sum(mask) / ((x2 - x1) * (y2 - y1))

    if occupancy_ratio < 0.2: # Threshold can be tuned

        unoccupied_count += 1

print(f"Detected {len(parking_boxes)} parking spots.")

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

2. AerialLaneNet (ICASSP 2024) for lane and traffic detection

import cv2

import numpy as np

import torch

from torchvision import transforms

from shapely.geometry import LineString

import matplotlib.pyplot as plt

# --- Load pretrained lane detection model (placeholder for AerialLaneNet) ---

class DummyLaneNet(torch.nn.Module):

    def forward(self, x):

        # Simulated output: binary mask of lane lines

        _, _, H, W = x.shape

        mask = np.zeros((H, W), dtype=np.uint8)

        cv2.line(mask, (int(W*0.3), H), (int(W*0.4), 0), 255, 5)

        cv2.line(mask, (int(W*0.7), H), (int(W*0.6), 0), 255, 5)

        return torch.tensor(mask).unsqueeze(0).unsqueeze(0)

model = DummyLaneNet()

# --- Load and preprocess aerial image ---

image_path = "aerial_street.jpg"

image = cv2.imread(image_path)

image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

transform = transforms.Compose([

    transforms.ToTensor(),

    transforms.Resize((512, 512)),

    transforms.Normalize(mean=[0.5]*3, std=[0.5]*3)

])

input_tensor = transform(image_rgb).unsqueeze(0)

# --- Run lane detection ---

with torch.no_grad():

    lane_mask = model(input_tensor).squeeze().numpy()

# --- Extract lane contours and bounding boxes ---

contours, _ = cv2.findContours(lane_mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

bounding_boxes = [cv2.boundingRect(cnt) for cnt in contours]

# --- Estimate traffic direction using line fitting ---

directions = []

for cnt in contours:

    [vx, vy, x, y] = cv2.fitLine(cnt, cv2.DIST_L2, 0, 0.01, 0.01)

    directions.append(((x, y), (vx, vy)))

# --- Visualize results ---

output = image_rgb.copy()

for (x, y, w, h), ((cx, cy), (dx, dy)) in zip(bounding_boxes, directions):

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

    pt1 = (int(cx), int(cy))

    pt2 = (int(cx + 100*dx), int(cy + 100*dy))

    cv2.arrowedLine(output, pt1, pt2, (255, 0, 0), 2)

plt.imshow(output)

plt.title("Detected Streets and Traffic Directions")

plt.axis('off')

plt.show()

And their corroboration with real-world maps helps to put names on them