# =================================== # ๐ PART 1: Silent Package Install and Library Imports # ================================== import subprocess, sys # Used for running pip commands and accessing the Python interpreter # Function to install all required Python packages silently def install_packages_p1(): # List of packages required for segmentation, image processing, UI, and Supabase DB access packages_p1 = [ "segmentation_models_pytorch", # Pre-trained segmentation architectures using PyTorch "opencv-python-headless", # OpenCV without GUI (headless, suitable for servers) "matplotlib", # For plotting graphs or visualizations (debugging, dev) "numpy", # Numerical array computations "torch", # PyTorch for deep learning model operations "torchvision", # Common datasets/models/transforms for vision "albumentations", # Fast image augmentations library "gradio", # Web UI library for model demos/apps "supabase" # Supabase Python client for auth/database ] # Run pip install silently (no stdout/stderr) for the given packages subprocess.run( [sys.executable, "-m", "pip", "install", *packages_p1], # Construct pip install command stdout=subprocess.DEVNULL, # Suppress standard output stderr=subprocess.DEVNULL # Suppress error output ) # Call the package installation function install_packages_p1() # Print success message after installing packages (visible to the user) print("โ Downloaded all necessary libraries successfully (silent mode)") # ================================ # ๐ฆ Import Required Python Libraries # ================================ # Standard and third-party libraries used across the app import os # File and directory operations import cv2 # OpenCV for image processing import numpy as np # Numerical computations and array handling import torch # PyTorch for ML models import urllib.request # To download remote resources/files if needed import albumentations as A # Image augmentations from albumentations.pytorch import ToTensorV2 # Converts numpy arrays to PyTorch tensors import segmentation_models_pytorch as smp # Deep segmentation architectures from matplotlib import pyplot as plt # Optional: for visual debugging or plotting from PIL import Image as PILImage_p3 # Pillow for image manipulation (renamed for consistency across parts) import gradio as gr # UI frontend for serving interactive demos from torch import tensor # Explicit import of `tensor` class (optional but handy) # ================================ # ๐ง PART 2: Download and Load All Segmentation Models # ================================ # ๐ Define GitHub URLs for models (fetched from environment variables) import os model_urls_p2 = { "toilet_holes": os.getenv("TOILET_HOLES_URL"), # URL for the toilet holes segmentation model "toilet_rim": os.getenv("TOILET_RIM_URL"), # URL for the toilet rim segmentation model "coin_5": os.getenv("COIN_URL") # URL for the 5-rupee coin segmentation model } # ๐ Define filenames to save the models locally after download model_paths_p2 = { "toilet_holes": "toilet_holes_segmentation_model.pth", # Local filename for toilet holes model "toilet_rim": "toilet_rim_segmentation_model.pth", # Local filename for toilet rim model "coin_5": "5coin_segmentation_model.pth" # Local filename for coin model } # ๐ Function to download model file only if it doesn't already exist locally def download_model_if_needed_p2(url, filename): if not os.path.exists(filename): # Check if model file is already cached print(f"โฌ๏ธ Downloading {filename}...") # Inform user urllib.request.urlretrieve(url, filename) # Download the file from the URL print(f"โ Downloaded: {filename}") # Confirm success else: print(f"๐ข Found cached model: {filename}") # Use cached file to save bandwidth/time # ๐ Function to download (if needed) and load all three segmentation models def load_models_p2(): # Step 1: Ensure all required models are downloaded for key in model_paths_p2: download_model_if_needed_p2(model_urls_p2[key], model_paths_p2[key]) # Step 2: Choose device (GPU if available, otherwise CPU) device = 'cuda' if torch.cuda.is_available() else 'cpu' # Step 3: Load Toilet Holes Model model_holes = smp.Unet("resnet18", encoder_weights="imagenet", in_channels=3, classes=1) model_holes.load_state_dict(torch.load(model_paths_p2["toilet_holes"], map_location=device)) # Load weights model_holes.to(device).eval() # Move to device and set to evaluation mode print("โ Loaded: Toilet Holes Identification") # Confirm success # Step 4: Load Toilet Rim Model model_rim = smp.Unet("resnet18", encoder_weights="imagenet", in_channels=3, classes=1) model_rim.load_state_dict(torch.load(model_paths_p2["toilet_rim"], map_location=device)) # Load weights model_rim.to(device).eval() # Move to device and set to evaluation mode print("โ Loaded: Toilet Rim Identification") # Confirm success # Step 5: Load 5-Rupee Coin Reference Model model_coinref = smp.Unet("resnet18", encoder_weights="imagenet", in_channels=3, classes=1) model_coinref.load_state_dict(torch.load(model_paths_p2["coin_5"], map_location=device)) # Load weights model_coinref.to(device).eval() # Move to device and set to evaluation mode print("โ Loaded: 5 Coin Identification") # Confirm success # Step 6: Return all models and device as a dictionary return { "device": device, "model_holes": model_holes, "model_rim": model_rim, "model_coinref": model_coinref } # ================================ # ๐ Load models once when the script starts # ================================ print("Loading models...") # Notify start of loading global_models_and_device = load_models_p2() # Load models and store in global variable print("Models loaded successfully.") # Notify completion # ================================ # ๐ฆ Extract models and device for easy global access # ================================ GLOBAL_DEVICE = global_models_and_device["device"] # CUDA or CPU GLOBAL_HOLES = global_models_and_device["model_holes"] # Toilet holes model GLOBAL_RIM = global_models_and_device["model_rim"] # Toilet rim model GLOBAL_COIN = global_models_and_device["model_coinref"] # Coin model models_dict = global_models_and_device # Dictionary holding all components # ================================ # ๐ธ PART 3: Start new session for each user # ================================ # โ Import required modules from PIL import Image as PILImage # PIL for image handling (renamed as PILImage to avoid naming conflict) import numpy as np # For numerical operations (may be used in image manipulation later) import gradio as gr # Gradio for building the web UI import os # For file/folder path handling import uuid # To generate unique session identifiers from datetime import datetime # (Optional) could be used for timestamped folders or logs # โ Base directory where all user sessions will be stored BASE_DIR = "user_uploads" # All user sessions will go under this root folder os.makedirs(BASE_DIR, exist_ok=True) # Create the base folder if it doesn't already exist # โ Function to initialize a new session folder with a unique ID def init_session(): session_id = str(uuid.uuid4())[:8] # Generate a short unique session ID using UUID (8 characters) session_path = os.path.join(BASE_DIR, session_id) # Path for this session's folder os.makedirs(session_path, exist_ok=True) # Create a directory for the session return session_id # Return the session ID (used to reference the folder in other parts) # ================================ # ๐ฏ PART 4: Segmentation & Overlay (Multi-user Safe) # ================================ # โ Import necessary libraries from torchvision import transforms # For image preprocessing from PIL import Image as PILImage_p4 # PIL for handling images (renamed to avoid conflict with other parts) from io import BytesIO # To store matplotlib plots as in-memory images # โ Define transformation for input images (to Tensor and Normalize) transform_p4 = transforms.Compose([ transforms.ToTensor(), # Convert image to tensor format (C x H x W) transforms.Normalize([0.5]*3, [0.5]*3) # Normalize RGB channels to range [-1, 1] ]) # โ Function to predict mask for a given PIL image using a PyTorch model def predict_mask_p4(model, device, image_pil): model.eval() if image_pil is None: raise ValueError("โ Image missing!") # --- Normalize input --- if isinstance(image_pil, str) and os.path.exists(image_pil): image_pil = PILImage.open(image_pil) elif hasattr(image_pil, "read"): # file-like object image_pil.seek(0) image_pil = PILImage.open(image_pil) elif isinstance(image_pil, PILImage.Image): image_pil = image_pil elif isinstance(image_pil, np.ndarray): if image_pil.ndim == 2: # grayscale to RGB image_pil = np.stack([image_pil]*3, axis=-1) elif image_pil.ndim == 3 and image_pil.shape[2] == 4: # RGBA image_pil = image_pil[:, :, :3] image_pil = PILImage.fromarray(image_pil.astype(np.uint8)) else: raise ValueError("โ Unsupported image input type.")# Input validation # --- Ensure RGB --- if image_pil.mode != 'RGB': image_pil = image_pil.convert('RGB') image_np = np.array(image_pil) # Convert PIL image to NumPy array if len(image_np.shape) != 3 or image_np.shape[2] != 3: raise ValueError("โ Must be RGB image") # Ensure it's a 3-channel RGB image original_size = (image_np.shape[1], image_np.shape[0]) # Store original size for resizing back later resized = cv2.resize(image_np, (256, 256), interpolation=cv2.INTER_LINEAR) # Resize to model input size tensor = transform_p4(resized).unsqueeze(0).to(device.value) # Apply transform and add batch dimension with torch.no_grad(): # Disable gradient computation for inference out = model(tensor) # Forward pass pred = torch.sigmoid(out).squeeze().cpu().numpy() # Apply sigmoid + remove batch/channel dims mask = (pred > 0.5).astype(np.uint8) # Threshold to binary mask return cv2.resize(mask, original_size, interpolation=cv2.INTER_NEAREST) # Resize mask back to original size # โ Function to apply a color overlay to the masked region of the input image def create_overlay_p4(image_pil, mask, color=(255, 0, 0)): image_np = np.array(image_pil).copy() # Convert image to NumPy array overlay = image_np.copy() # Duplicate for overlay overlay[mask == 1] = color # Color only where mask is 1 return overlay # Return overlaid image # โ Function to segment and overlay results on all 3 views: open_noseat, open_seat, closed def segment_and_overlay_all_p4(img1, img2, img3, model_holes_p2, model_rim_p2, model_coinref_p2, device_p2): # Get models and device from input state model_coin = model_coinref_p2 # Coin segmentation model model_holes = model_holes_p2 # Toilet holes segmentation model model_rim = model_rim_p2 # Toilet rim segmentation model device = device_p2 # Inference device (CPU or GPU) # Organize images by view name image_dict = { 'open_noseat': img1, # View 1 'open_seat': img2, # View 2 'closed': img3 # View 3 } # Initialize mask containers for each class and view binary_masks = {'ref': {}, 'holes': {}, 'rim': {}} grid = [] # To hold overlays for all images in grid format # Loop through all three input views for key in ['open_noseat', 'open_seat', 'closed']: img = image_dict[key] # Get image for current view # Predict each of the 3 masks using respective models mask_ref = predict_mask_p4(model_coin, device, img) # Reference object (coin) mask_holes = predict_mask_p4(model_holes, device, img) # Holes mask_rim = predict_mask_p4(model_rim, device, img) # Rim # Store masks in dictionary by type and view binary_masks['ref'][key] = mask_ref binary_masks['holes'][key] = mask_holes binary_masks['rim'][key] = mask_rim # Create overlays for each mask on top of original image overlay_ref = create_overlay_p4(img, mask_ref, (0, 255, 0)) # Green for coin overlay_holes = create_overlay_p4(img, mask_holes, (255, 0, 0)) # Red for holes overlay_rim = create_overlay_p4(img, mask_rim, (0, 0, 255)) # Blue for rim # Append original + overlays to grid grid.append([np.array(img), overlay_ref, overlay_holes, overlay_rim]) # โ Create a 3x4 visualization grid using matplotlib fig, axes = plt.subplots(3, 4, figsize=(18, 12)) # 3 rows (views) x 4 columns (original + 3 overlays) titles = ["Original", "Ref Coin", "Holes", "Rim"] # Column titles rows = ["No Seat", "With Seat", "Closed"] # Row titles for i in range(3): # Rows for j in range(4): # Columns axes[i][j].imshow(grid[i][j]) # Show image axes[i][j].axis('off') # Hide axis if i == 0: axes[i][j].set_title(titles[j]) # Set column title axes[i][0].text(-50, 128, rows[i], rotation=90, va='center') # Label row on left side plt.tight_layout() # Prevent overlaps buf = BytesIO() # Create in-memory buffer fig.savefig(buf, format='png') # Save figure to buffer as PNG plt.close(fig) # Close the plot to free memory buf.seek(0) # Move to start of buffer overlay_grid_image = PILImage_p4.open(buf) # Open saved plot as PIL image return overlay_grid_image, binary_masks, image_dict # Return image grid, masks, and original image dict # ================================ # ๐ฐ PART 5: โน5 Coin Detection & px/cm Ratio (Multi-user Safe) # ================================ from PIL import Image as PILImage_p5 # For image handling from io import BytesIO # For in-memory buffer image storage import matplotlib.pyplot as plt # For plotting the result overlays import numpy as np # For numerical operations on images import cv2 # OpenCV for image processing import gradio as gr # Gradio for UI elements # Small buffer to reduce fitted ellipse size (to correct overestimation of edge) reduce_radius_px_p5 = 0 # Actual diameter of โน5 coin in centimeters real_diameter_cm_p5 = 2.3 # Function to detect โน5 coin in each image, estimate its diameter in pixels, # and compute pixel-per-cm ratio for accurate real-world measurements def detect_and_plot_reference_p5(image_dict, mask_dict): # Dictionary to store calculated px/cm ratios per image ref_ratios = {} # Set up a 1-row, 3-column matplotlib plot to show results for each image fig, axes = plt.subplots(1, 3, figsize=(15, 5)) # Process all three images: open without seat, open with seat, closed for i, key in enumerate(['open_noseat', 'open_seat', 'closed']): # Convert PIL image to NumPy array image = np.array(image_dict[key]).copy() # Retrieve the binary mask for the โน5 coin mask = mask_dict['ref'][key] # Convert binary mask to 8-bit format for contour finding mask_u8 = (mask * 255).astype(np.uint8) # Find contours in the mask (external only) contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # If any contour is detected if contours: # Choose the largest contour by area (assumed to be the โน5 coin) largest = max(contours, key=cv2.contourArea) # Only fit ellipse if the contour has enough points (at least 5) if len(largest) >= 5: ellipse = cv2.fitEllipse(largest) (cx, cy), (major_axis, minor_axis), angle = ellipse # Reduce both axes slightly to avoid overestimation major_axis = max(major_axis - 2 * reduce_radius_px_p5, 1) minor_axis = max(minor_axis - 2 * reduce_radius_px_p5, 1) # Average the two axes to estimate the diameter avg_diameter_px = (major_axis + minor_axis) / 2 # Calculate pixel per centimeter ratio px_per_cm = avg_diameter_px / real_diameter_cm_p5 # Save the computed ratio ref_ratios[key] = px_per_cm else: # If ellipse fitting isn't possible, mark as None ref_ratios[key] = None # Draw a green circle around the coin using min enclosing circle (x, y), radius = cv2.minEnclosingCircle(largest) radius = max(radius - reduce_radius_px_p5, 0) cv2.circle(image, (int(x) - 1, int(y) - 2), int(radius), (0, 255, 0), 2) else: # If no contour found, set ratio as None ref_ratios[key] = None # Show result image in the subplot axes[i].imshow(image) axes[i].axis('off') # Set title with calculated px/cm ratio or indicate failure axes[i].set_title(f"{key}\n{ref_ratios[key]:.2f} px/cm" if ref_ratios[key] else f"{key}\nNot Detected") # Store the matplotlib figure into a PNG image buffer buf = BytesIO() plt.tight_layout() fig.savefig(buf, format='png') plt.close(fig) # Close plot to free memory buf.seek(0) # Convert buffer image to PIL Image for display in Gradio result_img = PILImage_p5.open(buf) # Construct a reference text summary for display in textbox ref_str = ( f"Open (No Seat):\n{ref_ratios['open_noseat']:4.2f}px/cm\n" f"\nOpen (With Seat):\n{ref_ratios['open_seat']:4.2f}px/cm\n" f"\nClosed:\n{ref_ratios['closed']:4.2f}px/cm" ) # Return: # - Updated image with overlays, # - px/cm ratios for each image, # - Text summary of ratios, # - Indicator to show text output component return gr.update(value=result_img, visible=True), ref_ratios, gr.update(value=ref_str, visible=True), gr.update(visible=True) # ================================ # ๐ PART 6: Rim Measurement + Visualization (Multi-user Safe) # ================================ from PIL import Image as PILImage_p6 # For working with final PIL image output from io import BytesIO # To handle in-memory image buffer import math # For trigonometric calculations import numpy as np # For numerical operations and arrays import cv2 # OpenCV for image processing import matplotlib.pyplot as plt # For plotting annotated visuals import gradio as gr # Gradio for UI components # Function to analyze inner and outer rim from the mask using ellipse and directional probing def analyze_rim_intersections_p6(image_dict, mask_dict, ref_ratios): image_key = 'open_seat' # Only operate on open seat image mask = mask_dict['rim'][image_key] # Get rim mask for selected image mask_u8 = (mask * 255).astype(np.uint8) # Convert binary mask to 8-bit for OpenCV # Find all contours in the rim mask contours, _ = cv2.findContours(mask_u8, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if len(contours) < 2: raise ValueError("โ Need both inner and outer contours.") # Need both rim edges # Sort by area: outer will be largest, inner next outer, inner = sorted(contours, key=cv2.contourArea, reverse=True)[:2] # Ensure inner contour has enough points to fit an ellipse if len(inner) < 5: raise ValueError("โ Inner contour too small for ellipse fitting.") ellipse = cv2.fitEllipse(inner) (xc, yc), (MA, ma), angle = ellipse # Center, axes, and angle from ellipse fit # Swap angle to be vertical major axis if needed if MA < ma: angle += 90 # Convert angle to radians and compute unit direction vector pointing 'down' angle_rad = np.deg2rad(angle) dir_down = np.array([math.cos(angle_rad), math.sin(angle_rad)]) if dir_down[1] < 0: # Ensure it points downward in image dir_down *= -1 # Get orthogonal 'right' direction by rotating 90 degrees clockwise dir_right = np.array([-dir_down[1], dir_down[0]]) if dir_right[0] < 0: # Ensure it points rightward dir_right *= -1 # Compute bounding points and center from inner contour topmost = tuple(inner[inner[:, :, 1].argmin()][0]) bottommost = tuple(inner[inner[:, :, 1].argmax()][0]) center_y = (topmost[1] + bottommost[1]) // 2 leftmost = tuple(inner[inner[:, :, 0].argmin()][0]) rightmost = tuple(inner[inner[:, :, 0].argmax()][0]) center_x = (leftmost[0] + rightmost[0]) // 2 center = np.array([center_x, center_y]) # Final center point of rim # Function to find intersection of a ray in a direction with the mask def get_intersections(mask, center, direction, max_steps=7000): prev = mask[int(center[1]), int(center[0])] outer_pt, inner_pt = None, None last_valid = None for step in range(1, max_steps): pt = center + step * direction x, y = int(round(pt[0])), int(round(pt[1])) if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]): break # Stop if point goes out of bounds val = mask[y, x] last_valid = np.array([x, y]) # Detect transition from background to mask (outer edge) if outer_pt is None and val == 1 and prev == 0: outer_pt = np.array([x, y]) # Detect transition from mask to background (inner edge) elif outer_pt is not None and val == 0 and prev == 1: inner_pt = np.array([x, y]) break prev = val if inner_pt is None: # If no edge found, use last seen point inner_pt = last_valid return outer_pt, inner_pt # Probe down and right directions from center to find outer and inner rim points inner_down, outer_down = get_intersections(mask, center, dir_down) inner_right, outer_right = get_intersections(mask, center, dir_right) # Helper function to calculate Euclidean distance def dist(a, b): return np.linalg.norm(a - b) if a is not None and b is not None else None # Compute distances from center to points (in pixels) d_down_outer_px = dist(center, outer_down) d_down_inner_px = dist(center, inner_down) d_right_inner_px = dist(center, inner_right) d_right_outer_px = dist(center, outer_right) # Rim width = outer - inner in both directions rim_width_down = d_down_outer_px - d_down_inner_px rim_width_right = d_right_outer_px - d_right_inner_px # Get pixel/cm ratio for conversion px_per_cm = ref_ratios[image_key] # Conversion lambdas from pixels to cm/inch to_cm = lambda px: px / px_per_cm if px is not None else None to_in = lambda px: px / px_per_cm / 2.54 if px is not None else None # Formatter for readable output fmt = lambda val: f"{val:.2f}" if val else "N/A" # Dictionary of dimensions in cm rim_cm = { "down_inner": to_cm(d_down_inner_px), "down_outer": to_cm(d_down_outer_px), "right_inner": to_cm(d_right_inner_px), "right_outer": to_cm(d_right_outer_px), "width_down": to_cm(rim_width_down), "width_right": to_cm(rim_width_right) } # Dictionary of dimensions in inches rim_inch = { "down_inner": to_in(d_down_inner_px), "down_outer": to_in(d_down_outer_px), "right_inner": to_in(d_right_inner_px), "right_outer": to_in(d_right_outer_px), "width_down": to_in(rim_width_down), "width_right": to_in(rim_width_right) } # Text labels for Gradio UI rim_ui = { "Down Inner": f"{fmt(rim_cm['down_inner'])} cm | {fmt(rim_inch['down_inner'])} in", "Down Outer": f"{fmt(rim_cm['down_outer'])} cm | {fmt(rim_inch['down_outer'])} in", "Right Inner": f"{fmt(rim_cm['right_inner'])} cm | {fmt(rim_inch['right_inner'])} in", "Right Outer": f"{fmt(rim_cm['right_outer'])} cm | {fmt(rim_inch['right_outer'])} in", } # Load the image to draw results on pil_img = image_dict[image_key] if pil_img is None: raise ValueError("โ No image found for analysis!") image_vis = np.array(pil_img) # Draw center point cv2.circle(image_vis, center, 4, (255, 255, 0), 5) # Draw measurement lines and dots for pt, color in zip( [outer_down, inner_down, outer_right, inner_right], [(255, 0, 0), (0, 255, 0), (255, 0, 255), (255, 255, 0)] ): if pt is not None: cv2.line(image_vis, center, pt, color, 2) cv2.circle(image_vis, pt, 4, color, 5) # Draw directional arrows for orientation cv2.arrowedLine(image_vis, center, (center + dir_down * 100).astype(int), (0, 255, 0), 2) cv2.arrowedLine(image_vis, center, (center + dir_right * 100).astype(int), (0, 0, 255), 2) # Helper to draw label near point def draw_label(pt, label, offset): if pt is not None: pos = pt + offset plt.text(pos[0], pos[1], label, fontsize=9, color='white', ha='center', va='center', bbox=dict(facecolor='black', alpha=0.6, boxstyle='round,pad=0.3')) # Visualize final annotated image plt.figure(figsize=(6, 6)) plt.imshow(image_vis) offset = np.array([0, 70]) draw_label(inner_down, rim_ui["Down Inner"], -offset) draw_label(outer_down, rim_ui["Down Outer"], -offset) draw_label(inner_right, rim_ui["Right Inner"], -offset) draw_label(outer_right, rim_ui["Right Outer"], offset) plt.axis("off") # Convert plot to PIL image for Gradio buf = BytesIO() plt.savefig(buf, format='png') plt.close() buf.seek(0) vis_img = PILImage_p6.open(buf) # Build textual summary of all measurements seat_str = ( f"Down Inner Diameter:\n{d_down_inner_px*2:4.1f}px | {rim_cm['down_inner']*2:4.2f}cm | {rim_inch['down_inner']*2:4.2f}in\n" f"\nRight Inner Diameter:\n{d_right_inner_px*2:4.1f}px | {rim_cm['right_inner']*2:4.2f}cm | {rim_inch['right_inner']*2:4.2f}in\n" f"\nRight Outer Diameter:\n{d_right_outer_px*2:4.1f}px | {rim_cm['right_outer']*2:4.2f}cm | {rim_inch['right_outer']*2:4.2f}in\n" f"\nDown Angle:\n{(angle - 90) % 360:4.1f}ยฐ\n" f"\nRight Angle:\n{angle:4.1f}ยฐ\n" f"\nWidth Down:\n{rim_width_down:4.1f}px | {rim_cm['width_down']:4.2f}cm | {rim_inch['width_down']:4.2f}in\n" f"\nWidth Right:\n{rim_width_right:4.1f}px | {rim_cm['width_right']:4.2f}cm | {rim_inch['width_right']:4.2f}in" ) # Return: # - Annotated image update # - Rim values for UI # - Rim values in cm # - Rim values in inches # - Full summary text # - Show result components return gr.update(value=vis_img, visible=True), rim_ui, rim_cm, rim_inch, gr.update(value=seat_str, visible=True), gr.update(visible=True) # ================================ # ๐ PART 7: Ellipse-Based Rim Width Measurement from Image (open_noseat) # ================================ from PIL import Image as PILImage_p7 # PIL for image manipulation from io import BytesIO # To handle image bytes buffer import math # For trigonometric functions import numpy as np # For array and math operations import cv2 # OpenCV for contour/ellipse analysis import matplotlib.pyplot as plt # For image plotting and annotation # function to analyze rim angles and calculate dimensions (down and right diameters) def analyze_rim_intersections_p7(image_dict, mask_dict, ref_ratios): image_key = 'open_noseat' # Key used to fetch image/mask mask = mask_dict['rim'][image_key] # Get binary mask for rim mask_u8 = (mask * 255).astype(np.uint8) # Convert mask to 8-bit image for OpenCV # Find contours in the mask contours, _ = cv2.findContours(mask_u8, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # Ensure we have both inner and outer contours if len(contours) < 2: raise ValueError("โ Need both inner and outer contours.") # Sort contours by area and select the two largest (outer and inner) outer, inner = sorted(contours, key=cv2.contourArea, reverse=True)[:2] # Ensure inner contour is large enough for ellipse fitting if len(inner) < 5: raise ValueError("โ Outer contour too small for ellipse fitting.") # Fit an ellipse to the inner contour ellipse = cv2.fitEllipse(inner) (xc, yc), (MA, ma), angle = ellipse # center, major axis, minor axis, and rotation angle # Ensure angle reflects long axis vertically if MA < ma: angle += 90 # Convert angle to radians and compute direction vectors angle_rad = np.deg2rad(angle) dir_down = np.array([math.cos(angle_rad), math.sin(angle_rad)]) if dir_down[1] < 0: dir_down *= -1 # Flip to ensure it points down # Right direction is perpendicular to down direction dir_right = np.array([-dir_down[1], dir_down[0]]) if dir_right[0] < 0: dir_right *= -1 # Flip to ensure rightward direction # Compute vertical center between top and bottom points topmost = tuple(inner[inner[:, :, 1].argmin()][0]) bottommost = tuple(inner[inner[:, :, 1].argmax()][0]) center_y = (topmost[1] + bottommost[1]) // 2 # Compute horizontal center between left and right points leftmost = tuple(inner[inner[:, :, 0].argmin()][0]) rightmost = tuple(inner[inner[:, :, 0].argmax()][0]) center_x = (leftmost[0] + rightmost[0]) // 2 center = np.array([center_x, center_y]) # Geometric center # function to get intersections of the line with the mask to get inner and outer points def get_intersections(mask, center, direction, max_steps=7000): prev = mask[int(center[1]), int(center[0])] # Start from center outer_pt, inner_pt = None, None last_valid = None for step in range(1, max_steps): pt = center + step * direction # Move along the direction x, y = int(round(pt[0])), int(round(pt[1])) # Stop if point is outside image bounds if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]): break val = mask[y, x] last_valid = np.array([x, y]) # Detect outer boundary if outer_pt is None and val == 1 and prev == 0: outer_pt = np.array([x, y]) # Detect inner boundary after crossing outer elif outer_pt is not None and val == 0 and prev == 1: inner_pt = np.array([x, y]) break prev = val # If no inner found, use last valid point if inner_pt is None: inner_pt = last_valid return outer_pt, inner_pt # Get inner/outer points in both down and right directions inner_down, outer_down = get_intersections(mask, center, dir_down) inner_right, outer_right = get_intersections(mask, center, dir_right) # function to calculate distance between two points def dist(a, b): return np.linalg.norm(a - b) if a is not None and b is not None else None # Compute distances from center to inner and outer rim boundaries d_down_outer_px = dist(center, outer_down) d_down_inner_px = dist(center, inner_down) d_right_inner_px = dist(center, inner_right) d_right_outer_px = dist(center, outer_right) # Rim thickness = outer - inner distances rim_width_down = d_down_outer_px - d_down_inner_px rim_width_right = d_right_outer_px - d_right_inner_px # Conversion factors: pixels per cm px_per_cm = ref_ratios[image_key] to_cm = lambda px: px / px_per_cm if px is not None else None to_in = lambda px: px / px_per_cm / 2.54 if px is not None else None fmt = lambda val: f"{val:.2f}" if val else "N/A" # formatted output # Measurements in cm rim_cm = { "down_inner": to_cm(d_down_inner_px), "down_outer": to_cm(d_down_outer_px), "right_inner": to_cm(d_right_inner_px), "right_outer": to_cm(d_right_outer_px), "width_down": to_cm(rim_width_down), "width_right": to_cm(rim_width_right) } # Measurements in inches rim_inch = { "down_inner": to_in(d_down_inner_px), "down_outer": to_in(d_down_outer_px), "right_inner": to_in(d_right_inner_px), "right_outer": to_in(d_right_outer_px), "width_down": to_in(rim_width_down), "width_right": to_in(rim_width_right) } # String for user interface display rim_ui = { "Down Inner": f"{fmt(rim_cm['down_inner'])} cm | {fmt(rim_inch['down_inner'])} in", "Down Outer": f"{fmt(rim_cm['down_outer'])} cm | {fmt(rim_inch['down_outer'])} in", "Right Inner": f"{fmt(rim_cm['right_inner'])} cm | {fmt(rim_inch['right_inner'])} in", "Right Outer": f"{fmt(rim_cm['right_outer'])} cm | {fmt(rim_inch['right_outer'])} in", } # Get original image pil_img = image_dict[image_key] if pil_img is None: raise ValueError("โ No image found for analysis!") image_vis = np.array(pil_img) # Draw central point cv2.circle(image_vis, center.astype(int), 4, (255, 255, 0), 5) # Draw lines and circles for each measurement point for pt, color in zip( [outer_down, inner_down, outer_right, inner_right], [(255, 0, 0), (0, 255, 0), (255, 0, 255), (255, 255, 0)] ): if pt is not None: cv2.line(image_vis, center.astype(int), pt, color, 2) cv2.circle(image_vis, pt, 4, color, 5) # Draw direction arrows for reference cv2.arrowedLine(image_vis, center.astype(int), (center + dir_down * 100).astype(int), (0, 255, 0), 2) cv2.arrowedLine(image_vis, center.astype(int), (center + dir_right * 100).astype(int), (0, 0, 255), 2) # function to draw labels on the image def draw_label(pt, label, offset): if pt is not None: pos = pt + offset plt.text(pos[0], pos[1], label, fontsize=9, color='white', ha='center', va='center', bbox=dict(facecolor='black', alpha=0.6, boxstyle='round,pad=0.3')) # Display image with annotations plt.figure(figsize=(6, 6)) plt.imshow(image_vis) offset = np.array([0, 70]) draw_label(inner_down, rim_ui["Down Inner"], -offset) draw_label(outer_down, rim_ui["Down Outer"], -offset) draw_label(inner_right, rim_ui["Right Inner"], -offset) draw_label(outer_right, rim_ui["Right Outer"], offset) plt.axis("off") # Save visualization to memory buffer buf = BytesIO() plt.savefig(buf, format='png') plt.close() buf.seek(0) vis_img = PILImage_p7.open(buf) # Open buffer as image # Formatted string summary of measurements rim_str = ( f"Down Inner Diameter:\n{d_down_inner_px*2:4.1f}px | {rim_cm['down_inner']*2:4.2f}cm | {rim_inch['down_inner']*2:4.2f}in\n" f"\nRight Inner Diameter:\n{d_right_inner_px*2:4.1f}px | {rim_cm['right_inner']*2:4.2f}cm | {rim_inch['right_inner']*2:4.2f}in\n" f"\nRight Outer Diameter:\n{d_right_outer_px*2:4.1f}px | {rim_cm['right_outer']*2:4.2f}cm | {rim_inch['right_outer']*2:4.2f}in\n" f"\nDown Angle:\n{(angle - 90) % 360:4.1f}ยฐ\n" f"\nRight Angle:\n{angle:4.1f}ยฐ\n" f"\nWidth Down:\n{rim_width_down:4.1f}px | {rim_cm['width_down']:4.2f}cm | {rim_inch['width_down']:4.2f}in\n" f"\nWidth Right:\n{rim_width_right:4.1f}px | {rim_cm['width_right']:4.2f}cm | {rim_inch['width_right']:4.2f}in\n" ) # Return: image update, UI string, numeric outputs, vector info, visibility updates return ( gr.update(value=vis_img, visible=True), # Annotated image rim_ui, # Display text (cm/in) rim_cm, # Numerical values in cm rim_inch, # Numerical values in inches center.tolist(), # Center coordinates as list dir_down.tolist(), # Down direction vector dir_right.tolist(), # Right direction vector gr.update(value=rim_str, visible=True), # Measurement summary text gr.update(visible=True) # Toggle visibility flag ) # ================================ # ๐งฉ PART 8: Rim Height Analyzer (Stateless, Session-Safe) # ================================ import gradio as gr import numpy as np import cv2 import math from PIL import Image as PILImage_p8 from io import BytesIO import matplotlib.pyplot as plt # ============================ # Helper function to find the inner top point of the rim (searching in "down" direction) # ============================ def find_inner_top_p8(mask, center, direction, max_steps=7000): prev = mask[int(center[1]), int(center[0])] # Get initial pixel value at center for step in range(1, max_steps): pt = center + step * direction # Step along the direction vector x, y = int(round(pt[0])), int(round(pt[1])) if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]): # If outside bounds, stop break val = mask[y, x] # Get pixel value at new location if prev == 1 and val == 0: # Detect the transition from rim (1) to background (0) return np.array([x, y]) # Return the topmost point of inner rim prev = val return None # Return None if not found # ============================ # Helper function to find the outer bottom point of the rim (searching in "up" direction) # ============================ def find_outer_bottom_p8(mask, center, direction, max_steps=7000): prev = mask[int(center[1]), int(center[0])] # Get initial pixel value for step in range(1, max_steps): pt = center - step * direction # Step in opposite direction x, y = int(round(pt[0])), int(round(pt[1])) if not (0 <= x < mask.shape[1] and 0 <= y < mask.shape[0]): # Out of bounds check break val = mask[y, x] # Pixel value at new point if prev == 0 and val == 1: # Transition from background (0) to rim (1) return np.array([x, y]) # Return bottommost outer rim point prev = val return None # Return None if not found # ============================ # Main analysis function to compute rim height and visualize it # ============================ def run_rim_height_analysis_p8( _trigger_button, # Dummy input for Gradio button triggering image_dict_p4, # Dictionary of input images (session state) binary_masks_p4, # Dictionary of binary masks (rim) ref_ratios_p5, # Reference pixel/cm conversion ratio center_p7, # Center point for analysis dir_down_p7 # Downward vector for analysis ): # Load image and relevant mask image = np.array(image_dict_p4["open_noseat"]).copy() mask = binary_masks_p4["rim"]["open_noseat"] px_per_cm = ref_ratios_p5["open_noseat"] # Convert center and direction to numpy arrays center_pt = np.array(center_p7) dir_vec = np.array(dir_down_p7) # Compute angles for display raw_angle = np.rad2deg(math.atan2(dir_vec[1], dir_vec[0])) down_angle_deg = (450 - raw_angle) % 360 perp_angle_deg = (down_angle_deg + 90) % 360 # Get the rim top and bottom using mask edge transitions inner_top = find_outer_bottom_p8(mask, center_pt, dir_vec) outer_bottom = find_inner_top_p8(mask, center_pt, dir_vec) # If any point is missing, abort and inform the user if inner_top is None or outer_bottom is None: return "โ ๏ธ Could not find both rim points", None, None, None, None, None, None, None # Calculate pixel distance between points (rim height) rim_height_px = np.linalg.norm(outer_bottom - inner_top) rim_height_cm = rim_height_px / px_per_cm rim_height_in = rim_height_cm / 2.54 # ------------------------------------ # Visualization of points and line # ------------------------------------ # Mark points cv2.circle(image, tuple(inner_top), 5, (0, 255, 255), -1) # Yellow inner top cv2.circle(image, tuple(outer_bottom), 5, (255, 255, 0), -1) # Cyan outer bottom # Draw connecting line cv2.line(image, tuple(inner_top), tuple(outer_bottom), (0, 0, 255), 2) # Red line # Draw direction arrow for debugging arrow_end = (center_pt + dir_vec * 100).astype(int) cv2.arrowedLine(image, center_pt.astype(int), arrow_end, (0, 255, 0), 2) # Green arrow # Add text overlay with rim height label = f"{rim_height_px:.1f}px | {rim_height_cm:.2f}cm | {rim_height_in:.2f}in" mid = ((inner_top + outer_bottom) / 2).astype(int) text_pos = (mid[0] + 10, mid[1] - 10) # Draw background rectangle behind text for readability overlay = image.copy() font = cv2.FONT_HERSHEY_SIMPLEX (tw, th), _ = cv2.getTextSize(label, font, 1, 2) rect_start = (text_pos[0] - 10, text_pos[1] - th - 10) rect_end = (text_pos[0] + tw + 10, text_pos[1] + 10) cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1) # Black background cv2.addWeighted(overlay, 0.5, image, 0.5, 0, image) # Blend it with image # Final text overlay cv2.putText(image, label, text_pos, font, 1, (255, 255, 255), 2, cv2.LINE_AA) # Convert final OpenCV image to PIL for Gradio buf = BytesIO() plt.imsave(buf, image) # Save with matplotlib to buffer buf.seek(0) rim_result_image_p8 = PILImage_p8.open(buf) # Format rim height string for display in output textbox rim_str = ( f"Rim Height:\n{rim_height_px:4.1f}px | {rim_height_cm:4.2f}cm | {rim_height_in:4.2f}in\n\nDown Angle:\n{down_angle_deg:4.1f}ยฐ" ) # Return outputs for Gradio UI: label string, annotated image, raw values, and updated States return ( label, # label string gr.update(value=rim_result_image_p8, visible=True), # image with annotations rim_height_px, # rim height in pixels rim_height_cm, # rim height in centimeters rim_height_in, # rim height in inches inner_top.tolist(), # top point of rim (to pass to later stages) gr.update(value=rim_str, visible=True), # rim result text box gr.update(visible=True) # signal visibility of any dependent outputs ) # ============================ # ๐ณ๏ธ PART 9: Hole Width Measurement (Stateless) # ============================ from PIL import Image as PILImage_p9 # Import PIL for image handling from io import BytesIO # For in-memory image saving/loading # Function to compute the perpendicular width of a hole (e.g. toilet bowl opening) def analyze_hole_width_perpendicular_p9( _trigger, # Dummy trigger input to enable button-based execution binary_masks_p4, # Dictionary containing binary segmentation masks ref_ratios_p5, # Reference pixel-per-cm conversion ratios image_dict_p4, # Dictionary of input images by category dir_right_p7, # Direction vector (e.g. right axis from ellipse) center_p7 # Ellipse center point (not used here, but may be useful contextually) ): # --- Input setup --- mask = binary_masks_p4["holes"]["open_noseat"] # Get binary mask for the hole (open_noseat type) mask_u8 = (mask * 255).astype(np.uint8) # Convert binary mask to uint8 for image ops ys, xs = np.where(mask == 1) # Get coordinates of all white pixels (non-zero) points = np.stack([xs, ys], axis=1).astype(float) # Stack as Nx2 float array of points # --- Compute perpendicular direction --- dir_scan = np.array(dir_right_p7) # Use provided right direction vector dir_scan = dir_scan / np.linalg.norm(dir_scan) # Normalize to unit vector dir_perp = np.array([-dir_scan[1], dir_scan[0]]) # Compute perpendicular direction # --- Project points on perpendicular axis --- projs = points @ dir_perp # Project each point on perpendicular axis proj_min, proj_max = np.min(projs), np.max(projs) # Get min and max projection values # --- Find widest span along perpendicular lines --- best_dist = -1 pt_min_p9 = None pt_max_p9 = None for offset in np.arange(proj_min, proj_max, 1.0): # Slide line across projection axis mask_line = np.abs(projs - offset) < 0.5 # Select nearby points close to this offset line_points = points[mask_line] # Get actual coordinates for those points if len(line_points) >= 2: # Only consider if line has โฅ 2 points line_proj = line_points @ dir_scan # Project onto scanning direction i_min = np.argmin(line_proj) # Get point with min projection i_max = np.argmax(line_proj) # Get point with max projection d = np.linalg.norm(line_points[i_max] - line_points[i_min]) # Compute distance if d > best_dist: # If it's the longest so far, save it best_dist = d pt_min_p9 = line_points[i_min] pt_max_p9 = line_points[i_max] # --- Handle edge case where no line pair found --- if pt_min_p9 is None or pt_max_p9 is None: return None, None, None, None, None, None, None # --- Compute pixel width --- hole_width_px_p9 = best_dist # Best span found is hole width in pixels # --- Compute orientation angle of detected line --- dir_line = pt_max_p9 - pt_min_p9 dir_line = dir_line / np.linalg.norm(dir_line) # Normalize angle_rad = np.arctan2(dir_line[1], dir_line[0]) # Angle in radians angle_deg_p9 = (450 - np.rad2deg(angle_rad)) % 360 # Convert to degrees (clockwise from up) # --- Convert width from pixels to cm/inch --- px_per_cm = ref_ratios_p5["open_noseat"] # Get px/cm ratio cm_per_px = 1.0 / px_per_cm # Inverse gives cm/px hole_width_cm_p9 = hole_width_px_p9 * cm_per_px # Convert to cm hole_width_inch_p9 = hole_width_cm_p9 / 2.54 # Convert to inches # --- Visualization on image --- image = np.array(image_dict_p4["open_noseat"]).copy() # Copy image for annotation cv2.circle(image, pt_min_p9.astype(int), 5, (0, 255, 0), -1) # Draw green circle at pt1 cv2.circle(image, pt_max_p9.astype(int), 5, (0, 0, 255), -1) # Draw red circle at pt2 cv2.line(image, pt_min_p9.astype(int), pt_max_p9.astype(int), (255, 255, 0), 2) # Yellow line # --- Prepare label text --- label = f"{hole_width_px_p9:.1f}px | {hole_width_cm_p9:.2f}cm | {hole_width_inch_p9:.2f}in" mid = ((pt_min_p9 + pt_max_p9) / 2).astype(int) # Midpoint between two points text_pos = (mid[0] - 30, mid[1] - 30) # Text offset for visibility overlay = image.copy() # For semi-transparent text box font = cv2.FONT_HERSHEY_SIMPLEX (tw, th), _ = cv2.getTextSize(label, font, 1, 2) # Get text width/height rect_start = (text_pos[0] - 10, text_pos[1] - th - 10) # Top-left of background rectangle rect_end = (text_pos[0] + tw + 10, text_pos[1] + 10) # Bottom-right of rectangle cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1) # Draw black box behind text cv2.addWeighted(overlay, 0.7, image, 0.3, 0, image) # Blend with original cv2.putText(image, label, text_pos, font, 1, (255, 255, 255), 2) # Final text on image # --- Save result image to in-memory buffer --- buf = BytesIO() plt.imsave(buf, image) # Save annotated image to buffer buf.seek(0) hole_result_image_p9 = PILImage_p9.open(buf) # Load back as PIL image # --- Prepare measurement string for display --- hole_width_str = ( f"Hole Width:\n{hole_width_px_p9:4.1f}px | {hole_width_cm_p9:4.2f}cm | {hole_width_inch_p9:4.2f}in\n" f"\nOrientation Angle:\n{angle_deg_p9:4.1f}ยฐ" ) # --- Return multiple outputs for Gradio UI --- return ( gr.update(value=hole_result_image_p9, visible=True), # Annotated image output hole_width_px_p9, # Raw width in pixels hole_width_cm_p9, # Width in cm hole_width_inch_p9, # Width in inches angle_deg_p9, # Orientation angle pt_min_p9.tolist(), # Point 1 coords pt_max_p9.tolist(), # Point 2 coords gr.update(value=hole_width_str, visible=True), # Text summary gr.update(visible=True) # Trigger visibility for some UI element ) # ============================= # ๐งฉ PART 10: Rim-to-Hole Line Distance (Stateless) # ============================= from PIL import Image as PILImage_p10 from io import BytesIO def compute_top_to_hole_distance_p10( _trigger, # dummy trigger to force execution in Gradio pipeline inner_top_p8, # topmost point on the inner rim (from Part 8) pt_min_p9, # one end of the horizontal hole line (from Part 9) pt_max_p9, # other end of the horizontal hole line (from Part 9) dir_down_p7, # direction vector pointing downward from rim (from Part 7) ref_ratios_p5, # px/cm ratios per view (from Part 5) image_dict_p4 # original rotated images (from Part 4) ): # Helper function to find intersection point between two parametric lines def line_intersection_p10(p1, d1, p2, d2): A = np.array([d1, -d2]).T # construct matrix from direction vectors b = p2 - p1 # vector between starting points if np.linalg.matrix_rank(A) < 2: return None # lines are parallel; no intersection t_s = np.linalg.lstsq(A, b, rcond=None)[0] # solve A*[t, s] = b return p1 + t_s[0] * d1 # return intersection point along line 1 # Convert input points and vectors to numpy arrays for math operations pt_min = np.array(pt_min_p9) pt_max = np.array(pt_max_p9) inner_top = np.array(inner_top_p8) dir_down = np.array(dir_down_p7) # Compute the direction vector of the hole line (horizontal across hole) dir_hole_line = pt_max - pt_min dir_hole_line = dir_hole_line / np.linalg.norm(dir_hole_line) # normalize # Compute intersection point between inner rim line and hole line intersection_point = line_intersection_p10(inner_top, dir_down, pt_min, dir_hole_line) if intersection_point is None: print("โ Lines are parallel.") # alert if intersection fails return None, None, None, None, None, None, None # --- Distance Calculation --- dist_px = np.linalg.norm(intersection_point - inner_top) # pixel distance px_per_cm = ref_ratios_p5["open_noseat"] # px/cm for current view dist_cm = dist_px / px_per_cm # convert to cm dist_inch = dist_cm / 2.54 # convert to inches # --- Angle Calculations --- # Convert angle of rim direction to degrees (clockwise from top) angle_down_deg = (450 - np.rad2deg(np.arctan2(dir_down[1], dir_down[0]))) % 360 # Convert angle of hole line direction to degrees vec = dir_hole_line angle_perp_deg = (450 - np.rad2deg(np.arctan2(vec[1], vec[0]))) % 360 # --- Visualization --- image = np.array(image_dict_p4["open_noseat"]).copy() # load view image # Draw points and lines cv2.circle(image, inner_top.astype(int), 4, (0, 255, 0), 5) # green: inner top point cv2.circle(image, intersection_point.astype(int), 4, (0, 0, 255), 5) # red: intersection point cv2.line(image, inner_top.astype(int), intersection_point.astype(int), (255, 255, 0), 2) # yellow: vertical cv2.line(image, pt_min.astype(int), pt_max.astype(int), (255, 0, 255), 1) # magenta: hole width line # Add measurement text label = f"{dist_px:.1f}px | {dist_cm:.2f}cm | {dist_inch:.2f}in" mid = ((inner_top + intersection_point) / 2).astype(int) text_pos = (mid[0] + 10, mid[1] - 10) # Draw black background box behind text for readability overlay = image.copy() (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2) rect_start = (text_pos[0] - 10, text_pos[1] - th - 10) rect_end = (text_pos[0] + tw + 10, text_pos[1] + 10) cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1) cv2.addWeighted(overlay, 0.7, image, 0.3, 0, image) # Add label text in white cv2.putText(image, label, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) # Save resulting image to a PIL object buf = BytesIO() plt.imsave(buf, image) buf.seek(0) rim_result_image_p10 = PILImage_p10.open(buf) # Text summary output rim_to_hole_str = ( f"Inner Rim to Hole Distance:\n{dist_px:4.1f}px | {dist_cm:4.2f}cm | {dist_inch:4.2f}in\n" f"\nRim Direction Angle (Down):\n{angle_down_deg:4.1f}ยฐ\n" f"\nHole Width Angle:\n{angle_perp_deg:4.1f}ยฐ" ) # Return everything needed for Gradio UI return ( gr.update(value=rim_result_image_p10, visible=True), # image with overlays dist_px, # raw distance in pixels dist_cm, # distance in cm dist_inch, # distance in inches angle_down_deg, # angle of downward rim direction angle_perp_deg, # angle of hole width line intersection_point.tolist(), # intersection point as list gr.update(value=rim_to_hole_str, visible=True), # summary string gr.update(visible=True) # show result box ) # ================================ # ๐ PART 10: Ellipse-Based Rim Orientation (Closed Lid, Stateless) # ================================ from PIL import Image as PILImage_p10 from io import BytesIO def analyze_closed_rim_orientation_p10(_trigger, binary_masks_p4, image_dict_p4): # --------------------------------------------------------------- # Subfunction to remove top portion of the mask based on angle # --------------------------------------------------------------- def remove_top_based_on_angle(mask, center, angle_deg, threshold): mask = (mask > 0).astype(np.uint8) # Ensure binary format h, w = mask.shape cx, cy = center angle_rad = np.deg2rad(angle_deg) # Direction vector along the angle dx = math.cos(angle_rad) dy = math.sin(angle_rad) # Perpendicular direction (used for computing distances from axis) perp_dx = -dy perp_dy = dx # Iterate over all rows in the mask for y in range(h): x_coords = np.where(mask[y] == 1)[0] # Get all foreground pixels in row if len(x_coords) == 0: continue # Skip if row is empty distances = [] for x in x_coords: px, py = x, y dxp = px - cx dyp = py - cy # Distance of (x,y) from ellipse axis using dot product with perpendicular vector dist = abs(dxp * perp_dx + dyp * perp_dy) distances.append(dist) if max(distances) < threshold: # If max distance is small, likely part of the top region โ remove mask[y, x_coords] = 0 else: # Stop removing when actual rim area is reached break return mask * 255 # Return as 255-mask # ---------------------- # Step 1: Clean the mask # ---------------------- mask = binary_masks_p4["rim"]["closed"] # Get closed image rim mask center_estimate = (150, 220) # Approx center for removal reference clean_mask = remove_top_based_on_angle(mask, center_estimate, 23, 30) # Update the rim mask after cleaning binary_masks_p4["rim"]["closed"] = clean_mask # -------------------------- # Step 2: Fit ellipse on mask # -------------------------- mask_u8 = (clean_mask * 255).astype(np.uint8) # Convert to 8-bit image contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) assert contours, "โ No contours found in closed rim mask!" rim_contour = max(contours, key=cv2.contourArea) # Take largest contour assert len(rim_contour) >= 5, "โ Need at least 5 points to fit an ellipse!" # Fit ellipse to contour ellipse = cv2.fitEllipse(rim_contour) (center_x, center_y), (major_axis, minor_axis), angle_deg = ellipse ellipse_center = np.array([int(center_x), int(center_y)]) # Center of ellipse # --------------------------- # Step 3: Calculate directions # --------------------------- angle_deg += 90 # Rotate so long axis is considered vertical angle_rad = math.radians(angle_deg) # Calculate downward vector (unit vector) dir_down = np.array([math.cos(angle_rad), math.sin(angle_rad)]) dir_down /= np.linalg.norm(dir_down) # โ Ensure dir_down is pointing downward (positive Y direction) if dir_down[1] < 0: dir_down *= -1 # Get rightward direction as perpendicular vector to dir_down dir_right = np.array([-dir_down[1], dir_down[0]]) # โ Ensure dir_right points rightward (positive X direction) if dir_right[0] < 0: dir_right *= -1 # Recompute angle from dir_down to correct it for rendering angle_rad_back = math.atan2(dir_down[1], dir_down[0]) angle_deg = math.degrees(angle_rad_back) # Calculate final angle in degrees (adjusted to 0โ360ยฐ range) ellipse_angle_deg = (450 - angle_deg) % 360 # -------------------------- # Step 4: Visualize results # -------------------------- image = np.array(image_dict_p4["closed"]).copy() # Get original closed image cv2.circle(image, ellipse_center, 4, (255, 255, 0), 5) # Draw center point # Draw downward direction (green) pt_down = (ellipse_center + dir_down * 100).astype(int) cv2.arrowedLine(image, ellipse_center, pt_down, (0, 255, 0), 3) # Draw rightward direction (cyan) pt_right = (ellipse_center + dir_right * 100).astype(int) cv2.arrowedLine(image, ellipse_center, pt_right, (0, 255, 255), 3) # Convert visualized image to displayable PNG plt.figure(figsize=(6, 6)) plt.imshow(image) plt.title("Toilet Rim Orientation using Ellipse Fitting") plt.axis("off") buf = BytesIO() plt.savefig(buf, format="png") plt.close() buf.seek(0) ellipse_viz_image = PILImage_p10.open(buf) # -------------------------- # Step 5: Prepare output text # -------------------------- closed_orientation_str = ( f"Downward Direction Angle:\n{ellipse_angle_deg:4.1f}ยฐ\n" f"\nPerpendicular Direction Angle:\n{(ellipse_angle_deg + 90) % 360:4.1f}ยฐ\n" f"\nEllipse Center:\n({ellipse_center[0]}, {ellipse_center[1]})" ) # -------------------------- # Step 6: Return results # -------------------------- return ( gr.update(value=ellipse_viz_image, visible=True), # Displayed image ellipse_angle_deg, # Angle of downward direction ellipse_center.tolist(), # Center coordinates dir_down.tolist(), # Downward unit vector dir_right.tolist(), # Rightward unit vector binary_masks_p4, # Updated binary masks gr.update(value=closed_orientation_str, visible=True), # Text summary gr.update(visible=True) # Make text box visible ) # ================================ # ๐งฎ PART 11: Rim Height Along Downward Direction (Closed Rim, Stateless) # ================================ from PIL import Image as PILImage_p11 from io import BytesIO # Main function to analyze rim height along vertical direction on closed toilet seat def analyze_rim_height_on_closed_p11( _trigger, # Dummy trigger input to control Gradio interaction binary_masks_p4, # Dictionary of segmentation masks (rim masks in this case) image_closed_lid_rotated_p3, # Rotated image for closed-lid condition ellipse_angle_deg_p10, # Angle of fitted ellipse (used to determine vertical direction) ref_ratios_p5, # Pixel per cm reference for conversion rim_height_cm_p8 # Precomputed rim height (open) from earlier stage ): # Utility function to trace along a vector direction from a center point # and find the furthest foreground (non-zero) points on both sides of the direction def find_extreme_points_along_line(mask, center, direction, max_steps=2000): H, W = mask.shape pt1 = pt2 = None # Move forward from center along direction vector for step in range(1, max_steps): pt = center + step * direction x, y = int(round(pt[0])), int(round(pt[1])) if not (0 <= x < W and 0 <= y < H): break if mask[y, x] > 0: pt2 = np.array([x, y]) # furthest found point elif pt2 is not None: break # exit once we leave the foreground area # Move backward from center along opposite direction for step in range(1, max_steps): pt = center - step * direction x, y = int(round(pt[0])), int(round(pt[1])) if not (0 <= x < W and 0 <= y < H): break if mask[y, x] > 0: pt1 = np.array([x, y]) # furthest found point elif pt1 is not None: break return pt1, pt2 # Utility function to calculate Euclidean distance between two points def dist(a, b): return np.linalg.norm(a - b) if a is not None and b is not None else None # --- Step 1: Get mask and image --- mask = binary_masks_p4["rim"]["closed"] # Binary mask for closed rim image = np.array(image_closed_lid_rotated_p3) # Convert PIL image to NumPy array # --- Step 2: Compute center of the mask contour --- mask_u8 = (mask * 255).astype(np.uint8) # Convert to 8-bit mask for contour detection contours, _ = cv2.findContours(mask_u8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) assert contours, "โ No contours found in closed rim mask!" # Raise error if no contours cnt = max(contours, key=cv2.contourArea) # Largest contour M = cv2.moments(cnt) # Calculate image moments cx = int(M["m10"] / M["m00"]) # X coordinate of centroid cy = int(M["m01"] / M["m00"]) # Y coordinate of centroid center = np.array([cx, cy]) # Center point # --- Step 3: Determine direction vector using ellipse angle --- angle_rad = np.deg2rad((450 - ellipse_angle_deg_p10) % 360) # Convert to radian (corrected for rotation) dir_vec = np.array([math.cos(angle_rad), math.sin(angle_rad)]) # Unit vector in direction # --- Step 4: Trace extreme points along the direction vector --- pt_start, pt_end = find_extreme_points_along_line(mask, center, dir_vec) rim_height_px = dist(pt_start, pt_end) # Height in pixels px_per_cm = ref_ratios_p5['closed'] # Pixel-per-cm ratio full_rim_height_cm = rim_height_px / px_per_cm # Total rim height in cm full_rim_height_in = full_rim_height_cm / 2.54 # Convert to inches # --- Step 5: Use rim height from open seat to compute overlap --- rim_height_cm = rim_height_cm_p8 # Rim height from open seat (in cm) rim_height_in = rim_height_cm / 2.54 # Convert to inches closed_remaining_cm = full_rim_height_cm - rim_height_cm # Height still covered by lid closed_remaining_in = closed_remaining_cm / 2.54 # Convert to inches # --- Step 6: Visualize the result if both points were found --- if pt_start is not None and pt_end is not None: vis = image.copy() # Draw points and line on visualization cv2.circle(vis, tuple(center), 4, (255, 255, 0), -1) # center (yellow) cv2.circle(vis, tuple(pt_start), 5, (0, 0, 255), -1) # start point (red) cv2.circle(vis, tuple(pt_end), 5, (0, 255, 0), -1) # end point (green) cv2.line(vis, tuple(pt_start), tuple(pt_end), (0, 255, 255), 2) # vertical line (cyan) # Create overlay label label = f"{rim_height_px:.1f}px | {full_rim_height_cm:.2f}cm | {full_rim_height_in:.2f}in" mid_point = ((pt_start + pt_end) / 2).astype(int) text_pos = (mid_point[0] + 10, mid_point[1] - 10) # Create label background box overlay = vis.copy() (text_w, text_h), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 1, 2) rect_start = (text_pos[0] - 10, text_pos[1] - text_h - 10) rect_end = (text_pos[0] + text_w + 10, text_pos[1] + 10) cv2.rectangle(overlay, rect_start, rect_end, (0, 0, 0), -1) # background cv2.addWeighted(overlay, 0.7, vis, 0.3, 0, vis) # apply overlay cv2.putText(vis, label, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2) # Save visualization to in-memory image plt.figure(figsize=(6, 6)) plt.imshow(vis) plt.title("Rim Height on Closed Lid") plt.axis("off") buf = BytesIO() plt.savefig(buf, format='png') plt.close() buf.seek(0) rim_height_vis = PILImage_p11.open(buf) # PIL Image for output pt_start_p11 = pt_start # Store start point # Final string to show in UI rim_closed_str = ( f"Total Height:\n{rim_height_px:4.1f}px | {full_rim_height_cm:4.2f}cm | {full_rim_height_in:4.2f}in" ) # Return all values to Gradio UI return ( gr.update(value=rim_height_vis, visible=True), # Updated visualization rim_height_cm, # Open rim height (cm) rim_height_in, # Open rim height (in) full_rim_height_cm, # Closed rim full height (cm) full_rim_height_in, # Closed rim full height (in) closed_remaining_cm, # Height still hidden by lid (cm) closed_remaining_in, # Height still hidden by lid (in) pt_start_p11, # Topmost point on closed rim gr.update(value=rim_closed_str, visible=True), # Text summary for UI gr.update(visible=True) # Enable output group in Gradio ) else: # If points couldn't be found, return empty outputs print("โ ๏ธ Could not find valid intersection points.") return (None, None, None, None, None, None, None, None, None, None) # ================================ # ๐งฑ PART 12: Draw Remaining Closed Lid Portion (Stateless) # ================================ def draw_remaining_closed_portion_p12( _trigger, pt_start_p11, # Starting point from Part 11 (top of closed rim) closed_remaining_cm_p11, # Remaining distance (in cm) to be drawn ref_ratios_p5, # Dictionary with px/cm ratios for all image types ellipse_dir_down_p10, # Unit vector pointing downward from ellipse analysis (Part 10) image_closed_lid_rotated_p3 # The closed lid image, rotated for alignment ): # --- Clone and compute points --- pt_start = np.array(pt_start_p11) # Convert input start point to NumPy array dir_down = np.array(ellipse_dir_down_p10) # Direction unit vector from ellipse (downward) remaining_cm = closed_remaining_cm_p11 # Distance to draw, in cm px_per_cm = ref_ratios_p5['closed'] # Get pixel-to-cm conversion for closed image remaining_px = remaining_cm * px_per_cm # Convert remaining cm to pixels pt_end = (pt_start + dir_down * remaining_px).astype(int) # Compute end point using direction and distance # --- Compute angle --- vec = pt_end - pt_start # Vector between start and end point angle_rad = np.arctan2(vec[1], vec[0]) # Angle in radians using arctangent of vector remaining_angle_deg = (450 - np.rad2deg(angle_rad)) % 360 # Convert to clockwise angle in degrees (0ยฐ at top) # --- Draw on image --- image = np.array(image_closed_lid_rotated_p3).copy() # Make a copy of the rotated image to draw on cv2.line(image, tuple(pt_start), tuple(pt_end), (0, 0, 255), 3) # Draw red line from start to end cv2.circle(image, tuple(pt_start), 5, (0, 255, 0), -1) # Draw green circle at start cv2.circle(image, tuple(pt_end), 5, (255, 0, 0), -1) # Draw blue circle at end # --- Add label --- remaining_in = remaining_cm / 2.54 # Convert cm to inches label = f"({remaining_cm:.2f} cm) | ({remaining_in:.2f} in)" # Create label with both units text_pos = (pt_end[0] + 10, pt_end[1] - 10) # Position label near end point cv2.putText(image, label, text_pos, cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2, cv2.LINE_AA) # Draw label text in red # --- Compose info string for UI --- closed_remaining_str = ( f"Remaining Portion Length:\n{remaining_cm:4.2f}cm | {remaining_in:4.2f}in\n" f"\nDirection Angle:\n{remaining_angle_deg:4.1f}ยฐ" ) # --- Return image + values for display --- return ( gr.update(value=(PILImage_p11.fromarray(image)), visible=True), # Show annotated image remaining_angle_deg, # Angle of the drawn segment pt_start, # Start point pt_end, # End point gr.update(value=closed_remaining_str, visible=True), # Display stats string gr.update(visible=True) # Make result components visible ) # ================================ # ๐งฑ PART 13: Top Rim Width Measurement (Stateless) # ================================ def analyze_top_rim_width_p13( _trigger, binary_masks_p4, # Dictionary containing binary masks, here we use the 'rim' mask for the closed lid image ellipse_center_p10, # Center of fitted ellipse (from Part 10) ellipse_angle_deg_p10, # Angle of fitted ellipse in degrees (from Part 10) ref_ratios_p5, # Reference px/cm ratios (from Part 5) image_closed_lid_rotated_p3 # Rotated closed-lid image for overlay (from Part 3) ): import matplotlib.pyplot as plt from PIL import Image as PILImage_p13 from io import BytesIO # Extract the necessary variables from inputs mask = binary_masks_p4['rim']['closed'] center = np.array(ellipse_center_p10) angle_deg = (450 - ellipse_angle_deg_p10) % 360 # Adjust angle for image coordinate system px_per_cm = ref_ratios_p5['closed'] image = np.array(image_closed_lid_rotated_p3).copy() # Convert PIL image to NumPy array # Bresenhamโs line algorithm for drawing a line between two points def bresenham_line(x0, y0, x1, y1): points = [] steep = abs(y1 - y0) > abs(x1 - x0) if steep: x0, y0, x1, y1 = y0, x0, y1, x1 swapped = False if x0 > x1: x0, x1 = x1, x0 y0, y1 = y1, y0 swapped = True dx = x1 - x0 dy = abs(y1 - y0) error = dx / 2 ystep = 1 if y0 < y1 else -1 y = y0 for x in range(x0, x1 + 1): pt = (y, x) if steep else (x, y) points.append(pt) error -= dy if error < 0: y += ystep error += dx if swapped: points.reverse() return points # Core logic to find the topmost rim point and the width across it def find_top_and_perpendicular_extremes(mask, center, angle_deg, perp_halfwidth=7000, max_up_scan=6500): mask = (mask > 0).astype(np.uint8) # Ensure binary mask h, w = mask.shape angle_rad = np.deg2rad(angle_deg) dx, dy = np.cos(angle_rad), np.sin(angle_rad) # Upward direction is negative direction of ellipse angle up_dir = np.array([-dx, -dy]) # Perpendicular to rim direction (cross-section) perp_dir = np.array([-dy, dx]) pt_top = None # Scan upward from center pixel to find the first mask pixel for i in range(max_up_scan): pt = center + up_dir * i x, y = int(round(pt[0])), int(round(pt[1])) if 0 <= x < w and 0 <= y < h and mask[y, x] == 1: pt_top = (x, y) if pt_top is None: raise ValueError("No mask pixel found when scanning upward from center.") # Slight offset downward for more reliable width reading pt_top = (pt_top[0], int(pt_top[1] + h * 0.02)) # Generate line endpoints left and right of pt_top along the perpendicular axis left_pt = (int(round(pt_top[0] - perp_dir[0] * perp_halfwidth)), int(round(pt_top[1] - perp_dir[1] * perp_halfwidth))) right_pt = (int(round(pt_top[0] + perp_dir[0] * perp_halfwidth)), int(round(pt_top[1] + perp_dir[1] * perp_halfwidth))) # Draw a line across the rim at the top point to find edges line_pts = bresenham_line(left_pt[0], left_pt[1], right_pt[0], right_pt[1]) valid_pts = [pt for pt in line_pts if 0 <= pt[0] < w and 0 <= pt[1] < h and mask[pt[1], pt[0]] == 1] if len(valid_pts) < 2: raise ValueError("Not enough mask pixels found along perpendicular line.") # Take first and last valid pixels on rim as width endpoints pt1, pt2 = valid_pts[0], valid_pts[-1] dist_px = np.linalg.norm(np.array(pt2) - np.array(pt1)) return pt_top, pt1, pt2, dist_px # Run the function to find points and distance pt_top, pt1, pt2, dist_px = find_top_and_perpendicular_extremes(mask, center, angle_deg) # Convert from pixels to cm and inches dist_cm = dist_px / px_per_cm dist_in = dist_cm / 2.54 # Calculate angle of this actual width vector for reference vec = np.array(pt2) - np.array(pt1) angle_rad_actual = np.arctan2(vec[1], vec[0]) angle_deg_actual = (450 - np.rad2deg(angle_rad_actual)) % 360 # ------------------------------- # ๐ผ Visualization for feedback # ------------------------------- fig, ax = plt.subplots(figsize=(8, 8)) ax.imshow(image) # Show image ax.plot([pt1[0], pt2[0]], [pt1[1], pt2[1]], 'r-', linewidth=1) # Draw width line ax.scatter(*pt1, color='lime', s=20) # Start point ax.scatter(*pt2, color='cyan', s=20) # End point # Display measurements in the middle of the line mid_x = (pt1[0] + pt2[0]) / 2 mid_y = (pt1[1] + pt2[1]) / 2 ax.text(mid_x, mid_y + 100, f"{dist_px:.1f}px | {dist_cm:.2f}cm | {dist_in:.2f}in", fontsize=10, color='white', bbox=dict(facecolor='black', alpha=0.6)) ax.set_title("Top Rim Width Measurement") ax.axis('off') # Save plot as image buf = BytesIO() plt.savefig(buf, format='png') plt.close() buf.seek(0) vis_image = PILImage_p13.open(buf) # Return readable output string for textbox top_rim_str = ( f"Top Rim Width:\n{dist_px:4.1f}px | {dist_cm:4.2f}cm | {dist_in:4.2f}in\n" f"\nOrientation Angle:\n{angle_deg_actual % 180:4.1f}ยฐ" ) # Return everything required for UI return ( gr.update(value=vis_image, visible=True), # Output image pt_top, # Topmost point pt1, pt2, # Width line endpoints dist_px, dist_cm, dist_in, # Measurements angle_deg_actual, # Measured orientation gr.update(value=top_rim_str, visible=True), # Text output gr.update(visible=True) # Control for showing card or section ) # ================================ # ๐งฉ FINAL PART: Combined App Launcher # ================================ # This part contains logic for launching the final combined Gradio app, # including Supabase authentication, file upload functionality, and login logic. import gradio as gr import os from supabase import create_client, Client from datetime import datetime # ========== Supabase Configuration ========== # Load environment variables for Supabase project # SUPABASE_URL = os.getenv("SUPABASE_URL") # Supabase project URL # SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") # Service role key # SUPABASE_BUCKET = os.getenv("SUPABASE_BUCKET_NAME") # Bucket name to upload ZIPs # # Check if keys are present, else raise an error # if not SUPABASE_URL or not SUPABASE_KEY: # raise Exception("Supabase keys not set properly!") # # Create the Supabase client # supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) # ===== Upload Function: App Output ZIP ===== def upload_zip(email, zip_file): # Validate presence of both email and file if not email or not zip_file: print("โ Email and ZIP file are required.") return gr.update(visible=True) # Ensure it's a .zip file if not zip_file.name.endswith('.zip'): print("โ Only ZIP files are allowed.") return gr.update(visible=True) # Create a unique filename using email and timestamp timestamp = datetime.now().isoformat().replace(":", "-").split(".")[0] safe_email = email.replace("@", "_at_").replace(".", "_") filename = f"{safe_email}_{timestamp}.zip" path_in_bucket = f"zips/{filename}" # Folder path inside the bucket # Read uploaded file bytes with open(zip_file.name, "rb") as f: file_bytes = f.read() # Upload file to Supabase storage response = supabase.storage.from_(SUPABASE_BUCKET).upload( path_in_bucket, file_bytes, {"content-type": "application/zip"} ) # Log upload response print(response) return gr.update(visible=True) # ===== Upload Function: Error ZIP Upload ===== def upload_zip_error(email, zip_file): # Check email and zip are both provided if not email or not zip_file: print("โ Email and ZIP file are required.") return # Ensure it's a ZIP file if not zip_file.name.endswith('.zip'): print("โ Only ZIP files are allowed.") return # Generate timestamped safe filename timestamp = datetime.now().isoformat().replace(":", "-").split(".")[0] safe_email = email.replace("@", "_at_").replace(".", "_") filename = f"error_analysis_{safe_email}_{timestamp}.zip" path_in_bucket = f"zips/{filename}" # Target location in bucket # Read file as binary with open(zip_file.name, "rb") as f: file_bytes = f.read() # Upload the error ZIP to Supabase storage response = supabase.storage.from_(SUPABASE_BUCKET).upload( path_in_bucket, file_bytes, {"content-type": "application/zip"} ) # Print result to console print(response) return # ===== Supabase Login Authentication ===== # def supa_login(email, password): # try: # # Query Supabase users table for user with given email # result = supabase.table("users").select("*").eq("email", email).execute() # users = result.data # # If no such user exists # if not users: # return "โ No user found with that email", False # # Validate password # user = users[0] # if user["password"] == password: # return "โ Login successful!", True # else: # return "โ Incorrect password", False # except Exception as e: # # Handle any exceptions that occur during login # return f"โ Login error: {e}", True # Import necessary libraries import matplotlib.pyplot as plt # For plotting import matplotlib.patches as patches # For drawing shapes like polygons and ellipses import numpy as np # For numerical computations from PIL import Image # For image saving/loading as PIL object import os # For file operations # ================================ # ๐ซ Define chassis image data # ================================ my_polygons = [ { 'points': [(0.56, 14.72), (0.56, 19.05), (19.5, 19.05), (19.5, 14.53)], 'facecolor': "#E6ADAD", # Light red fill 'edgecolor': 'brown', # Border color (not visible if linewidth is 0) 'linewidth': 0, # No border 'zorder': 2 # Renders above background but below zorder=3 or 4 }, { 'points': [(3, 14.72), (3, 9.45), (17, 9.45), (17, 14.6)], 'facecolor': '#E6ADAD', # Same fill as above 'edgecolor': 'darkgreen', 'linewidth': 0, 'zorder': 1 # Rendered below most elements }, { 'points': [(4.24, 18.74), (9.68, 18.74), (9.68, 17.34), (4.24, 17.34)], 'facecolor': '#f0f0f0', # Matches background 'edgecolor': 'brown', 'linewidth': 0, 'zorder': 4 # Highest priority, renders on top }, { 'points': [(12.25, 18.74), (15.88, 18.74), (15.88, 17.34), (12.25, 17.34)], 'facecolor': '#f0f0f0', 'edgecolor': 'darkgreen', 'linewidth': 0, 'zorder': 4 # Also renders on top } ] # ================================ # ๐ก Define chassis image data # ================================ my_ellipses = [ { 'center_x': 10.0, 'center_y': 9.45, 'width': 9.0, 'height': 12.0, 'angle': 0, 'facecolor': '#f0f0f0', # Same as background 'edgecolor': 'blue', 'linewidth': 0, 'zorder': 4 # Render on top of all lower z-order shapes }, { 'center_x': 10.0, 'center_y': 9.45, 'width': 14.0, 'height': 17.0, 'angle': 0, 'facecolor': '#E6ADAD', # Light red 'edgecolor': 'purple', 'linewidth': 0, 'zorder': 3 # Under the inner ellipse but above most polygons } ] def generate_reload_js(email, password): # JS that reloads the page with email and password in the hash email_enc = urllib.parse.quote(email) password_enc = urllib.parse.quote(password) return gr.update(_js=f"() => location.href = location.origin + location.pathname + '#email={email_enc}&password={password_enc}'") # ================================ # ๐ Final App Launch Logic # ================================ # Preload email/password if found in URL fragment (e.g., #email=...&password=...) def restore_from_url(url): parsed = urllib.parse.urlparse(url) query = urllib.parse.parse_qs(parsed.fragment or parsed.query) email = query.get("email", [""])[0] password = query.get("password", [""])[0] return email, password def launch_main_app(): import gradio as gr import urllib.parse url_box = gr.Textbox(visible=False) email = gr.Textbox(value="no_login_user", visible=False) password = gr.Textbox(value="", visible=False) # On app load, prefill email/password from URL #full_app_interface.load(restore_from_url, [url_box], [email, password]) # Main container column with gr.Column("๐ Main App"): # --- Row for Logout Button --- with gr.Row(elem_id="logout-row"): logout_btn = gr.Button("Logout", elem_id="logout-btn") # JS to reload the app on logout click (clears session) logout_btn.click(fn=None, js="() => location.reload()") # --- App Title and Intro --- gr.Markdown("# ๐ฝ Smart Toilet Image Checker", elem_id="centered-title") gr.Markdown("Just upload your toilet photos โ then sit back and watch the AI work its magic!", elem_id="centered-title") gr.Markdown("๐ **New here?** If you're not sure how to use this app, please check out the [step-by-step instructions](https://drive.google.com/file/d/1qCwzaePLexOF8Ti-O-bhC-oPsB14lbFj/view?usp=sharing).") # Create a Gradio session state for current user interaction session_state = gr.State(init_session()) # --- Example Upload Section --- with gr.Row(): gr.Markdown("### ๐ท How to Upload Images Correctly (Example)") # Show 3 wrong upload examples and 1 correct example with gr.Row(): with gr.Column(): gr.Markdown("โ Wrong") wrong1 = gr.Image(value="./static/im1.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False) with gr.Column(): gr.Markdown("โ Wrong") wrong2 = gr.Image(value="./static/im2.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False) with gr.Column(): gr.Markdown("โ Wrong") wrong3 = gr.Image(value="./static/im3.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False) # Display correct upload example in center with gr.Row(): with gr.Column(): z = "filler" # Just spacing with gr.Column(): gr.Markdown("โ Correct") correct = gr.Image(value="./static/im4.jpg", show_share_button=False, show_fullscreen_button=False, show_label=False, interactive=False, height=200, show_download_button=False, container=False) with gr.Column(): z = "filler" # --- Session-wide Global State Declarations (Gradio-safe) --- # These maintain state across tab interactions # Part 4 binary_masks_p4 = gr.State() image_dict_p4 = gr.State() # Part 5 out_ref_ratios_p5 = gr.State() # Part 7 (Rim ellipse measurements) out_rimellipse_ui_p7 = gr.State() out_rimellipse_cm_p7 = gr.State() out_rimellipse_inch_p7 = gr.State() inner_top_p7 = gr.State() dir_down_p7 = gr.State() dir_right_p7 = gr.State() # Part 8 btn_rimheight_p8 = gr.State() rimheight_text_p8 = gr.State() rim_height_px_p8 = gr.State() rim_height_cm_p8 = gr.State() rim_height_inch_p8 = gr.State() inner_top_p8 = gr.State() # Part 9 btn_measure_holewidth_p9 = gr.State() hole_width_px_p9 = gr.State() hole_width_cm_p9 = gr.State() hole_width_inch_p9 = gr.State() angle_deg_p9 = gr.State() pt_min_p9 = gr.State() pt_max_p9 = gr.State() # Part 10 btn_top_to_hole_p10 = gr.State() top_to_hole_line_px_p10 = gr.State() top_to_hole_line_cm_p10 = gr.State() top_to_hole_line_inch_p10 = gr.State() angle_down_deg_p10 = gr.State() angle_perp_deg_p10 = gr.State() intersection_point_p10 = gr.State() btn_ellipse_orient_p10 = gr.State() ellipse_angle_deg_p10 = gr.State() ellipse_center_p10 = gr.State() ellipse_dir_down_p10 = gr.State() ellipse_dir_right_p10 = gr.State() # Part 11 (Closed lid rim) updated_binary_masks_p4 = gr.State() btn_rim_height_closed_p11 = gr.State() rim_height_cm_p11 = gr.State() rim_height_in_p11 = gr.State() full_rim_height_cm_p11 = gr.State() full_rim_height_in_p11 = gr.State() closed_remaining_cm_p11 = gr.State() closed_remaining_in_p11 = gr.State() pt_start_p11 = gr.State() # Part 12 (Remaining portion drawing) btn_draw_remaining_p12 = gr.State() remaining_angle_deg_p12 = gr.State() pt_start_p12 = gr.State() pt_end_p12 = gr.State() # Generic measurement tool btn_measure = gr.State() out_pt_top = gr.State() out_pt1 = gr.State() out_pt2 = gr.State() out_dist_px = gr.State() out_dist_cm = gr.State() out_dist_in = gr.State() out_angle_deg = gr.State() # Part 6 rim overlay results out_rim_measurements_p6 = gr.State() out_rim_measurements_cm_p6 = gr.State() out_rim_measurements_inch_p6 = gr.State() # Part 2 Models (Segmentation models) models_holes_p2 = gr.State(None) models_rim_p2 = gr.State(None) models_coinref_p2 = gr.State(None) device_p2 = gr.State(None) # Optional debugging masks (commented) gallery_segmentation_p4 = gr.State() # For previewing polygons/ellipses in mask viewer polygons, ellipses = gr.State(), gr.State() polygons1, ellipses1 = gr.State(), gr.State() # --- Load models and device globally at app load --- full_app_interface.load( fn=lambda: [GLOBAL_HOLES, GLOBAL_RIM, GLOBAL_COIN, device_p2], # Initialize models and device outputs=[models_holes_p2, models_rim_p2, models_coinref_p2, device_p2], queue=False # Avoid blocking queue during load ) # Injecting custom CSS into the Gradio app using gr.HTML gr.HTML("""""") # =============================================== # ๐ Function to rotate an uploaded image live # =============================================== def rotate_image_live(img): # If no image is provided, return None if img is None: return None # Import required libraries import numpy as np import cv2 from PIL import Image # Convert PIL image to NumPy array img_np = np.array(img) # Get height and width of the image h, w = img_np.shape[:2] # Rotate image 90 degrees clockwise rotated = cv2.rotate(img_np, cv2.ROTATE_90_CLOCKWISE) # Convert back to PIL image and return return Image.fromarray(rotated) import gradio as gr from PIL import Image import os # Layout section with 3 image upload columns (for different toilet conditions) with gr.Row(): with gr.Column(): gr.Markdown("### ๐ธ Open Toilet (No Seat)") # Section heading uploader1 = gr.UploadButton("Upload Image", file_types=["image"]) # Upload button input1 = gr.Image(height=500, width=800, label="Preview", interactive=False, visible=False) # Image display (initially hidden) filename1 = gr.Markdown() # To show filename rotte1 = gr.Button("Rotate", visible=False) # Rotate button (initially hidden) with gr.Column(): gr.Markdown("### ๐ธ Open Toilet (With Seat)") uploader2 = gr.UploadButton("Upload Image", file_types=["image"]) input2 = gr.Image(height=500, width=800, label="Preview", interactive=False, visible=False) filename2 = gr.Markdown() rotte2 = gr.Button("Rotate", visible=False) with gr.Column(): gr.Markdown("### ๐ธ Closed Lid Toilet") uploader3 = gr.UploadButton("Upload Image", file_types=["image"]) input3 = gr.Image(height=500, width=800, label="Preview", interactive=False, visible=False) filename3 = gr.Markdown() rotte3 = gr.Button("Rotate", visible=False) # Utility function to resize an image while keeping aspect ratio def resize_keep_aspect(image, target_size): """ Resize image to fit within target_size (width, height), keeping aspect ratio. Returns same format (PIL.Image or numpy). """ is_numpy = isinstance(image, np.ndarray) # Check input type if is_numpy: image = Image.fromarray(image) # Convert to PIL if needed image = image.copy() # Avoid modifying original image.thumbnail(target_size, Image.LANCZOS) # Resize with high quality filter if is_numpy: return np.array(image) # Return in original format else: return image # Function to handle uploaded image, resize, and extract filename def load_image_and_filename(file_obj): if file_obj is None: return None, "", gr.update(visible=False), gr.update(visible=False) # If no file, return nothing and hide widgets filepath = file_obj.name img = Image.open(filepath) img = resize_keep_aspect(img, (960, 1280)) # Resize image to max bounds filename = os.path.basename(filepath) # Extract name from path return img, f"๐ Filename: {filename}", gr.update(visible=True), gr.update(visible=True) # Set upload callbacks for all 3 image uploaders uploader1.upload(load_image_and_filename, inputs=uploader1, outputs=[input1, filename1, input1, rotte1]) uploader2.upload(load_image_and_filename, inputs=uploader2, outputs=[input2, filename2, input2, rotte2]) uploader3.upload(load_image_and_filename, inputs=uploader3, outputs=[input3, filename3, input3, rotte3]) # Rotate buttons for each image โ rotate image when clicked rotte1.click(fn=rotate_image_live, inputs=[input1], outputs=input1) rotte2.click(fn=rotate_image_live, inputs=[input2], outputs=input2) rotte3.click(fn=rotate_image_live, inputs=[input3], outputs=input3) output = gr.State() # Placeholder for later step output (stateful variable) # ========================= # Next Step Trigger Section # ========================= # Main button to trigger the full pipeline with gr.Row(): run_pipeline_btn = gr.Button("๐ง 'Let AI Do the Work'") # Calls processing logic # Processing status placeholder with gr.Row(): process = gr.Markdown("") # Will show dynamic messages/status # Hidden output section (only shown after pipeline is run) with gr.Column(visible=False) as group_to_show: # Coin reference detection result header with gr.Row(): ref = gr.Markdown("# Coin Reference Detection:", elem_id="centered-title", visible=False) # Output image for coin reference detection with gr.Row(): out_ref_image_p5 = gr.Image( label="๐ช Coin Reference Detection", show_share_button=False, show_fullscreen_button=False, height=600, width=800, visible=False, interactive=False, show_download_button=False, container=False ) # Nicely formatted textbox for coin reference ratio value with gr.Row(): with gr.Column(): z = "filler" # Just padding / alignment element with gr.Column(elem_id="fit-box"): ref_ratios_str = gr.Textbox( label="๐ช Coin Reference Ratios", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): z = "filler" # ---------- Seat Dimensions ---------- with gr.Row(): # Title row for seat dimensions seat = gr.Markdown("# Seat Dimensions:", elem_id="centered-title", visible=False) with gr.Row(): # Image row for rim width (seat view) out_rim_image_p6 = gr.Image( label="๐ Rim Width", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Centered measurement display for seat with gr.Column(): # Left spacer z = "filler" with gr.Column(elem_id="fit-box"): # Center column for measurement text seat_measurement_str = gr.Textbox( label="๐ Seat Dimensions:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): # Right spacer z = "filler" # ---------- Rim Dimensions ---------- with gr.Row(): # Title row for rim dimensions rim = gr.Markdown("# Rim Dimensions:", elem_id="centered-title", visible=False) with gr.Row(): # Image showing ellipse-based rim dimensioning out_rimellipse_image_p7 = gr.Image( label="๐ Rim Dimensions", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Centered measurement display for rim dimensions with gr.Column(): # Left spacer z = "filler" with gr.Column(elem_id="fit-box"): # Measurement text rim_measurement_str = gr.Textbox( label="๐ Rim Dimensions:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): # Right spacer z = "filler" # ---------- Rim Height (Inner Top to Outer Bottom) ---------- with gr.Row(): # Title row for height from inner to outer rim inlen = gr.Markdown("# Length of Rim (Inner Top to Outer Bottom):", elem_id="centered-title", visible=False) with gr.Row(): # Red line visualization image rimheight_image_p8 = gr.Image( label="๐ Red Line Rim", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Measurement display for rim height with gr.Column(): z = "filler" with gr.Column(elem_id="fit-box"): rim_height_str = gr.Textbox( label="๐ Rim Height:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): z = "filler" # ---------- Hole Width ---------- with gr.Row(): # Title row for hole width hw = gr.Markdown("# Hole Width:", elem_id="centered-title", visible=False) with gr.Row(): # Image showing hole width holewidth_image_p9 = gr.Image( label="๐ณ๏ธ Hole Width", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Measurement display for hole width with gr.Column(): z = "filler" with gr.Column(elem_id="fit-box"): hole_width_str = gr.Textbox( label="๐ณ๏ธ Hole Width:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): z = "filler" # ---------- Rim to Hole Top Distance ---------- with gr.Row(): # Title for rim to hole top hr = gr.Markdown("# Distance from Holes to Top of Inner Rim:", elem_id="centered-title", visible=False) with gr.Row(): # Image showing arrow from hole to rim top rim_to_hole_img_p10 = gr.Image( label="โฌ๏ธ Rim Line (Open)", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Measurement display for rim to hole distance with gr.Column(): z = "filler" with gr.Column(elem_id="fit-box"): hole_to_top_str = gr.Textbox( label="โฌ๏ธ Rim to Hole Dimensions:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): z = "filler" # ---------- Closed Lid Direction ---------- with gr.Row(): # Title for toilet orientation cl = gr.Markdown("# Direction of Closed Lid Toilet:", elem_id="centered-title", visible=False) with gr.Row(): # Arrows on ellipse image ellipse_viz_image_p10 = gr.Image( label="๐ Ellipse Arrows", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Measurement display for toilet orientation with gr.Column(): z = "filler" with gr.Column(elem_id="fit-box"): direction_str = gr.Textbox( label="๐ Direction:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): z = "filler" # ---------- Total Height ---------- with gr.Row(): # Title for total toilet height th = gr.Markdown("# Total Height of Entire Toilet:", elem_id="centered-title", visible=False) with gr.Row(): # Image showing toilet height on closed lid rim_height_vis_p11 = gr.Image( label="๐ Rim Height on Closed", height=600, width=800, show_share_button=False, show_fullscreen_button=False, show_download_button=False, interactive=False, visible=False, container=False, ) with gr.Row(): # Measurement display for total height with gr.Column(): z = "filler" with gr.Column(elem_id="fit-box"): total_height_str = gr.Textbox( label="๐ Total Height:", interactive=False, elem_id='pretty-box', visible=False ) with gr.Column(): z = "filler" # Title for remaining lid to holes measurement with gr.Row(): tt = gr.Markdown("# Distance from Top Portion of Toilet to Holes:", elem_id="centered-title", visible=False) # Display image showing remaining lid portion with gr.Row(): remaining_lid_img_p12 = gr.Image(label="๐ Remaining Lid Portion", show_share_button=False, show_fullscreen_button=False, height=600, width=800, visible=False, interactive=False, show_download_button=False, container=False) # Show predicted measurement for remaining portion with gr.Row(): with gr.Column(): z="filler" with gr.Column(elem_id="fit-box"): remaining_str = gr.Textbox(label="๐ Remaining Portion Dimensions:", interactive=False, elem_id='pretty-box', visible=False) with gr.Column(): z="filler" # Title for top rim width with gr.Row(): wt = gr.Markdown("# Width of Top part of Toilet:", elem_id="centered-title", visible=False) # Image showing top rim width with gr.Row(): out_image = gr.Image(label="๐ Top Rim Width", show_share_button=False, show_fullscreen_button=False, height=600, width=800, visible=False, interactive=False, show_download_button=False, container=False) # Show top width measurement string with gr.Row(): with gr.Column(): z="filler" with gr.Column(elem_id="fit-box"): top_width_str = gr.Textbox(label="๐ Top Width:", interactive=False, elem_id='pretty-box', visible=False) with gr.Column(): z="filler" # Hidden file components for downloadable results or error files download_all_file = gr.File(visible=False) download_all_error = gr.File(visible=False) # Hidden session state holders (used for pipeline steps/status) res_stat1 = gr.State() step1 = gr.State() # Final result section (chassy, toilet, overlapped image and result text) with gr.Column(): with gr.Row(): result_status1 = gr.Textbox(label="", visible=False, interactive=False, elem_id='pretty-box') with gr.Row(): with gr.Column(): result_image11 = gr.Image(label="Chassy Image", show_share_button=False, show_fullscreen_button=False, visible=False, interactive=False, show_download_button=False, container=False) with gr.Column(): result_image21 = gr.Image(label="Toilet Image", show_share_button=False, show_fullscreen_button=False, visible=False, interactive=False, show_download_button=False, container=False) with gr.Column(): result_image31 = gr.Image(label="Overlapped Image", show_share_button=False, show_fullscreen_button=False, visible=False, interactive=False, show_download_button=False, container=False) # Hidden column: entire error analysis and comparison UI with gr.Column(visible=False) as col: # Title gr.Markdown("## ๐ฏ Prediction Accuracy Analysis", elem_id="centered-title") # Subheader for reference diagram with gr.Row(): gr.Markdown("### ๐ผ๏ธ Reference Diagram", elem_id="centered-title") # Static reference image for measurement guidance with gr.Row(): ref_image = gr.Image(value="./static/reference.jpg", interactive=False, label="Measurement Guide", show_share_button=False, show_fullscreen_button=False, height=600, show_download_button=False, container=False) # Unit selection (inches or cm) unit_dropdown = gr.Radio(choices=["in", "cm"], label="Select Unit", value="in") # Subheader for comparison table with gr.Row(): gr.Markdown("### ๐งพ Predicted vs Actual Values Table", elem_id='centered-title') # Subheader for predicted vs actual plot with gr.Row(): gr.Markdown("### ๐ Predicted vs Actual Comparison") with gr.Column(): # First row: input a, b, c, d (predicted and actual) with gr.Row(): with gr.Column(): a1 = gr.Number(label="a (Predicted)", interactive=False) a2 = gr.Number(label="a (Actual)") with gr.Column(): b1 = gr.Number(label="b (Predicted)", interactive=False) b2 = gr.Number(label="b (Actual)") with gr.Row(): with gr.Column(): c1 = gr.Number(label="c (Predicted)", interactive=False) c2 = gr.Number(label="c (Actual)") with gr.Column(): d1 = gr.Number(label="d (Predicted)", interactive=False) d2 = gr.Number(label="d (Actual)") # Second row: input e, f, g, h (predicted and actual) with gr.Row(): with gr.Column(): e1 = gr.Number(label="e (Predicted)", interactive=False) e2 = gr.Number(label="e (Actual)") with gr.Column(): f1 = gr.Number(label="f (Predicted)", interactive=False) f2 = gr.Number(label="f (Actual)") with gr.Row(): with gr.Column(): g1 = gr.Number(label="g (Predicted)", interactive=False) g2 = gr.Number(label="g (Actual)") with gr.Column(): h1 = gr.Number(label="h (Predicted)", interactive=False) h2 = gr.Number(label="h (Actual)") # Submit button to trigger error analysis submit_btn = gr.Button("๐ฏ Evaluate Accuracy") # Display progress/status of error evaluation with gr.Row(): progress = gr.Markdown("") # Hidden section for error result and visualization with gr.Column(visible=False) as error: with gr.Row(): with gr.Column(): gr.Markdown("๐งพ Individual Error %", elem_id='centered-title') result_json = gr.JSON(label="๐งพ Individual Error %") with gr.Column(): gr.Markdown("๐ Error Plot", elem_id='centered-title') error_plot = gr.Image(label="๐ Error Plot", height=400, width=600, interactive=False, visible=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False) with gr.Row(): avg_error_text = gr.Textbox(label="๐ฏ Average Error %", interactive=False, elem_id='centered-title') # Hidden states to hold pipeline results and flow tracking res_stat = gr.State() step = gr.State() # Final output images and status (chassy, toilet, overlay) with gr.Column(): with gr.Row(): result_status = gr.Textbox(label="", visible=False, interactive=False, elem_id='pretty-box') with gr.Row(): with gr.Column(): result_image1 = gr.Image(label="Chassy Image", visible=False, interactive=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False) with gr.Column(): result_image2 = gr.Image(label="Toilet Image", visible=False, interactive=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False) with gr.Column(): result_image3 = gr.Image(label="Overlapped Image", visible=False, interactive=False, show_download_button=False, container=False, show_share_button=False, show_fullscreen_button=False) # When Run Pipeline button is clicked: show the group section run_pipeline_btn.click(fn=lambda: gr.update(visible=True), outputs=group_to_show) # Update the processing message while running the pipeline run_pipeline_btn.click(fn=lambda: gr.update(value="๐ Processing... (Please Wait)"), outputs=process) # When submit button is clicked: show calculation status submit_btn.click(fn=lambda: gr.update(value="๐ Calculating Error Percentages... (Please Wait)"), outputs=progress) import gradio as gr # Function to compute predicted measurements based on selected unit def get_predictions_by_unit(unit, closed_remaining_in, top_to_hole_line_inch, hole_width_inch, out_rimellipse_inch, rim_height_inch, out_dist_in): # Compute each parameter in inches a1 = abs(closed_remaining_in - top_to_hole_line_inch) b1 = top_to_hole_line_inch c1 = hole_width_inch d1 = float(out_rimellipse_inch["right_inner"]) * 2 # Inner ellipse width e1 = float(out_rimellipse_inch["down_inner"]) * 2 # Inner ellipse height f1 = float(out_rimellipse_inch["right_outer"]) * 2 # Outer ellipse width g1 = rim_height_inch h1 = out_dist_in # If user selected inches, return raw inch values if unit == "in": return [round(a1, 2), round(b1, 2), round(c1, 2), round(d1, 2), round(e1, 2), round(f1, 2), round(g1, 2), round(h1, 2)] + [gr.update(value="โ Process Complete.")] # If user selected cm, convert inch values to cm and return elif unit == "cm": return [round(x * 2.54, 2) for x in [a1, b1, c1, d1, e1, f1, g1, h1]] + [gr.update(value="โ Process Complete.")] # If unknown unit, return default zeros else: return [0.0] * 8 + [gr.update(value="โ Process Complete.")] # Function to compare predicted and ground truth measurements and compute percentage error def compare_measurements(a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2): # Convert all values to float (if they are passed as strings) a2, b2, c2, d2, e2, f2, g2, h2 = float(a2), float(b2), float(c2), float(d2), float(e2), float(f2), float(g2), float(h2) a1, b1, c1, d1, e1, f1, g1, h1 = float(a1), float(b1), float(c1), float(d1), float(e1), float(f1), float(g1), float(h1) # Helper function to compute error percentage def error(gt, pred): if gt == 0: return "N/A" return f"{abs(gt - pred) / gt * 100:.2f}%" # Calculate individual errors for each parameter errors = { "a": error(a2, a1), "b": error(b2, b1), "c": error(c2, c1), "d": error(d2, d1), "e": error(e2, e1), "f": error(f2, f1), "g": error(g2, g1), "h": error(h2, h1) } # Compute valid numerical errors for average calculation valid_errors = [ abs(gt - pred) / gt * 100 for gt, pred in [ (a2, a1), (b2, b1), (c2, c1), (d2, d1), (e2, e1), (f2, f1), (g2, g1), (h2, h1) ] if gt != 0 ] # Calculate average error across all valid parameters avg_error = f"{sum(valid_errors)/len(valid_errors):.2f}%" if valid_errors else "N/A" return errors, f"โ Average Error: {avg_error}" # Wrapper function to update predictions whenever unit or inputs change def update_preds(unit, closed_remaining_in_p11, top_to_hole_line_inch_p10, hole_width_inch_p9, out_rimellipse_inch_p7, rim_height_inch_p8, out_dist_in): return get_predictions_by_unit(unit, closed_remaining_in_p11, top_to_hole_line_inch_p10,hole_width_inch_p9, out_rimellipse_inch_p7,rim_height_inch_p8, out_dist_in) # Set up the Gradio interaction: when dropdown changes, update prediction values unit_dropdown.change( fn=update_preds, inputs=[unit_dropdown, closed_remaining_in_p11, top_to_hole_line_inch_p10, hole_width_inch_p9, out_rimellipse_inch_p7, rim_height_inch_p8, out_dist_in], outputs=[a1, b1, c1, d1, e1, f1, g1, h1, process] ) import matplotlib.pyplot as plt from PIL import Image import io # Function to generate error bar graph comparing predicted vs actual values def plot_error_graph(a2, b2, c2, d2, e2, f2, g2, h2, a1, b1, c1, d1, e1, f1, g1, h1): labels = list("abcdefgh") pred_vals = [float(x) for x in [a1, b1, c1, d1, e1, f1, g1, h1]] true_vals = [float(x) for x in [a2, b2, c2, d2, e2, f2, g2, h2]] # Calculate percentage error for each parameter errors = [] for pred, true in zip(pred_vals, true_vals): if true == 0: errors.append(0) else: err = abs(pred - true) / true * 100 errors.append(err) # Plotting error bars fig, ax = plt.subplots(figsize=(8, 4)) ax.bar(labels, errors, color="#f05a28") # Use orange for bars ax.set_ylim(0, max(errors) * 1.2 if errors else 1) ax.set_ylabel("Error (%)") ax.set_xlabel("Parameter") ax.set_title("Individual Error per Parameter") # Convert Matplotlib plot to PIL image buf = io.BytesIO() plt.tight_layout() plt.savefig(buf, format='png') plt.close(fig) buf.seek(0) img = Image.open(buf) return gr.update(value=img, visible=True) # Function to download all results in a zipped file: image + text summary def download_all_results(a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2, avg_error_text, error_plot): # Skip download if no valid error if "N/A" in avg_error_text: return None, gr.update(value="") import os, zipfile, tempfile from PIL import Image # Create temporary directory to store files before zipping temp_dir = tempfile.mkdtemp() zip_path = os.path.join(tempfile.gettempdir(), "error_all_results.zip") with zipfile.ZipFile(zip_path, "w") as zipf: # Save images (only error plot for now) image_dict = {"error plot": error_plot} for name, img in image_dict.items(): if isinstance(img, np.ndarray): img = Image.fromarray(img) if isinstance(img, Image.Image): img_path = os.path.join(temp_dir, f"{name}.png") img.save(img_path) zipf.write(img_path, arcname=f"{name}.png") # Save a detailed text summary of measurements text_lines = [ "๐ Measurement Summary", "----------------------", "๐ Predicted Values:", f"a: {a1}", f"b: {b1}", f"c: {c1}", f"d: {d1}", f"e: {e1}", f"f: {f1}", f"g: {g1}", f"h: {h1}", "", "๐ Actual Values:", f"a: {a2}", f"b: {b2}", f"c: {c2}", f"d: {d2}", f"e: {e2}", f"f: {f2}", f"g: {g2}", f"h: {h2}", "", "๐ Average Error:", f"average error %: {avg_error_text}", ] summary_path = os.path.join(temp_dir, "error_analysis.txt") with open(summary_path, "w", encoding="utf-8") as f: f.write("\n".join(text_lines)) zipf.write(summary_path, arcname="error_analysis.txt") return zip_path, gr.update(value="") # Import necessary libraries import matplotlib.pyplot as plt # For creating plots and figures import matplotlib.patches as patches # For drawing polygons and ellipses on plot import numpy as np # For numerical operations (array min/max etc.) from PIL import Image # For image I/O with Pillow import os # For interacting with the file system def draw_shapes_with_zorder( polygons_data=None, # List of dictionaries representing polygons (each with 'points', 'facecolor', etc.) ellipses_data=None, # List of dictionaries representing ellipses (each with 'center_x', 'width', etc.) res_stat=True, # Boolean to control whether to draw anything step=0, # Vertical shift step (multiplied by reference ratio) out_ref_ratios_p5=None, # Dictionary with reference ratios, e.g., {'closed': 1.0} filename="chasis_image.png", # Output filename for the saved figure fig_size=(18.1, 15), # Size of the matplotlib figure transparent_bg=False # Whether to make the saved image have transparent background ): # If result status is False, return None and skip drawing if not res_stat: return None # Compute vertical shift based on the 'step' and 'closed' reference ratio stepsize = step * out_ref_ratios_p5.get('closed', 1.0) if out_ref_ratios_p5 else 1.0 # Create figure and axis for drawing fig, ax = plt.subplots(figsize=fig_size) ax.set_aspect('equal', adjustable='box') # Maintain equal aspect ratio ax.set_axis_off() # Hide axes ax.set_facecolor('#f0f0f0') # Light gray background # Initialize bounding box tracking variables min_x, max_x = float('inf'), float('-inf') min_y, max_y = float('inf'), float('-inf') # List to store all coordinates for bounding box computation all_coords = [] # ----------------- Draw Polygons ----------------- if polygons_data: for poly_info in polygons_data: points = poly_info.get('points') # Get points of the polygon if points: # Apply vertical shift (stepsize) to all y-coordinates shifted_points = [(x, y + stepsize) for x, y in points] all_coords.extend(shifted_points) # Store for bounds computation # Create and style the polygon patch polygon = patches.Polygon( shifted_points, closed=True, facecolor=poly_info.get('facecolor', '#ADD8E6'), # Default face color = light blue edgecolor=poly_info.get('edgecolor', 'blue'), # Default edge color = blue linewidth=poly_info.get('linewidth', 2), # Default border width zorder=poly_info.get('zorder', 1) # Layering order ) ax.add_patch(polygon) # Add polygon to plot # ----------------- Draw Ellipses ----------------- if ellipses_data: for ellipse_info in ellipses_data: # Extract and apply vertical shift to ellipse center e_cx = ellipse_info.get('center_x') e_cy = ellipse_info.get('center_y') + stepsize e_w = ellipse_info.get('width') e_h = ellipse_info.get('height') e_angle = ellipse_info.get('angle', 0) # Skip if any critical ellipse parameter is missing if e_cx is None or e_cy is None or e_w is None or e_h is None: continue # Add ellipse bounding box corners to coord list for computing limits all_coords.append((e_cx - e_w / 2, e_cy - e_h / 2)) all_coords.append((e_cx + e_w / 2, e_cy + e_h / 2)) all_coords.append((e_cx - e_w / 2, e_cy + e_h / 2)) all_coords.append((e_cx + e_w / 2, e_cy - e_h / 2)) # Create and style the ellipse patch ellipse = patches.Ellipse( (e_cx, e_cy), e_w, e_h, angle=e_angle, facecolor=ellipse_info.get('facecolor', '#FFD700'), # Default face color = gold edgecolor=ellipse_info.get('edgecolor', 'orange'), # Default edge color = orange linewidth=ellipse_info.get('linewidth', 2), # Default border width zorder=ellipse_info.get('zorder', 1) # Layering order ) ax.add_patch(ellipse) # Add ellipse to plot # ----------------- Auto-fit Axis Limits ----------------- if all_coords: coords_array = np.array(all_coords) # Convert list to NumPy array min_x, min_y = np.min(coords_array, axis=0) # Find min x, y max_x, max_y = np.max(coords_array, axis=0) # Find max x, y # Add 20% padding around bounding box padding_x = (max_x - min_x) * 0.2 if (max_x - min_x) > 0 else 1.0 padding_y = (max_y - min_y) * 0.2 if (max_y - min_y) > 0 else 1.0 # Set axis limits accordingly ax.set_xlim(min_x - padding_x, max_x + padding_x) ax.set_ylim(min_y - padding_y, max_y + padding_y) else: # Default axis range when no shapes present ax.set_xlim(0, 10) ax.set_ylim(0, 8) # ----------------- Save and Return Image ----------------- plt.savefig(filename, dpi=300, bbox_inches='tight', pad_inches=0, transparent=transparent_bg) # Save figure plt.close(fig) # Always close the figure to avoid memory leaks res = Image.open(filename) # Open saved image as PIL.Image return gr.update(visible=True, value=res) # Return it in a Gradio-compatible format # State to store multiple polygon definitions my_polygons = gr.State([ { 'points': [(0.56, 14.72), (0.56, 19.05), (19.5, 19.05), (19.5, 14.53)], 'facecolor': "#E6ADAD", # Fill color 'edgecolor': 'brown', # Border color 'linewidth': 0, # No border line 'zorder': 2 # Drawing order }, { 'points': [(3, 14.72), (3, 9.45), (17, 9.45), (17, 14.6)], 'facecolor': '#E6ADAD', 'edgecolor': 'darkgreen', 'linewidth': 0, 'zorder': 1 }, { 'points': [(4.24, 18.74), (9.68, 18.74), (9.68, 17.34), (4.24, 17.34)], 'facecolor': '#f0f0f0', 'edgecolor': 'brown', 'linewidth': 0, 'zorder': 4 }, { 'points': [(12.25, 18.74), (15.88, 18.74), (15.88, 17.34), (12.25, 17.34)], 'facecolor': '#f0f0f0', 'edgecolor': 'darkgreen', 'linewidth': 0, 'zorder': 4 } ]) # State to store multiple ellipse definitions my_ellipses = gr.State([ { 'center_x': 10.0, 'center_y': 9.45, 'width': 9.0, 'height': 12.0, 'angle': 0, 'facecolor': '#f0f0f0', 'edgecolor': 'blue', 'linewidth': 0, 'zorder': 4 }, { 'center_x': 10.0, 'center_y': 9.45, 'width': 14.0, 'height': 17.0, 'angle': 0, 'facecolor': '#E6ADAD', 'edgecolor': 'purple', 'linewidth': 0, 'zorder': 3 } ]) # Required imports import matplotlib.pyplot as plt import matplotlib.patches as patches import numpy as np from PIL import Image import os # ========================= # Function to draw the custom shapes (polygons & ellipses) with vertical shift support # ========================= def draw_shapes_with_zorder2( a,b,g, polygons_data=None, # List of polygon specs ellipses_data=None, # List of ellipse specs res_stat=True, # Whether to proceed with drawing or not step=0, # Step used to calculate vertical shift out_ref_ratios_p5=None, # Dictionary for px/cm conversion for scaling filename="toilet_image.png", # Output image file path title="Custom Shapes Drawing" # Plot title (unused but helpful for debug) ): if not res_stat: return None # If drawing is disabled, return nothing # Setup figure and axes for plotting fig, ax = plt.subplots(figsize=(20, 15)) ax.set_axis_off() # Remove axis lines ax.set_aspect('equal', adjustable='box') # Keep equal aspect ratio ax.set_facecolor('#f0f0f0') # Background color # Determine vertical pixel shift from step using reference ratio ratio = out_ref_ratios_p5.get("closed", 1.0) if out_ref_ratios_p5 else 1.0 vertical_shift = int(step * ratio) # Bounding box variables min_x, max_x = float('inf'), float('-inf') min_y, max_y = float('inf'), float('-inf') # Draw polygons if provided if polygons_data: for poly_info in polygons_data: points = poly_info.get('points') if not points: continue # Skip if no points defined # Shift each point vertically shifted_points = [(x, (y - vertical_shift)) for (x, y) in points] # Update bounding box for x, y in shifted_points: min_x = min(min_x, x) max_x = max(max_x, x) min_y = min(min_y, y) max_y = max(max_y, y) # Create and add polygon patch polygon = patches.Polygon( shifted_points, closed=True, facecolor=poly_info.get('facecolor', '#ADD8E6'), edgecolor=poly_info.get('edgecolor', 'blue'), linewidth=poly_info.get('linewidth', 2), label=poly_info.get('label', 'Polygon'), zorder=poly_info.get('zorder', 1) ) ax.add_patch(polygon) # Draw ellipses if provided if ellipses_data: for ellipse_info in ellipses_data: e_cx = ellipse_info.get('center_x') e_cy = ellipse_info.get('center_y') e_w = ellipse_info.get('width') e_h = ellipse_info.get('height') e_angle = ellipse_info.get('angle', 0) # Skip if required values are missing if e_cx is None or e_cy is None or e_w is None or e_h is None: continue # Apply vertical shift e_cy_shifted = e_cy - vertical_shift # Update bounding box min_x = min(min_x, e_cx - e_w / 2) max_x = max(max_x, e_cx + e_w / 2) min_y = min(min_y, e_cy_shifted - e_h / 2) max_y = max(max_y, e_cy_shifted + e_h / 2) # Create and add ellipse patch ellipse = patches.Ellipse( (e_cx, e_cy_shifted), e_w, e_h, angle=e_angle, facecolor=ellipse_info.get('facecolor', '#f0f0f0'), edgecolor=ellipse_info.get('edgecolor', '#f0f0f0'), linewidth=ellipse_info.get('linewidth', 2), label=ellipse_info.get('label', 'Ellipse'), zorder=ellipse_info.get('zorder', 1) ) ax.add_patch(ellipse) # Adjust axis limits using computed bounding box if min_x != float('inf') and max_x != float('-inf'): padding_x = (max_x - min_x) * 0.2 if (max_x - min_x) > 0 else 1.0 padding_y = (max_y - min_y) * 0.2 if (max_y - min_y) > 0 else 1.0 ax.set_xlim(min_x - padding_x, max_x + padding_x) ax.set_ylim(min_y - padding_y, max_y + padding_y) else: # Default limits if nothing was drawn ax.set_xlim(0, 10) ax.set_ylim(0, 8) # Save the resulting image to disk plt.savefig(filename, dpi=300, bbox_inches='tight') plt.close(fig) # Load saved image into PIL and return for Gradio UI res = Image.open(filename) return gr.update(visible=True, value=res) # Import symbolic computation tools from sympy import sympy from sympy import symbols, Eq, solve, N # Function to find the points of tangency from an external point (px, py) # to an ellipse centered at (cx, cy) with axes lengths a and b def find_tangent_points_from_external(px, py, cx, cy, a, b): # Define symbolic variables x, y = symbols('x y') # Ellipse equation in canonical form ellipse_eq_sym = Eq((x - cx)**2 / a**2 + (y - cy)**2 / b**2, 1) # Equation for polar line from point (px, py) to the ellipse # This represents the tangent condition polar_eq_sym = Eq( (x - cx) * (px - cx) / a**2 + (y - cy) * (py - cy) / b**2, 1 ) # Solve the system of equations: ellipse + polar line solutions = solve([ellipse_eq_sym, polar_eq_sym], (x, y)) tangent_points = [] for sol in solutions: # Two types of solutions might be returned: dict or tuple if isinstance(sol, dict): # If solution is a dictionary {x: val_x, y: val_y} if all(s.is_real for s in sol.values()): # Only accept real solutions tangent_points.append((float(N(sol[x])), float(N(sol[y])))) elif isinstance(sol, tuple) and len(sol) == 2: # If solution is a tuple (val_x, val_y) if sol[0].is_real and sol[1].is_real: tangent_points.append((float(N(sol[0])), float(N(sol[1])))) # Return the list of real tangent points return tangent_points # Wrapper function to compute one "left" and one "right" tangent point # from two external points to a given ellipse def get_specific_tangent_points( center_x, center_y, width, height, point1_x, point1_y, point2_x, point2_y ): # Convert width and height to ellipse semi-axes a = width / 2 b = height / 2 # Find all tangent points from external point 1 tangent_points_p1 = find_tangent_points_from_external(point1_x, point1_y, center_x, center_y, a, b) # Find all tangent points from external point 2 tangent_points_p2 = find_tangent_points_from_external(point2_x, point2_y, center_x, center_y, a, b) # --- Select the "left" tangent point for point 1 --- tangent_points_p1.sort(key=lambda p: p[0]) # Sort by x to pick leftmost selected_tangent_point_p1 = tangent_points_p1[0] # --- Select the "right" tangent point for point 2 --- tangent_points_p2.sort(key=lambda p: p[0]) # Sort by x to pick rightmost selected_tangent_point_p2 = tangent_points_p2[1] # Return the selected tangent points return selected_tangent_point_p2, selected_tangent_point_p1 # --- Main function for drawing and guiding polygon visualization --- def guide(a2, b2, c2, d2, e2, f2, g2, h2, res_stat): # If toggle is off, return nothing if not res_stat: return None, None # Convert all inputs to float for computation a2, b2, c2, d2, e2, f2, g2, h2 = float(a2), float(b2), float(c2), float(d2), float(e2), float(f2), float(g2), float(h2) # Define fixed center for ellipse ellipse_center_x = 10 ellipse_center_y = 10 # Ellipse dimensions ellipse_width = f2 # major axis length (a*2) ellipse_height = g2 # minor axis length (b*2) # Define external points above the ellipse for tangents point_1_x, point_1_y = 10 - (h2/2), 10 + (e2/2) + a2 + b2 point_2_x, point_2_y = 10 + (h2/2), 10 + (e2/2) + a2 + b2 # Get the left and right tangent points tangent_points = get_specific_tangent_points( ellipse_center_x, ellipse_center_y, ellipse_width, ellipse_height, point_1_x, point_1_y, point_2_x, point_2_y ) # Define polygon (irregular quadrilateral) from external and tangent points polygons = [ { 'points': [ (10 - (h2/2), 10 + (e2/2) + a2 + b2), # External point 1 (left) (10 + (h2/2), 10 + (e2/2) + a2 + b2), # External point 2 (right) (tangent_points[0][0], tangent_points[0][1]), # Right tangent (tangent_points[1][0], tangent_points[1][1]) # Left tangent ], 'facecolor': '#ADD8E6', # Light blue fill 'edgecolor': 'brown', # Border color 'linewidth': 0, # No border line 'label': 'Irregular Quad', 'zorder': 2 # Above other shapes with lower zorder } ] # Define ellipse shapes to overlay on canvas ellipses = [ { 'center_x': 10.0, 'center_y': 10, 'width': d2, # Outer ellipse width 'height': e2, # Outer ellipse height 'angle': 0, 'facecolor': '#f0f0f0', # Very light gray 'edgecolor': 'blue', 'linewidth': 0, 'label': 'Rotated Ellipse', 'zorder': 4 # Top-most }, { 'center_x': 10.0, 'center_y': 10, 'width': f2, # Inner ellipse width 'height': g2, # Inner ellipse height 'angle': 0, 'facecolor': '#ADD8E6', # Light blue 'edgecolor': 'purple', 'linewidth': 0, 'label': 'Small Circle', 'zorder': 3 }, { 'center_x': 10 - (c2/2) + 0.75, 'center_y': 10 + (e2/2) + b2, 'width': 0.75, 'height': 0.75, 'angle': 0, 'facecolor': '#f0f0f0', 'edgecolor': 'blue', 'linewidth': 0, 'label': 'Rotated Ellipse', 'zorder': 4 }, { 'center_x': 10 + (c2/2) - 0.75, 'center_y': 10 + (e2/2) + b2, 'width': 0.75, 'height': 0.75, 'angle': 0, 'facecolor': '#f0f0f0', 'edgecolor': 'purple', 'linewidth': 0, 'label': 'Small Circle', 'zorder': 4 } ] # Return both shapes: polygon and ellipses return polygons, ellipses from PIL import Image # Function to overlay two images (foreground onto background) with centering and opacity def overlay_images_centered(background_path, foreground_path, res_stat, step, out_ref_ratios_p5): if not res_stat: # If product doesn't fit, skip overlay return None # Convert NumPy arrays to PIL Images and ensure they're in RGBA format bg = Image.fromarray(background_path).convert("RGBA") fg = Image.fromarray(foreground_path).convert("RGBA") # Make specific light gray color (#f0f0f0) transparent in the foreground new_data = [ (255, 255, 255, 0) if pixel[:3] == (240, 240, 240) else pixel for pixel in fg.getdata() ] fg.putdata(new_data) # Get width and height of both images bg_w, bg_h = bg.size fg_w, fg_h = fg.size # Retrieve real-world px/cm ratio from previous part for accurate shift ratio = out_ref_ratios_p5.get("closed", 1.0) # Vertical shift is scaled by this ratio to maintain real-world measurement vertical_shift = int(step * ratio) # Create a transparent canvas same size as background shifted_fg = Image.new("RGBA", bg.size, (255, 255, 255, 0)) # Calculate position to center the foreground and shift vertically upwards pos_x = (bg_w - fg_w) // 2 pos_y = (bg_h - fg_h) // 2 - vertical_shift shifted_fg.paste(fg, (pos_x, pos_y), fg) # Paste using alpha mask # Reduce opacity of foreground to 50% by adjusting the alpha channel alpha = shifted_fg.split()[3].point(lambda a: int(a * 0.5)) shifted_fg.putalpha(alpha) # Overlay the semi-transparent foreground onto the background result = Image.alpha_composite(bg, shifted_fg).convert("RGB") # Return the final image and make it visible in the UI return gr.update(visible=True, value=result) # Logic to determine if product fits user's washroom based on input parameters def checkstatus(b2, c2, g2): hole_rad = 0.375 # Radius of the reference hole (in inches) max_range = 18.1 # Maximum allowed top clearance (inches) # Calculate top and bottom vertical bounds of fit area top_val = max_range - 0.31 - hole_rad bottom_val = top_val - 1.4 + hole_rad * 2 # Minimum and maximum acceptable width values (inches) min_width = 4.5 + hole_rad * 2 max_width = 11.52 - hole_rad * 2 # Loop through step values from 0.0 to 0.5 in 0.05 increments max_step = 0.5 step_size = 0.05 steps = int(max_step / step_size) + 1 # Includes step=0 for i in range(steps): step = i * step_size # Check if input values fall within allowed fit boundaries if float(bottom_val) < float(g2) + float(b2) + float(step) < float(top_val) and float(min_width) < float(c2) < float(max_width): msg = "โ Our Product can fit in your Washroom." return gr.update(value=msg, visible=True), True, step # If not within any valid range, declare not fit msg = "โ Unfortunately we cannot fit our product in your washroom." return gr.update(value=msg, visible=True), False, None # On submit: compare measurements, generate JSON, and plot error graph submit_btn.click( fn=lambda *args: ( compare_measurements(*args)[0], # JSON result compare_measurements(*args)[1], # Average error text (plot_error_graph(*args)) # Error plot ), inputs=[a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2], outputs=[result_json, avg_error_text, error_plot] ).\ then(fn=download_all_results, # Then: prepare downloadable zip with results and update progress inputs=[a1, b1, c1, d1, e1, f1, g1, h1, a2, b2, c2, d2, e2, f2, g2, h2, avg_error_text, error_plot], outputs=[download_all_error, progress]).\ then(checkstatus, # Then: check if the product fits and determine optimal vertical step inputs=[b2, c2, g2], outputs=[result_status, res_stat, step]).\ then(fn=draw_shapes_with_zorder, # Then: draw background result image using input shape data inputs=[my_polygons, my_ellipses, res_stat, step, out_ref_ratios_p5], outputs=[result_image1]).\ then(fn=guide, # Then: generate guide overlay polygons and ellipses for fitting aid inputs=[a2, b2, c2, d2, e2, f2, g2, h2, res_stat], outputs=[polygons, ellipses]).\ then(fn=draw_shapes_with_zorder2, # Then: draw the guide shapes onto the second image inputs=[a2, b2, g2, polygons, ellipses, res_stat, step, out_ref_ratios_p5], outputs=[result_image2]).\ then(fn=overlay_images_centered, # Then: overlay guide image onto base result to create final preview inputs=[result_image1, result_image2, res_stat, step, out_ref_ratios_p5], outputs=[result_image3]) # On submit, also reveal the general error message area in UI submit_btn.click(fn=lambda: gr.update(visible=True), inputs=[], outputs=[error]) def download_all_results_combined( input1, input2, input3, gallery_segmentation_p4, out_ref_image_p5, ref_ratios_str, out_rim_image_p6, seat_measurement_str, out_rimellipse_image_p7, rim_measurement_str, rimheight_image_p8, rim_height_str, holewidth_image_p9, hole_width_str, rim_to_hole_img_p10, hole_to_top_str, ellipse_viz_image_p10, direction_str, rim_height_vis_p11, total_height_str, remaining_lid_img_p12, remaining_str, out_image, top_width_str, a1, b1, c1, d1, e1, f1, g1, h1 ): import os, zipfile, tempfile from PIL import Image # Create a temporary directory to store intermediate image/text files temp_dir = tempfile.mkdtemp() # Define the path for the final zip file inside system temp directory zip_path = os.path.join(tempfile.gettempdir(), "all_results.zip") # Start creating a zip archive with zipfile.ZipFile(zip_path, "w") as zipf: # Dictionary mapping variable names to image objects (can be PIL or NumPy arrays) image_dict = { "input1": input1, "input2": input2, "input3": input3, "gallery_segmentation_p4": gallery_segmentation_p4, "out_ref_image_p5": out_ref_image_p5, "out_rim_image_p6": out_rim_image_p6, "out_rimellipse_image_p7": out_rimellipse_image_p7, "rimheight_image_p8": rimheight_image_p8, "holewidth_image_p9": holewidth_image_p9, "rim_to_hole_img_p10": rim_to_hole_img_p10, "ellipse_viz_image_p10": ellipse_viz_image_p10, "rim_height_vis_p11": rim_height_vis_p11, "remaining_lid_img_p12": remaining_lid_img_p12, "out_image": out_image, } # Loop through each image, save it as PNG if valid, and add it to the zip archive for name, img in image_dict.items(): if isinstance(img, np.ndarray): img = Image.fromarray(img) # Convert NumPy array to PIL Image if needed if isinstance(img, Image.Image): # Only process if it's a valid PIL Image img_path = os.path.join(temp_dir, f"{name}.png") # Temp image path img.save(img_path) # Save image to temp directory zipf.write(img_path, arcname=f"{name}.png") # Add to zip with name.png # Prepare a text summary with all the measurement-related strings and variables text_lines = [ "๐ Measurement Summary", "----------------------", f"ref_ratios_str:\n{ref_ratios_str}", # Reference ratios from Part 5 f"seat_measurement_str:\n{seat_measurement_str}", # Seat width (Part 6) f"rim_measurement_str:\n{rim_measurement_str}", # Rim ellipse (Part 7) f"rim_height_str:\n{rim_height_str}", # Rim height (Part 8) f"hole_width_str:\n{hole_width_str}", # Hole width (Part 9) f"hole_to_top_str:\n{hole_to_top_str}", # Rim to top (Part 10) f"direction_str:\n{direction_str}", # Direction analysis (Part 10) f"total_height_str:\n{total_height_str}", # Rim-to-ground height (Part 11) f"remaining_str:\n{remaining_str}", # Remaining lid space (Part 12) f"top_width_str:\n{top_width_str}", # Top width (Part 13) "", "๐ Predicted Values:", # Model-predicted values f"a: {a1}", f"b: {b1}", f"c: {c1}", f"d: {d1}", f"e: {e1}", f"f: {f1}", f"g: {g1}", f"h: {h1}", "", ] # Save the text summary into a .txt file inside the temporary directory summary_path = os.path.join(temp_dir, "summary.txt") with open(summary_path, "w", encoding="utf-8") as f: f.write("\n".join(text_lines)) # Write summary contents line-by-line # Add the summary.txt file to the zip archive zipf.write(summary_path, arcname="summary.txt") # Return path to the created zip file and reset a Gradio state (usually download message) return zip_path, gr.update(value="") # ๐ Pipeline chaining starting from Part 4 run_pipeline_btn.click(fn=segment_and_overlay_all_p4, # Step 1: Segment and overlay masks for all models inputs=[ input1, # Open seat image input2, # Open no seat image input3, # Closed lid image models_holes_p2, # Hole model models_rim_p2, # Rim model models_coinref_p2, # Coin/matchbox reference model device_p2 # Device: 'cuda' or 'cpu' ], outputs=[ gallery_segmentation_p4, # Gallery showing overlays binary_masks_p4, # Segmentation masks image_dict_p4, # Rotated/processed images ]).\ then(fn=detect_and_plot_reference_p5, # Step 2: Detect coin/matchbox and calculate px/cm ratios inputs=[ image_dict_p4, binary_masks_p4 ], outputs=[ out_ref_image_p5, # Overlay image with reference out_ref_ratios_p5, # Dictionary with px/cm ratios ref_ratios_str, # Stringified ratios for display ref # Raw reference size ]).\ then(fn=analyze_rim_intersections_p6, # Step 3: Measure open seat width (ellipse intersect) inputs=[ image_dict_p4, binary_masks_p4, out_ref_ratios_p5 ], outputs=[ out_rim_image_p6, # Image with measurement lines out_rim_measurements_p6, # Raw measurements in px out_rim_measurements_cm_p6, # Converted to cm out_rim_measurements_inch_p6, # Converted to inches seat_measurement_str, # Display string for seat seat # Measurement object ]).\ then(fn=analyze_rim_intersections_p7, # Step 4: Measure inner ellipse rim width inputs=[ image_dict_p4, binary_masks_p4, out_ref_ratios_p5 ], outputs=[ out_rimellipse_image_p7, # Visual result out_rimellipse_ui_p7, # UI plot out_rimellipse_cm_p7, # cm value out_rimellipse_inch_p7, # inch value inner_top_p7, # Topmost point of inner rim dir_down_p7, # Direction vector (down) dir_right_p7, # Direction vector (right) rim_measurement_str, # Display string rim # Measurement object ]).\ then(fn=run_rim_height_analysis_p8, # Step 5: Analyze rim height using open seat image inputs=[ btn_rimheight_p8, # Dummy trigger button image_dict_p4, binary_masks_p4, out_ref_ratios_p5, inner_top_p7, dir_down_p7 ], outputs=[ rimheight_text_p8, # Textual explanation rimheight_image_p8, # Annotated image rim_height_px_p8, # px height rim_height_cm_p8, # cm height rim_height_inch_p8, # inch height inner_top_p8, # Same as p7, but passed onward rim_height_str, # Display string inlen # Measurement object ]).\ then(fn=analyze_hole_width_perpendicular_p9, # Step 6: Measure hole width perpendicular to rim inputs=[ btn_measure_holewidth_p9, # Dummy trigger binary_masks_p4, out_ref_ratios_p5, image_dict_p4, dir_right_p7, inner_top_p7 ], outputs=[ holewidth_image_p9, # Annotated result hole_width_px_p9, hole_width_cm_p9, hole_width_inch_p9, angle_deg_p9, pt_min_p9, # Leftmost point pt_max_p9, # Rightmost point hole_width_str, hw ]).\ then(fn=compute_top_to_hole_distance_p10, # Step 7: Vertical distance between top rim and hole inputs=[ btn_top_to_hole_p10, # Trigger inner_top_p8, pt_min_p9, pt_max_p9, dir_down_p7, out_ref_ratios_p5, image_dict_p4 ], outputs=[ rim_to_hole_img_p10, # Annotated image top_to_hole_line_px_p10, top_to_hole_line_cm_p10, top_to_hole_line_inch_p10, angle_down_deg_p10, angle_perp_deg_p10, intersection_point_p10, hole_to_top_str, hr ]).\ then(fn=analyze_closed_rim_orientation_p10, # Step 8: Fit ellipse on closed rim to get orientation inputs=[ btn_ellipse_orient_p10, binary_masks_p4, image_dict_p4 ], outputs=[ ellipse_viz_image_p10, # Ellipse fit result ellipse_angle_deg_p10, # Angle of major axis ellipse_center_p10, ellipse_dir_down_p10, # New downward vector ellipse_dir_right_p10, # New rightward vector updated_binary_masks_p4, # Updated with filtered contours direction_str, cl ]).\ then(fn=analyze_rim_height_on_closed_p11, # Step 9: Estimate full rim height using closed image inputs=[ btn_rim_height_closed_p11, binary_masks_p4, input3, ellipse_angle_deg_p10, out_ref_ratios_p5, rim_height_cm_p8 ], outputs=[ rim_height_vis_p11, rim_height_cm_p11, rim_height_in_p11, full_rim_height_cm_p11, full_rim_height_in_p11, closed_remaining_cm_p11, closed_remaining_in_p11, pt_start_p11, total_height_str, th ]).\ then(fn=draw_remaining_closed_portion_p12, # Step 10: Draw estimated remaining closed portion inputs=[ btn_draw_remaining_p12, pt_start_p11, closed_remaining_cm_p11, out_ref_ratios_p5, ellipse_dir_down_p10, input3 ], outputs=[ remaining_lid_img_p12, remaining_angle_deg_p12, pt_start_p12, pt_end_p12, remaining_str, tt ]).\ then(fn=analyze_top_rim_width_p13, # Step 11: Measure top closed rim width inputs=[ btn_measure, binary_masks_p4, ellipse_center_p10, ellipse_angle_deg_p10, out_ref_ratios_p5, input3, ], outputs=[ out_image, out_pt_top, out_pt1, out_pt2, out_dist_px, out_dist_cm, out_dist_in, out_angle_deg, top_width_str, wt ]).\ then(fn=update_preds, # Step 12: Compute final values and update UI inputs=[ unit_dropdown, closed_remaining_in_p11, top_to_hole_line_inch_p10, hole_width_inch_p9, out_rimellipse_inch_p7, rim_height_inch_p8, out_dist_in ], outputs=[ a1, b1, c1, d1, e1, f1, g1, h1, process ]).\ then(fn=download_all_results_combined, # Step 13: Prepare downloadable zip inputs=[ input1, input2, input3, gallery_segmentation_p4, out_ref_image_p5, ref_ratios_str, out_rim_image_p6, seat_measurement_str, out_rimellipse_image_p7, rim_measurement_str, rimheight_image_p8, rim_height_str, holewidth_image_p9, hole_width_str, rim_to_hole_img_p10, hole_to_top_str, ellipse_viz_image_p10, direction_str, rim_height_vis_p11, total_height_str, remaining_lid_img_p12, remaining_str, out_image, top_width_str, a1, b1, c1, d1, e1, f1, g1, h1 ], outputs=[download_all_file, progress] ).\ then(checkstatus, # Step 15: Verify that all required outputs are valid inputs=[b1, c1, g1], outputs=[result_status1, res_stat1, step1] ).\ then(fn=draw_shapes_with_zorder, # Step 16: Draw polygons/ellipses (step 1) inputs=[my_polygons, my_ellipses, res_stat1, step1, out_ref_ratios_p5], outputs=[result_image11] ).\ then(fn=guide, # Step 17: Generate additional shapes to draw (step 2) inputs=[a1, b1, c1, d1, e1, f1, g1, h1, res_stat1], outputs=[polygons1, ellipses1] ).\ then(fn=draw_shapes_with_zorder2, # Step 18: Draw second layer of shapes inputs=[a1, b1, g1, polygons1, ellipses1, res_stat1, step1, out_ref_ratios_p5], outputs=[result_image21] ).\ then(fn=overlay_images_centered, # Step 19: Final overlay combining result_image11 and 21 inputs=[result_image11, result_image21, res_stat1, step1, out_ref_ratios_p5], outputs=[result_image31]) # Create a Gradio Blocks interface titled "๐งช Toilet Segmentation & Measurement App" with gr.Blocks(title="๐งช Toilet Segmentation & Measurement App") as full_app_interface: # Define a Gradio State to store whether the user is authenticated or not #auth_state = gr.State(False) # Initially set to False (not logged in) # ================================ # ๐ LOGIN SECTION # ================================ # with gr.Column("๐ Login") as login: # # Section Title # gr.Markdown("### Login", elem_id='centered-title') # # Email input row # with gr.Row(): # with gr.Column(): # Left spacer # filler = '' # with gr.Column(): # Center column with email input # email = gr.Text(label="Email") # with gr.Column(): # Right spacer # filler = '' # # Password input row # with gr.Row(): # with gr.Column(): # Left spacer # filler = '' # with gr.Column(): # Center column with password input # password = gr.Text(label="Password", type="password") # with gr.Column(): # Right spacer # filler = '' # # Login button row # with gr.Row(): # with gr.Column(): # Left spacer # filler = '' # with gr.Column(): # Center column with Login button # login_btn = gr.Button("Login") # with gr.Column(): # Right spacer # filler = '' # # Authentication status message row # with gr.Row(): # with gr.Column(): # Left spacer # filler = '' # with gr.Column(): # Center column with status message # auth_msg = gr.Textbox(label="Status", interactive=False) # with gr.Column(): # Right spacer # filler = '' # # Function to handle login logic # def handle_login(email, password): # # Call Supabase login helper with credentials # msg, success = supa_login(email, password) # # Return: message, show main app if success, update state, hide login if success # return msg, gr.update(visible=success), success, gr.update(visible=not success) # ================================ # ๐ MAIN APP SECTION (hidden by default) # ================================ with gr.Column(visible=True) as protected_content: launch_main_app() # Call to external function that builds the full app # # Attach login button to login handler # login_btn.click( # handle_login, # inputs=[email, password], # outputs=[auth_msg, protected_content, auth_state, login] # ) # # Automatically toggle visibility of main app when authentication state changes # def toggle_app(auth): # return gr.update(visible=auth) # # Bind state change to toggle visibility # auth_state.change(toggle_app, inputs=auth_state, outputs=protected_content) # ================================ # ๐ป FOOTER SECTION # ================================ with gr.Row(elem_id='custom_footer'): with gr.Column(): # Line 1 - Centered contact info gr.Markdown(" ") gr.Markdown(" ") gr.Markdown( "