Spaces:
Sleeping
Sleeping
File size: 13,782 Bytes
e916c68 bf87041 945b29b bf87041 9dc758e bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 e916c68 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 67dea58 bf87041 e916c68 bf87041 e916c68 bf87041 57527ae bf87041 57527ae bf87041 9dc758e bf87041 57527ae e916c68 57527ae bf87041 e916c68 bf87041 e916c68 bf87041 e916c68 bf87041 57527ae bf87041 e916c68 bf87041 e916c68 bf87041 e916c68 9dc758e bf87041 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | 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()
|