feat: add User Interface
This commit is contained in:
		
							parent
							
								
									6d83f0ed93
								
							
						
					
					
						commit
						f0636b85b7
					
				
							
								
								
									
										199
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										199
									
								
								main.py
									
									
									
									
									
								
							| @ -1,116 +1,123 @@ | ||||
| import math | ||||
| from dataclasses import dataclass | ||||
| import tkinter as tk | ||||
| from tkinter import ttk | ||||
| from process import process_frame | ||||
| from PIL import Image, ImageTk  # Required for displaying images | ||||
| import cv2  # OpenCV for numpy array to image conversion | ||||
| 
 | ||||
| import numpy as np | ||||
| import cv2 | ||||
| import tkinter | ||||
| import os | ||||
| import sys | ||||
| from pathlib import Path | ||||
| class OpenCVInterface: | ||||
|     def __init__(self, root): | ||||
|         self.root = root | ||||
|         self.root.title("Yiking") | ||||
| 
 | ||||
| dir_path = Path(".").absolute() | ||||
| TYPE_1 = "_________" | ||||
| TYPE_2 = "___   ___" | ||||
|         # Variables for sliders with min, max, and default values | ||||
|         self.variables_config = { | ||||
|             "minDist": (0, 500, 100), | ||||
|             "param1": (0, 500, 30), | ||||
|             "param2": (0, 400, 25), | ||||
|             "minRadius": (0, 100, 5), | ||||
|             "maxRadius": (0, 1000, 1000), | ||||
|             "color1_R": (0, 64, 5), | ||||
|             "color1_V": (0, 64, 5), | ||||
|             "color1_B": (0, 64, 5), | ||||
|             "color2_R": (0, 64, 5), | ||||
|             "color2_V": (0, 64, 5), | ||||
|             "color2_B": (0, 64, 5), | ||||
|         } | ||||
| 
 | ||||
|         self.variables = { | ||||
|             name: tk.IntVar(value=config[2]) for name, config in self.variables_config.items() | ||||
|         } | ||||
| 
 | ||||
| @dataclass | ||||
| class Object: | ||||
|     x: int | ||||
|     y: int | ||||
|     diametre: int | ||||
|         # GUI Layout | ||||
|         self.setup_gui() | ||||
| 
 | ||||
|     def setup_gui(self): | ||||
|         # Root grid layout | ||||
|         self.root.rowconfigure(1, weight=1) | ||||
|         self.root.columnconfigure(0, weight=1) | ||||
|         self.root.columnconfigure(1, weight=1) | ||||
| 
 | ||||
| # 1. Acquisition de l'image | ||||
| src = dir_path.joinpath('tests/images/balls-full-small.jpg') | ||||
| raw_image = cv2.imread(str(src)) | ||||
|         # Left Column: Sliders | ||||
|         left_frame = ttk.Frame(self.root) | ||||
|         left_frame.grid(row=0, column=0, sticky="nswe") | ||||
| 
 | ||||
| # 2. Boxing des objets via opencv | ||||
| gray = cv2.cvtColor(raw_image, cv2.COLOR_BGR2GRAY) | ||||
| blurred = cv2.medianBlur(gray, 25) | ||||
| minDist = 100 | ||||
| param1 = 30  # 500 | ||||
| param2 = 25  # 200 #smaller value-> more false circles | ||||
| minRadius = 5 | ||||
| maxRadius = 1000  # 10 | ||||
| circles = cv2.HoughCircles(blurred, cv2.HOUGH_GRADIENT, 1, minDist, param1=param1, param2=param2, minRadius=minRadius, | ||||
|                            maxRadius=maxRadius) | ||||
|         for var_name, var in self.variables.items(): | ||||
|             min_val, max_val, _ = self.variables_config[var_name] | ||||
|             self.create_slider(left_frame, var_name, var, min_val, max_val) | ||||
| 
 | ||||
| min_diameter = 9999 | ||||
| cochonnet = None | ||||
| boules = [] | ||||
| if circles is not None: | ||||
|     circles = np.uint16(np.around(circles)) | ||||
|     for i in circles[0, :]: | ||||
|         boule = Object(x=int(i[0]), y=int(i[1]), diametre=int(i[2])) | ||||
|         # cv2.circle(img, (boule.x, boule.y), boule.diametre, (0, 255, 0), 2) | ||||
|         # Right Column: Image Placeholder | ||||
|         self.image_canvas = tk.Canvas(self.root, bg="gray", width=1024, height=768) | ||||
|         self.image_canvas.grid(row=0, column=1, sticky="nswe") | ||||
| 
 | ||||
|         # 3. Détection de la box la plus petite : cochonnet | ||||
|         if boule.diametre < min_diameter: | ||||
|             min_diameter = boule.diametre | ||||
|             if cochonnet != None: | ||||
|                 boules.append(cochonnet) | ||||
|             cochonnet = boule | ||||
|         else: | ||||
|             boules.append(boule) | ||||
|         # Bottom Row: Run Button and Result | ||||
|         run_button = ttk.Button(self.root, text="Run", command=self.run_process) | ||||
|         run_button.grid(row=1, column=0, sticky="we") | ||||
| 
 | ||||
| img_check_shapes = raw_image.copy() | ||||
| for boule in boules: | ||||
|     cv2.circle(img_check_shapes, (boule.x, boule.y), boule.diametre, (0, 255, 0), 2) | ||||
| cv2.circle(img_check_shapes, (cochonnet.x, cochonnet.y), cochonnet.diametre, (255, 255, 0), -1) | ||||
| # cv2.imshow('img_check_shapes', img_check_shapes) | ||||
| # cv2.waitKey(0) | ||||
| # cv2.destroyAllWindows() | ||||
|         self.result_text = tk.Text(self.root, height=10, width=40) | ||||
|         self.result_text.grid(row=1, column=1, sticky="nswe") | ||||
| 
 | ||||
| # 4. Regroupement en liste de boules 1 ou 2 selon la couleur principale de chaque box restante | ||||
|     def create_slider(self, parent, name, variable, min_val, max_val): | ||||
|         frame = ttk.Frame(parent) | ||||
|         frame.pack(fill="x", padx=5, pady=2) | ||||
| 
 | ||||
| hsv = cv2.cvtColor(raw_image, cv2.COLOR_BGR2HSV) | ||||
| (h, s, v) = cv2.split(hsv) | ||||
| s = s * 2 | ||||
| s = np.clip(s, 0, 255) | ||||
| imghsv = cv2.merge([h, s, v]) | ||||
| # cv2.imshow('imghsv', imghsv) | ||||
| # cv2.waitKey(0) | ||||
| # cv2.destroyAllWindows() | ||||
|         # Label | ||||
|         label = ttk.Label(frame, text=name) | ||||
|         label.pack(side="left") | ||||
| 
 | ||||
| boules_couleurs = [] | ||||
| for boule in boules: | ||||
|     half_diametre = int(boule.diametre / 2) | ||||
|     crop = imghsv[ | ||||
|            boule.y - half_diametre:boule.y + half_diametre, | ||||
|            boule.x - half_diametre:boule.x + half_diametre, | ||||
|            ].copy() | ||||
|     pixels = np.float32(crop.reshape(-1, 3)) | ||||
|     n_colors = 2 | ||||
|     criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, .1) | ||||
|     _, labels, palette = cv2.kmeans(pixels, n_colors, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) | ||||
|     _, counts = np.unique(labels, return_counts=True) | ||||
|     (b, g, r) = palette[np.argmax(counts)] / 16 | ||||
|         def on_slide(value): | ||||
|             # Round value to nearest multiple of 5 | ||||
|             rounded_value = round(float(value) / 5) * 5 | ||||
|             variable.set(int(rounded_value))  # Update the variable with the rounded value | ||||
| 
 | ||||
|     # A modulariser | ||||
|     boules_couleurs.append(TYPE_1 if b > 4 else TYPE_2) | ||||
|         # Slider | ||||
|         slider = ttk.Scale( | ||||
|             frame, from_=min_val, to=max_val, | ||||
|             variable=variable, orient="horizontal", command=on_slide | ||||
|         ) | ||||
|         slider.pack(side="left", fill="x", expand=True, padx=5) | ||||
| 
 | ||||
|     # cv2.imshow('crop', crop) | ||||
|     # cv2.waitKey(0) | ||||
|         # Entry box | ||||
|         entry = ttk.Entry(frame, textvariable=variable, width=5) | ||||
|         entry.pack(side="left") | ||||
| 
 | ||||
| # 5. Calcul des distances entre chaque boule et le cochonnet selon le centre des boxs | ||||
| boules_distance = {} | ||||
| for i, boule in enumerate(boules): | ||||
|     dist = int(math.sqrt(math.pow(cochonnet.x - boule.x, 2) + math.pow(cochonnet.y - boule.y, 2))) | ||||
|     boules_distance[i] = dist | ||||
| boules_distance = dict(sorted(boules_distance.items(), key=lambda item: item[1])) | ||||
|     def run_process(self): | ||||
|         # Collect slider values | ||||
|         parameters = {key: var.get() for key, var in self.variables.items()} | ||||
| 
 | ||||
| # 6. Liste ordonnée des 6 distances les plus faibles | ||||
|         try: | ||||
|             # Call process function | ||||
|             image, result_text = process_frame(parameters) | ||||
| 
 | ||||
| boules_proches = [x for x in list(boules_distance)[0:6]] | ||||
|             # Convert OpenCV image (numpy array) to PIL Image for Tkinter display | ||||
|             if image is not None: | ||||
|                 image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # Convert BGR to RGB | ||||
|                 pil_image = Image.fromarray(image)  # Convert numpy array to PIL Image | ||||
| 
 | ||||
| # 7. Sortie des 6 couleurs en  --- ou - - | ||||
| img_final = raw_image.copy() | ||||
| for i in boules_proches: | ||||
|     boule = boules[i] | ||||
|     print(boules_couleurs[i]) | ||||
|     cv2.circle(img_final, (boule.x, boule.y), boule.diametre, (0, 255, 0), 2) | ||||
|                 # Rescale image to fit within 1024x768 while preserving aspect ratio | ||||
|                 max_width, max_height = 1024, 768 | ||||
|                 original_width, original_height = pil_image.size | ||||
|                 aspect_ratio = min(max_width / original_width, max_height / original_height) | ||||
|                 new_width = int(original_width * aspect_ratio) | ||||
|                 new_height = int(original_height * aspect_ratio) | ||||
|                 pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS) | ||||
| 
 | ||||
| # Show result for testing: | ||||
| cv2.imshow('img_final', img_final) | ||||
| cv2.waitKey(0) | ||||
| cv2.destroyAllWindows() | ||||
|                 tk_image = ImageTk.PhotoImage(pil_image)  # Convert PIL Image to Tkinter Image | ||||
| 
 | ||||
|                 # Clear canvas and display the image | ||||
|                 self.image_canvas.delete("all") | ||||
|                 self.image_canvas.create_image(512, 384, image=tk_image, anchor="center") | ||||
|                 self.image_canvas.image = tk_image  # Keep a reference to prevent garbage collection | ||||
| 
 | ||||
|             # Update result text | ||||
|             self.result_text.delete(1.0, tk.END) | ||||
|             self.result_text.insert(tk.END, result_text) | ||||
|         except Exception as exc: | ||||
|             # Handle and display exceptions | ||||
|             self.result_text.delete(1.0, tk.END) | ||||
|             self.result_text.insert(tk.END, str(exc)) | ||||
|             self.image_canvas.delete("all") | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     root = tk.Tk() | ||||
|     app = OpenCVInterface(root) | ||||
|     root.mainloop() | ||||
|  | ||||
							
								
								
									
										120
									
								
								process.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								process.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| from operator import itemgetter | ||||
| 
 | ||||
| from PIL import Image, ImageTk | ||||
| 
 | ||||
| import math | ||||
| from dataclasses import dataclass | ||||
| 
 | ||||
| import numpy as np | ||||
| import cv2 | ||||
| from pathlib import Path | ||||
| 
 | ||||
| dir_path = Path(".").absolute() | ||||
| TYPE_1 = "_________" | ||||
| TYPE_2 = "___   ___" | ||||
| 
 | ||||
| 
 | ||||
| @dataclass | ||||
| class Object: | ||||
|     x: int | ||||
|     y: int | ||||
|     rayon: int | ||||
| 
 | ||||
| 
 | ||||
| def process_frame(params): | ||||
|     """ | ||||
|     Simulates OpenCV processing using parameters from the GUI. | ||||
| 
 | ||||
|     Args: | ||||
|         params (dict): A dictionary of variable values passed from the GUI. | ||||
| 
 | ||||
|     Returns: | ||||
|         ImageTk.PhotoImage: A Tkinter-compatible image. | ||||
|         str: A result text description. | ||||
|     """ | ||||
|     # Simulate processing: for now, return a dummy image and text. | ||||
|     # width, height = 400, 300 | ||||
|     # image = Image.new("RGB", (width, height), | ||||
|     #                   color=(params["color1_R"] * 4, params["color1_V"] * 4, params["color1_B"] * 4)) | ||||
|     # image_tk = ImageTk.PhotoImage(image) | ||||
|     # | ||||
|     # result_text = f"Processed image with params: {params}" | ||||
|     # return image_tk, result_text | ||||
| 
 | ||||
|     (minDist, param1, param2, minRadius, maxRadius, | ||||
|      color1_R, color1_V, color1_B, color2_R, color2_V, color2_B) = itemgetter( | ||||
|         'minDist', 'param1', 'param2', 'minRadius', 'maxRadius', | ||||
|         'color1_R', 'color1_V', 'color1_B', 'color2_R', 'color2_V', 'color2_B' | ||||
|     )(params) | ||||
| 
 | ||||
|     # 1. Acquisition de l'image | ||||
|     src = dir_path.joinpath('tests/images/balls-full-small.jpg') | ||||
|     raw_image = cv2.imread(str(src)) | ||||
| 
 | ||||
|     # 2. Boxing des objets via opencv | ||||
|     gray = cv2.cvtColor(raw_image, cv2.COLOR_BGR2GRAY) | ||||
|     blurred = cv2.medianBlur(gray, 25) | ||||
| 
 | ||||
|     circles = cv2.HoughCircles(blurred, cv2.HOUGH_GRADIENT, 1, minDist, param1=param1, param2=param2, | ||||
|                                minRadius=minRadius, | ||||
|                                maxRadius=maxRadius) | ||||
| 
 | ||||
|     min_rayon = 9999 | ||||
|     cochonnet = None | ||||
|     boules = [] | ||||
|     if circles is not None: | ||||
|         circles = np.uint16(np.around(circles)) | ||||
|         for i in circles[0, :]: | ||||
|             boule = Object(x=int(i[0]), y=int(i[1]), rayon=int(i[2])) | ||||
| 
 | ||||
|             # 3. Détection de la box la plus petite : cochonnet | ||||
|             if boule.rayon < min_rayon: | ||||
|                 min_rayon = boule.rayon | ||||
|                 if cochonnet is not None: | ||||
|                     boules.append(cochonnet) | ||||
|                 cochonnet = boule | ||||
|             else: | ||||
|                 boules.append(boule) | ||||
| 
 | ||||
|     # 4. Regroupement en liste de boules 1 ou 2 selon la couleur principale de chaque box restante | ||||
|     hsv = cv2.cvtColor(raw_image, cv2.COLOR_BGR2HSV) | ||||
|     (h, s, v) = cv2.split(hsv) | ||||
|     s = s * 2 | ||||
|     s = np.clip(s, 0, 255) | ||||
|     imghsv = cv2.merge([h, s, v]) | ||||
|     boules_couleurs = [] | ||||
|     for boule in boules: | ||||
|         half_diametre = int(boule.rayon / 2) | ||||
|         crop = imghsv[ | ||||
|                boule.y - half_diametre:boule.y + half_diametre, | ||||
|                boule.x - half_diametre:boule.x + half_diametre, | ||||
|                ].copy() | ||||
|         pixels = np.float32(crop.reshape(-1, 3)) | ||||
|         n_colors = 2 | ||||
|         criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 200, .1) | ||||
|         _, labels, palette = cv2.kmeans(pixels, n_colors, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) | ||||
|         _, counts = np.unique(labels, return_counts=True) | ||||
|         (b, g, r) = palette[np.argmax(counts)] / 16 | ||||
| 
 | ||||
|         # A modulariser | ||||
|         boules_couleurs.append(TYPE_1 if b > 4 else TYPE_2) | ||||
| 
 | ||||
|     # 5. Calcul des distances entre chaque boule et le cochonnet selon le centre des boxs | ||||
|     boules_distance = {} | ||||
|     for i, boule in enumerate(boules): | ||||
|         dist = int(math.sqrt(math.pow(cochonnet.x - boule.x, 2) + math.pow(cochonnet.y - boule.y, 2))) | ||||
|         boules_distance[i] = dist | ||||
|     boules_distance = dict(sorted(boules_distance.items(), key=lambda item: item[1])) | ||||
| 
 | ||||
|     # 6. Liste ordonnée des 6 distances les plus faibles | ||||
|     boules_proches = [x for x in list(boules_distance)[0:6]] | ||||
| 
 | ||||
|     # 7. Sortie des 6 couleurs en  --- ou - - | ||||
|     return_text = "" | ||||
|     img_final = raw_image.copy() | ||||
|     for i in boules_proches: | ||||
|         boule = boules[i] | ||||
|         return_text += f"{boules_couleurs[i]}\n" | ||||
|         cv2.circle(img_final, (boule.x, boule.y), boule.rayon, (0, 255, 0), 2) | ||||
| 
 | ||||
|     return img_final, return_text | ||||
| @ -1,2 +1,3 @@ | ||||
| numpy~=2.1.3 | ||||
| opencv-python | ||||
| opencv-python | ||||
| pillow | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user