|
|
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...") |
|
|
|
|
|
|
|
|
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) |
|
|
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) |
|
|
|
|
|
|
|
|
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_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, |
|
|
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(5 * self.zoom_factor) |
|
|
|
|
|
original_pan_x = self.pan_x |
|
|
original_pan_y = self.pan_y |
|
|
|
|
|
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) |
|
|
|
|
|
print(f"Pan X: {self.pan_x} (was {original_pan_x})") |
|
|
print(f"Pan Y: {self.pan_y} (was {original_pan_y})") |
|
|
print(f"Max Pan X: {self.max_pan_x}") |
|
|
print(f"Max Pan Y: {self.max_pan_y}") |
|
|
|
|
|
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" |
|
|
|
|
|
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)}" |
|
|
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...") |
|
|
|
|
|
|
|
|
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) |
|
|
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) |
|
|
|
|
|
|
|
|
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_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, |
|
|
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(5 * self.zoom_factor) |
|
|
|
|
|
original_pan_x = self.pan_x |
|
|
original_pan_y = self.pan_y |
|
|
|
|
|
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) |
|
|
|
|
|
print(f"Pan X: {self.pan_x} (was {original_pan_x})") |
|
|
print(f"Pan Y: {self.pan_y} (was {original_pan_y})") |
|
|
print(f"Max Pan X: {self.max_pan_x}") |
|
|
print(f"Max Pan Y: {self.max_pan_y}") |
|
|
|
|
|
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" |
|
|
|
|
|
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)}" |