| import gradio as gr |
| import cv2 |
| import numpy as np |
| import pandas as pd |
| import pydicom |
| import io |
| from PIL import Image |
|
|
| print("Starting imports completed...") |
|
|
| 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 = [] |
| self.original_image = None |
| self.original_display = None |
| |
| self.pan_x = 0 |
| self.pan_y = 0 |
| self.max_pan_x = 0 |
| self.max_pan_y = 0 |
| print("DicomAnalyzer initialized...") |
|
|
| 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() |
| |
| |
| self.reset_view() |
| print("DICOM file loaded successfully") |
| |
| return self.display_image, "DICOM file loaded successfully" |
| except Exception as e: |
| print(f"Error loading DICOM file: {str(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): |
| 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): |
| print("Zooming in...") |
| self.zoom_factor = min(20.0, self.zoom_factor + 0.5) |
| return self.update_display() |
|
|
| def zoom_out(self, image): |
| print("Zooming out...") |
| 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 |
|
|
| |
| height, width = self.original_display.shape[:2] |
| new_height = int(height * self.zoom_factor) |
| new_width = int(width * self.zoom_factor) |
|
|
| |
| zoomed = cv2.resize(self.original_display, (new_width, new_height), |
| interpolation=cv2.INTER_CUBIC) |
|
|
| |
| for x, y, diameter in self.marks: |
| |
| 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), |
| 2, |
| lineType=cv2.LINE_AA) |
|
|
| |
| visible_height = min(height, new_height) |
| visible_width = min(width, new_width) |
| |
| |
| 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): |
| try: |
| print(f"Handling key press: {key}") |
| 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" |
|
|
| 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)) |
| print(f"ROI analyzed at point ({x}, {y})") |
|
|
| 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(): |
| print("Creating 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)" |
| ) |
| |
| 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") |
| |
| 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 |
| """) |
| |
| def update_diameter(x): |
| analyzer.circle_diameter = x |
| print(f"Diameter updated to: {x}") |
| return f"Diameter set to {x} pixels" |
|
|
| |
| 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=update_diameter, |
| 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 |
| ) |
| |
| 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] |
| ) |
|
|
| js = """ |
| <script> |
| document.addEventListener('keydown', function(e) { |
| if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { |
| e.preventDefault(); |
| const keyPressElement = document.querySelector('#key_press textarea'); |
| if (keyPressElement) { |
| keyPressElement.value = e.key; |
| keyPressElement.dispatchEvent(new Event('input')); |
| } |
| } |
| }); |
| </script> |
| """ |
| gr.HTML(js) |
| |
| print("Interface created successfully") |
| return interface |
|
|
| if __name__ == "__main__": |
| try: |
| print("Starting application...") |
| interface = create_interface() |
| print("Launching interface...") |
| interface.launch( |
| server_name="0.0.0.0", |
| server_port=7860, |
| share=True, |
| debug=True |
| ) |
| except Exception as e: |
| print(f"Error launching application: {str(e)}") |
| raise e |