#!/usr/bin/env python3
"""
Demo for MedicalImageAnalyzer - Enhanced with file upload and overlay visualization
"""
import gradio as gr
import numpy as np
import sys
import os
import cv2
from pathlib import Path
# Add backend to path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.dirname(__file__)), 'backend'))
from gradio_medical_image_analyzer import MedicalImageAnalyzer
def draw_roi_on_image(image, roi_x, roi_y, roi_radius):
"""Draw ROI circle on the image"""
# Convert to RGB if grayscale
if len(image.shape) == 2:
image_rgb = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)
else:
image_rgb = image.copy()
# Draw ROI circle
center = (int(roi_x), int(roi_y))
radius = int(roi_radius)
# Draw outer circle (white)
cv2.circle(image_rgb, center, radius, (255, 255, 255), 2)
# Draw inner circle (red)
cv2.circle(image_rgb, center, radius-1, (255, 0, 0), 2)
# Draw center cross
cv2.line(image_rgb, (center[0]-5, center[1]), (center[0]+5, center[1]), (255, 0, 0), 2)
cv2.line(image_rgb, (center[0], center[1]-5), (center[0], center[1]+5), (255, 0, 0), 2)
return image_rgb
def create_fat_overlay(base_image, segmentation_results):
"""Create overlay image with fat segmentation highlighted"""
# Convert to RGB
if len(base_image.shape) == 2:
overlay_img = cv2.cvtColor(base_image, cv2.COLOR_GRAY2RGB)
else:
overlay_img = base_image.copy()
# Check if we have segmentation masks
if not segmentation_results or 'segments' not in segmentation_results:
return overlay_img
segments = segmentation_results.get('segments', {})
# Apply subcutaneous fat overlay (yellow)
if 'subcutaneous' in segments and segments['subcutaneous'].get('mask') is not None:
mask = segments['subcutaneous']['mask']
yellow_overlay = np.zeros_like(overlay_img)
yellow_overlay[mask > 0] = [255, 255, 0] # Yellow
overlay_img = cv2.addWeighted(overlay_img, 0.7, yellow_overlay, 0.3, 0)
# Apply visceral fat overlay (red)
if 'visceral' in segments and segments['visceral'].get('mask') is not None:
mask = segments['visceral']['mask']
red_overlay = np.zeros_like(overlay_img)
red_overlay[mask > 0] = [255, 0, 0] # Red
overlay_img = cv2.addWeighted(overlay_img, 0.7, red_overlay, 0.3, 0)
# Add legend
cv2.putText(overlay_img, "Yellow: Subcutaneous Fat", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2)
cv2.putText(overlay_img, "Red: Visceral Fat", (10, 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
return overlay_img
def process_and_analyze(file_obj, modality, task, roi_x, roi_y, roi_radius, symptoms, show_overlay=False):
"""
Processes uploaded file and performs analysis
"""
if file_obj is None:
return None, "No file selected", None, {}, None
# Create analyzer instance
analyzer = MedicalImageAnalyzer(
analysis_mode="structured",
include_confidence=True,
include_reasoning=True
)
try:
# Process the file (DICOM or image)
file_path = file_obj.name if hasattr(file_obj, 'name') else str(file_obj)
pixel_array, display_array, metadata = analyzer.process_file(file_path)
# Update modality from file metadata if it's a DICOM
if metadata.get('file_type') == 'DICOM' and 'modality' in metadata:
modality = metadata['modality']
# Prepare analysis parameters
analysis_params = {
"image": pixel_array,
"modality": modality,
"task": task
}
# Add ROI if applicable
if task in ["analyze_point", "full_analysis"]:
# Scale ROI coordinates to image size
h, w = pixel_array.shape
roi_x_scaled = int(roi_x * w / 512) # Assuming slider max is 512
roi_y_scaled = int(roi_y * h / 512)
analysis_params["roi"] = {
"x": roi_x_scaled,
"y": roi_y_scaled,
"radius": roi_radius
}
# Add clinical context
if symptoms:
analysis_params["clinical_context"] = {"symptoms": symptoms}
# Perform analysis
results = analyzer.analyze_image(**analysis_params)
# Create visual report
visual_report = create_visual_report(results, metadata)
# Add metadata info
info = f"📄 {metadata.get('file_type', 'Unknown')} | "
info += f"🏥 {modality} | "
info += f"📐 {metadata.get('shape', 'Unknown')}"
if metadata.get('window_center'):
info += f" | Window C:{metadata['window_center']:.0f} W:{metadata['window_width']:.0f}"
# Create overlay image if requested
overlay_image = None
if show_overlay:
# For ROI visualization
if task in ["analyze_point", "full_analysis"] and roi_x and roi_y:
overlay_image = draw_roi_on_image(display_array.copy(), roi_x_scaled, roi_y_scaled, roi_radius)
# For fat segmentation overlay (simplified version since we don't have masks in current implementation)
elif task == "segment_fat" and 'segmentation' in results and modality == 'CT':
# For now, just draw ROI since we don't have actual masks
overlay_image = display_array.copy()
if len(overlay_image.shape) == 2:
overlay_image = cv2.cvtColor(overlay_image, cv2.COLOR_GRAY2RGB)
# Add text overlay about fat percentages
if 'statistics' in results['segmentation']:
stats = results['segmentation']['statistics']
cv2.putText(overlay_image, f"Total Fat: {stats.get('total_fat_percentage', 0):.1f}%",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv2.putText(overlay_image, f"Subcutaneous: {stats.get('subcutaneous_fat_percentage', 0):.1f}%",
(10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2)
cv2.putText(overlay_image, f"Visceral: {stats.get('visceral_fat_percentage', 0):.1f}%",
(10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
return display_array, info, visual_report, results, overlay_image
except Exception as e:
error_msg = f"Error: {str(e)}"
return None, error_msg, f"
❌ {error_msg}
", {"error": error_msg}, None
def create_visual_report(results, metadata):
"""Creates a visual HTML report with improved styling"""
html = f"""
🏥 Medical Image Analysis Report
📋 Metadata
| File Type: |
{metadata.get('file_type', 'Unknown')} |
| Modality: |
{results.get('modality', 'Unknown')} |
| Image Size: |
{metadata.get('shape', 'Unknown')} |
| Timestamp: |
{results.get('timestamp', 'N/A')} |
"""
# Point Analysis
if 'point_analysis' in results:
pa = results['point_analysis']
tissue = pa.get('tissue_type', {})
html += f"""
🎯 Point Analysis
| Position: |
({pa.get('location', {}).get('x', 'N/A')}, {pa.get('location', {}).get('y', 'N/A')}) |
"""
if results.get('modality') == 'CT':
html += f"""
| HU Value: |
{pa.get('hu_value', 'N/A'):.1f} |
"""
else:
html += f"""
| Intensity: |
{pa.get('intensity', 'N/A'):.3f} |
"""
html += f"""
| Tissue Type: |
{tissue.get('icon', '')}
{tissue.get('type', 'Unknown').replace('_', ' ')}
|
| Confidence: |
{pa.get('confidence', 'N/A')} |
"""
if 'reasoning' in pa:
html += f"""
"""
html += "
"
# Segmentation Results
if 'segmentation' in results and results['segmentation']:
seg = results['segmentation']
if 'statistics' in seg:
# Fat segmentation for CT
stats = seg['statistics']
html += f"""
🔬 Fat Segmentation Analysis
Total Fat
{stats.get('total_fat_percentage', 0):.1f}%
Subcutaneous
{stats.get('subcutaneous_fat_percentage', 0):.1f}%
Visceral
{stats.get('visceral_fat_percentage', 0):.1f}%
V/S Ratio
{stats.get('visceral_subcutaneous_ratio', 0):.2f}
"""
if 'interpretation' in seg:
interp = seg['interpretation']
obesity_color = "#16a34a" if interp.get("obesity_risk") == "normal" else "#d97706" if interp.get("obesity_risk") == "moderate" else "#dc2626"
visceral_color = "#16a34a" if interp.get("visceral_risk") == "normal" else "#d97706" if interp.get("visceral_risk") == "moderate" else "#dc2626"
html += f"""
Risk Assessment
Obesity Risk:
{interp.get('obesity_risk', 'N/A').upper()}
Visceral Risk:
{interp.get('visceral_risk', 'N/A').upper()}
"""
if interp.get('recommendations'):
html += """
💡 Recommendations
"""
for rec in interp['recommendations']:
html += f"- {rec}
"
html += "
"
html += "
"
html += "
"
# Quality Assessment
if 'quality_metrics' in results:
quality = results['quality_metrics']
quality_colors = {
'excellent': '#16a34a',
'good': '#16a34a',
'fair': '#d97706',
'poor': '#dc2626',
'unknown': '#6b7280'
}
q_color = quality_colors.get(quality.get('overall_quality', 'unknown'), '#6b7280')
html += f"""
📊 Image Quality Assessment
Overall Quality:
{quality.get('overall_quality', 'unknown').upper()}
"""
if quality.get('issues'):
html += f"""
Issues Detected:
"""
for issue in quality['issues']:
html += f"- {issue}
"
html += "
"
html += "
"
html += "
"
return html
def create_demo():
with gr.Blocks(
title="Medical Image Analyzer - Enhanced Demo",
theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="blue",
neutral_hue="slate",
text_size="md",
spacing_size="md",
radius_size="md",
).set(
# Medical blue theme colors
body_background_fill="*neutral_950",
body_background_fill_dark="*neutral_950",
block_background_fill="*neutral_900",
block_background_fill_dark="*neutral_900",
border_color_primary="*primary_600",
border_color_primary_dark="*primary_600",
# Text colors for better contrast
body_text_color="*neutral_100",
body_text_color_dark="*neutral_100",
body_text_color_subdued="*neutral_300",
body_text_color_subdued_dark="*neutral_300",
# Button colors
button_primary_background_fill="*primary_600",
button_primary_background_fill_dark="*primary_600",
button_primary_text_color="white",
button_primary_text_color_dark="white",
),
css="""
/* Medical blue theme with high contrast */
:root {
--medical-blue: #1e40af;
--medical-blue-light: #3b82f6;
--medical-blue-dark: #1e3a8a;
--text-primary: #f9fafb;
--text-secondary: #e5e7eb;
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
}
/* Override default text colors for medical theme */
* {
color: var(--text-primary) !important;
}
/* Style the file upload area */
.file-upload {
border: 2px dashed var(--medical-blue-light) !important;
border-radius: 8px !important;
padding: 20px !important;
text-align: center !important;
background: var(--bg-secondary) !important;
transition: all 0.3s ease !important;
color: var(--text-primary) !important;
}
.file-upload:hover {
border-color: var(--medical-blue) !important;
background: var(--bg-tertiary) !important;
box-shadow: 0 0 20px rgba(59, 130, 246, 0.2) !important;
}
/* Ensure report text is readable with white background */
.medical-report {
background: #ffffff !important;
border: 2px solid var(--medical-blue-light) !important;
border-radius: 8px !important;
padding: 16px !important;
color: #1a1a1a !important;
}
.medical-report * {
color: #1f2937 !important; /* Dark gray text */
}
.medical-report h2 {
color: #1e40af !important; /* Medical blue for main heading */
}
.medical-report h3, .medical-report h4 {
color: #1e3a8a !important; /* Darker medical blue for subheadings */
}
.medical-report strong {
color: #374151 !important; /* Darker gray for labels */
}
.medical-report td {
color: #1f2937 !important; /* Ensure table text is dark */
}
/* Report sections with light blue background */
.medical-report > div {
background: #f0f9ff !important;
color: #1f2937 !important;
}
/* Medical blue accents for UI elements */
.gr-button-primary {
background: var(--medical-blue) !important;
border-color: var(--medical-blue) !important;
}
.gr-button-primary:hover {
background: var(--medical-blue-dark) !important;
border-color: var(--medical-blue-dark) !important;
}
/* Tab styling */
.gr-tab-item {
border-color: var(--medical-blue-light) !important;
}
.gr-tab-item.selected {
background: var(--medical-blue) !important;
color: white !important;
}
/* Accordion styling */
.gr-accordion {
border-color: var(--medical-blue-light) !important;
}
/* Slider track in medical blue */
input[type="range"]::-webkit-slider-track {
background: var(--bg-tertiary) !important;
}
input[type="range"]::-webkit-slider-thumb {
background: var(--medical-blue) !important;
}
"""
) as demo:
gr.Markdown("""
# 🏥 Medical Image Analyzer
Supports **DICOM** (.dcm) and all image formats with automatic modality detection!
""")
with gr.Row():
with gr.Column(scale=1):
# File upload - no file type restrictions
with gr.Group():
gr.Markdown("### 📤 Upload Medical Image")
file_input = gr.File(
label="Select Medical Image File (.dcm, .dicom, IM_*, .png, .jpg, etc.)",
file_count="single",
type="filepath",
elem_classes="file-upload"
# Note: NO file_types parameter = accepts ALL files
)
gr.Markdown("""
Accepts: DICOM (.dcm, .dicom), Images (.png, .jpg, .jpeg, .tiff, .bmp),
and files without extensions (e.g., IM_0001, IM_0002, etc.)
""")
# Modality selection
modality = gr.Radio(
choices=["CT", "CR", "DX", "RX", "DR"],
value="CT",
label="Modality",
info="Will be auto-detected for DICOM files"
)
# Task selection
task = gr.Dropdown(
choices=[
("🎯 Point Analysis", "analyze_point"),
("🔬 Fat Segmentation (CT only)", "segment_fat"),
("📊 Full Analysis", "full_analysis")
],
value="full_analysis",
label="Analysis Task"
)
# ROI settings
with gr.Accordion("🎯 Region of Interest (ROI)", open=True):
roi_x = gr.Slider(0, 512, 256, label="X Position", step=1)
roi_y = gr.Slider(0, 512, 256, label="Y Position", step=1)
roi_radius = gr.Slider(5, 50, 10, label="Radius", step=1)
# Clinical context
with gr.Accordion("🏥 Clinical Context", open=False):
symptoms = gr.CheckboxGroup(
choices=[
"dyspnea", "chest_pain", "abdominal_pain",
"trauma", "obesity_screening", "routine_check"
],
label="Symptoms/Indication"
)
# Visualization options
with gr.Accordion("🎨 Visualization Options", open=True):
show_overlay = gr.Checkbox(
label="Show ROI/Segmentation Overlay",
value=True,
info="Display ROI circle or fat segmentation info on the image"
)
analyze_btn = gr.Button("🔬 Analyze", variant="primary", size="lg")
with gr.Column(scale=2):
# Results with tabs for different views
with gr.Tab("🖼️ Original Image"):
image_display = gr.Image(label="Medical Image", type="numpy")
with gr.Tab("🎯 Overlay View"):
overlay_display = gr.Image(label="Image with Overlay", type="numpy")
file_info = gr.Textbox(label="File Information", lines=1)
with gr.Tab("📊 Visual Report"):
report_html = gr.HTML()
with gr.Tab("🔧 JSON Output"):
json_output = gr.JSON(label="Structured Data for AI Agents")
# Examples and help
with gr.Row():
gr.Markdown("""
### 📁 Supported Formats
- **DICOM**: Automatic HU value extraction and modality detection
- **PNG/JPG**: Interpreted based on selected modality
- **All Formats**: Automatic grayscale conversion
- **Files without extension**: Supported (e.g., IM_0001) - will try DICOM first
### 🎯 Usage
1. Upload a medical image file
2. Select modality (auto-detected for DICOM)
3. Choose analysis task
4. Adjust ROI position for point analysis
5. Click "Analyze"
### 💡 Features
- **ROI Visualization**: See the exact area being analyzed
- **Fat Segmentation**: Visual percentages for CT scans
- **Multi-format Support**: Works with any medical image format
- **AI Agent Ready**: Structured JSON output for integration
""")
# Connect the interface
analyze_btn.click(
fn=process_and_analyze,
inputs=[file_input, modality, task, roi_x, roi_y, roi_radius, symptoms, show_overlay],
outputs=[image_display, file_info, report_html, json_output, overlay_display]
)
# Auto-update ROI limits when image is loaded
def update_roi_on_upload(file_obj):
if file_obj is None:
return gr.update(), gr.update()
try:
analyzer = MedicalImageAnalyzer()
_, _, metadata = analyzer.process_file(file_obj.name if hasattr(file_obj, 'name') else str(file_obj))
if 'shape' in metadata:
h, w = metadata['shape']
return gr.update(maximum=w-1, value=w//2), gr.update(maximum=h-1, value=h//2)
except:
pass
return gr.update(), gr.update()
file_input.change(
fn=update_roi_on_upload,
inputs=[file_input],
outputs=[roi_x, roi_y]
)
return demo
if __name__ == "__main__":
demo = create_demo()
demo.launch()