import gradio as gr import cv2 import numpy as np import pandas as pd import pydicom import io from PIL import Image import openpyxl from openpyxl.utils import get_column_letter, column_index_from_string import logging import time import traceback from functools import wraps import sys print("Starting imports completed...") # Set up logging logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('dicom_analyzer_debug.log'), logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger(__name__) def debug_decorator(func): @wraps(func) def wrapper(*args, **kwargs): logger.debug(f"Entering {func.__name__}") start_time = time.time() try: result = func(*args, **kwargs) logger.debug(f"Function {func.__name__} completed successfully") return result except Exception as e: logger.error(f"Error in {func.__name__}: {str(e)}") logger.error(traceback.format_exc()) raise finally: end_time = time.time() logger.debug(f"Execution time: {end_time - start_time:.4f} seconds") return wrapper class DicomAnalyzer: def __init__(self): self.results = [] self.circle_diameter = 9.0 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 self.CIRCLE_COLOR = (0, 255, 255) # BGR format self.SMALL_CIRCLES_COLOR = (255, 255, 255) # BGR white print("DicomAnalyzer initialized...") def save_results(self): try: if not self.results: logger.warning("Attempted to save with no 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] timestamp = time.strftime("%Y%m%d_%H%M%S") output_file = f"analysis_results_{timestamp}.xlsx" with pd.ExcelWriter(output_file, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name='Results') worksheet = writer.sheets['Results'] for idx, col in enumerate(df.columns): max_length = max( df[col].astype(str).apply(len).max(), len(str(col)) ) + 2 worksheet.column_dimensions[get_column_letter(idx + 1)].width = max_length logger.info(f"Results saved successfully to {output_file}") return output_file, f"Results saved successfully to {output_file}" except Exception as e: error_msg = f"Error saving results: {str(e)}" logger.error(error_msg) logger.error(traceback.format_exc()) return None, error_msg def reset_all(self, image): self.results = [] self.marks = [] self.reset_view() return self.update_display(), "All data has been reset" 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) self.original_image = image.copy() 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.dicom_data = dicom_data self.display_image = self.normalize_image(image) self.original_display = self.display_image.copy() self.reset_all(None) 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, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8U ) if len(normalized.shape) == 2: normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR) 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 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 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 ) zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR) for x, y, diameter in self.marks: zoomed_x = int(x * self.zoom_factor) zoomed_y = int(y * self.zoom_factor) zoomed_radius = int((diameter / 2.0) * self.zoom_factor) # Draw the main yellow circle cv2.circle( zoomed_bgr, (zoomed_x, zoomed_y), zoomed_radius, self.CIRCLE_COLOR, 1, lineType=cv2.LINE_AA ) # Draw 8 small white circles around num_points = 8 for i in range(num_points): angle = 2 * np.pi * i / num_points point_x = int(zoomed_x + zoomed_radius * np.cos(angle)) point_y = int(zoomed_y + zoomed_radius * np.sin(angle)) cv2.circle( zoomed_bgr, (point_x, point_y), 1, self.SMALL_CIRCLES_COLOR, -1, lineType=cv2.LINE_AA ) zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB) self.max_pan_x = max(0, new_width - width) self.max_pan_y = max(0, new_height - height) self.pan_x = min(max(0, self.pan_x), self.max_pan_x) self.pan_y = min(max(0, self.pan_y), self.max_pan_y) visible = zoomed[ int(self.pan_y):int(self.pan_y + height), int(self.pan_x):int(self.pan_x + width) ] return visible except Exception as e: print(f"Error updating display: {str(e)}") return self.original_display def analyze_roi(self, evt: gr.SelectData): try: if self.current_image is None: return None, "No image loaded" clicked_x = evt.index[0] clicked_y = evt.index[1] x = clicked_x + self.pan_x y = clicked_y + self.pan_y if self.zoom_factor != 1.0: x = x / self.zoom_factor y = y / self.zoom_factor x = int(round(x)) y = int(round(y)) height, width = self.original_image.shape[:2] Y, X = np.ogrid[:height, :width] radius = self.circle_diameter / 2.0 r_squared = radius * radius dx = X - x dy = Y - y dist_squared = dx * dx + dy * dy mask = np.zeros((height, width), dtype=bool) mask[dist_squared <= r_squared] = True roi_pixels = self.original_image[mask] if len(roi_pixels) == 0: return self.display_image, "Error: No pixels selected" pixel_spacing = float(self.dicom_data.PixelSpacing[0]) n_pixels = np.sum(mask) area = n_pixels * (pixel_spacing ** 2) mean_value = np.mean(roi_pixels) std_dev = np.std(roi_pixels, ddof=1) min_val = np.min(roi_pixels) max_val = np.max(roi_pixels) rescale_slope = getattr(self.dicom_data, 'RescaleSlope', 1) rescale_intercept = getattr(self.dicom_data, 'RescaleIntercept', 0) mean_value = (mean_value * rescale_slope) + rescale_intercept std_dev = std_dev * rescale_slope min_val = (min_val * rescale_slope) + rescale_intercept max_val = (max_val * rescale_slope) + rescale_intercept result = { 'Area (mm²)': f"{area:.3f}", 'Mean': f"{mean_value:.3f}", 'StdDev': f"{std_dev:.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 add_formulas_to_template(self, ws, row_pair, col_group, red_font): """ Inserts SNR (first row) and CNR (second row) formulas with IFERROR. """ try: base_col = col_group[1] # Mean column std_col = col_group[2] # StdDev column row1, row2 = row_pair # SNR formula formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")" formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1) cell1 = ws[f"{formula_col}{row1}"] cell1.value = formula1 cell1.font = red_font cell1.alignment = openpyxl.styles.Alignment(horizontal='center') # CNR formula formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")" cell2 = ws[f"{formula_col}{row2}"] cell2.value = formula2 cell2.font = red_font cell2.alignment = openpyxl.styles.Alignment(horizontal='center') logger.debug(f"Added formulas for rows {row1},{row2} in column {formula_col}") except Exception as e: logger.error(f"Error adding formulas: {str(e)}") def save_formatted_results(self, output_path): try: if not self.results: return None, "No results to save" wb = openpyxl.Workbook() ws = wb.active red_font = openpyxl.styles.Font(color="FF0000") center_alignment = openpyxl.styles.Alignment(horizontal='center') headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max'] column_groups = [ ('B', 'C', 'D', 'E', 'F'), ('H', 'I', 'J', 'K', 'L'), ('N', 'O', 'P', 'Q', 'R'), ('T', 'U', 'V', 'W', 'X'), ('Z', 'AA', 'AB', 'AC', 'AD'), ('AF', 'AG', 'AH', 'AI', 'AJ'), ('AL', 'AM', 'AN', 'AO', 'AP'), ('AR', 'AS', 'AT', 'AU', 'AV'), ('AX', 'AY', 'AZ', 'BA', 'BB'), ('BD', 'BE', 'BF', 'BG', 'BH'), ('BJ', 'BK', 'BL', 'BM', 'BN'), ('BP', 'BQ', 'BR', 'BS', 'BT'), ('BV', 'BW', 'BX', 'BY', 'BZ') ] for cols in column_groups: for i, header in enumerate(headers): cell = ws[f"{cols[i]}1"] cell.value = header cell.alignment = center_alignment row_pairs = [ (2, 3), (5, 6), (8, 9), (11, 12), (14, 15), (17, 18), (20, 21), (23, 24), (26, 27), (29, 30) ] phantom_sizes = [ '(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)', '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)' ] for i, size in enumerate(phantom_sizes): header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size) header_cell.font = red_font header_cell.alignment = center_alignment result_idx = 0 current_col_group = 0 current_row_pair = 0 while result_idx < len(self.results): if current_row_pair >= len(row_pairs): break cols = column_groups[current_col_group] rows = row_pairs[current_row_pair] if result_idx < len(self.results): result = self.results[result_idx] self._write_result_to_cells(ws, result, cols, rows[0]) result_idx += 1 if result_idx < len(self.results): result = self.results[result_idx] self._write_result_to_cells(ws, result, cols, rows[1]) result_idx += 1 self.add_formulas_to_template(ws, rows, cols, red_font) current_col_group += 1 if current_col_group >= len(column_groups): current_col_group = 0 current_row_pair += 1 for cols in column_groups: for col in cols: for row in range(2, 31): cell = ws[f"{col}{row}"] if cell.value is not None: cell.alignment = center_alignment # StdDev Averages current_row = 32 stddev_header = ws.cell(row=current_row, column=1, value="StdDev Averages") stddev_header.font = red_font stddev_header.alignment = center_alignment current_row += 1 for i, size in enumerate(phantom_sizes): row_number = row_pairs[i][0] stddev_values = [] for cols in column_groups: stddev_col = cols[2] cell_value = ws[f"{stddev_col}{row_number}"].value if cell_value not in [0, None, '']: stddev_values.append(float(cell_value)) size_cell = ws.cell(row=current_row, column=1, value=size) size_cell.alignment = center_alignment if stddev_values: avg_stddev = sum(stddev_values) / len(stddev_values) avg_cell = ws.cell(row=current_row, column=2, value=avg_stddev) avg_cell.number_format = '0.000' avg_cell.alignment = center_alignment current_row += 1 # Mean Averages current_row += 2 mean_header = ws.cell(row=current_row, column=1, value="Mean Averages") mean_header.font = red_font mean_header.alignment = center_alignment current_row += 1 for i, size in enumerate(phantom_sizes): row_number = row_pairs[i][0] # نأخذ الصف الأول فقط mean_values = [] for cols in column_groups: mean_col = cols[1] # The Mean column cell_value = ws[f"{mean_col}{row_number}"].value if cell_value not in [0, None, '']: mean_values.append(float(cell_value)) size_cell = ws.cell(row=current_row, column=1, value=size) size_cell.alignment = center_alignment if mean_values: avg_mean = sum(mean_values) / len(mean_values) avg_cell = ws.cell(row=current_row, column=2, value=avg_mean) avg_cell.number_format = '0.000' avg_cell.alignment = center_alignment current_row += 1 current_row += 2 # CNR Averages cnr_header = ws.cell(row=current_row, column=1, value="CNR Averages") cnr_header.font = red_font cnr_header.alignment = center_alignment current_row += 1 for i, size in enumerate(phantom_sizes): row_number = row_pairs[i][1] cnr_cells = [] for cols in column_groups: formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1) cnr_cell_ref = f"{formula_col}{row_number}" mean_col = cols[1] std_col = cols[2] mean1_val = ws[f"{mean_col}{row_pairs[i][0]}"].value mean2_val = ws[f"{mean_col}{row_pairs[i][1]}"].value std2_val = ws[f"{std_col}{row_pairs[i][1]}"].value try: mean1_val = float(mean1_val) if mean1_val not in [None, ''] else 0 mean2_val = float(mean2_val) if mean2_val not in [None, ''] else 0 std2_val = float(std2_val) if std2_val not in [None, ''] else 0 except: mean1_val, mean2_val, std2_val = 0, 0, 0 if not (mean1_val == 0 and mean2_val == 0 and std2_val == 0): cnr_cells.append(cnr_cell_ref) size_cell = ws.cell(row=current_row, column=1, value=size) size_cell.alignment = center_alignment if cnr_cells: average_formula = f'=IFERROR(AVERAGE({",".join(cnr_cells)}), "")' avg_cell = ws.cell(row=current_row, column=2) avg_cell.value = average_formula avg_cell.number_format = '0.000' avg_cell.alignment = center_alignment current_row += 1 for row in range(32, current_row): for col in range(1, 3): cell = ws.cell(row=row, column=col) cell.alignment = center_alignment wb.save(output_path) return output_path, f"Results saved successfully ({len(self.results)} measurements)" except Exception as e: logger.error(f"Error saving formatted results: {str(e)}") return None, f"Error saving results: {str(e)}" def _write_result_to_cells(self, ws, result, cols, row): center_alignment = openpyxl.styles.Alignment(horizontal='center') value_mapping = { 'Area': 'Area (mm²)', 'Mean': 'Mean', 'StdDev': 'StdDev', 'Min': 'Min', 'Max': 'Max' } for i, (header, key) in enumerate(value_mapping.items()): cell = ws[f"{cols[i]}{row}"] val = result[key] cell.value = float(val) if val not in ['', None] else '' cell.alignment = center_alignment 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_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 add_two_zero_rows(self, image): for _ in range(2): 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 not self.results: # لو مفيش نتائج أصلاً return self.update_display(), self.format_results() last_result = self.results[-1] # نتحقق إذا كان آخر إجراء قياس حقيقي أم صف صفري is_measurement = last_result['Point'] != '(0, 0)' # نمسح آخر نتيجة self.results.pop() # لو كان قياس حقيقي، نمسح العلامة المقابلة له if is_measurement and self.marks: self.marks.pop() return self.update_display(), self.format_results() 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") reset_all_btn = gr.Button("Reset All") with gr.Column(): image_display = gr.Image( label="DICOM Image", interactive=True, elem_id="image_display" ) with gr.Row(): zero_btn = gr.Button("Add Zero Row") zero2_btn = gr.Button("Add Two Zero Rows") undo_btn = gr.Button("Undo Last") save_btn = gr.Button("Save Results") save_formatted_btn = gr.Button("Save Formatted 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. Movement is now larger. - Click points to measure ROI. - Use Zoom In/Out buttons or Reset View to adjust zoom level. - Use Reset All to clear all measurements. - "Save Results": basic Excel with raw data. - "Save Formatted Results": Excel with advanced formatting & formulas. """) def update_diameter(x): analyzer.circle_diameter = float(x) print(f"Diameter updated to: {x}") return f"Diameter set to {x} pixels" def save_formatted(): output_path = "analysis_results_formatted.xlsx" return analyzer.save_formatted_results(output_path) 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, queue=False ) zoom_out_btn.click( fn=analyzer.zoom_out, inputs=image_display, outputs=image_display, queue=False ) reset_btn.click( fn=analyzer.reset_view, outputs=image_display ) reset_all_btn.click( fn=analyzer.reset_all, inputs=image_display, outputs=[image_display, results_display] ) key_press.change( fn=analyzer.handle_keyboard, inputs=key_press, outputs=image_display ) zero_btn.click( fn=analyzer.add_zero_row, inputs=image_display, outputs=[image_display, results_display] ) zero2_btn.click( fn=analyzer.add_two_zero_rows, 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] ) save_formatted_btn.click( fn=save_formatted, outputs=[file_output, results_display] ) js = """ """ gr.HTML(js) print("Interface created successfully") return interface if __name__ == "__main__": try: print("Starting application...") interface = create_interface() print("Launching interface...") interface.queue() interface.launch( server_name="0.0.0.0", server_port=7860, share=True, debug=True, show_error=True, quiet=False ) except Exception as e: print(f"Error launching application: {str(e)}") logger.error(f"Error launching application: {str(e)}") logger.error(traceback.format_exc()) raise e