aromidvar's picture
Update app.py
bf87041 verified
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()