From 1c650ab04cb682999c45dbe224fc3df5ecb921a6 Mon Sep 17 00:00:00 2001 From: Muhammad Rizwan Munawar Date: Thu, 17 Oct 2024 21:51:52 +0500 Subject: [PATCH] Update `parking-management` solution (#16990) Co-authored-by: UltralyticsAssistant --- docs/en/guides/parking-management.md | 10 +- ultralytics/cfg/solutions/default.yaml | 1 + ultralytics/solutions/parking_management.py | 270 ++++++++------------ 3 files changed, 105 insertions(+), 176 deletions(-) diff --git a/docs/en/guides/parking-management.md b/docs/en/guides/parking-management.md index 6cf07e4847..b6140181ae 100644 --- a/docs/en/guides/parking-management.md +++ b/docs/en/guides/parking-management.md @@ -102,12 +102,10 @@ Parking management with [Ultralytics YOLO11](https://github.com/ultralytics/ultr ### Optional Arguments `ParkingManagement` -| Name | Type | Default | Description | -| ------------------------ | ------- | ------------- | -------------------------------------------------------------- | -| `model` | `str` | `None` | Path to the YOLO11 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. | +| Name | Type | Default | Description | +| ----------- | ----- | ------- | -------------------------------------------------------------- | +| `model` | `str` | `None` | Path to the YOLO11 model. | +| `json_file` | `str` | `None` | Path to the JSON file, that have all parking coordinates data. | ### Arguments `model.track` diff --git a/ultralytics/cfg/solutions/default.yaml b/ultralytics/cfg/solutions/default.yaml index e4e1b845a0..a353fd2a21 100644 --- a/ultralytics/cfg/solutions/default.yaml +++ b/ultralytics/cfg/solutions/default.yaml @@ -15,3 +15,4 @@ down_angle: 90 # Workouts down_angle for counts, 90 is default value. You can ch kpts: [6, 8, 10] # Keypoints for workouts monitoring, i.e. If you want to consider keypoints for pushups that have mostly values of [6, 8, 10]. colormap: # Colormap for heatmap, Only OPENCV supported colormaps can be used. By default COLORMAP_PARULA will be used for visualization. analytics_type: "line" # Analytics type i.e "line", "pie", "bar" or "area" charts. By default, "line" analytics will be used for processing. +json_file: # parking system regions file path. diff --git a/ultralytics/solutions/parking_management.py b/ultralytics/solutions/parking_management.py index 3a1cd21972..e7afda8598 100644 --- a/ultralytics/solutions/parking_management.py +++ b/ultralytics/solutions/parking_management.py @@ -4,8 +4,9 @@ import json import cv2 import numpy as np +from PIL import Image, ImageTk -from ultralytics.utils.checks import check_imshow, check_requirements +from ultralytics.solutions.solutions import LOGGER, BaseSolution, check_requirements from ultralytics.utils.plotting import Annotator @@ -13,229 +14,158 @@ class ParkingPtsSelection: """Class for selecting and managing parking zone points on images using a Tkinter-based UI.""" def __init__(self): - """Initializes the UI for selecting parking zone points in a tkinter window.""" + """Class initialization method.""" check_requirements("tkinter") + import tkinter as tk + from tkinter import filedialog, messagebox - import tkinter as tk # scope for multi-environment compatibility + self.tk, self.filedialog, self.messagebox = tk, filedialog, messagebox + self.setup_ui() + self.initialize_properties() + self.master.mainloop() - self.tk = tk - self.master = tk.Tk() + def setup_ui(self): + """Sets up the Tkinter UI components.""" + self.master = self.tk.Tk() self.master.title("Ultralytics Parking Zones Points Selector") - - # Disable window resizing self.master.resizable(False, False) - # Setup canvas for image display + # Canvas for image display self.canvas = self.tk.Canvas(self.master, bg="white") + self.canvas.pack(side=self.tk.BOTTOM) - # Setup buttons + # Button frame with buttons button_frame = self.tk.Frame(self.master) button_frame.pack(side=self.tk.TOP) - - self.tk.Button(button_frame, text="Upload Image", command=self.upload_image).grid(row=0, column=0) - self.tk.Button(button_frame, text="Remove Last BBox", command=self.remove_last_bounding_box).grid( - row=0, column=1 - ) - self.tk.Button(button_frame, text="Save", command=self.save_to_json).grid(row=0, column=2) - - # Initialize properties - self.image_path = None - self.image = None - self.canvas_image = None - self.rg_data = [] # region coordinates - self.current_box = [] - self.imgw = 0 # image width - self.imgh = 0 # image height - - # Constants - self.canvas_max_width = 1280 - self.canvas_max_height = 720 - - self.master.mainloop() + for text, cmd in [ + ("Upload Image", self.upload_image), + ("Remove Last BBox", self.remove_last_bounding_box), + ("Save", self.save_to_json), + ]: + self.tk.Button(button_frame, text=text, command=cmd).pack(side=self.tk.LEFT) + + def initialize_properties(self): + """Initialize the necessary properties.""" + self.image = self.canvas_image = None + self.rg_data, self.current_box = [], [] + self.imgw = self.imgh = 0 + self.canvas_max_width, self.canvas_max_height = 1280, 720 def upload_image(self): - """Upload an image and resize it to fit canvas.""" - from tkinter import filedialog - - from PIL import Image, ImageTk # scope because ImageTk requires tkinter package - - self.image_path = filedialog.askopenfilename(filetypes=[("Image Files", "*.png;*.jpg;*.jpeg")]) - if not self.image_path: + """Uploads an image, resizes it to fit the canvas, and displays it.""" + self.image = Image.open(self.filedialog.askopenfilename(filetypes=[("Image Files", "*.png;*.jpg;*.jpeg")])) + if not self.image: return - self.image = Image.open(self.image_path) self.imgw, self.imgh = self.image.size - - # Calculate the aspect ratio and resize image aspect_ratio = self.imgw / self.imgh - if aspect_ratio > 1: - # Landscape orientation - 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.imgh) - canvas_width = int(canvas_height * aspect_ratio) - - # Check if canvas is already initialized - if self.canvas: - self.canvas.destroy() # Destroy previous canvas - - self.canvas = self.tk.Canvas(self.master, bg="white", width=canvas_width, height=canvas_height) - resized_image = self.image.resize((canvas_width, canvas_height), Image.LANCZOS) - self.canvas_image = ImageTk.PhotoImage(resized_image) - self.canvas.create_image(0, 0, anchor=self.tk.NW, image=self.canvas_image) + canvas_width = ( + min(self.canvas_max_width, self.imgw) if aspect_ratio > 1 else int(self.canvas_max_height * aspect_ratio) + ) + canvas_height = ( + min(self.canvas_max_height, self.imgh) if aspect_ratio <= 1 else int(canvas_width / aspect_ratio) + ) - self.canvas.pack(side=self.tk.BOTTOM) - self.canvas.bind("", self.on_canvas_click) + self.canvas.config(width=canvas_width, height=canvas_height) + self.display_image(canvas_width, canvas_height) + self.rg_data.clear(), self.current_box.clear() - # Reset bounding boxes and current box - self.rg_data = [] - self.current_box = [] + def display_image(self, width, height): + """Displays the resized image on the canvas.""" + self.canvas_image = ImageTk.PhotoImage(self.image.resize((width, height), Image.LANCZOS)) + self.canvas.create_image(0, 0, anchor=self.tk.NW, image=self.canvas_image) + self.canvas.bind("", self.on_canvas_click) def on_canvas_click(self, event): - """Handle mouse clicks on canvas to create points for bounding boxes.""" + """Handles mouse clicks to add points for bounding boxes.""" self.current_box.append((event.x, event.y)) 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.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 = [] + self.rg_data.append(self.current_box.copy()) + self.draw_box(self.current_box) + self.current_box.clear() - def remove_last_bounding_box(self): - """Remove the last drawn bounding box from canvas.""" - from tkinter import messagebox # scope for multi-environment compatibility + def draw_box(self, box): + """Draws a bounding box on the canvas.""" + for i in range(4): + self.canvas.create_line(box[i], box[(i + 1) % 4], fill="blue", width=2) - 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 + def remove_last_bounding_box(self): + """Removes the last bounding box and redraws the canvas.""" + if not self.rg_data: + self.messagebox.showwarning("Warning", "No bounding boxes to remove.") + return + self.rg_data.pop() + self.redraw_canvas() - # Redraw all bounding boxes - 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.") + def redraw_canvas(self): + """Redraws the canvas with the image and all bounding boxes.""" + self.canvas.delete("all") + self.canvas.create_image(0, 0, anchor=self.tk.NW, image=self.canvas_image) + for box in self.rg_data: + self.draw_box(box) def save_to_json(self): - """Saves rescaled bounding boxes to 'bounding_boxes.json' based on image-to-canvas size ratio.""" - from tkinter import messagebox # scope for multi-environment compatibility - - rg_data = [] # regions data - for box in self.rg_data: - rs_box = [ - ( - int(x * self.imgw / self.canvas.winfo_width()), # width scaling - int(y * self.imgh / self.canvas.winfo_height()), # height scaling - ) - for x, y in box - ] - rg_data.append({"points": rs_box}) + """Saves the bounding boxes to a JSON file.""" + scale_w, scale_h = self.imgw / self.canvas.winfo_width(), self.imgh / self.canvas.winfo_height() + data = [{"points": [(int(x * scale_w), int(y * scale_h)) for x, y in box]} for box in self.rg_data] with open("bounding_boxes.json", "w") as f: - json.dump(rg_data, f, indent=4) - - messagebox.showinfo("Success", "Bounding boxes saved to bounding_boxes.json") + json.dump(data, f, indent=4) + self.messagebox.showinfo("Success", "Bounding boxes saved to bounding_boxes.json") -class ParkingManagement: +class ParkingManagement(BaseSolution): """Manages parking occupancy and availability using YOLO model for real-time monitoring and visualization.""" - def __init__( - self, - 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 YOLO model and visualization settings. + def __init__(self, **kwargs): + """Initializes the parking management system with a YOLO model and visualization settings.""" + super().__init__(**kwargs) - Args: - model (str): Path to the YOLO 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. - """ - # Model initialization - from ultralytics import YOLO + self.json_file = self.CFG["json_file"] # Load JSON data + if self.json_file is None: + LOGGER.warning("❌ json_file argument missing. Parking region details required.") + raise ValueError("❌ Json file path can not be empty") - self.model = YOLO(model) - - # Load JSON data - with open(json_file) as f: - self.json_data = json.load(f) + with open(self.json_file) as f: + self.json = json.load(f) 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 + self.arc = (0, 0, 255) # available region color + self.occ = (0, 255, 0) # occupied region color + self.dc = (255, 0, 189) # centroid color for each box def process_data(self, im0): """ Process the model data for parking lot management. Args: - im0 (ndarray): inference image + im0 (ndarray): inference image. """ - results = self.model.track(im0, persist=True, show=False) # object tracking + self.extract_tracks(im0) # extract tracks from im0 + es, fs = len(self.json), 0 # empty slots, filled slots + annotator = Annotator(im0, self.line_width) # init annotator - 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: + for region in self.json: # 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): - xc = int((box[0] + box[2]) / 2) - yc = int((box[1] + box[3]) / 2) - annotator.display_objects_labels( - im0, self.model.names[int(cls)], (104, 31, 17), (255, 255, 255), xc, yc, 10 - ) + for box, cls in zip(self.boxes, self.clss): + xc, yc = int((box[0] + box[2]) / 2), int((box[1] + box[3]) / 2) dist = cv2.pointPolygonTest(pts_array, (xc, yc), False) if dist >= 0: + # cv2.circle(im0, (xc, yc), radius=self.line_width * 4, color=self.dc, thickness=-1) + annotator.display_objects_labels( + im0, self.model.names[int(cls)], (104, 31, 17), (255, 255, 255), xc, yc, 10 + ) rg_occupied = True break - if rg_occupied: - fs += 1 - es -= 1 - + fs, es = (fs + 1, es - 1) if rg_occupied else (fs, es) # Plotting regions - color = self.occ if rg_occupied else self.arc - cv2.polylines(im0, [pts_array], isClosed=True, color=color, thickness=2) + cv2.polylines(im0, [pts_array], isClosed=True, color=self.occ if rg_occupied else self.arc, thickness=2) - self.pr_info["Occupancy"] = fs - self.pr_info["Available"] = es + self.pr_info["Occupancy"], self.pr_info["Available"] = fs, es 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): - """ - Display frame. - - Args: - im0 (ndarray): inference image - """ - if self.env_check: - cv2.imshow("Ultralytics Parking Manager", im0) - # Break Window - if cv2.waitKey(1) & 0xFF == ord("q"): - return + self.display_output(im0) # display output with base class function + return im0 # return output image for more usage