File size: 5,119 Bytes
d1ede70
 
 
 
 
b2945a5
 
 
 
 
d1ede70
b2945a5
 
 
 
d1ede70
b2945a5
 
 
d1ede70
 
b2945a5
d1ede70
 
b2945a5
 
 
 
 
 
 
 
 
 
 
 
d1ede70
 
 
 
 
 
 
 
 
 
b2945a5
d1ede70
 
b2945a5
d1ede70
b2945a5
 
d1ede70
 
 
 
b2945a5
d1ede70
 
b2945a5
d1ede70
 
 
b2945a5
d1ede70
 
 
 
 
b2945a5
d1ede70
 
 
b2945a5
 
d1ede70
 
 
 
 
 
 
 
 
 
b2945a5
 
 
 
d1ede70
 
b2945a5
d1ede70
b2945a5
 
 
d1ede70
b2945a5
 
d1ede70
b2945a5
 
 
 
 
 
 
 
d1ede70
b2945a5
 
 
d1ede70
b2945a5
d1ede70
b2945a5
d1ede70
b2945a5
 
 
d1ede70
 
 
b2945a5
 
d1ede70
 
 
b2945a5
d1ede70
 
b2945a5
 
d1ede70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2945a5
 
 
 
 
 
d1ede70
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
"""Gradio app: detect cells in a fluorescence image and return red-channel
grayscale images with cell + nucleus outlines drawn in yellow.

One output image is produced per detected cell, matching the documentation
style: grayscale background + two concentric yellow outlines, nothing else.
"""
from __future__ import annotations

import os

import cv2
import gradio as gr
import numpy as np
from PIL import Image

from quantification import analyze_image

DEFAULT_N_CELLS = 5
DEFAULT_DILATION_RADIUS = 12
OUTLINE_COLOR_BGR_AS_RGB = (255, 255, 0)  # yellow in RGB
OUTLINE_THICKNESS = 2

EXAMPLES_DIR = os.path.join(os.path.dirname(__file__), "examples")
DEFAULT_EXAMPLE = os.path.join(EXAMPLES_DIR, "Picture1.jpg")


def _ensure_rgb(arr: np.ndarray) -> np.ndarray:
    if arr.ndim == 2:
        arr = np.stack([arr, arr, arr], axis=-1)
    if arr.shape[2] == 4:
        arr = arr[..., :3]
    if arr.dtype != np.uint8:
        arr = np.clip(arr, 0, 255).astype(np.uint8)
    return arr


def _draw_cell_outline(
    gray_rgb: np.ndarray,
    cell_mask: np.ndarray,
    nucleus_mask: np.ndarray,
) -> np.ndarray:
    """Draw the outer (cell) and inner (nucleus) outlines on a copy of `gray_rgb`."""
    out = gray_rgb.copy()
    for mask in (cell_mask, nucleus_mask):
        contours, _ = cv2.findContours(
            mask.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
        )
        cv2.drawContours(
            out, contours, -1, OUTLINE_COLOR_BGR_AS_RGB, OUTLINE_THICKNESS
        )
    return out


def process_image(image_path: str | None, n_cells: int, dilation_radius: int):
    """Return a list of one annotated image per detected cell."""
    if image_path is None:
        return []

    image_pil = Image.open(image_path).convert("RGB")
    image_rgb = _ensure_rgb(np.array(image_pil))

    # Background for outputs: the red channel rendered as a grayscale RGB.
    red = image_rgb[..., 0]
    gray_rgb = np.stack([red, red, red], axis=-1)

    cells = analyze_image(
        image_rgb,
        n_cells=int(n_cells),
        dilation_radius=int(dilation_radius),
    )

    return [
        _draw_cell_outline(gray_rgb, c.cell_mask, c.nucleus_mask) for c in cells
    ]


def build_demo() -> gr.Blocks:
    description = (
        "Upload a fluorescence image (RGB: blue = nuclei, red = cytoplasm). "
        "The app detects representative cells and returns the red channel as "
        "grayscale with the cell + nucleus boundaries drawn in yellow — one "
        "output image per cell."
    )

    with gr.Blocks(title="Cell Boundary Detection") as demo:
        gr.Markdown("# Cell Boundary Detection")
        gr.Markdown(description)

        with gr.Row():
            with gr.Column(scale=1):
                image_in = gr.Image(
                    label="Input image",
                    type="filepath",
                    value=DEFAULT_EXAMPLE if os.path.exists(DEFAULT_EXAMPLE) else None,
                )
                n_cells_slider = gr.Slider(
                    minimum=1,
                    maximum=10,
                    value=DEFAULT_N_CELLS,
                    step=1,
                    label="Number of cells",
                )
                dilation_slider = gr.Slider(
                    minimum=4,
                    maximum=30,
                    value=DEFAULT_DILATION_RADIUS,
                    step=1,
                    label="Cytoplasm ring thickness (pixels)",
                )
                run_btn = gr.Button("Detect cells", variant="primary")

            with gr.Column(scale=2):
                gallery = gr.Gallery(
                    label="Detected cells (one per image)",
                    columns=2,
                    height=620,
                    show_label=True,
                    object_fit="contain",
                )

        run_btn.click(
            fn=process_image,
            inputs=[image_in, n_cells_slider, dilation_slider],
            outputs=[gallery],
        )

        # Example images (other defaults from prior dataset).
        example_files = []
        if os.path.isdir(EXAMPLES_DIR):
            example_files = sorted(
                os.path.join(EXAMPLES_DIR, f)
                for f in os.listdir(EXAMPLES_DIR)
                if f.lower().endswith((".jpg", ".jpeg", ".png", ".tif", ".tiff"))
            )
        if example_files:
            gr.Examples(
                examples=[[p, DEFAULT_N_CELLS, DEFAULT_DILATION_RADIUS]
                          for p in example_files],
                inputs=[image_in, n_cells_slider, dilation_slider],
                outputs=[gallery],
                fn=process_image,
                cache_examples=False,
                label="Example images",
            )

        # Preload outputs for the default image on app start.
        if os.path.exists(DEFAULT_EXAMPLE):
            demo.load(
                fn=process_image,
                inputs=[image_in, n_cells_slider, dilation_slider],
                outputs=[gallery],
            )

    return demo


if __name__ == "__main__":
    demo = build_demo()
    demo.launch()