Spaces:
Sleeping
Sleeping
| """ | |
| Microscopy CV Toolkit — classical computer-vision tools for microscopy image QC. | |
| No ML models, pure OpenCV + NumPy + SciPy. | |
| """ | |
| import cv2 | |
| import numpy as np | |
| import gradio as gr | |
| import matplotlib | |
| matplotlib.use("Agg") | |
| import matplotlib.pyplot as plt | |
| from matplotlib.colors import Normalize | |
| from scipy import ndimage | |
| from PIL import Image | |
| import io | |
| # --------------------------------------------------------------------------- | |
| # Utilities | |
| # --------------------------------------------------------------------------- | |
| def _to_gray(img: np.ndarray) -> np.ndarray: | |
| if len(img.shape) == 2: | |
| return img | |
| return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) | |
| def _fig_to_image(fig) -> np.ndarray: | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format="png", bbox_inches="tight", dpi=120) | |
| plt.close(fig) | |
| buf.seek(0) | |
| return np.array(Image.open(buf)) | |
| # --------------------------------------------------------------------------- | |
| # Tab 1 — Focus Quality | |
| # --------------------------------------------------------------------------- | |
| def _tenengrad(gray: np.ndarray) -> float: | |
| gx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) | |
| gy = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) | |
| return float(np.mean(gx ** 2 + gy ** 2)) | |
| def _laplacian_variance(gray: np.ndarray) -> float: | |
| lap = cv2.Laplacian(gray, cv2.CV_64F) | |
| return float(lap.var()) | |
| def _normalized_variance(gray: np.ndarray) -> float: | |
| mean = gray.mean() | |
| if mean < 1e-6: | |
| return 0.0 | |
| return float(gray.astype(np.float64).var() / mean) | |
| def _vollath_f4(gray: np.ndarray) -> float: | |
| g = gray.astype(np.float64) | |
| h, w = g.shape | |
| t1 = np.sum(g[:, :w - 1] * g[:, 1:w]) | |
| t2 = np.sum(g[:, :w - 2] * g[:, 2:w]) | |
| return float(t1 - t2) | |
| def _focus_heatmap(gray: np.ndarray, block: int = 64) -> np.ndarray: | |
| h, w = gray.shape | |
| rows = h // block | |
| cols = w // block | |
| hmap = np.zeros((rows, cols), dtype=np.float64) | |
| for r in range(rows): | |
| for c in range(cols): | |
| patch = gray[r * block:(r + 1) * block, c * block:(c + 1) * block] | |
| lap = cv2.Laplacian(patch, cv2.CV_64F) | |
| hmap[r, c] = lap.var() | |
| return hmap | |
| def _score_label(val: float, low: float, high: float) -> str: | |
| if val >= high: | |
| return "PASS (sharp)" | |
| elif val >= low: | |
| return "MARGINAL" | |
| return "FAIL (blurry)" | |
| def analyze_focus(image: np.ndarray): | |
| if image is None: | |
| return None, "Upload an image first." | |
| gray = _to_gray(image) | |
| tenen = _tenengrad(gray) | |
| lap_var = _laplacian_variance(gray) | |
| norm_var = _normalized_variance(gray) | |
| vollath = _vollath_f4(gray) | |
| # Thresholds (heuristic, tuned for typical microscopy) | |
| tenen_verdict = _score_label(tenen, 200, 1000) | |
| lap_verdict = _score_label(lap_var, 50, 300) | |
| norm_verdict = _score_label(norm_var, 5, 20) | |
| vollath_verdict = _score_label(vollath, 1e5, 1e6) | |
| overall_sharp = sum([ | |
| tenen >= 1000, | |
| lap_var >= 300, | |
| norm_var >= 20, | |
| vollath >= 1e6, | |
| ]) | |
| if overall_sharp >= 3: | |
| overall = "PASS — image is in focus" | |
| elif overall_sharp >= 1: | |
| overall = "MARGINAL — some metrics indicate softness" | |
| else: | |
| overall = "FAIL — image appears out of focus" | |
| report = ( | |
| f"## Focus Quality Report\n\n" | |
| f"| Metric | Value | Verdict |\n" | |
| f"|--------|-------|---------|\n" | |
| f"| Tenengrad | {tenen:.1f} | {tenen_verdict} |\n" | |
| f"| Laplacian Variance | {lap_var:.1f} | {lap_verdict} |\n" | |
| f"| Normalized Variance | {norm_var:.2f} | {norm_verdict} |\n" | |
| f"| Vollath F4 | {vollath:.0f} | {vollath_verdict} |\n\n" | |
| f"**Overall: {overall}**" | |
| ) | |
| # Heatmap overlay | |
| hmap = _focus_heatmap(gray, block=max(32, min(gray.shape) // 16)) | |
| fig, axes = plt.subplots(1, 2, figsize=(14, 5)) | |
| axes[0].imshow(cv2.cvtColor(image, cv2.COLOR_RGB2BGR) if len(image.shape) == 3 else gray, cmap="gray") | |
| axes[0].set_title("Original", fontsize=12, fontweight="bold") | |
| axes[0].axis("off") | |
| im = axes[1].imshow(hmap, cmap="inferno", interpolation="bilinear") | |
| axes[1].set_title("Focus Heatmap (Laplacian Var per block)", fontsize=12, fontweight="bold") | |
| axes[1].axis("off") | |
| fig.colorbar(im, ax=axes[1], fraction=0.046, pad=0.04, label="Sharpness") | |
| fig.tight_layout() | |
| overlay_img = _fig_to_image(fig) | |
| return overlay_img, report | |
| # --------------------------------------------------------------------------- | |
| # Tab 2 — Illumination Analysis | |
| # --------------------------------------------------------------------------- | |
| def _nine_zone_map(gray: np.ndarray) -> np.ndarray: | |
| h, w = gray.shape | |
| rh, rw = h // 3, w // 3 | |
| zones = np.zeros((3, 3), dtype=np.float64) | |
| for r in range(3): | |
| for c in range(3): | |
| patch = gray[r * rh:(r + 1) * rh, c * rw:(c + 1) * rw] | |
| zones[r, c] = patch.mean() | |
| return zones | |
| def analyze_illumination(image: np.ndarray): | |
| if image is None: | |
| return None, "Upload an image first." | |
| gray = _to_gray(image) | |
| h, w = gray.shape | |
| mean_b = float(gray.mean()) | |
| std_b = float(gray.std()) | |
| min_b = int(gray.min()) | |
| max_b = int(gray.max()) | |
| dynamic_range = max_b - min_b | |
| # Clipping detection | |
| total_px = h * w | |
| clipped_low = int(np.sum(gray <= 5)) | |
| clipped_high = int(np.sum(gray >= 250)) | |
| pct_low = 100.0 * clipped_low / total_px | |
| pct_high = 100.0 * clipped_high / total_px | |
| clip_warning = "" | |
| if pct_low > 5: | |
| clip_warning += f" - {pct_low:.1f}% pixels crushed to black (underexposed regions)\n" | |
| if pct_high > 5: | |
| clip_warning += f" - {pct_high:.1f}% pixels blown to white (overexposed regions)\n" | |
| if not clip_warning: | |
| clip_warning = " - No significant clipping detected\n" | |
| # Vignetting: compare center vs corners | |
| cy, cx = h // 2, w // 2 | |
| r = min(h, w) // 8 | |
| center_mean = float(gray[cy - r:cy + r, cx - r:cx + r].mean()) | |
| corner_vals = [] | |
| for yr, xr in [(0, 0), (0, w - r * 2), (h - r * 2, 0), (h - r * 2, w - r * 2)]: | |
| corner_vals.append(float(gray[yr:yr + r * 2, xr:xr + r * 2].mean())) | |
| corner_mean = np.mean(corner_vals) | |
| if center_mean > 1e-3: | |
| vig_ratio = corner_mean / center_mean | |
| else: | |
| vig_ratio = 1.0 | |
| if vig_ratio < 0.75: | |
| vig_verdict = f"SIGNIFICANT vignetting (corner/center = {vig_ratio:.2f})" | |
| elif vig_ratio < 0.90: | |
| vig_verdict = f"Mild vignetting (corner/center = {vig_ratio:.2f})" | |
| else: | |
| vig_verdict = f"No significant vignetting (corner/center = {vig_ratio:.2f})" | |
| zones = _nine_zone_map(gray) | |
| # Build figure: histogram + zone map | |
| fig, axes = plt.subplots(1, 3, figsize=(18, 5)) | |
| # Histogram | |
| axes[0].hist(gray.ravel(), bins=256, range=(0, 256), color="#448AFF", alpha=0.85, edgecolor="none") | |
| axes[0].axvline(mean_b, color="#FF1744", linestyle="--", linewidth=1.5, label=f"Mean={mean_b:.0f}") | |
| axes[0].set_title("Brightness Histogram", fontsize=12, fontweight="bold") | |
| axes[0].set_xlabel("Pixel value") | |
| axes[0].set_ylabel("Count") | |
| axes[0].legend() | |
| # Zone brightness map | |
| im = axes[1].imshow(zones, cmap="YlOrRd", vmin=0, vmax=255, interpolation="nearest") | |
| for r in range(3): | |
| for c in range(3): | |
| axes[1].text(c, r, f"{zones[r, c]:.0f}", ha="center", va="center", | |
| fontsize=14, fontweight="bold", | |
| color="black" if zones[r, c] > 128 else "white") | |
| axes[1].set_title("9-Zone Brightness Map", fontsize=12, fontweight="bold") | |
| axes[1].set_xticks([0, 1, 2]) | |
| axes[1].set_xticklabels(["L", "C", "R"]) | |
| axes[1].set_yticks([0, 1, 2]) | |
| axes[1].set_yticklabels(["T", "M", "B"]) | |
| fig.colorbar(im, ax=axes[1], fraction=0.046, pad=0.04) | |
| # Original image | |
| axes[2].imshow(image if len(image.shape) == 3 else gray, cmap="gray") | |
| axes[2].set_title("Original", fontsize=12, fontweight="bold") | |
| axes[2].axis("off") | |
| fig.tight_layout() | |
| vis = _fig_to_image(fig) | |
| report = ( | |
| f"## Illumination Analysis\n\n" | |
| f"| Metric | Value |\n" | |
| f"|--------|-------|\n" | |
| f"| Mean Brightness | {mean_b:.1f} / 255 |\n" | |
| f"| Std Dev | {std_b:.1f} |\n" | |
| f"| Min / Max | {min_b} / {max_b} |\n" | |
| f"| Dynamic Range | {dynamic_range} |\n" | |
| f"| Clipped Low (<=5) | {clipped_low} px ({pct_low:.2f}%) |\n" | |
| f"| Clipped High (>=250) | {clipped_high} px ({pct_high:.2f}%) |\n\n" | |
| f"**Clipping:**\n{clip_warning}\n" | |
| f"**Vignetting:** {vig_verdict}\n\n" | |
| f"**Zone Brightness (3x3 grid):**\n" | |
| f"```\n" | |
| f" {zones[0,0]:6.1f} {zones[0,1]:6.1f} {zones[0,2]:6.1f}\n" | |
| f" {zones[1,0]:6.1f} {zones[1,1]:6.1f} {zones[1,2]:6.1f}\n" | |
| f" {zones[2,0]:6.1f} {zones[2,1]:6.1f} {zones[2,2]:6.1f}\n" | |
| f"```" | |
| ) | |
| return vis, report | |
| # --------------------------------------------------------------------------- | |
| # Tab 3 — Microscopy Type Detection | |
| # --------------------------------------------------------------------------- | |
| def _histogram_features(gray: np.ndarray) -> dict: | |
| hist = cv2.calcHist([gray], [0], None, [256], [0, 256]).ravel() | |
| hist_norm = hist / hist.sum() | |
| mean_int = float(gray.mean()) | |
| std_int = float(gray.std()) | |
| median_int = float(np.median(gray)) | |
| # Skewness | |
| if std_int > 1e-6: | |
| skew = float(np.mean(((gray.astype(np.float64) - mean_int) / std_int) ** 3)) | |
| else: | |
| skew = 0.0 | |
| # Peak count (modes) | |
| from scipy.signal import find_peaks | |
| smoothed = ndimage.gaussian_filter1d(hist_norm, sigma=5) | |
| peaks, props = find_peaks(smoothed, height=0.002, distance=20) | |
| n_peaks = len(peaks) | |
| # Edge density | |
| edges = cv2.Canny(gray, 50, 150) | |
| edge_density = float(np.sum(edges > 0)) / (gray.shape[0] * gray.shape[1]) | |
| # Fraction of dark pixels | |
| dark_frac = float(np.sum(gray < 40)) / gray.size | |
| bright_frac = float(np.sum(gray > 215)) / gray.size | |
| return { | |
| "mean": mean_int, | |
| "std": std_int, | |
| "median": median_int, | |
| "skew": skew, | |
| "n_peaks": n_peaks, | |
| "edge_density": edge_density, | |
| "dark_frac": dark_frac, | |
| "bright_frac": bright_frac, | |
| "hist_norm": hist_norm, | |
| } | |
| _MODALITIES = ["Brightfield", "Darkfield", "Phase Contrast", "Fluorescence", "Polarized Light"] | |
| def _classify_modality(feats: dict) -> list[tuple[str, float]]: | |
| scores = {m: 0.0 for m in _MODALITIES} | |
| mean = feats["mean"] | |
| std = feats["std"] | |
| skew = feats["skew"] | |
| dark_frac = feats["dark_frac"] | |
| bright_frac = feats["bright_frac"] | |
| edge_density = feats["edge_density"] | |
| n_peaks = feats["n_peaks"] | |
| # Brightfield: medium-high mean, moderate std, near-zero skew, low dark fraction | |
| if 80 < mean < 200: | |
| scores["Brightfield"] += 2.0 | |
| if std < 60: | |
| scores["Brightfield"] += 1.0 | |
| if abs(skew) < 1.0: | |
| scores["Brightfield"] += 1.0 | |
| if dark_frac < 0.15: | |
| scores["Brightfield"] += 1.5 | |
| # Darkfield: low mean, high dark fraction, positive skew | |
| if mean < 60: | |
| scores["Darkfield"] += 2.5 | |
| if dark_frac > 0.5: | |
| scores["Darkfield"] += 2.0 | |
| if skew > 1.0: | |
| scores["Darkfield"] += 1.5 | |
| if bright_frac < 0.05: | |
| scores["Darkfield"] += 0.5 | |
| # Phase contrast: bimodal histogram, halos (high edge density), medium mean | |
| if n_peaks >= 2: | |
| scores["Phase Contrast"] += 2.0 | |
| if edge_density > 0.08: | |
| scores["Phase Contrast"] += 2.0 | |
| if 50 < mean < 160: | |
| scores["Phase Contrast"] += 1.0 | |
| if std > 40: | |
| scores["Phase Contrast"] += 0.5 | |
| # Fluorescence: very dark background, sparse bright spots, very high skew | |
| if mean < 40: | |
| scores["Fluorescence"] += 2.0 | |
| if dark_frac > 0.7: | |
| scores["Fluorescence"] += 2.0 | |
| if skew > 2.0: | |
| scores["Fluorescence"] += 2.5 | |
| if bright_frac > 0.001 and bright_frac < 0.15: | |
| scores["Fluorescence"] += 1.0 | |
| # Polarized: high contrast, possible birefringence colors (high std in color) | |
| if std > 50: | |
| scores["Polarized Light"] += 1.0 | |
| if n_peaks >= 2: | |
| scores["Polarized Light"] += 0.5 | |
| if 40 < mean < 140: | |
| scores["Polarized Light"] += 0.5 | |
| total = sum(scores.values()) | |
| if total < 1e-6: | |
| return [(m, 1.0 / len(_MODALITIES)) for m in _MODALITIES] | |
| confidences = [(m, scores[m] / total) for m in _MODALITIES] | |
| confidences.sort(key=lambda x: -x[1]) | |
| return confidences | |
| def detect_microscopy_type(image: np.ndarray): | |
| if image is None: | |
| return None, "Upload an image first." | |
| gray = _to_gray(image) | |
| feats = _histogram_features(gray) | |
| confidences = _classify_modality(feats) | |
| best_name, best_conf = confidences[0] | |
| # Build histogram plot | |
| fig, axes = plt.subplots(1, 2, figsize=(14, 5)) | |
| axes[0].bar(range(256), feats["hist_norm"], color="#448AFF", width=1.0, edgecolor="none") | |
| axes[0].set_title("Intensity Histogram", fontsize=12, fontweight="bold") | |
| axes[0].set_xlabel("Pixel value") | |
| axes[0].set_ylabel("Normalized frequency") | |
| # Confidence bar chart | |
| names = [c[0] for c in confidences] | |
| vals = [c[1] * 100 for c in confidences] | |
| colors = ["#00E676" if i == 0 else "#448AFF" for i in range(len(names))] | |
| bars = axes[1].barh(names[::-1], vals[::-1], color=colors[::-1], edgecolor="none") | |
| axes[1].set_title("Modality Confidence", fontsize=12, fontweight="bold") | |
| axes[1].set_xlabel("Confidence (%)") | |
| axes[1].set_xlim(0, 100) | |
| for bar, v in zip(bars, vals[::-1]): | |
| axes[1].text(bar.get_width() + 1, bar.get_y() + bar.get_height() / 2, | |
| f"{v:.1f}%", va="center", fontsize=10) | |
| fig.tight_layout() | |
| vis = _fig_to_image(fig) | |
| report = ( | |
| f"## Microscopy Type Detection\n\n" | |
| f"**Detected: {best_name}** (confidence: {best_conf * 100:.1f}%)\n\n" | |
| f"| Modality | Confidence |\n" | |
| f"|----------|------------|\n" | |
| ) | |
| for name, conf in confidences: | |
| marker = " <<" if name == best_name else "" | |
| report += f"| {name} | {conf * 100:.1f}%{marker} |\n" | |
| report += ( | |
| f"\n**Image Features:**\n" | |
| f"- Mean intensity: {feats['mean']:.1f}\n" | |
| f"- Std deviation: {feats['std']:.1f}\n" | |
| f"- Skewness: {feats['skew']:.2f}\n" | |
| f"- Histogram peaks: {feats['n_peaks']}\n" | |
| f"- Edge density: {feats['edge_density']:.4f}\n" | |
| f"- Dark pixel fraction: {feats['dark_frac']:.3f}\n" | |
| f"- Bright pixel fraction: {feats['bright_frac']:.3f}\n" | |
| ) | |
| return vis, report | |
| # --------------------------------------------------------------------------- | |
| # Tab 4 — Image Enhancement | |
| # --------------------------------------------------------------------------- | |
| def _apply_clahe(img: np.ndarray, clip_limit: float = 3.0, grid_size: int = 8) -> np.ndarray: | |
| if len(img.shape) == 3: | |
| lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB) | |
| clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(grid_size, grid_size)) | |
| lab[:, :, 0] = clahe.apply(lab[:, :, 0]) | |
| return cv2.cvtColor(lab, cv2.COLOR_LAB2RGB) | |
| else: | |
| clahe = cv2.createCLAHE(clipLimit=clip_limit, tileGridSize=(grid_size, grid_size)) | |
| return clahe.apply(img) | |
| def _apply_unsharp(img: np.ndarray, sigma: float = 2.0, strength: float = 1.5) -> np.ndarray: | |
| blurred = cv2.GaussianBlur(img, (0, 0), sigma) | |
| sharpened = cv2.addWeighted(img, 1.0 + strength, blurred, -strength, 0) | |
| return np.clip(sharpened, 0, 255).astype(np.uint8) | |
| def _apply_denoise(img: np.ndarray, h: float = 10.0) -> np.ndarray: | |
| if len(img.shape) == 3: | |
| return cv2.fastNlMeansDenoisingColored(img, None, h, h, 7, 21) | |
| else: | |
| return cv2.fastNlMeansDenoising(img, None, h, 7, 21) | |
| def _apply_white_balance(img: np.ndarray) -> np.ndarray: | |
| if len(img.shape) != 3: | |
| return img | |
| result = img.copy().astype(np.float64) | |
| for c in range(3): | |
| ch = result[:, :, c] | |
| low = np.percentile(ch, 1) | |
| high = np.percentile(ch, 99) | |
| if high - low < 1: | |
| continue | |
| ch = (ch - low) / (high - low) * 255.0 | |
| result[:, :, c] = ch | |
| return np.clip(result, 0, 255).astype(np.uint8) | |
| def enhance_image(image: np.ndarray, method: str, | |
| clahe_clip: float = 3.0, clahe_grid: int = 8, | |
| unsharp_sigma: float = 2.0, unsharp_strength: float = 1.5, | |
| denoise_h: float = 10.0): | |
| if image is None: | |
| return None, "Upload an image first." | |
| if method == "CLAHE (Contrast Enhancement)": | |
| enhanced = _apply_clahe(image, clip_limit=clahe_clip, grid_size=int(clahe_grid)) | |
| desc = f"CLAHE — clipLimit={clahe_clip}, gridSize={int(clahe_grid)}" | |
| elif method == "Unsharp Mask (Sharpening)": | |
| enhanced = _apply_unsharp(image, sigma=unsharp_sigma, strength=unsharp_strength) | |
| desc = f"Unsharp Mask — sigma={unsharp_sigma}, strength={unsharp_strength}" | |
| elif method == "NLM Denoising": | |
| enhanced = _apply_denoise(image, h=denoise_h) | |
| desc = f"Non-Local Means Denoising — h={denoise_h}" | |
| elif method == "Auto White Balance": | |
| enhanced = _apply_white_balance(image) | |
| desc = "Auto White Balance (percentile stretch per channel)" | |
| else: | |
| enhanced = image | |
| desc = "No method selected" | |
| # Side-by-side | |
| fig, axes = plt.subplots(1, 2, figsize=(14, 6)) | |
| axes[0].imshow(image) | |
| axes[0].set_title("Before", fontsize=14, fontweight="bold") | |
| axes[0].axis("off") | |
| axes[1].imshow(enhanced) | |
| axes[1].set_title("After", fontsize=14, fontweight="bold") | |
| axes[1].axis("off") | |
| fig.suptitle(desc, fontsize=12, y=0.02) | |
| fig.tight_layout() | |
| comparison = _fig_to_image(fig) | |
| report = ( | |
| f"## Enhancement Applied\n\n" | |
| f"**Method:** {desc}\n\n" | |
| f"| Metric | Before | After |\n" | |
| f"|--------|--------|-------|\n" | |
| ) | |
| for label, a, b in [ | |
| ("Mean", image, enhanced), | |
| ("Std", image, enhanced), | |
| ]: | |
| ga = _to_gray(a).astype(np.float64) | |
| gb = _to_gray(b).astype(np.float64) | |
| if label == "Mean": | |
| report += f"| Mean Brightness | {ga.mean():.1f} | {gb.mean():.1f} |\n" | |
| else: | |
| report += f"| Std Dev | {ga.std():.1f} | {gb.std():.1f} |\n" | |
| # Sharpness comparison | |
| ga = _to_gray(image) | |
| gb = _to_gray(enhanced) | |
| lap_before = cv2.Laplacian(ga, cv2.CV_64F).var() | |
| lap_after = cv2.Laplacian(gb, cv2.CV_64F).var() | |
| report += f"| Laplacian Var (sharpness) | {lap_before:.1f} | {lap_after:.1f} |\n" | |
| return comparison, enhanced, report | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| css = """ | |
| .gr-block { border-radius: 12px !important; } | |
| footer { display: none !important; } | |
| """ | |
| with gr.Blocks(title="Microscopy CV Toolkit", css=css, theme=gr.themes.Base()) as demo: | |
| gr.Markdown( | |
| "# Microscopy CV Toolkit\n" | |
| "Classical computer-vision tools for microscopy image quality analysis. " | |
| "No ML models — pure OpenCV, NumPy, SciPy. Upload any microscopy image to get started." | |
| ) | |
| with gr.Tabs(): | |
| # ---- Tab 1: Focus Quality ---- | |
| with gr.Tab("Focus Quality"): | |
| gr.Markdown( | |
| "Measures image sharpness using four complementary metrics. " | |
| "The heatmap shows per-block focus quality across the field of view." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| focus_input = gr.Image(label="Upload Image", type="numpy") | |
| focus_btn = gr.Button("Analyze Focus", variant="primary") | |
| with gr.Column(scale=2): | |
| focus_output = gr.Image(label="Focus Heatmap", type="numpy") | |
| focus_report = gr.Markdown() | |
| focus_btn.click( | |
| fn=analyze_focus, | |
| inputs=[focus_input], | |
| outputs=[focus_output, focus_report], | |
| ) | |
| # ---- Tab 2: Illumination Analysis ---- | |
| with gr.Tab("Illumination Analysis"): | |
| gr.Markdown( | |
| "Checks brightness distribution, clipping, dynamic range, vignetting, " | |
| "and displays a 9-zone brightness map for Kohler illumination assessment." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| illum_input = gr.Image(label="Upload Image", type="numpy") | |
| illum_btn = gr.Button("Analyze Illumination", variant="primary") | |
| with gr.Column(scale=2): | |
| illum_output = gr.Image(label="Analysis", type="numpy") | |
| illum_report = gr.Markdown() | |
| illum_btn.click( | |
| fn=analyze_illumination, | |
| inputs=[illum_input], | |
| outputs=[illum_output, illum_report], | |
| ) | |
| # ---- Tab 3: Microscopy Type Detection ---- | |
| with gr.Tab("Microscopy Type Detection"): | |
| gr.Markdown( | |
| "Auto-detects imaging modality based on histogram shape, intensity statistics, " | |
| "contrast, and edge density. Works best on standard preparations." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| type_input = gr.Image(label="Upload Image", type="numpy") | |
| type_btn = gr.Button("Detect Type", variant="primary") | |
| with gr.Column(scale=2): | |
| type_output = gr.Image(label="Analysis", type="numpy") | |
| type_report = gr.Markdown() | |
| type_btn.click( | |
| fn=detect_microscopy_type, | |
| inputs=[type_input], | |
| outputs=[type_output, type_report], | |
| ) | |
| # ---- Tab 4: Image Enhancement ---- | |
| with gr.Tab("Image Enhancement"): | |
| gr.Markdown( | |
| "Apply classical enhancement techniques. Adjust parameters and compare side-by-side." | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| enhance_input = gr.Image(label="Upload Image", type="numpy") | |
| enhance_method = gr.Radio( | |
| choices=[ | |
| "CLAHE (Contrast Enhancement)", | |
| "Unsharp Mask (Sharpening)", | |
| "NLM Denoising", | |
| "Auto White Balance", | |
| ], | |
| value="CLAHE (Contrast Enhancement)", | |
| label="Enhancement Method", | |
| ) | |
| with gr.Accordion("Parameters"): | |
| clahe_clip = gr.Slider(0.5, 10.0, value=3.0, step=0.5, label="CLAHE Clip Limit") | |
| clahe_grid = gr.Slider(2, 16, value=8, step=1, label="CLAHE Grid Size") | |
| unsharp_sigma = gr.Slider(0.5, 5.0, value=2.0, step=0.5, label="Unsharp Sigma") | |
| unsharp_strength = gr.Slider(0.5, 5.0, value=1.5, step=0.5, label="Unsharp Strength") | |
| denoise_h = gr.Slider(1.0, 30.0, value=10.0, step=1.0, label="Denoise Strength (h)") | |
| enhance_btn = gr.Button("Enhance", variant="primary") | |
| with gr.Column(scale=2): | |
| enhance_comparison = gr.Image(label="Before / After", type="numpy") | |
| enhance_result = gr.Image(label="Enhanced Image (downloadable)", type="numpy") | |
| enhance_report = gr.Markdown() | |
| enhance_btn.click( | |
| fn=enhance_image, | |
| inputs=[enhance_input, enhance_method, | |
| clahe_clip, clahe_grid, | |
| unsharp_sigma, unsharp_strength, | |
| denoise_h], | |
| outputs=[enhance_comparison, enhance_result, enhance_report], | |
| ) | |
| gr.Markdown( | |
| "<center style='color:#888;font-size:0.85em;'>" | |
| "Microscopy CV Toolkit | Pure OpenCV, no ML models | " | |
| "<a href='https://huggingface.co/spaces/Laborator/microscopy-cv-toolkit'>HuggingFace Space</a>" | |
| "</center>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |