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()