Spaces:
Build error
Build error
| 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() |