Untitled
unknown
plain_text
10 months ago
18 kB
19
Indexable
import cv2
import numpy as np
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import os
class HopperClassifierGUI:
def **init**(self, root):
self.root = root
self.root.title(“Hopper Shape Classifier”)
self.root.geometry(“1200x800”)
```
# Variables
self.image_path = None
self.original_image = None
self.processed_image = None
# Parameters
self.blur_kernel = tk.IntVar(value=5)
self.canny_low = tk.IntVar(value=50)
self.canny_high = tk.IntVar(value=150)
self.min_area = tk.IntVar(value=100)
self.circularity_threshold = tk.DoubleVar(value=0.6)
self.morphology_size = tk.IntVar(value=3)
self.contour_approximation = tk.DoubleVar(value=0.02)
self.setup_ui()
def setup_ui(self):
# Main frame
main_frame = ttk.Frame(self.root, padding="10")
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
# Configure grid weights
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=1)
main_frame.rowconfigure(1, weight=1)
# Controls frame
controls_frame = ttk.LabelFrame(main_frame, text="Controls", padding="10")
controls_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
# File selection
file_frame = ttk.Frame(controls_frame)
file_frame.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(0, 10))
ttk.Button(file_frame, text="Select Image", command=self.load_image).grid(row=0, column=0, padx=(0, 10))
ttk.Button(file_frame, text="Process Image", command=self.process_image).grid(row=0, column=1, padx=(0, 10))
ttk.Button(file_frame, text="Reset Parameters", command=self.reset_parameters).grid(row=0, column=2)
# Parameters frame
params_frame = ttk.LabelFrame(controls_frame, text="Detection Parameters", padding="10")
params_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=(10, 0))
# Parameter controls
row = 0
# Blur kernel
ttk.Label(params_frame, text="Blur Kernel Size:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=1, to=15, orient=tk.HORIZONTAL, variable=self.blur_kernel,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, textvariable=self.blur_kernel).grid(row=row, column=2)
row += 1
# Canny low threshold
ttk.Label(params_frame, text="Canny Low Threshold:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=10, to=200, orient=tk.HORIZONTAL, variable=self.canny_low,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, textvariable=self.canny_low).grid(row=row, column=2)
row += 1
# Canny high threshold
ttk.Label(params_frame, text="Canny High Threshold:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=50, to=300, orient=tk.HORIZONTAL, variable=self.canny_high,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, textvariable=self.canny_high).grid(row=row, column=2)
row += 1
# Minimum area
ttk.Label(params_frame, text="Minimum Area:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=10, to=1000, orient=tk.HORIZONTAL, variable=self.min_area,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, textvariable=self.min_area).grid(row=row, column=2)
row += 1
# Circularity threshold
ttk.Label(params_frame, text="Circularity Threshold:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=0.1, to=1.0, orient=tk.HORIZONTAL, variable=self.circularity_threshold,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, text=f"{self.circularity_threshold.get():.2f}").grid(row=row, column=2)
row += 1
# Morphology kernel size
ttk.Label(params_frame, text="Morphology Kernel:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=1, to=10, orient=tk.HORIZONTAL, variable=self.morphology_size,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, textvariable=self.morphology_size).grid(row=row, column=2)
row += 1
# Contour approximation
ttk.Label(params_frame, text="Contour Approximation:").grid(row=row, column=0, sticky=tk.W, padx=(0, 10))
ttk.Scale(params_frame, from_=0.01, to=0.1, orient=tk.HORIZONTAL, variable=self.contour_approximation,
command=self.on_parameter_change).grid(row=row, column=1, sticky=(tk.W, tk.E), padx=(0, 10))
ttk.Label(params_frame, text=f"{self.contour_approximation.get():.3f}").grid(row=row, column=2)
# Configure parameter frame columns
params_frame.columnconfigure(1, weight=1)
# Images frame
images_frame = ttk.Frame(main_frame)
images_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
images_frame.columnconfigure(0, weight=1)
images_frame.columnconfigure(1, weight=1)
images_frame.rowconfigure(0, weight=1)
# Original image
self.original_frame = ttk.LabelFrame(images_frame, text="Original Image", padding="5")
self.original_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
self.original_canvas = tk.Canvas(self.original_frame, bg="white", width=400, height=400)
self.original_canvas.pack(fill=tk.BOTH, expand=True)
# Processed image
self.processed_frame = ttk.LabelFrame(images_frame, text="Processed Image", padding="5")
self.processed_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
self.processed_canvas = tk.Canvas(self.processed_frame, bg="white", width=400, height=400)
self.processed_canvas.pack(fill=tk.BOTH, expand=True)
# Results frame
results_frame = ttk.LabelFrame(main_frame, text="Classification Results", padding="10")
results_frame.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(10, 0))
self.results_text = tk.Text(results_frame, height=6, wrap=tk.WORD)
self.results_text.pack(fill=tk.BOTH, expand=True)
# Scrollbar for results
results_scrollbar = ttk.Scrollbar(results_frame, orient=tk.VERTICAL, command=self.results_text.yview)
results_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.results_text.configure(yscrollcommand=results_scrollbar.set)
def load_image(self):
file_path = filedialog.askopenfilename(
title="Select Image",
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp *.tiff *.tif")]
)
if file_path:
self.image_path = file_path
self.original_image = cv2.imread(file_path)
if self.original_image is None:
messagebox.showerror("Error", "Could not load the image file.")
return
self.display_original_image()
self.results_text.delete(1.0, tk.END)
self.results_text.insert(tk.END, f"Image loaded: {os.path.basename(file_path)}\n")
self.results_text.insert(tk.END, f"Image size: {self.original_image.shape[1]}x{self.original_image.shape[0]}\n\n")
def display_original_image(self):
if self.original_image is None:
return
# Resize image to fit canvas
canvas_width = self.original_canvas.winfo_width()
canvas_height = self.original_canvas.winfo_height()
if canvas_width <= 1 or canvas_height <= 1:
canvas_width, canvas_height = 400, 400
img_rgb = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2RGB)
img_pil = Image.fromarray(img_rgb)
# Calculate scaling
scale = min(canvas_width / img_pil.width, canvas_height / img_pil.height)
new_width = int(img_pil.width * scale)
new_height = int(img_pil.height * scale)
img_resized = img_pil.resize((new_width, new_height), Image.Resampling.LANCZOS)
img_tk = ImageTk.PhotoImage(img_resized)
self.original_canvas.delete("all")
self.original_canvas.create_image(canvas_width//2, canvas_height//2, image=img_tk)
self.original_canvas.image = img_tk # Keep a reference
def display_processed_image(self, processed_img):
canvas_width = self.processed_canvas.winfo_width()
canvas_height = self.processed_canvas.winfo_height()
if canvas_width <= 1 or canvas_height <= 1:
canvas_width, canvas_height = 400, 400
if len(processed_img.shape) == 3:
img_rgb = cv2.cvtColor(processed_img, cv2.COLOR_BGR2RGB)
else:
img_rgb = cv2.cvtColor(processed_img, cv2.COLOR_GRAY2RGB)
img_pil = Image.fromarray(img_rgb)
# Calculate scaling
scale = min(canvas_width / img_pil.width, canvas_height / img_pil.height)
new_width = int(img_pil.width * scale)
new_height = int(img_pil.height * scale)
img_resized = img_pil.resize((new_width, new_height), Image.Resampling.LANCZOS)
img_tk = ImageTk.PhotoImage(img_resized)
self.processed_canvas.delete("all")
self.processed_canvas.create_image(canvas_width//2, canvas_height//2, image=img_tk)
self.processed_canvas.image = img_tk # Keep a reference
def process_image(self):
if self.original_image is None:
messagebox.showwarning("Warning", "Please load an image first.")
return
try:
result = self.classify_hopper_advanced(self.original_image)
self.update_results(result)
except Exception as e:
messagebox.showerror("Error", f"Processing failed: {str(e)}")
def classify_hopper_advanced(self, img):
"""Enhanced hopper classification with better preprocessing and analysis"""
# Convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Apply adaptive histogram equalization for better contrast
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
gray = clahe.apply(gray)
# Gaussian blur with adjustable kernel size
kernel_size = self.blur_kernel.get()
if kernel_size % 2 == 0: # Ensure odd kernel size
kernel_size += 1
blur = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
# Morphological operations to clean up noise
morph_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,
(self.morphology_size.get(), self.morphology_size.get()))
cleaned = cv2.morphologyEx(blur, cv2.MORPH_CLOSE, morph_kernel)
cleaned = cv2.morphologyEx(cleaned, cv2.MORPH_OPEN, morph_kernel)
# Edge detection with adjustable thresholds
edges = cv2.Canny(cleaned, self.canny_low.get(), self.canny_high.get())
# Additional morphology on edges to connect broken contours
edge_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, edge_kernel)
# Find contours
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# Create visualization image
vis_img = img.copy()
# Analyze shapes
results = {
'total_shapes': 0,
'circles': 0,
'crosses': 0,
'other': 0,
'shape_details': [],
'classification': '',
'confidence': 0.0
}
valid_contours = []
circularities = []
aspect_ratios = []
solidity_values = []
for i, cnt in enumerate(contours):
area = cv2.contourArea(cnt)
if area < self.min_area.get():
continue
# Calculate contour properties
perimeter = cv2.arcLength(cnt, True)
if perimeter == 0:
continue
# Approximate contour
epsilon = self.contour_approximation.get() * perimeter
approx = cv2.approxPolyDP(cnt, epsilon, True)
# Calculate circularity
circularity = 4 * np.pi * (area / (perimeter * perimeter))
# Calculate bounding rectangle
x, y, w, h = cv2.boundingRect(cnt)
aspect_ratio = w / h if h > 0 else 0
# Calculate solidity (area ratio to convex hull)
hull = cv2.convexHull(cnt)
hull_area = cv2.contourArea(hull)
solidity = area / hull_area if hull_area > 0 else 0
# Calculate extent (area ratio to bounding rectangle)
rect_area = w * h
extent = area / rect_area if rect_area > 0 else 0
valid_contours.append(cnt)
circularities.append(circularity)
aspect_ratios.append(aspect_ratio)
solidity_values.append(solidity)
# Classify individual shape
if circularity > self.circularity_threshold.get() and solidity > 0.8:
shape_type = "Circle (Male)"
color = (0, 255, 0) # Green for circles
results['circles'] += 1
elif len(approx) >= 8 and solidity < 0.7: # More complex shape with lower solidity
shape_type = "Cross (Sprue)"
color = (0, 0, 255) # Red for crosses
results['crosses'] += 1
else:
shape_type = "Other"
color = (255, 0, 0) # Blue for other
results['other'] += 1
# Draw contour and label
cv2.drawContours(vis_img, [cnt], -1, color, 2)
cv2.putText(vis_img, f"{i+1}", (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2)
# Store shape details
results['shape_details'].append({
'id': i+1,
'type': shape_type,
'area': area,
'circularity': circularity,
'aspect_ratio': aspect_ratio,
'solidity': solidity,
'extent': extent,
'vertices': len(approx)
})
results['total_shapes'] += 1
# Overall classification
if results['total_shapes'] == 0:
results['classification'] = "No valid shapes detected"
results['confidence'] = 0.0
elif results['circles'] > results['crosses']:
ratio = results['circles'] / results['total_shapes']
results['classification'] = f"Predominantly Males (circles) - {ratio:.1%}"
results['confidence'] = ratio
elif results['crosses'] > results['circles']:
ratio = results['crosses'] / results['total_shapes']
results['classification'] = f"Predominantly Sprues (crosses) - {ratio:.1%}"
results['confidence'] = ratio
else:
results['classification'] = "Mixed or uncertain classification"
results['confidence'] = 0.5
# Display processed image
self.display_processed_image(vis_img)
return results
def update_results(self, results):
"""Update the results text widget with classification results"""
self.results_text.delete(1.0, tk.END)
# Overall classification
self.results_text.insert(tk.END, f"=== CLASSIFICATION RESULTS ===\n")
self.results_text.insert(tk.END, f"Overall: {results['classification']}\n")
self.results_text.insert(tk.END, f"Confidence: {results['confidence']:.1%}\n\n")
# Shape counts
self.results_text.insert(tk.END, f"=== SHAPE SUMMARY ===\n")
self.results_text.insert(tk.END, f"Total shapes detected: {results['total_shapes']}\n")
self.results_text.insert(tk.END, f"Circles (Males): {results['circles']}\n")
self.results_text.insert(tk.END, f"Crosses (Sprues): {results['crosses']}\n")
self.results_text.insert(tk.END, f"Other shapes: {results['other']}\n\n")
# Detailed shape analysis
if results['shape_details']:
self.results_text.insert(tk.END, f"=== DETAILED ANALYSIS ===\n")
for shape in results['shape_details']:
self.results_text.insert(tk.END,
f"Shape {shape['id']}: {shape['type']}\n"
f" Area: {shape['area']:.0f} pixels\n"
f" Circularity: {shape['circularity']:.3f}\n"
f" Aspect Ratio: {shape['aspect_ratio']:.3f}\n"
f" Solidity: {shape['solidity']:.3f}\n"
f" Vertices: {shape['vertices']}\n\n"
)
def on_parameter_change(self, value=None):
"""Auto-process when parameters change if image is loaded"""
if self.original_image is not None:
# Update display labels for float values
for widget in self.root.winfo_children():
self.update_float_labels(widget)
self.process_image()
def update_float_labels(self, widget):
"""Recursively update labels for float variables"""
try:
for child in widget.winfo_children():
if isinstance(child, ttk.Label):
text = child.cget('text')
if '.' in text and text.replace('.', '').replace('-', '').isdigit():
# This might be a float label, update it
pass
self.update_float_labels(child)
except:
pass
def reset_parameters(self):
"""Reset all parameters to default values"""
self.blur_kernel.set(5)
self.canny_low.set(50)
self.canny_high.set(150)
self.min_area.set(100)
self.circularity_threshold.set(0.6)
self.morphology_size.set(3)
self.contour_approximation.set(0.02)
if self.original_image is not None:
self.process_image()
```
def main():
root = tk.Tk()
app = HopperClassifierGUI(root)
root.mainloop()
if **name** == “**main**”:
main()Editor is loading...
Leave a Comment