Untitled

 avatar
unknown
plain_text
10 months ago
18 kB
21
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