import gradio as gr import cv2 import numpy as np import pandas as pd import pydicom import io from PIL import Image class DicomAnalyzer: def __init__(self): self.results = [] self.circle_diameter = 9 self.zoom_factor = 1.0 self.current_image = None self.dicom_data = None self.display_image = None self.marks = [] # Store (x, y, diameter) for each mark self.original_image = None self.original_display = None # Pan position self.pan_x = 0 self.pan_y = 0 self.max_pan_x = 0 self.max_pan_y = 0 def load_dicom(self, file): try: if file is None: return None, "No file uploaded" if hasattr(file, 'name'): dicom_data = pydicom.dcmread(file.name) else: dicom_data = pydicom.dcmread(file) image = dicom_data.pixel_array.astype(np.float32) rescale_slope = getattr(dicom_data, 'RescaleSlope', 1) rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0) image = (image * rescale_slope) + rescale_intercept self.current_image = image self.original_image = image.copy() self.dicom_data = dicom_data self.display_image = self.normalize_image(image) self.original_display = self.display_image.copy() # Reset view on new image self.reset_view() return self.display_image, "DICOM file loaded successfully" except Exception as e: return None, f"Error loading DICOM file: {str(e)}" def normalize_image(self, image): try: normalized = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) if len(normalized.shape) == 2: normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2RGB) return normalized except Exception as e: print(f"Error normalizing image: {str(e)}") return None def reset_view(self): """Reset zoom and center the image""" self.zoom_factor = 1.0 self.pan_x = 0 self.pan_y = 0 if self.original_display is not None: return self.update_display() return None def zoom_in(self, image): """Increase zoom factor""" self.zoom_factor = min(20.0, self.zoom_factor + 0.5) return self.update_display() def zoom_out(self, image): """Decrease zoom factor""" self.zoom_factor = max(1.0, self.zoom_factor - 0.5) return self.update_display() def update_display(self): try: if self.original_display is None: return None # Calculate zoomed size height, width = self.original_display.shape[:2] new_height = int(height * self.zoom_factor) new_width = int(width * self.zoom_factor) # Create zoomed image zoomed = cv2.resize(self.original_display, (new_width, new_height), interpolation=cv2.INTER_CUBIC) # Draw marks on the zoomed image for x, y, diameter in self.marks: # Calculate zoomed coordinates zoomed_x = int(x * self.zoom_factor) zoomed_y = int(y * self.zoom_factor) zoomed_diameter = int(diameter * self.zoom_factor) cv2.circle(zoomed, (zoomed_x, zoomed_y), zoomed_diameter // 2, (255, 255, 0), # BGR: Yellow 2, lineType=cv2.LINE_AA) # Extract visible portion considering pan visible_height = min(height, new_height) visible_width = min(width, new_width) # Ensure pan values don't exceed bounds self.pan_x = min(self.pan_x, max(0, new_width - width)) self.pan_y = min(self.pan_y, max(0, new_height - height)) visible = zoomed[ self.pan_y:self.pan_y + visible_height, self.pan_x:self.pan_x + visible_width ] return visible except Exception as e: print(f"Error updating display: {str(e)}") return self.original_display def handle_keyboard(self, key): """Handle keyboard inputs for pan""" try: print(f"Handling key press: {key}") # Debug print # Reduce pan amount for finer control pan_amount = int(10 * self.zoom_factor) if key == 'ArrowLeft': self.pan_x = max(0, self.pan_x - pan_amount) elif key == 'ArrowRight': self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount) elif key == 'ArrowUp': self.pan_y = max(0, self.pan_y - pan_amount) elif key == 'ArrowDown': self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount) return self.update_display() except Exception as e: print(f"Error handling keyboard input: {str(e)}") return self.display_image def analyze_roi(self, evt: gr.SelectData): try: if self.current_image is None: return None, "No image loaded" # Convert clicked coordinates to original image coordinates x = int((evt.index[0] + self.pan_x) / self.zoom_factor) y = int((evt.index[1] + self.pan_y) / self.zoom_factor) mask = np.zeros_like(self.current_image, dtype=np.uint8) y_indices, x_indices = np.ogrid[:self.current_image.shape[0], :self.current_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 roi_pixels = self.current_image[mask == 1] pixel_spacing = float(self.dicom_data.PixelSpacing[0]) area_pixels = np.sum(mask) area_mm2 = area_pixels * (pixel_spacing ** 2) mean = np.mean(roi_pixels) stddev = np.std(roi_pixels) min_val = np.min(roi_pixels) max_val = np.max(roi_pixels) result = { 'Area (mm²)': f"{area_mm2:.3f}", 'Mean': f"{mean:.3f}", 'StdDev': f"{stddev:.3f}", 'Min': f"{min_val:.3f}", 'Max': f"{max_val:.3f}", 'Point': f"({x}, {y})" } self.results.append(result) self.marks.append((x, y, self.circle_diameter)) return self.update_display(), self.format_results() except Exception as e: print(f"Error analyzing ROI: {str(e)}") return self.display_image, f"Error analyzing ROI: {str(e)}" def format_results(self): if not self.results: return "No measurements yet" df = pd.DataFrame(self.results) columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point'] df = df[columns_order] return df.to_string(index=False) def add_blank_row(self, image): self.results.append({ 'Area (mm²)': '', 'Mean': '', 'StdDev': '', 'Min': '', 'Max': '', 'Point': '' }) return image, self.format_results() def add_zero_row(self, image): self.results.append({ 'Area (mm²)': '0.000', 'Mean': '0.000', 'StdDev': '0.000', 'Min': '0.000', 'Max': '0.000', 'Point': '(0, 0)' }) return image, self.format_results() def undo_last(self, image): if self.results: self.results.pop() if self.marks: self.marks.pop() return self.update_display(), self.format_results() def save_results(self): try: if not self.results: return None, "No results to save" df = pd.DataFrame(self.results) columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point'] df = df[columns_order] temp_file = "analysis_results.xlsx" df.to_excel(temp_file, index=False) return temp_file, "Results saved successfully" except Exception as e: return None, f"Error saving results: {str(e)}" def create_interface(): analyzer = DicomAnalyzer() with gr.Blocks(css="#image_display { outline: none; }") as interface: gr.Markdown("# DICOM Image Analyzer") with gr.Row(): with gr.Column(): file_input = gr.File(label="Upload DICOM file") diameter_slider = gr.Slider( minimum=1, maximum=20, value=9, step=1, label="ROI Diameter (pixels)" ) # Zoom controls with gr.Row(): zoom_in_btn = gr.Button("Zoom In (+)") zoom_out_btn = gr.Button("Zoom Out (-)") reset_btn = gr.Button("Reset View") with gr.Column(): image_display = gr.Image(label="DICOM Image", interactive=True, elem_id="image_display") with gr.Row(): blank_btn = gr.Button("Add Blank Row") zero_btn = gr.Button("Add Zero Row") undo_btn = gr.Button("Undo Last") save_btn = gr.Button("Save Results") results_display = gr.Textbox(label="Results", interactive=False) file_output = gr.File(label="Download Results") key_press = gr.Textbox(visible=False, elem_id="key_press") # Instructions gr.Markdown(""" ### Controls: - Use arrow keys to pan when zoomed in - Click points to measure - Use Zoom In/Out buttons or Reset View to adjust zoom level """) # Event handlers file_input.change( fn=analyzer.load_dicom, inputs=file_input, outputs=[image_display, results_display] ) image_display.select( fn=analyzer.analyze_roi, outputs=[image_display, results_display] ) diameter_slider.change( fn=lambda x: (analyzer.circle_diameter := x, f"Diameter set to {x} pixels")[1], inputs=diameter_slider, outputs=gr.Textbox(label="Status") ) zoom_in_btn.click( fn=analyzer.zoom_in, inputs=image_display, outputs=image_display ) zoom_out_btn.click( fn=analyzer.zoom_out, inputs=image_display, outputs=image_display ) reset_btn.click( fn=analyzer.reset_view, outputs=image_display ) key_press.change( fn=analyzer.handle_keyboard, inputs=key_press, outputs=image_display, api_name="handle_keyboard" ) blank_btn.click( fn=analyzer.add_blank_row, inputs=image_display, outputs=[image_display, results_display] ) zero_btn.click( fn=analyzer.add_zero_row, inputs=image_display, outputs=[image_display, results_display] ) undo_btn.click( fn=analyzer.undo_last, inputs=image_display, outputs=[image_display, results_display] ) save_btn.click( fn=analyzer.save_results, outputs=[file_output, results_display] ) # JavaScript for keyboard handling js = """ """ gr.HTML(js) return interface if __name__ == "__main__": interface = create_interface() interface.launch()