HeshamAI commited on
Commit
d20572c
·
verified ·
1 Parent(s): 45c39ed

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +370 -0
app.py ADDED
@@ -0,0 +1,370 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import cv2
3
+ import numpy as np
4
+ import pandas as pd
5
+ import pydicom
6
+ import io
7
+ from PIL import Image
8
+
9
+ class DicomAnalyzer:
10
+ def __init__(self):
11
+ self.results = []
12
+ self.circle_diameter = 9
13
+ self.zoom_factor = 1.0
14
+ self.current_image = None
15
+ self.dicom_data = None
16
+ self.display_image = None
17
+ self.marks = [] # Store (x, y, diameter) for each mark
18
+ self.original_image = None
19
+ self.original_display = None
20
+ # Pan position
21
+ self.pan_x = 0
22
+ self.pan_y = 0
23
+ self.max_pan_x = 0
24
+ self.max_pan_y = 0
25
+
26
+ def load_dicom(self, file):
27
+ try:
28
+ if file is None:
29
+ return None, "No file uploaded"
30
+
31
+ if hasattr(file, 'name'):
32
+ dicom_data = pydicom.dcmread(file.name)
33
+ else:
34
+ dicom_data = pydicom.dcmread(file)
35
+
36
+ image = dicom_data.pixel_array.astype(np.float32)
37
+
38
+ rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
39
+ rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
40
+ image = (image * rescale_slope) + rescale_intercept
41
+
42
+ self.current_image = image
43
+ self.original_image = image.copy()
44
+ self.dicom_data = dicom_data
45
+
46
+ self.display_image = self.normalize_image(image)
47
+ self.original_display = self.display_image.copy()
48
+
49
+ # Reset view on new image
50
+ self.reset_view()
51
+
52
+ return self.display_image, "DICOM file loaded successfully"
53
+ except Exception as e:
54
+ return None, f"Error loading DICOM file: {str(e)}"
55
+
56
+ def normalize_image(self, image):
57
+ try:
58
+ normalized = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
59
+ if len(normalized.shape) == 2:
60
+ normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2RGB)
61
+ return normalized
62
+ except Exception as e:
63
+ print(f"Error normalizing image: {str(e)}")
64
+ return None
65
+
66
+ def reset_view(self):
67
+ """Reset zoom and center the image"""
68
+ self.zoom_factor = 1.0
69
+ self.pan_x = 0
70
+ self.pan_y = 0
71
+ if self.original_display is not None:
72
+ return self.update_display()
73
+ return None
74
+
75
+ def update_display(self):
76
+ """Update display with current zoom and pan"""
77
+ try:
78
+ if self.original_display is None:
79
+ return None
80
+
81
+ # Calculate zoomed size
82
+ height, width = self.original_display.shape[:2]
83
+ new_height = int(height * self.zoom_factor)
84
+ new_width = int(width * self.zoom_factor)
85
+
86
+ # Update max pan values
87
+ self.max_pan_x = max(0, new_width - width)
88
+ self.max_pan_y = max(0, new_height - height)
89
+
90
+ # Create zoomed image
91
+ zoomed = cv2.resize(self.original_display, (new_width, new_height),
92
+ interpolation=cv2.INTER_CUBIC)
93
+
94
+ # Draw marks
95
+ for x, y, diameter in self.marks:
96
+ display_x = int(x * self.zoom_factor - self.pan_x)
97
+ display_y = int(y * self.zoom_factor - self.pan_y)
98
+ display_diameter = int(diameter * self.zoom_factor)
99
+
100
+ # Only draw if mark is in view
101
+ if (0 <= display_x < new_width and 0 <= display_y < new_height):
102
+ cv2.circle(zoomed,
103
+ (display_x, display_y),
104
+ display_diameter // 2,
105
+ (0, 255, 255), # Yellow in BGR
106
+ 1,
107
+ lineType=cv2.LINE_AA)
108
+
109
+ # Extract visible portion
110
+ visible = zoomed[
111
+ self.pan_y:self.pan_y + height,
112
+ self.pan_x:self.pan_x + width
113
+ ]
114
+
115
+ return visible
116
+ except Exception as e:
117
+ print(f"Error updating display: {str(e)}")
118
+ return self.original_display
119
+
120
+ def handle_keyboard(self, key):
121
+ """Handle keyboard inputs for pan"""
122
+ try:
123
+ # Pan amount depends on zoom level
124
+ pan_amount = int(20 / self.zoom_factor)
125
+
126
+ if key == 'ArrowLeft':
127
+ self.pan_x = max(0, self.pan_x - pan_amount)
128
+ elif key == 'ArrowRight':
129
+ self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount)
130
+ elif key == 'ArrowUp':
131
+ self.pan_y = max(0, self.pan_y - pan_amount)
132
+ elif key == 'ArrowDown':
133
+ self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
134
+
135
+ return self.update_display()
136
+ except Exception as e:
137
+ print(f"Error handling keyboard input: {str(e)}")
138
+ return self.display_image
139
+
140
+ def analyze_roi(self, evt: gr.SelectData):
141
+ try:
142
+ if self.current_image is None:
143
+ return None, "No image loaded"
144
+
145
+ # Convert clicked coordinates to original image coordinates
146
+ x = int((evt.index[0] + self.pan_x) / self.zoom_factor)
147
+ y = int((evt.index[1] + self.pan_y) / self.zoom_factor)
148
+
149
+ mask = np.zeros_like(self.current_image, dtype=np.uint8)
150
+ y_indices, x_indices = np.ogrid[:self.current_image.shape[0], :self.current_image.shape[1]]
151
+ distance_from_center = np.sqrt((x_indices - x) ** 2 + (y_indices - y) ** 2)
152
+ mask[distance_from_center <= self.circle_diameter / 2] = 1
153
+
154
+ roi_pixels = self.current_image[mask == 1]
155
+
156
+ pixel_spacing = float(self.dicom_data.PixelSpacing[0])
157
+ area_pixels = np.sum(mask)
158
+ area_mm2 = area_pixels * (pixel_spacing ** 2)
159
+ mean = np.mean(roi_pixels)
160
+ stddev = np.std(roi_pixels)
161
+ min_val = np.min(roi_pixels)
162
+ max_val = np.max(roi_pixels)
163
+
164
+ result = {
165
+ 'Area (mm²)': f"{area_mm2:.3f}",
166
+ 'Mean': f"{mean:.3f}",
167
+ 'StdDev': f"{stddev:.3f}",
168
+ 'Min': f"{min_val:.3f}",
169
+ 'Max': f"{max_val:.3f}",
170
+ 'Point': f"({x}, {y})"
171
+ }
172
+ self.results.append(result)
173
+ self.marks.append((x, y, self.circle_diameter))
174
+
175
+ return self.update_display(), self.format_results()
176
+ except Exception as e:
177
+ print(f"Error analyzing ROI: {str(e)}")
178
+ return self.display_image, f"Error analyzing ROI: {str(e)}"
179
+
180
+ def update_zoom(self, zoom_factor, image):
181
+ try:
182
+ if self.original_display is None:
183
+ return image
184
+
185
+ self.zoom_factor = zoom_factor
186
+ return self.update_display()
187
+ except Exception as e:
188
+ print(f"Error updating zoom: {str(e)}")
189
+ return image
190
+
191
+ def format_results(self):
192
+ if not self.results:
193
+ return "No measurements yet"
194
+ df = pd.DataFrame(self.results)
195
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
196
+ df = df[columns_order]
197
+ return df.to_string(index=False)
198
+
199
+ def add_blank_row(self, image):
200
+ self.results.append({
201
+ 'Area (mm²)': '',
202
+ 'Mean': '',
203
+ 'StdDev': '',
204
+ 'Min': '',
205
+ 'Max': '',
206
+ 'Point': ''
207
+ })
208
+ return image, self.format_results()
209
+
210
+ def add_zero_row(self, image):
211
+ self.results.append({
212
+ 'Area (mm²)': '0.000',
213
+ 'Mean': '0.000',
214
+ 'StdDev': '0.000',
215
+ 'Min': '0.000',
216
+ 'Max': '0.000',
217
+ 'Point': '(0, 0)'
218
+ })
219
+ return image, self.format_results()
220
+
221
+ def undo_last(self, image):
222
+ if self.results:
223
+ self.results.pop()
224
+ if self.marks:
225
+ self.marks.pop()
226
+ return self.update_display(), self.format_results()
227
+
228
+ def save_results(self):
229
+ try:
230
+ if not self.results:
231
+ return None, "No results to save"
232
+
233
+ df = pd.DataFrame(self.results)
234
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
235
+ df = df[columns_order]
236
+
237
+ temp_file = "analysis_results.xlsx"
238
+ df.to_excel(temp_file, index=False)
239
+
240
+ return temp_file, "Results saved successfully"
241
+ except Exception as e:
242
+ return None, f"Error saving results: {str(e)}"
243
+
244
+ def create_interface():
245
+ analyzer = DicomAnalyzer()
246
+
247
+ with gr.Blocks(css="#image_display { outline: none; }") as interface:
248
+ gr.Markdown("# DICOM Image Analyzer")
249
+
250
+ with gr.Row():
251
+ with gr.Column():
252
+ file_input = gr.File(label="Upload DICOM file")
253
+ diameter_slider = gr.Slider(
254
+ minimum=1,
255
+ maximum=20,
256
+ value=9,
257
+ step=1,
258
+ label="ROI Diameter (pixels)"
259
+ )
260
+ zoom_slider = gr.Slider(
261
+ minimum=1.0,
262
+ maximum=20.0,
263
+ value=1.0,
264
+ step=0.5,
265
+ label="Zoom Factor"
266
+ )
267
+ reset_btn = gr.Button("Reset View")
268
+
269
+ with gr.Column():
270
+ image_display = gr.Image(label="DICOM Image", interactive=True, elem_id="image_display")
271
+
272
+ with gr.Row():
273
+ blank_btn = gr.Button("Add Blank Row")
274
+ zero_btn = gr.Button("Add Zero Row")
275
+ undo_btn = gr.Button("Undo Last")
276
+ save_btn = gr.Button("Save Results")
277
+
278
+ results_display = gr.Textbox(label="Results", interactive=False)
279
+ file_output = gr.File(label="Download Results")
280
+ key_press = gr.Textbox(visible=False) # Hidden textbox for keyboard events
281
+
282
+ # Instructions
283
+ gr.Markdown("""
284
+ ### Controls:
285
+ - Use arrow keys to pan when zoomed in
286
+ - Click points to measure
287
+ - Press 'Reset View' to center image and reset zoom
288
+ """)
289
+
290
+ def update_diameter(x):
291
+ analyzer.circle_diameter = x
292
+ return f"Diameter set to {x} pixels"
293
+
294
+ # Event handlers
295
+ file_input.change(
296
+ fn=analyzer.load_dicom,
297
+ inputs=file_input,
298
+ outputs=[image_display, results_display]
299
+ )
300
+
301
+ image_display.select(
302
+ fn=analyzer.analyze_roi,
303
+ outputs=[image_display, results_display]
304
+ )
305
+
306
+ diameter_slider.change(
307
+ fn=update_diameter,
308
+ inputs=diameter_slider,
309
+ outputs=gr.Textbox(label="Status")
310
+ )
311
+
312
+ zoom_slider.change(
313
+ fn=analyzer.update_zoom,
314
+ inputs=[zoom_slider, image_display],
315
+ outputs=image_display
316
+ )
317
+
318
+ reset_btn.click(
319
+ fn=analyzer.reset_view,
320
+ outputs=image_display
321
+ )
322
+
323
+ key_press.change(
324
+ fn=analyzer.handle_keyboard,
325
+ inputs=[key_press],
326
+ outputs=[image_display]
327
+ )
328
+
329
+ blank_btn.click(
330
+ fn=analyzer.add_blank_row,
331
+ inputs=image_display,
332
+ outputs=[image_display, results_display]
333
+ )
334
+
335
+ zero_btn.click(
336
+ fn=analyzer.add_zero_row,
337
+ inputs=image_display,
338
+ outputs=[image_display, results_display]
339
+ )
340
+
341
+ undo_btn.click(
342
+ fn=analyzer.undo_last,
343
+ inputs=image_display,
344
+ outputs=[image_display, results_display]
345
+ )
346
+
347
+ save_btn.click(
348
+ fn=analyzer.save_results,
349
+ outputs=[file_output, results_display]
350
+ )
351
+
352
+ # Add JavaScript for keyboard handling
353
+ gr.HTML("""
354
+ <script>
355
+ function handleKey(e) {
356
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
357
+ e.preventDefault();
358
+ document.querySelector('#key_press textarea').value = e.key;
359
+ document.querySelector('#key_press textarea').dispatchEvent(new Event('input'));
360
+ }
361
+ }
362
+ document.addEventListener('keydown', handleKey);
363
+ </script>
364
+ """)
365
+
366
+ return interface
367
+
368
+ if __name__ == "__main__":
369
+ interface = create_interface()
370
+ interface.launch()