Spaces:
Running on Zero
Running on Zero
File size: 14,268 Bytes
cb6ac42 f66bbd0 cb6ac42 f66bbd0 cb6ac42 f66bbd0 cb6ac42 127b70f cb6ac42 ee3433a cb6ac42 0b93ab8 cb6ac42 ce0c162 f66bbd0 cb6ac42 f66bbd0 cb6ac42 127b70f f66bbd0 cb6ac42 127b70f f66bbd0 ce0c162 127b70f cb6ac42 f66bbd0 ce0c162 cec78e7 ce0c162 f66bbd0 cb6ac42 ce0c162 cec78e7 ce0c162 127b70f cec78e7 ce0c162 cec78e7 ce0c162 cec78e7 ce0c162 127b70f cec78e7 83bc33a cb6ac42 ce0c162 cb6ac42 83bc33a ce0c162 f66bbd0 ce0c162 f66bbd0 a73ece1 cb6ac42 a73ece1 cb6ac42 f66bbd0 cb6ac42 f795b32 cb6ac42 da4f3b2 0a40ab8 cb6ac42 0a40ab8 f66bbd0 cb6ac42 0a40ab8 da4f3b2 cb6ac42 da4f3b2 cb6ac42 f66bbd0 cb6ac42 a73ece1 cb6ac42 0a40ab8 cb6ac42 0a40ab8 cb6ac42 f66bbd0 cb6ac42 f66bbd0 cb6ac42 127b70f da4f3b2 cb6ac42 ce0c162 cec78e7 ce0c162 cec78e7 ce0c162 cb6ac42 a73ece1 cb6ac42 127b70f cb6ac42 ce0c162 cec78e7 0a40ab8 f66bbd0 0b93ab8 | 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 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 | """GazeCorrect: gaze -> Gaussian attention/noise -> text-guided regeneration."""
from __future__ import annotations
# ZeroGPU must be imported before torch. Safe fallback for local/regular Spaces.
try:
import spaces
except ImportError:
class _Spaces:
@staticmethod
def GPU(function): return function
spaces = _Spaces()
import os
from functools import lru_cache
from pathlib import Path
# Gradio 4.44 calls Starlette's template API using its pre-0.29 signature.
# Some current Space base images provide the newer signature instead.
import starlette.templating as _starlette_templating
_original_template_response = _starlette_templating.Jinja2Templates.TemplateResponse
def _compatible_template_response(self, *args, **kwargs):
if args and isinstance(args[0], str) and len(args) >= 2 and isinstance(args[1], dict):
template = self.get_template(args[0])
return _starlette_templating._TemplateResponse(
template,
args[1],
status_code=args[2] if len(args) > 2 else kwargs.get("status_code", 200),
headers=kwargs.get("headers"),
media_type=kwargs.get("media_type"),
background=kwargs.get("background"),
)
return _original_template_response(self, *args, **kwargs)
_starlette_templating.Jinja2Templates.TemplateResponse = _compatible_template_response
import gradio as gr
# Gradio 4.44 can encounter JSON-schema ``additionalProperties: false`` in
# newer Space dependencies. Avoid generating an invalid API schema at startup.
try:
import gradio_client.utils as _gradio_client_utils
_schema_to_python_type = _gradio_client_utils._json_schema_to_python_type
def _safe_schema_to_python_type(schema, defs=None):
if not isinstance(schema, dict):
return "Any"
if not isinstance(schema.get("additionalProperties"), dict):
schema = {key: value for key, value in schema.items() if key != "additionalProperties"}
return _schema_to_python_type(schema, defs)
_gradio_client_utils._json_schema_to_python_type = _safe_schema_to_python_type
except Exception:
pass
import numpy as np
import pandas as pd
import torch
from PIL import Image, ImageDraw, ImageFilter
MODEL_ID = "stanfordmimi/RoentGen-v2"
NO_DURATION = "— no duration column —"
def draw_points(image, points):
if image is None: return None
output = image.convert("RGB").copy(); draw = ImageDraw.Draw(output)
radius = max(6, min(output.size) // 70)
for i, (x, y, weight) in enumerate(points):
r = radius * (0.6 + 0.6 * weight)
draw.ellipse((x-r, y-r, x+r, y+r), outline=(255, 55, 45), width=3)
draw.text((x+r+2, y-r), str(i + 1), fill=(255, 55, 45))
return output
def load_image(file):
if file is None: return None, [], None, gr.update(value=None, visible=False)
try:
path = file if isinstance(file, str) else file.name
image = Image.open(path).convert("RGB")
# Keep the uploaded filename so a multi-image gaze export can be
# filtered by its ``id`` column when it is applied.
return image, [], Path(path).name, gr.update(value=image, visible=True)
except Exception as exc:
gr.Warning(f"Could not read image: {exc}")
return None, [], None, gr.update(value=None, visible=False)
def click_gaze(image, points, weight, event: gr.SelectData):
if image is None: return points, gr.update()
x, y = event.index
points = points + [(float(x), float(y), float(weight))]
return points, draw_points(image, points)
def prepare_csv(file):
"""Read a CSV and expose its columns for explicit user mapping."""
hidden = (None, gr.update(visible=False), gr.update(choices=[], value=None),
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
gr.update(choices=[], value=NO_DURATION),
gr.update(visible=False))
if file is None:
return hidden
try:
path = file if isinstance(file, str) else file.name
frame = pd.read_csv(path, sep=None, engine="python")
if frame.empty:
raise ValueError("CSV contains no rows.")
columns = [str(column) for column in frame.columns]
lower = {column.lower().strip(): column for column in columns}
id_guess = lower.get("id") or lower.get("image_id") or lower.get("image") or lower.get("filename") or columns[0]
x_guess = lower.get("x") or lower.get("gaze_x") or lower.get("fix_x") or columns[0]
y_guess = lower.get("y") or lower.get("gaze_y") or lower.get("fix_y") or columns[min(1, len(columns) - 1)]
duration_guess = lower.get("duration") or lower.get("weight") or lower.get("fixation_duration") or NO_DURATION
return (frame.to_json(orient="split"), gr.update(visible=True),
gr.update(choices=columns, value=id_guess),
gr.update(choices=columns, value=x_guess),
gr.update(choices=columns, value=y_guess),
gr.update(choices=[NO_DURATION] + columns, value=duration_guess),
gr.update(visible=True))
except Exception as exc:
gr.Warning(f"Could not read CSV: {exc}")
return hidden
def apply_csv(frame_json, id_col, x_col, y_col, duration_col, image, image_name):
if image is None:
gr.Warning("Upload an image before applying gaze CSV data.")
return gr.update(), gr.update()
if not frame_json or not id_col or not x_col or not y_col:
gr.Warning("Select the ID, X, and Y columns first.")
return gr.update(), gr.update()
try:
frame = pd.read_json(frame_json, orient="split")
if id_col not in frame.columns:
raise ValueError(f'ID column "{id_col}" was not found in the CSV.')
if not image_name:
raise ValueError("The uploaded image name is unavailable for matching the CSV id column.")
image_path = Path(image_name)
accepted_ids = {image_path.name.casefold(), image_path.stem.casefold()}
gaze_ids = frame[id_col].astype(str).str.strip().str.casefold()
frame = frame[gaze_ids.isin(accepted_ids)]
if frame.empty:
raise ValueError(
f'No gaze rows matched image "{image_path.name}" in the "{id_col}" column.'
)
x, y = frame[x_col].astype(float).to_numpy(), frame[y_col].astype(float).to_numpy()
duration = (frame[duration_col].astype(float).to_numpy()
if duration_col and duration_col != NO_DURATION else np.ones(len(x)))
w, h = image.size
if len(x) and min(x) >= 0 and min(y) >= 0 and max(x) <= 1.05 and max(y) <= 1.05: x, y = x*w, y*h
duration = duration / max(float(duration.max()), 1e-8)
points = [(float(np.clip(a, 0, w-1)), float(np.clip(b, 0, h-1)), float(c)) for a,b,c in zip(x,y,duration)]
gr.Info(f'Loaded {len(points)} gaze points for "{Path(image_name).name}" from CSV.')
return points, draw_points(image, points)
except Exception as exc:
gr.Warning(f"Could not apply CSV: {exc}")
return gr.update(), gr.update()
def clear_gaze(image):
return [], gr.update(value=image) if image else gr.update()
def attention(image, points, sigma):
w, h = image.size; yy, xx = np.mgrid[:h, :w]
sigma = max(1, float(sigma))
heat = np.zeros((h, w), dtype=np.float32)
for x, y, weight in points:
heat += max(weight, .05) * np.exp(-((xx-x)**2 + (yy-y)**2)/(2*sigma**2))
return heat / (heat.max() + 1e-8)
def preview(image, heat):
color = np.zeros((*heat.shape, 3), np.uint8); color[..., 0] = (heat*255).astype(np.uint8)
return Image.blend(image.convert("RGB"), Image.fromarray(color), .5)
def noisy_image(image, heat, degree, feather, seed):
mask = Image.fromarray((heat*255).astype(np.uint8), "L")
if feather: mask = mask.filter(ImageFilter.GaussianBlur(float(feather)))
alpha = np.asarray(mask, np.float32)[..., None]/255
source = np.asarray(image.convert("RGB"), np.float32)
# Blend toward a new Gaussian-noise image instead of merely adding noise
# to the source. At degree=1, pixels at the attention peak are entirely
# noise, so no part of the original anatomy remains visible there.
noise = np.random.default_rng(seed).normal(127.5, 70.0, source.shape)
noise_amount = np.clip(alpha * float(degree), 0, 1)
noised = source * (1 - noise_amount) + noise * noise_amount
return Image.fromarray(np.clip(noised, 0, 255).astype(np.uint8)), mask
def device_dtype():
return ("cuda", torch.float16) if torch.cuda.is_available() else ("cpu", torch.float32)
@lru_cache(maxsize=2)
def pipeline(token):
# RoentGen-v2 is published as a text-to-image DiffusionPipeline. Using
# StableDiffusionImg2ImgPipeline bypasses its supported inference path and
# can yield non-radiographic results for different random seeds.
from diffusers import DiffusionPipeline
device, dtype = device_dtype()
return DiffusionPipeline.from_pretrained(MODEL_ID, torch_dtype=dtype, token=token).to(device)
@spaces.GPU
def generate(image, points, description, sigma, degree, feather, steps, seed,
oauth_token: gr.OAuthToken | None = None, progress=gr.Progress()):
if image is None: return None, None, None, "Upload an image first."
if not points: return None, None, None, "Add clicked or CSV gaze points first."
token = oauth_token.token if oauth_token is not None else (
os.environ.get("HF_TOKEN") or os.environ.get("HUGGINGFACEHUB_API_TOKEN")
)
if not token:
return None, None, None, "Sign in with Hugging Face first, then click Generate."
seed = None if seed < 0 else int(seed)
try:
progress(.1, desc="Creating gaze attention")
heat = attention(image, points, sigma); gaze = preview(image, heat)
progress(.25, desc="Adding attention-weighted Gaussian noise")
noised, mask = noisy_image(image, heat, degree, feather, seed)
progress(.4, desc="Generating chest X-ray with RoentGen-v2")
device, _ = device_dtype(); generator = None if seed is None else torch.Generator(device=device).manual_seed(seed)
finding = description.strip() or "Normal chest radiograph."
prompt = f"Chest radiograph. {finding}"
output = pipeline(token)(prompt=prompt, guidance_scale=3.5,
num_inference_steps=int(steps), generator=generator).images[0].convert("RGB")
corrected = Image.composite(output.resize(image.size), image.convert("RGB"), mask)
return gaze, noised, corrected, "Completed."
except Exception as exc:
return None, None, None, "Generation failed. Accept RoentGen access and set HF_TOKEN. Error: " + str(exc)
with gr.Blocks(title="GazeCorrect") as demo:
gr.Markdown("# GazeCorrect\nImage + gaze clicks/CSV + disease description → attention noise → corrected regenerated image. Use chest X-rays only; research use only.")
image_state, points_state, csv_state, image_name_state = gr.State(None), gr.State([]), gr.State(None), gr.State(None)
gr.LoginButton("Sign in with Hugging Face")
with gr.Row():
with gr.Column():
upload = gr.File(label="1. Upload chest X-ray", file_types=[".png", ".jpg", ".jpeg", ".webp", ".bmp"])
panel = gr.Image(label="2. Add gaze points by clicking", type="pil", visible=False, interactive=False)
with gr.Row():
weight = gr.Slider(.1, 1, value=1, step=.1, label="Next click weight")
clear_button = gr.Button("Clear gaze", variant="secondary")
with gr.Accordion("Import gaze CSV", open=False):
gr.Markdown("Upload a CSV and select its image ID, fixation X/Y, and optional duration/weight columns. Only rows whose selected ID matches the uploaded image filename (or filename without its extension) are imported.")
csv_file = gr.File(label="Choose CSV", file_types=[".csv", ".tsv", ".txt"])
with gr.Row(visible=False) as csv_mapping:
id_column = gr.Dropdown(label="Image ID column")
x_column = gr.Dropdown(label="X column")
y_column = gr.Dropdown(label="Y column")
duration_column = gr.Dropdown(label="Duration / weight (optional)")
apply_csv_button = gr.Button("Apply CSV gaze points", visible=False, variant="secondary")
description = gr.Textbox(label="3. Disease / radiology description", placeholder="Example: Right lower-lobe opacity. No pleural effusion.")
with gr.Accordion("Settings", open=False):
sigma = gr.Slider(5, 250, value=50, step=1, label="Fixation heatmap σ (pixels)")
degree = gr.Slider(0, 1, value=.35, step=.05, label="Gaussian noise degree")
feather = gr.Slider(0, 20, value=4, step=1, label="Mask feather")
steps = gr.Slider(10, 50, value=25, step=1, label="Diffusion steps")
seed = gr.Number(value=42, precision=0, label="Seed (-1 random)")
button = gr.Button("4. Generate", variant="primary")
status = gr.Textbox(label="Generation status", interactive=False, lines=3)
with gr.Column():
gaze_out = gr.Image(label="Gaze attention map")
noise_out = gr.Image(label="Attention-weighted Gaussian-noise image")
corrected_out = gr.Image(label="Corrected regenerated image")
upload.upload(load_image, upload, [image_state, points_state, image_name_state, panel])
panel.select(click_gaze, [image_state, points_state, weight], [points_state, panel])
clear_button.click(clear_gaze, image_state, [points_state, panel])
csv_file.upload(prepare_csv, csv_file, [csv_state, csv_mapping, id_column, x_column, y_column, duration_column, apply_csv_button])
apply_csv_button.click(apply_csv, [csv_state, id_column, x_column, y_column, duration_column, image_state, image_name_state], [points_state, panel])
button.click(generate, [image_state, points_state, description, sigma, degree, feather, steps, seed], [gaze_out, noise_out, corrected_out, status])
demo.queue().launch(show_error=True)
|