Update app.py
Browse files
app.py
CHANGED
|
@@ -60,7 +60,15 @@ class DicomAnalyzer:
|
|
| 60 |
|
| 61 |
def normalize_image(self, image):
|
| 62 |
try:
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
if len(normalized.shape) == 2:
|
| 65 |
normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2RGB)
|
| 66 |
return normalized
|
|
@@ -100,21 +108,34 @@ class DicomAnalyzer:
|
|
| 100 |
zoomed = cv2.resize(self.original_display, (new_width, new_height),
|
| 101 |
interpolation=cv2.INTER_CUBIC)
|
| 102 |
|
| 103 |
-
# Draw marks
|
| 104 |
for x, y, diameter in self.marks:
|
| 105 |
-
# Calculate zoomed coordinates
|
| 106 |
zoomed_x = int(x * self.zoom_factor)
|
| 107 |
zoomed_y = int(y * self.zoom_factor)
|
| 108 |
zoomed_diameter = int(diameter * self.zoom_factor)
|
| 109 |
|
|
|
|
| 110 |
cv2.circle(zoomed,
|
| 111 |
(zoomed_x, zoomed_y),
|
| 112 |
zoomed_diameter // 2,
|
| 113 |
-
(
|
| 114 |
-
|
| 115 |
lineType=cv2.LINE_AA)
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
visible_height = min(height, new_height)
|
| 119 |
visible_width = min(width, new_width)
|
| 120 |
|
|
@@ -149,243 +170,4 @@ class DicomAnalyzer:
|
|
| 149 |
return self.update_display()
|
| 150 |
except Exception as e:
|
| 151 |
print(f"Error handling keyboard input: {str(e)}")
|
| 152 |
-
return self.display_image
|
| 153 |
-
|
| 154 |
-
def analyze_roi(self, evt: gr.SelectData):
|
| 155 |
-
try:
|
| 156 |
-
if self.current_image is None:
|
| 157 |
-
return None, "No image loaded"
|
| 158 |
-
|
| 159 |
-
x = int((evt.index[0] + self.pan_x) / self.zoom_factor)
|
| 160 |
-
y = int((evt.index[1] + self.pan_y) / self.zoom_factor)
|
| 161 |
-
|
| 162 |
-
mask = np.zeros_like(self.current_image, dtype=np.uint8)
|
| 163 |
-
y_indices, x_indices = np.ogrid[:self.current_image.shape[0], :self.current_image.shape[1]]
|
| 164 |
-
distance_from_center = np.sqrt((x_indices - x) ** 2 + (y_indices - y) ** 2)
|
| 165 |
-
mask[distance_from_center <= self.circle_diameter / 2] = 1
|
| 166 |
-
|
| 167 |
-
roi_pixels = self.current_image[mask == 1]
|
| 168 |
-
|
| 169 |
-
pixel_spacing = float(self.dicom_data.PixelSpacing[0])
|
| 170 |
-
area_pixels = np.sum(mask)
|
| 171 |
-
area_mm2 = area_pixels * (pixel_spacing ** 2)
|
| 172 |
-
mean = np.mean(roi_pixels)
|
| 173 |
-
stddev = np.std(roi_pixels)
|
| 174 |
-
min_val = np.min(roi_pixels)
|
| 175 |
-
max_val = np.max(roi_pixels)
|
| 176 |
-
|
| 177 |
-
result = {
|
| 178 |
-
'Area (mm²)': f"{area_mm2:.3f}",
|
| 179 |
-
'Mean': f"{mean:.3f}",
|
| 180 |
-
'StdDev': f"{stddev:.3f}",
|
| 181 |
-
'Min': f"{min_val:.3f}",
|
| 182 |
-
'Max': f"{max_val:.3f}",
|
| 183 |
-
'Point': f"({x}, {y})"
|
| 184 |
-
}
|
| 185 |
-
self.results.append(result)
|
| 186 |
-
self.marks.append((x, y, self.circle_diameter))
|
| 187 |
-
print(f"ROI analyzed at point ({x}, {y})")
|
| 188 |
-
|
| 189 |
-
return self.update_display(), self.format_results()
|
| 190 |
-
except Exception as e:
|
| 191 |
-
print(f"Error analyzing ROI: {str(e)}")
|
| 192 |
-
return self.display_image, f"Error analyzing ROI: {str(e)}"
|
| 193 |
-
|
| 194 |
-
def format_results(self):
|
| 195 |
-
if not self.results:
|
| 196 |
-
return "No measurements yet"
|
| 197 |
-
df = pd.DataFrame(self.results)
|
| 198 |
-
columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
|
| 199 |
-
df = df[columns_order]
|
| 200 |
-
return df.to_string(index=False)
|
| 201 |
-
|
| 202 |
-
def add_blank_row(self, image):
|
| 203 |
-
self.results.append({
|
| 204 |
-
'Area (mm²)': '',
|
| 205 |
-
'Mean': '',
|
| 206 |
-
'StdDev': '',
|
| 207 |
-
'Min': '',
|
| 208 |
-
'Max': '',
|
| 209 |
-
'Point': ''
|
| 210 |
-
})
|
| 211 |
-
return image, self.format_results()
|
| 212 |
-
|
| 213 |
-
def add_zero_row(self, image):
|
| 214 |
-
self.results.append({
|
| 215 |
-
'Area (mm²)': '0.000',
|
| 216 |
-
'Mean': '0.000',
|
| 217 |
-
'StdDev': '0.000',
|
| 218 |
-
'Min': '0.000',
|
| 219 |
-
'Max': '0.000',
|
| 220 |
-
'Point': '(0, 0)'
|
| 221 |
-
})
|
| 222 |
-
return image, self.format_results()
|
| 223 |
-
|
| 224 |
-
def undo_last(self, image):
|
| 225 |
-
if self.results:
|
| 226 |
-
self.results.pop()
|
| 227 |
-
if self.marks:
|
| 228 |
-
self.marks.pop()
|
| 229 |
-
return self.update_display(), self.format_results()
|
| 230 |
-
|
| 231 |
-
def save_results(self):
|
| 232 |
-
try:
|
| 233 |
-
if not self.results:
|
| 234 |
-
return None, "No results to save"
|
| 235 |
-
|
| 236 |
-
df = pd.DataFrame(self.results)
|
| 237 |
-
columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
|
| 238 |
-
df = df[columns_order]
|
| 239 |
-
|
| 240 |
-
temp_file = "analysis_results.xlsx"
|
| 241 |
-
df.to_excel(temp_file, index=False)
|
| 242 |
-
|
| 243 |
-
return temp_file, "Results saved successfully"
|
| 244 |
-
except Exception as e:
|
| 245 |
-
return None, f"Error saving results: {str(e)}"
|
| 246 |
-
|
| 247 |
-
def create_interface():
|
| 248 |
-
print("Creating interface...")
|
| 249 |
-
analyzer = DicomAnalyzer()
|
| 250 |
-
|
| 251 |
-
with gr.Blocks(css="#image_display { outline: none; }") as interface:
|
| 252 |
-
gr.Markdown("# DICOM Image Analyzer")
|
| 253 |
-
|
| 254 |
-
with gr.Row():
|
| 255 |
-
with gr.Column():
|
| 256 |
-
file_input = gr.File(label="Upload DICOM file")
|
| 257 |
-
diameter_slider = gr.Slider(
|
| 258 |
-
minimum=1,
|
| 259 |
-
maximum=20,
|
| 260 |
-
value=9,
|
| 261 |
-
step=1,
|
| 262 |
-
label="ROI Diameter (pixels)"
|
| 263 |
-
)
|
| 264 |
-
|
| 265 |
-
with gr.Row():
|
| 266 |
-
zoom_in_btn = gr.Button("Zoom In (+)")
|
| 267 |
-
zoom_out_btn = gr.Button("Zoom Out (-)")
|
| 268 |
-
reset_btn = gr.Button("Reset View")
|
| 269 |
-
|
| 270 |
-
with gr.Column():
|
| 271 |
-
image_display = gr.Image(label="DICOM Image", interactive=True, elem_id="image_display")
|
| 272 |
-
|
| 273 |
-
with gr.Row():
|
| 274 |
-
blank_btn = gr.Button("Add Blank Row")
|
| 275 |
-
zero_btn = gr.Button("Add Zero Row")
|
| 276 |
-
undo_btn = gr.Button("Undo Last")
|
| 277 |
-
save_btn = gr.Button("Save Results")
|
| 278 |
-
|
| 279 |
-
results_display = gr.Textbox(label="Results", interactive=False)
|
| 280 |
-
file_output = gr.File(label="Download Results")
|
| 281 |
-
key_press = gr.Textbox(visible=False, elem_id="key_press")
|
| 282 |
-
|
| 283 |
-
gr.Markdown("""
|
| 284 |
-
### Controls:
|
| 285 |
-
- Use arrow keys to pan when zoomed in
|
| 286 |
-
- Click points to measure
|
| 287 |
-
- Use Zoom In/Out buttons or Reset View to adjust zoom level
|
| 288 |
-
""")
|
| 289 |
-
|
| 290 |
-
def update_diameter(x):
|
| 291 |
-
analyzer.circle_diameter = x
|
| 292 |
-
print(f"Diameter updated to: {x}")
|
| 293 |
-
return f"Diameter set to {x} pixels"
|
| 294 |
-
|
| 295 |
-
# Event handlers
|
| 296 |
-
file_input.change(
|
| 297 |
-
fn=analyzer.load_dicom,
|
| 298 |
-
inputs=file_input,
|
| 299 |
-
outputs=[image_display, results_display]
|
| 300 |
-
)
|
| 301 |
-
|
| 302 |
-
image_display.select(
|
| 303 |
-
fn=analyzer.analyze_roi,
|
| 304 |
-
outputs=[image_display, results_display]
|
| 305 |
-
)
|
| 306 |
-
|
| 307 |
-
diameter_slider.change(
|
| 308 |
-
fn=update_diameter,
|
| 309 |
-
inputs=diameter_slider,
|
| 310 |
-
outputs=gr.Textbox(label="Status")
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
-
zoom_in_btn.click(
|
| 314 |
-
fn=analyzer.zoom_in,
|
| 315 |
-
inputs=image_display,
|
| 316 |
-
outputs=image_display
|
| 317 |
-
)
|
| 318 |
-
|
| 319 |
-
zoom_out_btn.click(
|
| 320 |
-
fn=analyzer.zoom_out,
|
| 321 |
-
inputs=image_display,
|
| 322 |
-
outputs=image_display
|
| 323 |
-
)
|
| 324 |
-
|
| 325 |
-
reset_btn.click(
|
| 326 |
-
fn=analyzer.reset_view,
|
| 327 |
-
outputs=image_display
|
| 328 |
-
)
|
| 329 |
-
|
| 330 |
-
key_press.change(
|
| 331 |
-
fn=analyzer.handle_keyboard,
|
| 332 |
-
inputs=key_press,
|
| 333 |
-
outputs=image_display
|
| 334 |
-
)
|
| 335 |
-
|
| 336 |
-
blank_btn.click(
|
| 337 |
-
fn=analyzer.add_blank_row,
|
| 338 |
-
inputs=image_display,
|
| 339 |
-
outputs=[image_display, results_display]
|
| 340 |
-
)
|
| 341 |
-
|
| 342 |
-
zero_btn.click(
|
| 343 |
-
fn=analyzer.add_zero_row,
|
| 344 |
-
inputs=image_display,
|
| 345 |
-
outputs=[image_display, results_display]
|
| 346 |
-
)
|
| 347 |
-
|
| 348 |
-
undo_btn.click(
|
| 349 |
-
fn=analyzer.undo_last,
|
| 350 |
-
inputs=image_display,
|
| 351 |
-
outputs=[image_display, results_display]
|
| 352 |
-
)
|
| 353 |
-
|
| 354 |
-
save_btn.click(
|
| 355 |
-
fn=analyzer.save_results,
|
| 356 |
-
outputs=[file_output, results_display]
|
| 357 |
-
)
|
| 358 |
-
|
| 359 |
-
js = """
|
| 360 |
-
<script>
|
| 361 |
-
document.addEventListener('keydown', function(e) {
|
| 362 |
-
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
| 363 |
-
e.preventDefault();
|
| 364 |
-
const keyPressElement = document.querySelector('#key_press textarea');
|
| 365 |
-
if (keyPressElement) {
|
| 366 |
-
keyPressElement.value = e.key;
|
| 367 |
-
keyPressElement.dispatchEvent(new Event('input'));
|
| 368 |
-
}
|
| 369 |
-
}
|
| 370 |
-
});
|
| 371 |
-
</script>
|
| 372 |
-
"""
|
| 373 |
-
gr.HTML(js)
|
| 374 |
-
|
| 375 |
-
print("Interface created successfully")
|
| 376 |
-
return interface
|
| 377 |
-
|
| 378 |
-
if __name__ == "__main__":
|
| 379 |
-
try:
|
| 380 |
-
print("Starting application...")
|
| 381 |
-
interface = create_interface()
|
| 382 |
-
print("Launching interface...")
|
| 383 |
-
interface.launch(
|
| 384 |
-
server_name="0.0.0.0",
|
| 385 |
-
server_port=7860,
|
| 386 |
-
share=True,
|
| 387 |
-
debug=True
|
| 388 |
-
)
|
| 389 |
-
except Exception as e:
|
| 390 |
-
print(f"Error launching application: {str(e)}")
|
| 391 |
-
raise e
|
|
|
|
| 60 |
|
| 61 |
def normalize_image(self, image):
|
| 62 |
try:
|
| 63 |
+
# Improve image normalization
|
| 64 |
+
normalized = cv2.normalize(
|
| 65 |
+
image,
|
| 66 |
+
None,
|
| 67 |
+
alpha=0,
|
| 68 |
+
beta=255,
|
| 69 |
+
norm_type=cv2.NORM_MINMAX,
|
| 70 |
+
dtype=cv2.CV_8U
|
| 71 |
+
)
|
| 72 |
if len(normalized.shape) == 2:
|
| 73 |
normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2RGB)
|
| 74 |
return normalized
|
|
|
|
| 108 |
zoomed = cv2.resize(self.original_display, (new_width, new_height),
|
| 109 |
interpolation=cv2.INTER_CUBIC)
|
| 110 |
|
| 111 |
+
# Draw marks with ImageJ-like yellow circle
|
| 112 |
for x, y, diameter in self.marks:
|
|
|
|
| 113 |
zoomed_x = int(x * self.zoom_factor)
|
| 114 |
zoomed_y = int(y * self.zoom_factor)
|
| 115 |
zoomed_diameter = int(diameter * self.zoom_factor)
|
| 116 |
|
| 117 |
+
# Draw main circle like ImageJ
|
| 118 |
cv2.circle(zoomed,
|
| 119 |
(zoomed_x, zoomed_y),
|
| 120 |
zoomed_diameter // 2,
|
| 121 |
+
(0, 255, 255), # Yellow color
|
| 122 |
+
1, # Thinner line
|
| 123 |
lineType=cv2.LINE_AA)
|
| 124 |
+
|
| 125 |
+
# Add small points around circle perimeter (ImageJ style)
|
| 126 |
+
num_points = 8
|
| 127 |
+
for i in range(num_points):
|
| 128 |
+
angle = 2 * np.pi * i / num_points
|
| 129 |
+
point_x = int(zoomed_x + (zoomed_diameter/2) * np.cos(angle))
|
| 130 |
+
point_y = int(zoomed_y + (zoomed_diameter/2) * np.sin(angle))
|
| 131 |
+
cv2.circle(zoomed,
|
| 132 |
+
(point_x, point_y),
|
| 133 |
+
1,
|
| 134 |
+
(0, 255, 255),
|
| 135 |
+
-1,
|
| 136 |
+
lineType=cv2.LINE_AA)
|
| 137 |
+
|
| 138 |
+
# Extract visible portion
|
| 139 |
visible_height = min(height, new_height)
|
| 140 |
visible_width = min(width, new_width)
|
| 141 |
|
|
|
|
| 170 |
return self.update_display()
|
| 171 |
except Exception as e:
|
| 172 |
print(f"Error handling keyboard input: {str(e)}")
|
| 173 |
+
return self.display_image
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|