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

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +289 -0
  2. requirements.txt +9 -0
app.py ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.9.1
2
+ numpy>=1.25
3
+ pandas
4
+ opencv-python-headless
5
+ pydicom
6
+ pillow
7
+ openpyxl
8
+ streamlit==1.18.0
9
+ altair==4.2.2