From 6e3b7dc2f622c44b07dc70e7ed0d99b4a99c186f Mon Sep 17 00:00:00 2001 From: Muhammad Rizwan Munawar Date: Tue, 17 Sep 2024 23:27:03 +0500 Subject: [PATCH] Optimize `parking management` solution (#16288) Co-authored-by: UltralyticsAssistant Co-authored-by: Ultralytics Assistant <135830346+UltralyticsAssistant@users.noreply.github.com> --- docs/en/guides/parking-management.md | 33 ++-- ultralytics/solutions/parking_management.py | 192 +++++++++----------- 2 files changed, 92 insertions(+), 133 deletions(-) diff --git a/docs/en/guides/parking-management.md b/docs/en/guides/parking-management.md index 84a88f57..78686bd0 100644 --- a/docs/en/guides/parking-management.md +++ b/docs/en/guides/parking-management.md @@ -74,9 +74,6 @@ Parking management with [Ultralytics YOLOv8](https://github.com/ultralytics/ultr from ultralytics import solutions - # Path to json file, that created with above point selection app - polygon_json_path = "bounding_boxes.json" - # Video capture cap = cv2.VideoCapture("Path/to/video/file.mp4") assert cap.isOpened(), "Error reading video file" @@ -86,22 +83,16 @@ Parking management with [Ultralytics YOLOv8](https://github.com/ultralytics/ultr video_writer = cv2.VideoWriter("parking management.avi", cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h)) # Initialize parking management object - management = solutions.ParkingManagement(model_path="yolov8n.pt") + parking_manager = solutions.ParkingManagement( + model="yolov8n.pt", # path to model file + json_file="bounding_boxes.json", # path to parking annotations file + ) while cap.isOpened(): ret, im0 = cap.read() if not ret: break - - json_data = management.parking_regions_extraction(polygon_json_path) - results = management.model.track(im0, persist=True, show=False) - - if results[0].boxes.id is not None: - boxes = results[0].boxes.xyxy.cpu().tolist() - clss = results[0].boxes.cls.cpu().tolist() - management.process_data(json_data, im0, boxes, clss) - - management.display_frames(im0) + im0 = parking_manager.process_data(im0) video_writer.write(im0) cap.release() @@ -111,14 +102,12 @@ Parking management with [Ultralytics YOLOv8](https://github.com/ultralytics/ultr ### Optional Arguments `ParkingManagement` -| Name | Type | Default | Description | -| ------------------------ | ------- | ----------------- | -------------------------------------- | -| `model_path` | `str` | `None` | Path to the YOLOv8 model. | -| `txt_color` | `tuple` | `(0, 0, 0)` | RGB color tuple for text. | -| `bg_color` | `tuple` | `(255, 255, 255)` | RGB color tuple for background. | -| `occupied_region_color` | `tuple` | `(0, 255, 0)` | RGB color tuple for occupied regions. | -| `available_region_color` | `tuple` | `(0, 0, 255)` | RGB color tuple for available regions. | -| `margin` | `int` | `10` | Margin for text display. | +| Name | Type | Default | Description | +| ------------------------ | ------- | ------------- | -------------------------------------------------------------- | +| `model` | `str` | `None` | Path to the YOLOv8 model. | +| `json_file` | `str` | `None` | Path to the JSON file, that have all parking coordinates data. | +| `occupied_region_color` | `tuple` | `(0, 0, 255)` | RGB color for occupied regions. | +| `available_region_color` | `tuple` | `(0, 255, 0)` | RGB color for available regions. | ### Arguments `model.track` diff --git a/ultralytics/solutions/parking_management.py b/ultralytics/solutions/parking_management.py index 19a8ef16..61284933 100644 --- a/ultralytics/solutions/parking_management.py +++ b/ultralytics/solutions/parking_management.py @@ -42,10 +42,10 @@ class ParkingPtsSelection: self.image_path = None self.image = None self.canvas_image = None - self.bounding_boxes = [] + self.rg_data = [] # region coordinates self.current_box = [] - self.img_width = 0 - self.img_height = 0 + self.imgw = 0 # image width + self.imgh = 0 # image height # Constants self.canvas_max_width = 1280 @@ -64,17 +64,17 @@ class ParkingPtsSelection: return self.image = Image.open(self.image_path) - self.img_width, self.img_height = self.image.size + self.imgw, self.imgh = self.image.size # Calculate the aspect ratio and resize image - aspect_ratio = self.img_width / self.img_height + aspect_ratio = self.imgw / self.imgh if aspect_ratio > 1: # Landscape orientation - canvas_width = min(self.canvas_max_width, self.img_width) + canvas_width = min(self.canvas_max_width, self.imgw) canvas_height = int(canvas_width / aspect_ratio) else: # Portrait orientation - canvas_height = min(self.canvas_max_height, self.img_height) + canvas_height = min(self.canvas_max_height, self.imgh) canvas_width = int(canvas_height * aspect_ratio) # Check if canvas is already initialized @@ -90,46 +90,34 @@ class ParkingPtsSelection: self.canvas.bind("", self.on_canvas_click) # Reset bounding boxes and current box - self.bounding_boxes = [] + self.rg_data = [] self.current_box = [] def on_canvas_click(self, event): """Handle mouse clicks on canvas to create points for bounding boxes.""" self.current_box.append((event.x, event.y)) - x0, y0 = event.x - 3, event.y - 3 - x1, y1 = event.x + 3, event.y + 3 - self.canvas.create_oval(x0, y0, x1, y1, fill="red") + self.canvas.create_oval(event.x - 3, event.y - 3, event.x + 3, event.y + 3, fill="red") if len(self.current_box) == 4: - self.bounding_boxes.append(self.current_box) - self.draw_bounding_box(self.current_box) + self.rg_data.append(self.current_box) + [ + self.canvas.create_line(self.current_box[i], self.current_box[(i + 1) % 4], fill="blue", width=2) + for i in range(4) + ] self.current_box = [] - def draw_bounding_box(self, box): - """ - Draw bounding box on canvas. - - Args: - box (list): Bounding box data - """ - for i in range(4): - x1, y1 = box[i] - x2, y2 = box[(i + 1) % 4] - self.canvas.create_line(x1, y1, x2, y2, fill="blue", width=2) - def remove_last_bounding_box(self): """Remove the last drawn bounding box from canvas.""" from tkinter import messagebox # scope for multi-environment compatibility - if self.bounding_boxes: - self.bounding_boxes.pop() # Remove the last bounding box + if self.rg_data: + self.rg_data.pop() # Remove the last bounding box self.canvas.delete("all") # Clear the canvas self.canvas.create_image(0, 0, anchor=self.tk.NW, image=self.canvas_image) # Redraw the image # Redraw all bounding boxes - for box in self.bounding_boxes: - self.draw_bounding_box(box) - + for box in self.rg_data: + [self.canvas.create_line(box[i], box[(i + 1) % 4], fill="blue", width=2) for i in range(4)] messagebox.showinfo("Success", "Last bounding box removed.") else: messagebox.showwarning("Warning", "No bounding boxes to remove.") @@ -138,19 +126,19 @@ class ParkingPtsSelection: """Saves rescaled bounding boxes to 'bounding_boxes.json' based on image-to-canvas size ratio.""" from tkinter import messagebox # scope for multi-environment compatibility - canvas_width, canvas_height = self.canvas.winfo_width(), self.canvas.winfo_height() - width_scaling_factor = self.img_width / canvas_width - height_scaling_factor = self.img_height / canvas_height - bounding_boxes_data = [] - for box in self.bounding_boxes: - rescaled_box = [] + rg_data = [] # regions data + for box in self.rg_data: + rs_box = [] # rescaled box list for x, y in box: - rescaled_x = int(x * width_scaling_factor) - rescaled_y = int(y * height_scaling_factor) - rescaled_box.append((rescaled_x, rescaled_y)) - bounding_boxes_data.append({"points": rescaled_box}) + rs_box.append( + ( + int(x * self.imgw / self.canvas.winfo_width()), # width scaling + int(y * self.imgh / self.canvas.winfo_height()), + ) + ) # height scaling + rg_data.append({"points": rs_box}) with open("bounding_boxes.json", "w") as f: - json.dump(bounding_boxes_data, f, indent=4) + json.dump(rg_data, f, indent=4) messagebox.showinfo("Success", "Bounding boxes saved to bounding_boxes.json") @@ -160,102 +148,85 @@ class ParkingManagement: def __init__( self, - model_path, - txt_color=(0, 0, 0), - bg_color=(255, 255, 255), - occupied_region_color=(0, 255, 0), - available_region_color=(0, 0, 255), - margin=10, + model, # Ultralytics YOLO model file path + json_file, # Parking management annotation file created from Parking Annotator + occupied_region_color=(0, 0, 255), # occupied region color + available_region_color=(0, 255, 0), # available region color ): """ Initializes the parking management system with a YOLOv8 model and visualization settings. Args: - model_path (str): Path to the YOLOv8 model. - txt_color (tuple): RGB color tuple for text. - bg_color (tuple): RGB color tuple for background. + model (str): Path to the YOLOv8 model. + json_file (str): file that have all parking slot points data occupied_region_color (tuple): RGB color tuple for occupied regions. available_region_color (tuple): RGB color tuple for available regions. - margin (int): Margin for text display. """ - # Model path and initialization - self.model_path = model_path - self.model = self.load_model() - - # Labels dictionary - self.labels_dict = {"Occupancy": 0, "Available": 0} - - # Visualization details - self.margin = margin - self.bg_color = bg_color - self.txt_color = txt_color - self.occupied_region_color = occupied_region_color - self.available_region_color = available_region_color - - self.window_name = "Ultralytics YOLOv8 Parking Management System" - # Check if environment supports imshow - self.env_check = check_imshow(warn=True) - - def load_model(self): - """Load the Ultralytics YOLO model for inference and analytics.""" + # Model initialization from ultralytics import YOLO - return YOLO(self.model_path) + self.model = YOLO(model) - @staticmethod - def parking_regions_extraction(json_file): - """ - Extract parking regions from json file. - - Args: - json_file (str): file that have all parking slot points - """ + # Load JSON data with open(json_file) as f: - return json.load(f) + self.json_data = json.load(f) - def process_data(self, json_data, im0, boxes, clss): + self.pr_info = {"Occupancy": 0, "Available": 0} # dictionary for parking information + + self.occ = occupied_region_color + self.arc = available_region_color + + self.env_check = check_imshow(warn=True) # check if environment supports imshow + + def process_data(self, im0): """ Process the model data for parking lot management. Args: - json_data (str): json data for parking lot management im0 (ndarray): inference image - boxes (list): bounding boxes data - clss (list): bounding boxes classes list - - Returns: - filled_slots (int): total slots that are filled in parking lot - empty_slots (int): total slots that are available in parking lot """ - annotator = Annotator(im0) - empty_slots, filled_slots = len(json_data), 0 - for region in json_data: - points_array = np.array(region["points"], dtype=np.int32).reshape((-1, 1, 2)) - region_occupied = False + results = self.model.track(im0, persist=True, show=False) # object tracking + es, fs = len(self.json_data), 0 # empty slots, filled slots + annotator = Annotator(im0) # init annotator + + # extract tracks data + if results[0].boxes.id is None: + self.display_frames(im0) + return im0 + + boxes = results[0].boxes.xyxy.cpu().tolist() + clss = results[0].boxes.cls.cpu().tolist() + + for region in self.json_data: + # Convert points to a NumPy array with the correct dtype and reshape properly + pts_array = np.array(region["points"], dtype=np.int32).reshape((-1, 1, 2)) + rg_occupied = False # occupied region initialization for box, cls in zip(boxes, clss): - x_center = int((box[0] + box[2]) / 2) - y_center = int((box[1] + box[3]) / 2) - text = f"{self.model.names[int(cls)]}" - + xc = int((box[0] + box[2]) / 2) + yc = int((box[1] + box[3]) / 2) annotator.display_objects_labels( - im0, text, self.txt_color, self.bg_color, x_center, y_center, self.margin + im0, self.model.names[int(cls)], (104, 31, 17), (255, 255, 255), xc, yc, 10 ) - dist = cv2.pointPolygonTest(points_array, (x_center, y_center), False) + dist = cv2.pointPolygonTest(pts_array, (xc, yc), False) if dist >= 0: - region_occupied = True + rg_occupied = True break + if rg_occupied: + fs += 1 + es -= 1 - color = self.occupied_region_color if region_occupied else self.available_region_color - cv2.polylines(im0, [points_array], isClosed=True, color=color, thickness=2) - if region_occupied: - filled_slots += 1 - empty_slots -= 1 + # Plotting regions + color = self.occ if rg_occupied else self.arc + cv2.polylines(im0, [pts_array], isClosed=True, color=color, thickness=2) - self.labels_dict["Occupancy"] = filled_slots - self.labels_dict["Available"] = empty_slots + self.pr_info["Occupancy"] = fs + self.pr_info["Available"] = es - annotator.display_analytics(im0, self.labels_dict, self.txt_color, self.bg_color, self.margin) + annotator.display_analytics(im0, self.pr_info, (104, 31, 17), (255, 255, 255), 10) + + self.display_frames(im0) + return im0 def display_frames(self, im0): """ @@ -265,8 +236,7 @@ class ParkingManagement: im0 (ndarray): inference image """ if self.env_check: - cv2.namedWindow(self.window_name) - cv2.imshow(self.window_name, im0) + cv2.imshow("Ultralytics Parking Manager", im0) # Break Window if cv2.waitKey(1) & 0xFF == ord("q"): return