Testing / app.py
HeshamAI's picture
Update app.py
a6a3ff0 verified
raw
history blame
20.4 kB
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)
# Store original pixel values before any scaling
self.original_image = image.copy()
# Apply DICOM scaling for display
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()
# Reset view on new image
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 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)
# Store original pixel values before any scaling
self.original_image = image.copy()
# Apply DICOM scaling for display
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()
# Reset view on new image
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)}"
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)
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 save_results_to_template(self, template_path, output_path):
try:
if not self.results:
return None, "No results to save"
# Load the Excel template
wb = openpyxl.load_workbook(template_path)
ws = wb.active
# Define row groups and phantom sizes
row_groups = {
"7mm": [41, 42],
"6.5mm": [67, 68],
"6mm": [93, 94],
"5.5mm": [119, 120],
"5mm": [145, 146],
"4.5mm": [171, 172],
"4mm": [197, 198],
"3.5mm": [223, 224],
"3mm": [249, 250],
"2.5mm": [275, 276]
}
# Define columns for measurements
columns = {
'Area (mm²)': "B",
'Mean': "C",
'StdDev': "D",
'Min': "E",
'Max': "F"
}
# Map results to phantom sizes (assuming results are ordered for each size)
result_idx = 0
for size, rows in row_groups.items():
for row in rows:
if result_idx >= len(self.results):
break
result = self.results[result_idx]
# Write each metric to the appropriate cell
for metric, col in columns.items():
cell = f"{col}{row}"
value = result.get(metric, '')
if value:
try:
ws[cell] = float(value)
except ValueError:
ws[cell] = value
result_idx += 1
# Save the workbook
wb.save(output_path)
return output_path, "Results saved successfully in the template format"
except Exception as e:
print(f"Error saving results to template: {str(e)}")
return None, f"Error saving results to template: {str(e)}"
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 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")
template_input = gr.File(label="Upload Excel Template")
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")
save_template_btn = gr.Button("Save to Template")
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
- Upload template and use Save to Template for formatted results
""")
def update_diameter(x):
analyzer.circle_diameter = float(x)
print(f"Diameter updated to: {x}")
return f"Diameter set to {x} pixels"
def save_to_template(template_file):
if template_file is None:
return None, "No template file provided"
output_path = "analysis_results_from_template.xlsx"
return analyzer.save_results_to_template(template_file.name, output_path)
# Event handlers
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
)
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]
)
save_template_btn.click(
fn=save_to_template,
inputs=[template_input],
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)}")
logger.error(f"Error launching application: {str(e)}")
logger.error(traceback.format_exc())
raise e