|
|
|
@ -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("<Button-1>", 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("<Button-1>", 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 |
|
|
|
|