HeshamAI commited on
Commit
1e8a7b1
·
verified ·
1 Parent(s): 5a101c5

Update app.py

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