Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import cv2 | |
| import numpy as np | |
| import pandas as pd | |
| import os | |
| def process_image(image, txt_file, crop_size, blur_kernel_size, clahe_clip_limit, clahe_tile_grid_size, adaptive_thresh_block_size, adaptive_thresh_c, close_kernel_size_morph, open_kernel_size_morph, min_contour_area, max_contour_area, min_circularity, max_circularity, selection_method, px_per_um, manual_offset_x, manual_offset_y): | |
| # Convert Gradio image (RGB) to grayscale if it\\'s not already | |
| if len(image.shape) == 3 and image.shape[2] == 3: | |
| image_gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY) | |
| else: | |
| image_gray = image # Assume it\\\'s already grayscale | |
| # Step 1: Crop to Center to Focus on Small Cell | |
| height, width = image_gray.shape | |
| center_x, center_y = width // 2, height // 2 | |
| y_start = max(0, center_y - crop_size // 2) | |
| x_start = max(0, center_x - crop_size // 2) | |
| y_end = min(height, center_y + crop_size // 2) | |
| x_end = min(width, center_x + crop_size // 2) | |
| img_crop = image_gray[y_start:y_end, x_start:x_end] | |
| # Step 2: Enhance Contrast (CLAHE) | |
| clahe = cv2.createCLAHE(clipLimit=clahe_clip_limit, tileGridSize=(clahe_tile_grid_size, clahe_tile_grid_size)) | |
| enhanced_crop = clahe.apply(img_crop) | |
| # Step 3: Apply Gaussian blur to smooth and highlight contrasts | |
| # This blurred_crop is used for adaptive thresholding in Colab | |
| blurred_crop = cv2.GaussianBlur(enhanced_crop, (blur_kernel_size, blur_kernel_size), 0) | |
| # Step 4: Adaptive Thresholding | |
| thresh = cv2.adaptiveThreshold(blurred_crop, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, | |
| cv2.THRESH_BINARY_INV, adaptive_thresh_block_size, adaptive_thresh_c) | |
| # Step 5: Morphological Operations (after adaptive thresholding) | |
| close_kernel = np.ones((close_kernel_size_morph, close_kernel_size_morph), np.uint8) | |
| thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, close_kernel) | |
| open_kernel = np.ones((open_kernel_size_morph, open_kernel_size_morph), np.uint8) | |
| thresh = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, open_kernel) | |
| # Step 6: Connected Component Analysis (Crucial for isolating the main cell) | |
| num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, 8, cv2.CV_32S) | |
| best_component_mask = np.zeros_like(thresh, dtype=np.uint8) | |
| # Center of the cropped image for distance calculation, defined here for scope | |
| img_crop_center_x, img_crop_center_y = img_crop.shape[1] // 2, img_crop.shape[0] // 2 | |
| if num_labels > 1: # Exclude background | |
| max_area = 0 | |
| best_label = -1 | |
| for i in range(1, num_labels): # Iterate through components, skipping background (label 0) | |
| area = stats[i, cv2.CC_STAT_AREA] | |
| # Filter by area (adjust min/max as needed for cell size) | |
| if 200 < area < 200000: # These values are from Colab, can be exposed as Gradio sliders if needed | |
| # Calculate distance from centroid to image center | |
| centroid_x, centroid_y = centroids[i] | |
| distance = np.sqrt((centroid_x - img_crop_center_x)**2 + (centroid_y - img_crop_center_y)**2) | |
| # Prioritize components closer to the center and with reasonable area | |
| if area > max_area and distance < crop_size / 4: # Consider components within central quarter | |
| max_area = area | |
| best_label = i | |
| if best_label != -1: | |
| best_component_mask[labels == best_label] = 255 | |
| thresh = best_component_mask # Use the best component as the final thresholded image | |
| else: | |
| thresh = np.zeros_like(thresh) # No suitable component found | |
| else: | |
| thresh = np.zeros_like(thresh) # No components found (or only background) | |
| # Step 7: Find Contours on the (now clean) thresholded image | |
| contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
| filtered_contours = [] | |
| if contours: | |
| for i, cnt in enumerate(contours): | |
| area = cv2.contourArea(cnt) | |
| perimeter = cv2.arcLength(cnt, True) | |
| circularity = 0 | |
| if perimeter > 0: | |
| circularity = 4 * np.pi * (area / (perimeter * perimeter)) | |
| # Apply contour filtering parameters from Colab | |
| if min_contour_area < area < max_contour_area: | |
| if min_circularity < circularity < max_circularity: | |
| filtered_contours.append(cnt) | |
| # Step 8: Select Best Contour | |
| best_contour = None | |
| if filtered_contours: | |
| if selection_method == "Largest Area": | |
| best_contour = max(filtered_contours, key=cv2.contourArea) | |
| elif selection_method == "Most Circular": | |
| # Calculate circularity for each contour and find the one closest to 1 | |
| best_contour = min(filtered_contours, key=lambda cnt: abs(1 - (4 * np.pi * cv2.contourArea(cnt) / (cv2.arcLength(cnt, True)**2)))) | |
| elif selection_method == "Closest to Center": | |
| # img_crop_center_x, img_crop_center_y are already defined above | |
| def dist_to_center(cnt): | |
| M = cv2.moments(cnt) | |
| if M["m00"] == 0: return float("inf") | |
| cx = int(M["m10"] / M["m00"]) | |
| cy = int(M["m01"] / M["m00"]) | |
| return np.sqrt((cx - img_crop_center_x)**2 + (cy - img_crop_center_y)**2) | |
| best_contour = min(filtered_contours, key=dist_to_center) | |
| # Step 9: Final Visualization | |
| # Create a BGR image from the original (full size) for drawing | |
| final_image_display = cv2.cvtColor(image_gray, cv2.COLOR_GRAY2BGR) | |
| # Adjust best_contour to full image coordinates for drawing | |
| best_contour_full_coords = None | |
| ellipse_full_coords = None | |
| if best_contour is not None: | |
| # Shift contour points from cropped coordinates back to full image coordinates | |
| best_contour_full_coords = best_contour + np.array([x_start, y_start]) | |
| cv2.drawContours(final_image_display, [best_contour_full_coords], -1, (0, 0, 255), 2) # Draw in red | |
| if len(best_contour) >= 5: # Need at least 5 points to fit an ellipse | |
| ellipse = cv2.fitEllipse(best_contour) | |
| (ell_center_x, ell_center_y), (major, minor), angle = ellipse | |
| # Shift ellipse center from cropped coordinates back to full image coordinates | |
| ellipse_full_coords = ((ell_center_x + x_start, ell_center_y + y_start), (major, minor), angle) | |
| cv2.ellipse(final_image_display, ellipse_full_coords, (0, 255, 0), 2) # Draw in green | |
| # Process TXT file if provided | |
| modified_txt_output = None | |
| if txt_file is not None: | |
| df = pd.read_csv(txt_file.name, sep="\\s+", header=None) | |
| if df.shape[1] < 4: | |
| raise ValueError("TXT must have at least 4 columns: x_um, y_um, z_um/wavelength, count") | |
| df.columns = ["x_um", "y_um", "z_um", "count"] | |
| # Adjust y_um for large offset (normalize around 0 to ignore absolute stage position) | |
| df["y_um"] = df["y_um"] - df["y_um"].mean() | |
| # Calculate pixel coordinates for data points | |
| # Use ellipse center for offset if available, otherwise image center | |
| if ellipse_full_coords is not None: | |
| ellipse_center_x_full = ellipse_full_coords[0][0] | |
| ellipse_center_y_full = ellipse_full_coords[0][1] | |
| else: | |
| ellipse_center_x_full = width // 2 | |
| ellipse_center_y_full = height // 2 | |
| x_um_mean = df["x_um"].mean() + manual_offset_x | |
| y_um_mean = df["y_um"].mean() + manual_offset_y | |
| df["x_px"] = (df["x_um"] - x_um_mean) * px_per_um + ellipse_center_x_full | |
| df["y_px"] = (df["y_um"] - y_um_mean) * px_per_um + ellipse_center_y_full | |
| df["inside"] = df.apply(lambda row: cv2.pointPolygonTest(best_contour_full_coords, (row["x_px"], row["y_px"]), False) >= 0 if best_contour_full_coords is not None else False, axis=1) | |
| # Set \'count\' to 0 for points outside the contour | |
| df.loc[df["inside"] == False, "count"] = 0 | |
| for index, row in df.iterrows(): | |
| # Draw points on the full image | |
| color = (255, 0, 0) if row["inside"] else (0, 165, 255) # Blue for inside, Orange for outside | |
| cv2.circle(final_image_display, (int(row["x_px"]), int(row["y_px"])), 3, color, -1) | |
| # Save modified TXT | |
| modified_txt_path = "modified_streamline.txt" | |
| df_output = df[["x_um", "y_um", "z_um", "count"]].copy() # Ensure \'count\' column is the modified one | |
| df_output.to_csv(modified_txt_path, sep="\t", index=False, header=False, float_format="%.6f") | |
| modified_txt_output = modified_txt_path | |
| return final_image_display, modified_txt_output | |
| with gr.Blocks() as demo: | |
| gr.Markdown("## Cell Image Visualization Tool - Step by Step") | |
| gr.Markdown("Process your cell images step-by-step to detect boundaries and visualize data points.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### File Upload") | |
| image_input = gr.Image(type="numpy", label="Cell Image (BMP)") | |
| txt_input = gr.File(label="Data File (TXT)") | |
| gr.Markdown("### Step 1: Preprocess Image") | |
| with gr.Accordion("Adjust Preprocessing Parameters", open=True): | |
| crop_size = gr.Slider(100, 2000, value=1000, step=100, label="Crop Size (pixels)", info="Size of the square crop around the image center.") | |
| blur_kernel_size = gr.Slider(3, 15, step=2, value=7, label="Gaussian Blur Kernel Size (odd)", info="Size of the kernel for Gaussian blur. Must be odd.") | |
| clahe_clip_limit = gr.Slider(1, 100, value=50, step=1, label="CLAHE Clip Limit", info="Threshold for contrast limiting.") | |
| clahe_tile_grid_size = gr.Slider(2, 16, step=2, value=4, label="CLAHE Tile Grid Size", info="Size of grid for histogram equalization. e.g., 4 means 4x4 grid.") | |
| gr.Markdown("### Step 2: Adaptive Thresholding") | |
| with gr.Accordion("Adjust Thresholding Parameters", open=True): | |
| adaptive_thresh_block_size = gr.Slider(3, 101, step=2, value=41, label="Adaptive Threshold Block Size (odd)", info="Size of a pixel neighborhood that is used to calculate a threshold value. Must be odd.") | |
| adaptive_thresh_c = gr.Slider(1, 50, value=10, step=1, label="Adaptive Threshold C Value", info="Constant subtracted from the mean or weighted mean.") | |
| gr.Markdown("### Step 3: Morphological Operations") | |
| with gr.Accordion("Adjust Morphological Kernels", open=True): | |
| close_kernel_size_morph = gr.Slider(3, 15, step=2, value=9, label="Closing Kernel Size (odd)", info="Kernel size for morphological closing. Helps fill small holes. Must be odd.") | |
| open_kernel_size_morph = gr.Slider(3, 15, step=2, value=5, label="Opening Kernel Size (odd)", info="Kernel size for morphological opening. Helps remove small objects. Must be odd.") | |
| gr.Markdown("### Step 4: Find and Filter Contours") | |
| with gr.Accordion("Adjust Contour Filtering", open=True): | |
| min_contour_area = gr.Slider(0, 1000, value=100, step=10, label="Min Contour Area (pixels)", info="Minimum area for a contour to be considered.") | |
| max_contour_area = gr.Slider(1000, 1000000, value=500000, step=1000, label="Max Contour Area (pixels)", info="Maximum area for a contour to be considered.") | |
| min_circularity = gr.Slider(0.001, 1.0, value=0.010, step=0.001, label="Min Circularity", info="Minimum circularity (4π * Area / Perimeter^2) for a contour. 1.0 is a perfect circle.") | |
| max_circularity = gr.Slider(1.0, 2.0, value=1.2, step=0.01, label="Max Circularity", info="Maximum circularity for a contour.") | |
| gr.Markdown("### Step 5: Select Best Contour") | |
| with gr.Accordion("Choose Selection Method", open=True): | |
| selection_method = gr.Radio(["Largest Area", "Most Circular", "Closest to Center"], label="Selection Method", value="Closest to Center", info="Method to select the single best contour if multiple are found.") | |
| gr.Markdown("### Step 6: Final Visualization & Data Processing") | |
| with gr.Accordion("Adjust Visualization and Data Parameters", open=True): | |
| px_per_um = gr.Slider(0.1, 50, value=21.3, step=0.1, label="Pixels per Micrometer (px/µm)", info="Scale factor to convert micrometers to pixels.") | |
| manual_offset_x = gr.Slider(-100, 100, value=0, step=1, label="Manual Offset X (µm)", info="Manual adjustment for X-coordinate offset in micrometers.") | |
| manual_offset_y = gr.Slider(-100, 100, value=0, step=1, label="Manual Offset Y (µm)", info="Manual adjustment for Y-coordinate offset in micrometers.") | |
| run_button = gr.Button("Run Cell Vision Analysis") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Results") | |
| output_image = gr.Image(label="Processed Image with Detections") | |
| output_txt = gr.File(label="Download Modified TXT File") | |
| run_button.click( | |
| fn=process_image, | |
| inputs=[ | |
| image_input, txt_input, crop_size, blur_kernel_size, clahe_clip_limit, clahe_tile_grid_size, | |
| adaptive_thresh_block_size, adaptive_thresh_c, close_kernel_size_morph, open_kernel_size_morph, | |
| min_contour_area, max_contour_area, min_circularity, max_circularity, selection_method, | |
| px_per_um, manual_offset_x, manual_offset_y | |
| ], | |
| outputs=[output_image, output_txt] | |
| ) | |
| demo.launch() | |