Spaces:
Runtime error
Runtime error
| 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): | |
| 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 | |
| formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1) | |
| # SNR formula -> row1 | |
| formula_snr = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")" | |
| cell_snr = ws[f"{formula_col}{row1}"] | |
| cell_snr.value = formula_snr | |
| cell_snr.font = red_font | |
| cell_snr.alignment = openpyxl.styles.Alignment(horizontal='center') | |
| # CNR formula -> row2 | |
| formula_cnr = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")" | |
| cell_cnr = ws[f"{formula_col}{row2}"] | |
| cell_cnr.value = formula_cnr | |
| cell_cnr.font = red_font | |
| cell_cnr.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', vertical='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') | |
| ] | |
| # Write the headers in row1 for each column group. | |
| 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)' | |
| ] | |
| # Put phantom size labels in column A above each row pair. | |
| 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 | |
| # Write the results in the row pairs, add SNR/CNR formulas. | |
| 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] | |
| row1, row2 = row_pairs[current_row_pair] | |
| if result_idx < len(self.results): | |
| result = self.results[result_idx] | |
| self._write_result_to_cells(ws, result, cols, row1) | |
| result_idx += 1 | |
| if result_idx < len(self.results): | |
| result = self.results[result_idx] | |
| self._write_result_to_cells(ws, result, cols, row2) | |
| result_idx += 1 | |
| self.add_formulas_to_template(ws, (row1,row2), cols, red_font) | |
| current_col_group += 1 | |
| if current_col_group >= len(column_groups): | |
| current_col_group = 0 | |
| current_row_pair += 1 | |
| # Center-align the raw data rows (2..30) in all column groups. | |
| 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 | |
| ######################################################### | |
| # تصميم "1-AVG" في الصف 35، مع تجاهل الأصفار في الحساب | |
| ######################################################### | |
| start_row = 35 | |
| ws['C35'] = "1-AVG" | |
| ws['C35'].alignment = center_alignment | |
| ws.merge_cells('D35:E35') | |
| ws.merge_cells('F35:G35') | |
| ws.merge_cells('H35:I35') | |
| headers_avg = { | |
| 'D35': 'AVG MEAN', | |
| 'F35': 'AVG STDDEV', | |
| 'H35': 'AVG CNR' | |
| } | |
| for c_ref, text_val in headers_avg.items(): | |
| ws[c_ref] = text_val | |
| ws[c_ref].font = red_font | |
| ws[c_ref].alignment = center_alignment | |
| # We'll keep the same 10 phantom sizes, to fill rows 36..45. | |
| phantom_sizes2 = [ | |
| '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)', | |
| '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)' | |
| ] | |
| for i, size_label in enumerate(phantom_sizes2): | |
| row = start_row + i + 1 # 36..45 | |
| ws.merge_cells(f'D{row}:E{row}') | |
| ws.merge_cells(f'F{row}:G{row}') | |
| ws.merge_cells(f'H{row}:I{row}') | |
| c_cell = ws[f'C{row}'] | |
| c_cell.value = size_label | |
| c_cell.font = red_font | |
| c_cell.alignment = center_alignment | |
| if i >= len(row_pairs): | |
| continue | |
| (raw_row1, raw_row2) = row_pairs[i] | |
| mean_values = [] | |
| stddev_values = [] | |
| cnr_cells = [] # We'll store references to the row2 formula for CNR | |
| # Loop over column_groups to gather Mean (row1), StdDev (row1), and CNR references (row2). | |
| for group in column_groups: | |
| mean_col = group[1] # e.g. 'C' | |
| std_col = group[2] # e.g. 'D' | |
| # Read mean from row1 => if 0 => skip. | |
| m1_val = ws[f"{mean_col}{raw_row1}"].value | |
| try: | |
| m1_val = float(m1_val) if m1_val not in [None,''] else None | |
| except: | |
| m1_val = None | |
| # تجاهل أي خلية = 0 | |
| if m1_val == 0: | |
| m1_val = None | |
| if m1_val is not None: | |
| mean_values.append(m1_val) | |
| # Read std from row1 => if 0 => skip. | |
| s1_val = ws[f"{std_col}{raw_row1}"].value | |
| try: | |
| s1_val = float(s1_val) if s1_val not in [None,''] else None | |
| except: | |
| s1_val = None | |
| if s1_val == 0: | |
| s1_val = None | |
| if s1_val is not None: | |
| stddev_values.append(s1_val) | |
| # For CNR, we have formula in the column after group[-1], row2. | |
| formula_col = get_column_letter(column_index_from_string(group[-1]) + 1) | |
| cnr_cell_ref = f"{formula_col}{raw_row2}" | |
| # حتى لا نُدخل خلية الـCNR في الحساب إن كانت قيم الصف الثاني = 0 | |
| # مثلاً mean2=0 أو std2=0 => نعتبرها غير صالحة. | |
| mean2_val = ws[f"{mean_col}{raw_row2}"].value | |
| std2_val = ws[f"{std_col}{raw_row2}"].value | |
| try: | |
| mean2_val = float(mean2_val) if mean2_val not in [None,''] else None | |
| std2_val = float(std2_val) if std2_val not in [None,''] else None | |
| except: | |
| mean2_val, std2_val = None, None | |
| if mean2_val == 0: | |
| mean2_val = None | |
| if std2_val == 0: | |
| std2_val = None | |
| # لو عندك منطق إضافي: التحقق أن Mean1 !=0 أيضاً (m1_val) | |
| # إذا أردت تجاهل الخلية إن كان m1_val=0...الخ. | |
| # لكن عادة, تحسب CNR من Mean1,Mean2,Std2 => if any=0 => skip. | |
| # نحسب Mean1 من نفس row1_col. | |
| # m1_val = read it above, but we didn't store it if zero => might do it again. | |
| # Decide if we require m1_val !=0 too? If so: | |
| if (m1_val is not None) and (mean2_val is not None) and (std2_val is not None): | |
| cnr_cells.append(cnr_cell_ref) | |
| # حساب متوسط المين. | |
| final_mean = sum(mean_values)/len(mean_values) if mean_values else None | |
| if final_mean is not None: | |
| ws[f'D{row}'].value = final_mean | |
| ws[f'D{row}'].alignment = center_alignment | |
| ws[f'D{row}'].number_format = '0.0000' | |
| # حساب متوسط stddev | |
| final_std = sum(stddev_values)/len(stddev_values) if stddev_values else None | |
| if final_std is not None: | |
| ws[f'F{row}'].value = final_std | |
| ws[f'F{row}'].alignment = center_alignment | |
| ws[f'F{row}'].number_format = '0.0000' | |
| # أما الـCNR, فننشئ صيغة AVERAGE(...) لو عندنا cnr_cells. | |
| if cnr_cells: | |
| formula_avg_cnr = f"=IFERROR(AVERAGE({','.join(cnr_cells)}),\"\")" | |
| ws[f'H{row}'].value = formula_avg_cnr | |
| ws[f'H{row}'].alignment = center_alignment | |
| ws[f'H{row}'].number_format = '0.0000' | |
| # Add thin border around the region C35..I45. | |
| thin_side = openpyxl.styles.Side(style='thin') | |
| border = openpyxl.styles.Border( | |
| left=thin_side, right=thin_side, top=thin_side, bottom=thin_side | |
| ) | |
| for r in range(35, 46): | |
| for col in ['C','D','E','F','G','H','I']: | |
| ws[f"{col}{r}"].border = border | |
| 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] | |
| ) | |
| # JavaScript to capture arrow keys and pass to Gradio. | |
| js = """ | |
| <script> | |
| document.addEventListener('keydown', function(e) { | |
| if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) { | |
| e.preventDefault(); | |
| const el = document.querySelector('#key_press textarea'); | |
| if (el) { | |
| el.value = e.key; | |
| el.dispatchEvent(new Event('input')); | |
| setTimeout(() => { | |
| el.value = ''; | |
| el.dispatchEvent(new Event('input')); | |
| }, 100); | |
| } | |
| } | |
| }); | |
| </script> | |
| """ | |
| 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 | |