HeshamAI commited on
Commit
ba795fa
·
verified ·
1 Parent(s): 3e2cad2

Update App.py

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