HeshamAI commited on
Commit
cb43e84
·
verified ·
1 Parent(s): 743197f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +291 -94
app.py CHANGED
@@ -1,102 +1,299 @@
 
1
  import cv2
2
  import numpy as np
3
  import pandas as pd
4
  import pydicom
5
- import gradio as gr
6
  import os
 
 
7
 
8
- # Global variables to mimic the local functionality
9
- results = []
10
- image_display = None
11
- circle_diameter = 9
12
- zoom_factor = 1.0
13
-
14
- # Function to load DICOM files
15
- def load_dicom(file_path):
16
- dicom_data = pydicom.dcmread(file_path)
17
- image = dicom_data.pixel_array.astype(np.float32)
18
- rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
19
- rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
20
- image = (image * rescale_slope) + rescale_intercept
21
- pixel_spacing = dicom_data.PixelSpacing
22
- return image, dicom_data, pixel_spacing
23
-
24
- # Function to analyze ROI
25
- def analyze_roi(image, pixel_spacing, x, y, circle_diameter):
26
- mask = np.zeros_like(image, dtype=np.uint8)
27
- y_indices, x_indices = np.ogrid[:image.shape[0], :image.shape[1]]
28
- distance_from_center = np.sqrt((x_indices - x)**2 + (y_indices - y)**2)
29
- mask[distance_from_center <= circle_diameter / 2] = 1
30
- pixels = image[mask == 1]
31
-
32
- # Calculate metrics
33
- area_mm2 = np.sum(mask) * (float(pixel_spacing[0])**2)
34
- mean = np.mean(pixels)
35
- stddev = np.std(pixels)
36
- min_val = np.min(pixels)
37
- max_val = np.max(pixels)
38
-
39
- return {
40
- "Area (mm²)": f"{area_mm2:.3f}",
41
- "Mean": f"{mean:.3f}",
42
- "StdDev": f"{stddev:.3f}",
43
- "Min": f"{min_val:.3f}",
44
- "Max": f"{max_val:.3f}"
45
- }
46
-
47
- # Gradio callback function to simulate the local analyzer
48
- def process_image(file, x, y, zoom, diameter):
49
- global results, image_display, circle_diameter, zoom_factor
50
-
51
- # Load the DICOM image
52
- image, dicom_data, pixel_spacing = load_dicom(file.name)
53
- image_display = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
54
-
55
- # Adjust zoom and diameter
56
- zoom_factor = float(zoom)
57
- circle_diameter = int(diameter)
58
-
59
- # Analyze the clicked point
60
- x_original = int(float(x) / zoom_factor)
61
- y_original = int(float(y) / zoom_factor)
62
- analysis_result = analyze_roi(image, pixel_spacing, x_original, y_original, circle_diameter)
63
-
64
- # Append to results
65
- results.append(analysis_result)
66
- return analysis_result
67
-
68
- # Gradio app definition
69
- def reset_results():
70
- global results
71
- results = []
72
- return "Results cleared!"
73
-
74
- with gr.Blocks() as app:
75
- gr.Markdown("## DICOM Analyzer - Matches Local Features")
76
-
77
- with gr.Row():
78
- file_input = gr.File(label="Upload DICOM File", type="filepath")
79
- x_input = gr.Number(label="X Coordinate (px)", value=50)
80
- y_input = gr.Number(label="Y Coordinate (px)", value=50)
81
- zoom_input = gr.Number(label="Zoom Factor", value=1.0)
82
- diameter_input = gr.Number(label="Circle Diameter (px)", value=9)
83
-
84
- output_text = gr.Textbox(label="Analysis Results")
85
- clear_button = gr.Button("Clear Results")
86
-
87
- with gr.Row():
88
- analyze_button = gr.Button("Analyze")
89
- reset_button = gr.Button("Reset")
90
-
91
- analyze_button.click(
92
- process_image,
93
- inputs=[file_input, x_input, y_input, zoom_input, diameter_input],
94
- outputs=output_text
95
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- reset_button.click(
98
- reset_results,
99
- outputs=output_text
100
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- app.launch()
 
 
 
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
  import os
8
+ from PIL import Image
9
+ import tempfile
10
 
11
+ class DicomAnalyzer:
12
+ def __init__(self):
13
+ self.results = []
14
+ self.circle_diameter = 9
15
+ self.zoom_factor = 1.0
16
+ self.current_image1 = None
17
+ self.current_image2 = None
18
+ self.dicom_data1 = None
19
+ self.dicom_data2 = None
20
+ self.image_display1 = None
21
+ self.image_display2 = None
22
+ self.marks1 = []
23
+ self.marks2 = []
24
+
25
+ def load_dicom(self, file):
26
+ try:
27
+ if file is None:
28
+ return None, None, None
29
+
30
+ dicom_data = pydicom.dcmread(file.name)
31
+ image = dicom_data.pixel_array.astype(np.float32)
32
+
33
+ # Apply rescale slope and intercept
34
+ rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
35
+ rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
36
+ image = (image * rescale_slope) + rescale_intercept
37
+
38
+ # Store original image for analysis
39
+ original_image = image.copy()
40
+
41
+ # Normalize for display
42
+ image_display = cv2.normalize(image, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
43
+
44
+ # Convert to BGR for visualization
45
+ if len(image_display.shape) == 2:
46
+ image_display = cv2.cvtColor(image_display, cv2.COLOR_GRAY2BGR)
47
+
48
+ return original_image, image_display, dicom_data
49
+ except Exception as e:
50
+ print(f"Error loading DICOM file: {str(e)}")
51
+ return None, None, None
52
+
53
+ def analyze_point(self, image, dicom_data, x, y):
54
+ try:
55
+ # Create a circular mask
56
+ mask = np.zeros_like(image, dtype=np.uint8)
57
+ y_indices, x_indices = np.ogrid[:image.shape[0], :image.shape[1]]
58
+ distance_from_center = np.sqrt((x_indices - x)**2 + (y_indices - y)**2)
59
+ mask[distance_from_center <= self.circle_diameter / 2] = 1
60
+
61
+ # Extract pixel values within the circle
62
+ pixels = image[mask == 1]
63
+
64
+ # Calculate metrics
65
+ area_pixels = np.sum(mask)
66
+ pixel_spacing = float(dicom_data.PixelSpacing[0])
67
+ area_mm2 = area_pixels * (pixel_spacing**2)
68
+ mean = np.mean(pixels)
69
+ stddev = np.std(pixels)
70
+ min_val = np.min(pixels)
71
+ max_val = np.max(pixels)
72
+
73
+ return {
74
+ 'Area (mm²)': f"{area_mm2:.3f}",
75
+ 'Mean': f"{mean:.3f}",
76
+ 'StdDev': f"{stddev:.3f}",
77
+ 'Min': f"{min_val:.3f}",
78
+ 'Max': f"{max_val:.3f}"
79
+ }
80
+ except Exception as e:
81
+ print(f"Error analyzing point: {str(e)}")
82
+ return None
83
+
84
+ def draw_circle(self, image, x, y, is_image1=True):
85
+ try:
86
+ image_copy = image.copy()
87
+
88
+ # Draw all previous marks
89
+ marks = self.marks1 if is_image1 else self.marks2
90
+ for mark_x, mark_y in marks:
91
+ cv2.circle(image_copy,
92
+ (int(mark_x), int(mark_y)),
93
+ int(self.circle_diameter/2),
94
+ (0, 255, 255),
95
+ 1,
96
+ lineType=cv2.LINE_AA)
97
+
98
+ # Draw new mark
99
+ cv2.circle(image_copy,
100
+ (int(x), int(y)),
101
+ int(self.circle_diameter/2),
102
+ (0, 255, 255),
103
+ 1,
104
+ lineType=cv2.LINE_AA)
105
+
106
+ # Store new mark
107
+ if is_image1:
108
+ self.marks1.append((x, y))
109
+ else:
110
+ self.marks2.append((x, y))
111
+
112
+ return image_copy
113
+ except Exception as e:
114
+ print(f"Error drawing circle: {str(e)}")
115
+ return image
116
+
117
+ def process_image1(self, file):
118
+ image, image_display, dicom_data = self.load_dicom(file)
119
+ self.current_image1 = image
120
+ self.image_display1 = image_display
121
+ self.dicom_data1 = dicom_data
122
+ return image_display
123
+
124
+ def process_image2(self, file):
125
+ image, image_display, dicom_data = self.load_dicom(file)
126
+ self.current_image2 = image
127
+ self.image_display2 = image_display
128
+ self.dicom_data2 = dicom_data
129
+ return image_display
130
+
131
+ def handle_click1(self, evt: gr.SelectData):
132
+ if self.current_image1 is None:
133
+ return self.image_display1, "Please load Image 1 first"
134
+
135
+ try:
136
+ x, y = evt.index
137
+ marked_image = self.draw_circle(self.image_display1, x, y, is_image1=True)
138
+ self.image_display1 = marked_image
139
+
140
+ results = self.analyze_point(self.current_image1, self.dicom_data1, x, y)
141
+ if results:
142
+ results['Image'] = "Image 1"
143
+ results['Point'] = f"({x}, {y})"
144
+ self.results.append(results)
145
+
146
+ return self.image_display1, self.format_results()
147
+ except Exception as e:
148
+ print(f"Error in handle_click1: {str(e)}")
149
+ return self.image_display1, f"Error: {str(e)}"
150
+
151
+ def handle_click2(self, evt: gr.SelectData):
152
+ if self.current_image2 is None:
153
+ return self.image_display2, "Please load Image 2 first"
154
+
155
+ try:
156
+ x, y = evt.index
157
+ marked_image = self.draw_circle(self.image_display2, x, y, is_image1=False)
158
+ self.image_display2 = marked_image
159
+
160
+ results = self.analyze_point(self.current_image2, self.dicom_data2, x, y)
161
+ if results:
162
+ results['Image'] = "Image 2"
163
+ results['Point'] = f"({x}, {y})"
164
+ self.results.append(results)
165
+
166
+ return self.image_display2, self.format_results()
167
+ except Exception as e:
168
+ print(f"Error in handle_click2: {str(e)}")
169
+ return self.image_display2, f"Error: {str(e)}"
170
+
171
+ def format_results(self):
172
+ if not self.results:
173
+ return "No results yet"
174
+ df = pd.DataFrame(self.results)
175
+ return df.to_string()
176
+
177
+ def clear_results(self):
178
+ self.results = []
179
+ self.marks1 = []
180
+ self.marks2 = []
181
+ if self.current_image1 is not None:
182
+ self.image_display1 = cv2.cvtColor(
183
+ cv2.normalize(self.current_image1, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8),
184
+ cv2.COLOR_GRAY2BGR
185
+ )
186
+ if self.current_image2 is not None:
187
+ self.image_display2 = cv2.cvtColor(
188
+ cv2.normalize(self.current_image2, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8),
189
+ cv2.COLOR_GRAY2BGR
190
+ )
191
+ return "Results cleared", self.image_display1, self.image_display2
192
+
193
+ def add_blank_row(self):
194
+ self.results.append({
195
+ 'Image': '',
196
+ 'Point': '',
197
+ 'Area (mm²)': '',
198
+ 'Mean': '',
199
+ 'StdDev': '',
200
+ 'Min': '',
201
+ 'Max': ''
202
+ })
203
+ return self.format_results()
204
+
205
+ def update_circle_diameter(self, value):
206
+ self.circle_diameter = value
207
+ return f"Circle diameter set to {value}"
208
+
209
+ def save_results(self):
210
+ try:
211
+ if not self.results:
212
+ return None, "No results to save"
213
+
214
+ df = pd.DataFrame(self.results)
215
+
216
+ # Create temporary file
217
+ temp_dir = tempfile.gettempdir()
218
+ temp_file = os.path.join(temp_dir, "analysis_results.xlsx")
219
+
220
+ # Save to Excel
221
+ df.to_excel(temp_file, index=False, engine='openpyxl')
222
+
223
+ return temp_file, "Results saved successfully. Click to download."
224
+ except Exception as e:
225
+ print(f"Error saving results: {str(e)}")
226
+ return None, f"Error saving results: {str(e)}"
227
+
228
+ def create_interface():
229
+ analyzer = DicomAnalyzer()
230
 
231
+ with gr.Blocks() as interface:
232
+ gr.Markdown("# CT DICOM Image Analyzer")
233
+
234
+ with gr.Row():
235
+ with gr.Column():
236
+ file1 = gr.File(label="Upload first DICOM file")
237
+ image1 = gr.Image(label="Image 1", interactive=True, type="numpy")
238
+ file1.change(fn=analyzer.process_image1, inputs=file1, outputs=image1)
239
+
240
+ with gr.Column():
241
+ file2 = gr.File(label="Upload second DICOM file")
242
+ image2 = gr.Image(label="Image 2", interactive=True, type="numpy")
243
+ file2.change(fn=analyzer.process_image2, inputs=file2, outputs=image2)
244
+
245
+ with gr.Row():
246
+ circle_diameter = gr.Slider(
247
+ minimum=1,
248
+ maximum=20,
249
+ value=9,
250
+ step=1,
251
+ label="Circle Diameter"
252
+ )
253
+
254
+ with gr.Row():
255
+ clear_btn = gr.Button("Clear Results")
256
+ blank_row_btn = gr.Button("Add Blank Row")
257
+ save_btn = gr.Button("Save Results")
258
+
259
+ results = gr.Textbox(label="Results", interactive=False)
260
+ file_output = gr.File(label="Download Results")
261
+ status = gr.Textbox(label="Status")
262
+
263
+ # Connect events
264
+ circle_diameter.change(
265
+ fn=analyzer.update_circle_diameter,
266
+ inputs=circle_diameter,
267
+ outputs=status
268
+ )
269
+
270
+ image1.select(
271
+ fn=analyzer.handle_click1,
272
+ outputs=[image1, results]
273
+ )
274
+
275
+ image2.select(
276
+ fn=analyzer.handle_click2,
277
+ outputs=[image2, results]
278
+ )
279
+
280
+ clear_btn.click(
281
+ fn=analyzer.clear_results,
282
+ outputs=[status, image1, image2]
283
+ )
284
+
285
+ blank_row_btn.click(
286
+ fn=analyzer.add_blank_row,
287
+ outputs=results
288
+ )
289
+
290
+ save_btn.click(
291
+ fn=analyzer.save_results,
292
+ outputs=[file_output, status]
293
+ )
294
+
295
+ return interface
296
 
297
+ if __name__ == "__main__":
298
+ interface = create_interface()
299
+ interface.launch()