|
|
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.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 transform_coordinates(self, clicked_x, clicked_y): |
|
|
"""Transform screen coordinates to image coordinates using ImageJ method""" |
|
|
|
|
|
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 = round(x) |
|
|
y = round(y) |
|
|
|
|
|
return x, y |
|
|
|
|
|
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, |
|
|
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.current_image.shape[:2] |
|
|
|
|
|
|
|
|
Y, X = np.ogrid[:height, :width] |
|
|
center_x = x |
|
|
center_y = y |
|
|
|
|
|
|
|
|
radius = self.circle_diameter / 2.0 |
|
|
|
|
|
|
|
|
dx = X - center_x |
|
|
dy = Y - center_y |
|
|
dist_squared = dx * dx + dy * dy |
|
|
mask = dist_squared <= (radius * radius) |
|
|
|
|
|
|
|
|
roi_pixels = self.current_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) |
|
|
|
|
|
|
|
|
print(f"\nDetailed Analysis:") |
|
|
print(f"Coordinates: ({x}, {y})") |
|
|
print(f"Pixel count: {n_pixels}") |
|
|
print(f"Area: {area:.3f} mm²") |
|
|
print(f"Mean: {mean_value:.3f}") |
|
|
print(f"StdDev: {std_dev:.3f}") |
|
|
print(f"Min: {min_val}") |
|
|
print(f"Max: {max_val}") |
|
|
|
|
|
|
|
|
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 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 - 1) / 2) * self.zoom_factor) |
|
|
|
|
|
|
|
|
cv2.circle(zoomed_bgr, |
|
|
(zoomed_x, zoomed_y), |
|
|
zoomed_radius, |
|
|
self.CIRCLE_COLOR, |
|
|
1, |
|
|
lineType=cv2.LINE_AA) |
|
|
|
|
|
|
|
|
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.CIRCLE_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 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 = float(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 |