SmartCBC / app.py
KPrashanth's picture
Update app.py
ef29459 verified
# app.py
import os
from typing import List, Any
import gradio as gr
from PIL import Image
from pipeline import SmartCBC
# -------------------------------------------------
# Initialize pipeline ONCE (cached in Spaces)
# -------------------------------------------------
cbc = SmartCBC() # loads YOLO + classifier once
# -------------------------------------------------
# Helper: convert uploaded files to PIL Images
# -------------------------------------------------
def files_to_pil_list(files: List[Any]) -> List[Image.Image]:
"""
Gradio Files (file_count='multiple') returns a list of file objects or dicts.
Each item commonly looks like:
- {"name": "/tmp/....png", "orig_name": "...", ...}
- or a file-like object with .name
This helper normalizes them into a list of RGB PIL Images.
"""
pil_list: List[Image.Image] = []
if files is None:
return pil_list
for f in files:
# Newer gradio often returns dicts with "name"
if isinstance(f, dict) and "name" in f:
path = f["name"]
# Older style: File object with .name
elif hasattr(f, "name"):
path = f.name
# Fallback: assume it's already a path-like
else:
path = str(f)
if not os.path.isfile(path):
raise FileNotFoundError(f"Uploaded file not found on disk: {path}")
img = Image.open(path).convert("RGB")
pil_list.append(img)
return pil_list
# -------------------------------------------------
# Core Gradio wrapper
# -------------------------------------------------
def analyze_images(files, age, gender, output_mode):
"""
Wrapper for SmartCBC.analyze().
- Accepts one or multiple images from a Gradio Files input.
- If a single image -> sends a single PIL.Image
If multiple -> sends a list[Image.Image] (SmartCBC can route to analyze_batch).
- Returns either a human-readable text report or full JSON.
"""
if files is None or len(files) == 0:
return "Please upload at least one image.", None
pil_images = files_to_pil_list(files)
if len(pil_images) == 1:
image_input = pil_images[0]
else:
image_input = pil_images
# Run SmartCBC pipeline
result = cbc.analyze(
image=image_input,
age=age,
gender=gender,
)
# Choose output mode
if output_mode == "Text Report":
return result.get("report_text", "No report generated."), None
else:
return None, result
# -------------------------------------------------
# Gradio UI Layout (compatible with Gradio 4.0.0)
# -------------------------------------------------
with gr.Blocks(title="SmartCBC - Multimodal Blood Analysis") as demo:
gr.Markdown(
"""
# 🩸 SmartCBC — Multimodal AI Blood Smear Analysis
Upload **one or multiple** peripheral smear FOV images and get:
- RBC / WBC / Platelet counts
- WBC subtype classification
- Aggregated multi-FOV differential
- Age-specific reference comparisons
- Clinical insights (non-diagnostic)
"""
)
with gr.Row():
# Use Files for multi-image upload (works on Gradio 4.0.0)
img_in = gr.Files(
label="Upload 1 or Multiple Blood Smear Images (FOVs)",
file_count="multiple",
file_types=["image"],
)
with gr.Column():
age_in = gr.Number(label="Age (years)", value=30)
gender_in = gr.Dropdown(
["", "M", "F"],
label="Gender (optional)",
value=""
)
output_mode = gr.Radio(
["Text Report", "Structured JSON"],
value="Text Report",
label="Output Format",
)
btn = gr.Button("Analyze")
# OUTPUT AREAS
txt_out = gr.Textbox(
label="Report (Human Readable)",
visible=True,
lines=30,
interactive=False,
)
json_out = gr.JSON(
label="Structured Output (JSON)",
visible=False,
)
# Toggle visibility based on output mode
def toggle_output(mode):
return (
gr.update(visible=(mode == "Text Report")),
gr.update(visible=(mode == "Structured JSON")),
)
output_mode.change(toggle_output, [output_mode], [txt_out, json_out])
# Button Binding
btn.click(
analyze_images,
inputs=[img_in, age_in, gender_in, output_mode],
outputs=[txt_out, json_out],
)
# -------------------------------------------------
# HF Spaces entrypoint
# -------------------------------------------------
if __name__ == "__main__":
demo.launch()