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
|
import tkinter as tk
|
||||||
from dataclasses import dataclass
|
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
|
class OpenCVInterface:
|
||||||
import cv2
|
def __init__(self, root):
|
||||||
import tkinter
|
self.root = root
|
||||||
import os
|
self.root.title("Yiking")
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
dir_path = Path(".").absolute()
|
# Variables for sliders with min, max, and default values
|
||||||
TYPE_1 = "_________"
|
self.variables_config = {
|
||||||
TYPE_2 = "___ ___"
|
"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
|
# GUI Layout
|
||||||
class Object:
|
self.setup_gui()
|
||||||
x: int
|
|
||||||
y: int
|
|
||||||
diametre: int
|
|
||||||
|
|
||||||
|
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
|
# Left Column: Sliders
|
||||||
src = dir_path.joinpath('tests/images/balls-full-small.jpg')
|
left_frame = ttk.Frame(self.root)
|
||||||
raw_image = cv2.imread(str(src))
|
left_frame.grid(row=0, column=0, sticky="nswe")
|
||||||
|
|
||||||
# 2. Boxing des objets via opencv
|
for var_name, var in self.variables.items():
|
||||||
gray = cv2.cvtColor(raw_image, cv2.COLOR_BGR2GRAY)
|
min_val, max_val, _ = self.variables_config[var_name]
|
||||||
blurred = cv2.medianBlur(gray, 25)
|
self.create_slider(left_frame, var_name, var, min_val, max_val)
|
||||||
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)
|
|
||||||
|
|
||||||
min_diameter = 9999
|
# Right Column: Image Placeholder
|
||||||
cochonnet = None
|
self.image_canvas = tk.Canvas(self.root, bg="gray", width=1024, height=768)
|
||||||
boules = []
|
self.image_canvas.grid(row=0, column=1, sticky="nswe")
|
||||||
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)
|
|
||||||
|
|
||||||
# 3. Détection de la box la plus petite : cochonnet
|
# Bottom Row: Run Button and Result
|
||||||
if boule.diametre < min_diameter:
|
run_button = ttk.Button(self.root, text="Run", command=self.run_process)
|
||||||
min_diameter = boule.diametre
|
run_button.grid(row=1, column=0, sticky="we")
|
||||||
if cochonnet != None:
|
|
||||||
boules.append(cochonnet)
|
|
||||||
cochonnet = boule
|
|
||||||
else:
|
|
||||||
boules.append(boule)
|
|
||||||
|
|
||||||
img_check_shapes = raw_image.copy()
|
self.result_text = tk.Text(self.root, height=10, width=40)
|
||||||
for boule in boules:
|
self.result_text.grid(row=1, column=1, sticky="nswe")
|
||||||
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()
|
|
||||||
|
|
||||||
# 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)
|
# Label
|
||||||
(h, s, v) = cv2.split(hsv)
|
label = ttk.Label(frame, text=name)
|
||||||
s = s * 2
|
label.pack(side="left")
|
||||||
s = np.clip(s, 0, 255)
|
|
||||||
imghsv = cv2.merge([h, s, v])
|
|
||||||
# cv2.imshow('imghsv', imghsv)
|
|
||||||
# cv2.waitKey(0)
|
|
||||||
# cv2.destroyAllWindows()
|
|
||||||
|
|
||||||
boules_couleurs = []
|
def on_slide(value):
|
||||||
for boule in boules:
|
# Round value to nearest multiple of 5
|
||||||
half_diametre = int(boule.diametre / 2)
|
rounded_value = round(float(value) / 5) * 5
|
||||||
crop = imghsv[
|
variable.set(int(rounded_value)) # Update the variable with the rounded value
|
||||||
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
|
# Slider
|
||||||
boules_couleurs.append(TYPE_1 if b > 4 else TYPE_2)
|
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)
|
# Entry box
|
||||||
# cv2.waitKey(0)
|
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
|
def run_process(self):
|
||||||
boules_distance = {}
|
# Collect slider values
|
||||||
for i, boule in enumerate(boules):
|
parameters = {key: var.get() for key, var in self.variables.items()}
|
||||||
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
|
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 - -
|
# Rescale image to fit within 1024x768 while preserving aspect ratio
|
||||||
img_final = raw_image.copy()
|
max_width, max_height = 1024, 768
|
||||||
for i in boules_proches:
|
original_width, original_height = pil_image.size
|
||||||
boule = boules[i]
|
aspect_ratio = min(max_width / original_width, max_height / original_height)
|
||||||
print(boules_couleurs[i])
|
new_width = int(original_width * aspect_ratio)
|
||||||
cv2.circle(img_final, (boule.x, boule.y), boule.diametre, (0, 255, 0), 2)
|
new_height = int(original_height * aspect_ratio)
|
||||||
|
pil_image = pil_image.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||||
|
|
||||||
# Show result for testing:
|
tk_image = ImageTk.PhotoImage(pil_image) # Convert PIL Image to Tkinter Image
|
||||||
cv2.imshow('img_final', img_final)
|
|
||||||
cv2.waitKey(0)
|
# Clear canvas and display the image
|
||||||
cv2.destroyAllWindows()
|
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
|
numpy~=2.1.3
|
||||||
opencv-python
|
opencv-python
|
||||||
|
pillow
|
Loading…
Reference in New Issue
Block a user