HeshamAI's picture
Update app.py
92e84e5 verified
import gradio as gr
import cv2
import numpy as np
import pandas as pd
import pydicom
import io
import os
from PIL import Image
import tempfile
class DicomAnalyzer:
def __init__(self):
self.results = []
self.circle_diameter = 9
self.zoom_factor = 1.0
self.current_image1 = None
self.current_image2 = None
self.dicom_data1 = None
self.dicom_data2 = None
self.image_display1 = None
self.image_display2 = None
self.marks1 = []
self.marks2 = []
def load_dicom(self, file):
try:
if file is None:
return None, None, None
dicom_data = pydicom.dcmread(file.name)
image = dicom_data.pixel_array.astype(np.float32)
# Apply rescale slope and intercept
rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
image = (image * rescale_slope) + rescale_intercept
# Store original image for analysis
original_image = image.copy()
# Normalize for display
image_display = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
# Convert to BGR for visualization
if len(image_display.shape) == 2:
image_display = cv2.cvtColor(image_display, cv2.COLOR_GRAY2BGR)
return original_image, image_display, dicom_data
except Exception as e:
print(f"Error loading DICOM file: {str(e)}")
return None, None, None
def analyze_point(self, image, dicom_data, x, y):
try:
# Create a circular mask
mask = np.zeros_like(image, dtype=np.uint8)
y_indices, x_indices = np.ogrid[:image.shape[0], :image.shape[1]]
distance_from_center = np.sqrt((x_indices - x)**2 + (y_indices - y)**2)
mask[distance_from_center <= self.circle_diameter / 2] = 1
# Extract pixel values within the circle
pixels = image[mask == 1]
# Calculate metrics
area_pixels = np.sum(mask)
pixel_spacing = float(dicom_data.PixelSpacing[0])
area_mm2 = area_pixels * (pixel_spacing**2)
mean = np.mean(pixels)
stddev = np.std(pixels)
min_val = np.min(pixels)
max_val = np.max(pixels)
return {
'Area (mm²)': f"{area_mm2:.3f}",
'Mean': f"{mean:.3f}",
'StdDev': f"{stddev:.3f}",
'Min': f"{min_val:.3f}",
'Max': f"{max_val:.3f}"
}
except Exception as e:
print(f"Error analyzing point: {str(e)}")
return None
def draw_circle(self, image, x, y, is_image1=True):
try:
image_copy = image.copy()
# Draw all previous marks
marks = self.marks1 if is_image1 else self.marks2
for mark_x, mark_y in marks:
cv2.circle(image_copy,
(int(mark_x), int(mark_y)),
int(self.circle_diameter/2),
(0, 255, 255),
1,
lineType=cv2.LINE_AA)
# Draw new mark
cv2.circle(image_copy,
(int(x), int(y)),
int(self.circle_diameter/2),
(0, 255, 255),
1,
lineType=cv2.LINE_AA)
# Store new mark
if is_image1:
self.marks1.append((x, y))
else:
self.marks2.append((x, y))
return image_copy
except Exception as e:
print(f"Error drawing circle: {str(e)}")
return image
def process_image1(self, file):
image, image_display, dicom_data = self.load_dicom(file)
self.current_image1 = image
self.image_display1 = image_display
self.dicom_data1 = dicom_data
return image_display
def process_image2(self, file):
image, image_display, dicom_data = self.load_dicom(file)
self.current_image2 = image
self.image_display2 = image_display
self.dicom_data2 = dicom_data
return image_display
def handle_click1(self, evt: gr.SelectData):
if self.current_image1 is None:
return self.image_display1, "Please load Image 1 first"
try:
x, y = evt.index
marked_image = self.draw_circle(self.image_display1, x, y, is_image1=True)
self.image_display1 = marked_image
results = self.analyze_point(self.current_image1, self.dicom_data1, x, y)
if results:
results['Image'] = "Image 1"
results['Point'] = f"({x}, {y})"
self.results.append(results)
return self.image_display1, self.format_results()
except Exception as e:
print(f"Error in handle_click1: {str(e)}")
return self.image_display1, f"Error: {str(e)}"
def handle_click2(self, evt: gr.SelectData):
if self.current_image2 is None:
return self.image_display2, "Please load Image 2 first"
try:
x, y = evt.index
marked_image = self.draw_circle(self.image_display2, x, y, is_image1=False)
self.image_display2 = marked_image
results = self.analyze_point(self.current_image2, self.dicom_data2, x, y)
if results:
results['Image'] = "Image 2"
results['Point'] = f"({x}, {y})"
self.results.append(results)
return self.image_display2, self.format_results()
except Exception as e:
print(f"Error in handle_click2: {str(e)}")
return self.image_display2, f"Error: {str(e)}"
def format_results(self):
if not self.results:
return "No results yet"
df = pd.DataFrame(self.results)
return df.to_string()
def clear_results(self):
self.results = []
self.marks1 = []
self.marks2 = []
if self.current_image1 is not None:
self.image_display1 = cv2.cvtColor(
cv2.normalize(self.current_image1, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
if self.current_image2 is not None:
self.image_display2 = cv2.cvtColor(
cv2.normalize(self.current_image2, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8),
cv2.COLOR_GRAY2BGR
)
return "Results cleared", self.image_display1, self.image_display2
def add_blank_row(self):
self.results.append({
'Image': '',
'Point': '',
'Area (mm²)': '',
'Mean': '',
'StdDev': '',
'Min': '',
'Max': ''
})
return self.format_results()
def update_circle_diameter(self, value):
self.circle_diameter = value
return f"Circle diameter set to {value}"
def save_results(self):
try:
if not self.results:
return None, "No results to save"
df = pd.DataFrame(self.results)
# Create temporary file
temp_dir = tempfile.gettempdir()
temp_file = os.path.join(temp_dir, "analysis_results.xlsx")
# Save to Excel
df.to_excel(temp_file, index=False, engine='openpyxl')
return temp_file, "Results saved successfully. Click to download."
except Exception as e:
print(f"Error saving results: {str(e)}")
return None, f"Error saving results: {str(e)}"
def create_interface():
analyzer = DicomAnalyzer()
with gr.Blocks() as interface:
gr.Markdown("# CT DICOM Image Analyzer")
with gr.Row():
with gr.Column():
file1 = gr.File(label="Upload first DICOM file")
image1 = gr.Image(label="Image 1", interactive=True, type="numpy")
file1.change(fn=analyzer.process_image1, inputs=file1, outputs=image1)
with gr.Column():
file2 = gr.File(label="Upload second DICOM file")
image2 = gr.Image(label="Image 2", interactive=True, type="numpy")
file2.change(fn=analyzer.process_image2, inputs=file2, outputs=image2)
with gr.Row():
circle_diameter = gr.Slider(
minimum=1,
maximum=20,
value=9,
step=1,
label="Circle Diameter"
)
with gr.Row():
clear_btn = gr.Button("Clear Results")
blank_row_btn = gr.Button("Add Blank Row")
save_btn = gr.Button("Save Results")
results = gr.Textbox(label="Results", interactive=False)
file_output = gr.File(label="Download Results")
status = gr.Textbox(label="Status")
# Connect events
circle_diameter.change(
fn=analyzer.update_circle_diameter,
inputs=circle_diameter,
outputs=status
)
image1.select(
fn=analyzer.handle_click1,
outputs=[image1, results]
)
image2.select(
fn=analyzer.handle_click2,
outputs=[image2, results]
)
clear_btn.click(
fn=analyzer.clear_results,
outputs=[status, image1, image2]
)
blank_row_btn.click(
fn=analyzer.add_blank_row,
outputs=results
)
save_btn.click(
fn=analyzer.save_results,
outputs=[file_output, status]
)
return interface
if __name__ == "__main__":
interface = create_interface()
interface.launch()