HeshamAI commited on
Commit
41c8b42
·
verified ·
1 Parent(s): 7a68e53

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +820 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,820 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import openpyxl
9
+ from openpyxl.utils import get_column_letter, column_index_from_string
10
+ import logging
11
+ import time
12
+ import traceback
13
+ from functools import wraps
14
+ import sys
15
+
16
+ print("Starting imports completed...")
17
+
18
+ # Set up logging
19
+ logging.basicConfig(
20
+ level=logging.DEBUG,
21
+ format='%(asctime)s - %(levelname)s - %(message)s',
22
+ handlers=[
23
+ logging.FileHandler('dicom_analyzer_debug.log'),
24
+ logging.StreamHandler(sys.stdout)
25
+ ]
26
+ )
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ def debug_decorator(func):
31
+ @wraps(func)
32
+ def wrapper(*args, **kwargs):
33
+ logger.debug(f"Entering {func.__name__}")
34
+ start_time = time.time()
35
+ try:
36
+ result = func(*args, **kwargs)
37
+ logger.debug(f"Function {func.__name__} completed successfully")
38
+ return result
39
+ except Exception as e:
40
+ logger.error(f"Error in {func.__name__}: {str(e)}")
41
+ logger.error(traceback.format_exc())
42
+ raise
43
+ finally:
44
+ end_time = time.time()
45
+ logger.debug(f"Execution time: {end_time - start_time:.4f} seconds")
46
+ return wrapper
47
+
48
+
49
+ class DicomAnalyzer:
50
+ def __init__(self):
51
+ self.results = []
52
+ self.circle_diameter = 9.0
53
+ self.zoom_factor = 1.0
54
+ self.current_image = None
55
+ self.dicom_data = None
56
+ self.display_image = None
57
+ self.marks = []
58
+ self.original_image = None
59
+ self.original_display = None
60
+ self.pan_x = 0
61
+ self.pan_y = 0
62
+ self.max_pan_x = 0
63
+ self.max_pan_y = 0
64
+ self.CIRCLE_COLOR = (0, 255, 255) # BGR format
65
+ self.SMALL_CIRCLES_COLOR = (255, 255, 255) # BGR white
66
+ print("DicomAnalyzer initialized...")
67
+
68
+ def save_results(self):
69
+ try:
70
+ if not self.results:
71
+ logger.warning("Attempted to save with no results")
72
+ return None, "No results to save"
73
+
74
+ df = pd.DataFrame(self.results)
75
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
76
+ df = df[columns_order]
77
+
78
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
79
+ output_file = f"analysis_results_{timestamp}.xlsx"
80
+
81
+ with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
82
+ df.to_excel(writer, index=False, sheet_name='Results')
83
+
84
+ worksheet = writer.sheets['Results']
85
+ for idx, col in enumerate(df.columns):
86
+ max_length = max(
87
+ df[col].astype(str).apply(len).max(),
88
+ len(str(col))
89
+ ) + 2
90
+ worksheet.column_dimensions[get_column_letter(idx + 1)].width = max_length
91
+
92
+ logger.info(f"Results saved successfully to {output_file}")
93
+ return output_file, f"Results saved successfully to {output_file}"
94
+
95
+ except Exception as e:
96
+ error_msg = f"Error saving results: {str(e)}"
97
+ logger.error(error_msg)
98
+ logger.error(traceback.format_exc())
99
+ return None, error_msg
100
+
101
+ def reset_all(self, image):
102
+ self.results = []
103
+ self.marks = []
104
+ self.reset_view()
105
+ return self.update_display(), "All data has been reset"
106
+
107
+ def load_dicom(self, file):
108
+ try:
109
+ if file is None:
110
+ return None, "No file uploaded"
111
+
112
+ if hasattr(file, 'name'):
113
+ dicom_data = pydicom.dcmread(file.name)
114
+ else:
115
+ dicom_data = pydicom.dcmread(file)
116
+
117
+ image = dicom_data.pixel_array.astype(np.float32)
118
+ self.original_image = image.copy()
119
+
120
+ rescale_slope = getattr(dicom_data, 'RescaleSlope', 1)
121
+ rescale_intercept = getattr(dicom_data, 'RescaleIntercept', 0)
122
+ image = (image * rescale_slope) + rescale_intercept
123
+
124
+ self.current_image = image
125
+ self.dicom_data = dicom_data
126
+ self.display_image = self.normalize_image(image)
127
+ self.original_display = self.display_image.copy()
128
+
129
+ self.reset_all(None)
130
+ print("DICOM file loaded successfully")
131
+
132
+ return self.display_image, "DICOM file loaded successfully"
133
+ except Exception as e:
134
+ print(f"Error loading DICOM file: {str(e)}")
135
+ return None, f"Error loading DICOM file: {str(e)}"
136
+
137
+ def normalize_image(self, image):
138
+ try:
139
+ normalized = cv2.normalize(
140
+ image,
141
+ None,
142
+ alpha=0,
143
+ beta=255,
144
+ norm_type=cv2.NORM_MINMAX,
145
+ dtype=cv2.CV_8U
146
+ )
147
+ if len(normalized.shape) == 2:
148
+ normalized = cv2.cvtColor(normalized, cv2.COLOR_GRAY2BGR)
149
+ return normalized
150
+ except Exception as e:
151
+ print(f"Error normalizing image: {str(e)}")
152
+ return None
153
+
154
+ def reset_view(self):
155
+ self.zoom_factor = 1.0
156
+ self.pan_x = 0
157
+ self.pan_y = 0
158
+ if self.original_display is not None:
159
+ return self.update_display()
160
+ return None
161
+
162
+ def zoom_in(self, image):
163
+ print("Zooming in...")
164
+ self.zoom_factor = min(20.0, self.zoom_factor + 0.5)
165
+ return self.update_display()
166
+
167
+ def zoom_out(self, image):
168
+ print("Zooming out...")
169
+ self.zoom_factor = max(1.0, self.zoom_factor - 0.5)
170
+ return self.update_display()
171
+
172
+ def handle_keyboard(self, key):
173
+ try:
174
+ print(f"Handling key press: {key}")
175
+ pan_amount = int(10 * self.zoom_factor)
176
+
177
+ if key == 'ArrowLeft':
178
+ self.pan_x = max(0, self.pan_x - pan_amount)
179
+ elif key == 'ArrowRight':
180
+ self.pan_x = min(self.max_pan_x, self.pan_x + pan_amount)
181
+ elif key == 'ArrowUp':
182
+ self.pan_y = max(0, self.pan_y - pan_amount)
183
+ elif key == 'ArrowDown':
184
+ self.pan_y = min(self.max_pan_y, self.pan_y + pan_amount)
185
+
186
+ return self.update_display()
187
+ except Exception as e:
188
+ print(f"Error handling keyboard input: {str(e)}")
189
+ return self.display_image
190
+
191
+ def update_display(self):
192
+ try:
193
+ if self.original_display is None:
194
+ return None
195
+
196
+ height, width = self.original_display.shape[:2]
197
+ new_height = int(height * self.zoom_factor)
198
+ new_width = int(width * self.zoom_factor)
199
+
200
+ zoomed = cv2.resize(
201
+ self.original_display,
202
+ (new_width, new_height),
203
+ interpolation=cv2.INTER_CUBIC
204
+ )
205
+
206
+ zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
207
+
208
+ for x, y, diameter in self.marks:
209
+ zoomed_x = int(x * self.zoom_factor)
210
+ zoomed_y = int(y * self.zoom_factor)
211
+ zoomed_radius = int((diameter / 2.0) * self.zoom_factor)
212
+
213
+ # Draw the main yellow circle
214
+ cv2.circle(
215
+ zoomed_bgr,
216
+ (zoomed_x, zoomed_y),
217
+ zoomed_radius,
218
+ self.CIRCLE_COLOR,
219
+ 1,
220
+ lineType=cv2.LINE_AA
221
+ )
222
+
223
+ # Draw 8 small white circles around
224
+ num_points = 8
225
+ for i in range(num_points):
226
+ angle = 2 * np.pi * i / num_points
227
+ point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
228
+ point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
229
+ cv2.circle(
230
+ zoomed_bgr,
231
+ (point_x, point_y),
232
+ 1,
233
+ self.SMALL_CIRCLES_COLOR,
234
+ -1,
235
+ lineType=cv2.LINE_AA
236
+ )
237
+
238
+ zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
239
+
240
+ self.max_pan_x = max(0, new_width - width)
241
+ self.max_pan_y = max(0, new_height - height)
242
+ self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
243
+ self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
244
+
245
+ visible = zoomed[
246
+ int(self.pan_y):int(self.pan_y + height),
247
+ int(self.pan_x):int(self.pan_x + width)
248
+ ]
249
+
250
+ return visible
251
+ except Exception as e:
252
+ print(f"Error updating display: {str(e)}")
253
+ return self.original_display
254
+
255
+ def analyze_roi(self, evt: gr.SelectData):
256
+ try:
257
+ if self.current_image is None:
258
+ return None, "No image loaded"
259
+
260
+ clicked_x = evt.index[0]
261
+ clicked_y = evt.index[1]
262
+
263
+ x = clicked_x + self.pan_x
264
+ y = clicked_y + self.pan_y
265
+
266
+ if self.zoom_factor != 1.0:
267
+ x = x / self.zoom_factor
268
+ y = y / self.zoom_factor
269
+
270
+ x = int(round(x))
271
+ y = int(round(y))
272
+
273
+ height, width = self.original_image.shape[:2]
274
+ Y, X = np.ogrid[:height, :width]
275
+ radius = self.circle_diameter / 2.0
276
+ r_squared = radius * radius
277
+
278
+ dx = X - x
279
+ dy = Y - y
280
+ dist_squared = dx * dx + dy * dy
281
+
282
+ mask = np.zeros((height, width), dtype=bool)
283
+ mask[dist_squared <= r_squared] = True
284
+
285
+ roi_pixels = self.original_image[mask]
286
+
287
+ if len(roi_pixels) == 0:
288
+ return self.display_image, "Error: No pixels selected"
289
+
290
+ pixel_spacing = float(self.dicom_data.PixelSpacing[0])
291
+ n_pixels = np.sum(mask)
292
+ area = n_pixels * (pixel_spacing ** 2)
293
+
294
+ mean_value = np.mean(roi_pixels)
295
+ std_dev = np.std(roi_pixels, ddof=1)
296
+ min_val = np.min(roi_pixels)
297
+ max_val = np.max(roi_pixels)
298
+
299
+ rescale_slope = getattr(self.dicom_data, 'RescaleSlope', 1)
300
+ rescale_intercept = getattr(self.dicom_data, 'RescaleIntercept', 0)
301
+
302
+ mean_value = (mean_value * rescale_slope) + rescale_intercept
303
+ std_dev = std_dev * rescale_slope
304
+ min_val = (min_val * rescale_slope) + rescale_intercept
305
+ max_val = (max_val * rescale_slope) + rescale_intercept
306
+
307
+ result = {
308
+ 'Area (mm²)': f"{area:.3f}",
309
+ 'Mean': f"{mean_value:.3f}",
310
+ 'StdDev': f"{std_dev:.3f}",
311
+ 'Min': f"{min_val:.3f}",
312
+ 'Max': f"{max_val:.3f}",
313
+ 'Point': f"({x}, {y})"
314
+ }
315
+
316
+ self.results.append(result)
317
+ self.marks.append((x, y, self.circle_diameter))
318
+
319
+ return self.update_display(), self.format_results()
320
+ except Exception as e:
321
+ print(f"Error analyzing ROI: {str(e)}")
322
+ return self.display_image, f"Error analyzing ROI: {str(e)}"
323
+
324
+ def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
325
+ """
326
+ Inserts SNR (first row) and CNR (second row) formulas with IFERROR.
327
+ """
328
+ try:
329
+ base_col = col_group[1] # Mean column
330
+ std_col = col_group[2] # StdDev column
331
+
332
+ row1, row2 = row_pair
333
+
334
+ formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
335
+
336
+ # SNR formula -> row1
337
+ formula_snr = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
338
+ cell_snr = ws[f"{formula_col}{row1}"]
339
+ cell_snr.value = formula_snr
340
+ cell_snr.font = red_font
341
+ cell_snr.alignment = openpyxl.styles.Alignment(horizontal='center')
342
+
343
+ # CNR formula -> row2
344
+ formula_cnr = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
345
+ cell_cnr = ws[f"{formula_col}{row2}"]
346
+ cell_cnr.value = formula_cnr
347
+ cell_cnr.font = red_font
348
+ cell_cnr.alignment = openpyxl.styles.Alignment(horizontal='center')
349
+
350
+ logger.debug(f"Added formulas for rows {row1},{row2} in column {formula_col}")
351
+ except Exception as e:
352
+ logger.error(f"Error adding formulas: {str(e)}")
353
+
354
+ def save_formatted_results(self, output_path):
355
+ try:
356
+ if not self.results:
357
+ return None, "No results to save"
358
+
359
+ wb = openpyxl.Workbook()
360
+ ws = wb.active
361
+ red_font = openpyxl.styles.Font(color="FF0000")
362
+ center_alignment = openpyxl.styles.Alignment(horizontal='center', vertical='center')
363
+
364
+ headers = ['Area', 'Mean', 'StdDev', 'Min', 'Max']
365
+
366
+ column_groups = [
367
+ ('B', 'C', 'D', 'E', 'F'), ('H', 'I', 'J', 'K', 'L'),
368
+ ('N', 'O', 'P', 'Q', 'R'), ('T', 'U', 'V', 'W', 'X'),
369
+ ('Z', 'AA', 'AB', 'AC', 'AD'), ('AF', 'AG', 'AH', 'AI', 'AJ'),
370
+ ('AL', 'AM', 'AN', 'AO', 'AP'), ('AR', 'AS', 'AT', 'AU', 'AV'),
371
+ ('AX', 'AY', 'AZ', 'BA', 'BB'), ('BD', 'BE', 'BF', 'BG', 'BH'),
372
+ ('BJ', 'BK', 'BL', 'BM', 'BN'), ('BP', 'BQ', 'BR', 'BS', 'BT'),
373
+ ('BV', 'BW', 'BX', 'BY', 'BZ')
374
+ ]
375
+
376
+ # Write the headers in row1 for each column group.
377
+ for cols in column_groups:
378
+ for i, header in enumerate(headers):
379
+ cell = ws[f"{cols[i]}1"]
380
+ cell.value = header
381
+ cell.alignment = center_alignment
382
+
383
+ row_pairs = [
384
+ (2, 3), (5, 6), (8, 9), (11, 12), (14, 15),
385
+ (17, 18), (20, 21), (23, 24), (26, 27), (29, 30)
386
+ ]
387
+
388
+ phantom_sizes = [
389
+ '(7mm)', '(6.5mm)', '(6mm)', '(5.5mm)', '(5mm)',
390
+ '(4.5mm)', '(4mm)', '(3.5mm)', '(3mm)', '(2.5mm)'
391
+ ]
392
+
393
+ # Put phantom size labels in column A above each row pair.
394
+ for i, size in enumerate(phantom_sizes):
395
+ header_cell = ws.cell(row=row_pairs[i][0]-1, column=1, value=size)
396
+ header_cell.font = red_font
397
+ header_cell.alignment = center_alignment
398
+
399
+ # Write the results in the row pairs, add SNR/CNR formulas.
400
+ result_idx = 0
401
+ current_col_group = 0
402
+ current_row_pair = 0
403
+
404
+ while result_idx < len(self.results):
405
+ if current_row_pair >= len(row_pairs):
406
+ break
407
+
408
+ cols = column_groups[current_col_group]
409
+ row1, row2 = row_pairs[current_row_pair]
410
+
411
+ if result_idx < len(self.results):
412
+ result = self.results[result_idx]
413
+ self._write_result_to_cells(ws, result, cols, row1)
414
+ result_idx += 1
415
+
416
+ if result_idx < len(self.results):
417
+ result = self.results[result_idx]
418
+ self._write_result_to_cells(ws, result, cols, row2)
419
+ result_idx += 1
420
+
421
+ self.add_formulas_to_template(ws, (row1,row2), cols, red_font)
422
+
423
+ current_col_group += 1
424
+ if current_col_group >= len(column_groups):
425
+ current_col_group = 0
426
+ current_row_pair += 1
427
+
428
+ # Center-align the raw data rows (2..30) in all column groups.
429
+ for cols in column_groups:
430
+ for col in cols:
431
+ for row in range(2, 31):
432
+ cell = ws[f"{col}{row}"]
433
+ if cell.value is not None:
434
+ cell.alignment = center_alignment
435
+
436
+ #########################################################
437
+ # تصميم "1-AVG" في الصف 35، مع تجاهل الأصفار في الحساب
438
+ #########################################################
439
+ start_row = 35
440
+ ws['C35'] = "1-AVG"
441
+ ws['C35'].alignment = center_alignment
442
+
443
+ ws.merge_cells('D35:E35')
444
+ ws.merge_cells('F35:G35')
445
+ ws.merge_cells('H35:I35')
446
+
447
+ headers_avg = {
448
+ 'D35': 'AVG MEAN',
449
+ 'F35': 'AVG STDDEV',
450
+ 'H35': 'AVG CNR'
451
+ }
452
+ for c_ref, text_val in headers_avg.items():
453
+ ws[c_ref] = text_val
454
+ ws[c_ref].font = red_font
455
+ ws[c_ref].alignment = center_alignment
456
+
457
+ # We'll keep the same 10 phantom sizes, to fill rows 36..45.
458
+ phantom_sizes2 = [
459
+ '(7.0mm)', '(6.5mm)', '(6.0mm)', '(5.5mm)', '(5.0mm)',
460
+ '(4.5mm)', '(4.0mm)', '(3.5mm)', '(3.0mm)', '(2.5mm)'
461
+ ]
462
+
463
+ for i, size_label in enumerate(phantom_sizes2):
464
+ row = start_row + i + 1 # 36..45
465
+
466
+ ws.merge_cells(f'D{row}:E{row}')
467
+ ws.merge_cells(f'F{row}:G{row}')
468
+ ws.merge_cells(f'H{row}:I{row}')
469
+
470
+ c_cell = ws[f'C{row}']
471
+ c_cell.value = size_label
472
+ c_cell.font = red_font
473
+ c_cell.alignment = center_alignment
474
+
475
+ if i >= len(row_pairs):
476
+ continue
477
+ (raw_row1, raw_row2) = row_pairs[i]
478
+
479
+ mean_values = []
480
+ stddev_values = []
481
+ cnr_cells = [] # We'll store references to the row2 formula for CNR
482
+
483
+ # Loop over column_groups to gather Mean (row1), StdDev (row1), and CNR references (row2).
484
+ for group in column_groups:
485
+ mean_col = group[1] # e.g. 'C'
486
+ std_col = group[2] # e.g. 'D'
487
+
488
+ # Read mean from row1 => if 0 => skip.
489
+ m1_val = ws[f"{mean_col}{raw_row1}"].value
490
+ try:
491
+ m1_val = float(m1_val) if m1_val not in [None,''] else None
492
+ except:
493
+ m1_val = None
494
+ # تجاهل أي خلية = 0
495
+ if m1_val == 0:
496
+ m1_val = None
497
+
498
+ if m1_val is not None:
499
+ mean_values.append(m1_val)
500
+
501
+ # Read std from row1 => if 0 => skip.
502
+ s1_val = ws[f"{std_col}{raw_row1}"].value
503
+ try:
504
+ s1_val = float(s1_val) if s1_val not in [None,''] else None
505
+ except:
506
+ s1_val = None
507
+ if s1_val == 0:
508
+ s1_val = None
509
+
510
+ if s1_val is not None:
511
+ stddev_values.append(s1_val)
512
+
513
+ # For CNR, we have formula in the column after group[-1], row2.
514
+ formula_col = get_column_letter(column_index_from_string(group[-1]) + 1)
515
+ cnr_cell_ref = f"{formula_col}{raw_row2}"
516
+
517
+ # حتى لا نُدخل خلية الـCNR في الحساب إن كانت قيم الصف الثاني = 0
518
+ # مثلاً mean2=0 أو std2=0 => نعتبرها غير صالحة.
519
+ mean2_val = ws[f"{mean_col}{raw_row2}"].value
520
+ std2_val = ws[f"{std_col}{raw_row2}"].value
521
+ try:
522
+ mean2_val = float(mean2_val) if mean2_val not in [None,''] else None
523
+ std2_val = float(std2_val) if std2_val not in [None,''] else None
524
+ except:
525
+ mean2_val, std2_val = None, None
526
+
527
+ if mean2_val == 0:
528
+ mean2_val = None
529
+ if std2_val == 0:
530
+ std2_val = None
531
+
532
+ # لو عندك منطق إضافي: التحقق أن Mean1 !=0 أيضاً (m1_val)
533
+ # إذا أردت تجاهل الخلية إن كان m1_val=0...الخ.
534
+ # لكن عادة, تحسب CNR من Mean1,Mean2,Std2 => if any=0 => skip.
535
+ # نحسب Mean1 من نفس row1_col.
536
+ # m1_val = read it above, but we didn't store it if zero => might do it again.
537
+
538
+ # Decide if we require m1_val !=0 too? If so:
539
+ if (m1_val is not None) and (mean2_val is not None) and (std2_val is not None):
540
+ cnr_cells.append(cnr_cell_ref)
541
+
542
+ # حساب متوسط المين.
543
+ final_mean = sum(mean_values)/len(mean_values) if mean_values else None
544
+ if final_mean is not None:
545
+ ws[f'D{row}'].value = final_mean
546
+ ws[f'D{row}'].alignment = center_alignment
547
+ ws[f'D{row}'].number_format = '0.0000'
548
+
549
+ # حساب متوسط stddev
550
+ final_std = sum(stddev_values)/len(stddev_values) if stddev_values else None
551
+ if final_std is not None:
552
+ ws[f'F{row}'].value = final_std
553
+ ws[f'F{row}'].alignment = center_alignment
554
+ ws[f'F{row}'].number_format = '0.0000'
555
+
556
+ # أما الـCNR, فننشئ صيغة AVERAGE(...) لو عندنا cnr_cells.
557
+ if cnr_cells:
558
+ formula_avg_cnr = f"=IFERROR(AVERAGE({','.join(cnr_cells)}),\"\")"
559
+ ws[f'H{row}'].value = formula_avg_cnr
560
+ ws[f'H{row}'].alignment = center_alignment
561
+ ws[f'H{row}'].number_format = '0.0000'
562
+
563
+ # Add thin border around the region C35..I45.
564
+ thin_side = openpyxl.styles.Side(style='thin')
565
+ border = openpyxl.styles.Border(
566
+ left=thin_side, right=thin_side, top=thin_side, bottom=thin_side
567
+ )
568
+ for r in range(35, 46):
569
+ for col in ['C','D','E','F','G','H','I']:
570
+ ws[f"{col}{r}"].border = border
571
+
572
+ wb.save(output_path)
573
+ return output_path, f"Results saved successfully ({len(self.results)} measurements)"
574
+ except Exception as e:
575
+ logger.error(f"Error saving formatted results: {str(e)}")
576
+ return None, f"Error saving results: {str(e)}"
577
+
578
+ def _write_result_to_cells(self, ws, result, cols, row):
579
+ center_alignment = openpyxl.styles.Alignment(horizontal='center')
580
+
581
+ value_mapping = {
582
+ 'Area': 'Area (mm²)',
583
+ 'Mean': 'Mean',
584
+ 'StdDev': 'StdDev',
585
+ 'Min': 'Min',
586
+ 'Max': 'Max'
587
+ }
588
+
589
+ for i, (header, key) in enumerate(value_mapping.items()):
590
+ cell = ws[f"{cols[i]}{row}"]
591
+ val = result[key]
592
+ cell.value = float(val) if val not in ['', None] else ''
593
+ cell.alignment = center_alignment
594
+
595
+ def format_results(self):
596
+ if not self.results:
597
+ return "No measurements yet"
598
+ df = pd.DataFrame(self.results)
599
+ columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
600
+ df = df[columns_order]
601
+ return df.to_string(index=False)
602
+
603
+ def add_zero_row(self, image):
604
+ self.results.append({
605
+ 'Area (mm²)': '0.000',
606
+ 'Mean': '0.000',
607
+ 'StdDev': '0.000',
608
+ 'Min': '0.000',
609
+ 'Max': '0.000',
610
+ 'Point': '(0, 0)'
611
+ })
612
+ return image, self.format_results()
613
+
614
+ def add_two_zero_rows(self, image):
615
+ for _ in range(2):
616
+ self.results.append({
617
+ 'Area (mm²)': '0.000',
618
+ 'Mean': '0.000',
619
+ 'StdDev': '0.000',
620
+ 'Min': '0.000',
621
+ 'Max': '0.000',
622
+ 'Point': '(0, 0)'
623
+ })
624
+ return image, self.format_results()
625
+
626
+ def undo_last(self, image):
627
+ if not self.results: # لا توجد نتائج
628
+ return self.update_display(), self.format_results()
629
+
630
+ last_result = self.results[-1]
631
+ is_measurement = (last_result['Point'] != '(0, 0)')
632
+ self.results.pop()
633
+
634
+ if is_measurement and self.marks:
635
+ self.marks.pop()
636
+
637
+ return self.update_display(), self.format_results()
638
+
639
+
640
+ def create_interface():
641
+ print("Creating interface...")
642
+ analyzer = DicomAnalyzer()
643
+
644
+ with gr.Blocks(css="#image_display { outline: none; }") as interface:
645
+ gr.Markdown("# DICOM Image Analyzer")
646
+
647
+ with gr.Row():
648
+ with gr.Column():
649
+ file_input = gr.File(label="Upload DICOM file")
650
+ diameter_slider = gr.Slider(
651
+ minimum=1,
652
+ maximum=20,
653
+ value=9,
654
+ step=1,
655
+ label="ROI Diameter (pixels)"
656
+ )
657
+
658
+ with gr.Row():
659
+ zoom_in_btn = gr.Button("Zoom In (+)")
660
+ zoom_out_btn = gr.Button("Zoom Out (-)")
661
+ reset_btn = gr.Button("Reset View")
662
+ reset_all_btn = gr.Button("Reset All")
663
+
664
+ with gr.Column():
665
+ image_display = gr.Image(
666
+ label="DICOM Image",
667
+ interactive=True,
668
+ elem_id="image_display"
669
+ )
670
+
671
+ with gr.Row():
672
+ zero_btn = gr.Button("Add Zero Row")
673
+ zero2_btn = gr.Button("Add Two Zero Rows")
674
+ undo_btn = gr.Button("Undo Last")
675
+ save_btn = gr.Button("Save Results")
676
+ save_formatted_btn = gr.Button("Save Formatted Results")
677
+
678
+ results_display = gr.Textbox(label="Results", interactive=False)
679
+ file_output = gr.File(label="Download Results")
680
+ key_press = gr.Textbox(visible=False, elem_id="key_press")
681
+
682
+ gr.Markdown("""
683
+ ### Controls:
684
+ - Use arrow keys to pan when zoomed in. Movement is now larger.
685
+ - Click points to measure ROI.
686
+ - Use Zoom In/Out buttons or Reset View to adjust zoom level.
687
+ - Use Reset All to clear all measurements.
688
+ - "Save Results": basic Excel with raw data.
689
+ - "Save Formatted Results": Excel with advanced formatting & formulas.
690
+ """)
691
+
692
+ def update_diameter(x):
693
+ analyzer.circle_diameter = float(x)
694
+ print(f"Diameter updated to: {x}")
695
+ return f"Diameter set to {x} pixels"
696
+
697
+ def save_formatted():
698
+ output_path = "analysis_results_formatted.xlsx"
699
+ return analyzer.save_formatted_results(output_path)
700
+
701
+ file_input.change(
702
+ fn=analyzer.load_dicom,
703
+ inputs=file_input,
704
+ outputs=[image_display, results_display]
705
+ )
706
+
707
+ image_display.select(
708
+ fn=analyzer.analyze_roi,
709
+ outputs=[image_display, results_display]
710
+ )
711
+
712
+ diameter_slider.change(
713
+ fn=update_diameter,
714
+ inputs=diameter_slider,
715
+ outputs=gr.Textbox(label="Status")
716
+ )
717
+
718
+ zoom_in_btn.click(
719
+ fn=analyzer.zoom_in,
720
+ inputs=image_display,
721
+ outputs=image_display,
722
+ queue=False
723
+ )
724
+
725
+ zoom_out_btn.click(
726
+ fn=analyzer.zoom_out,
727
+ inputs=image_display,
728
+ outputs=image_display,
729
+ queue=False
730
+ )
731
+
732
+ reset_btn.click(
733
+ fn=analyzer.reset_view,
734
+ outputs=image_display
735
+ )
736
+
737
+ reset_all_btn.click(
738
+ fn=analyzer.reset_all,
739
+ inputs=image_display,
740
+ outputs=[image_display, results_display]
741
+ )
742
+
743
+ key_press.change(
744
+ fn=analyzer.handle_keyboard,
745
+ inputs=key_press,
746
+ outputs=image_display
747
+ )
748
+
749
+ zero_btn.click(
750
+ fn=analyzer.add_zero_row,
751
+ inputs=image_display,
752
+ outputs=[image_display, results_display]
753
+ )
754
+
755
+ zero2_btn.click(
756
+ fn=analyzer.add_two_zero_rows,
757
+ inputs=image_display,
758
+ outputs=[image_display, results_display]
759
+ )
760
+
761
+ undo_btn.click(
762
+ fn=analyzer.undo_last,
763
+ inputs=image_display,
764
+ outputs=[image_display, results_display]
765
+ )
766
+
767
+ save_btn.click(
768
+ fn=analyzer.save_results,
769
+ outputs=[file_output, results_display]
770
+ )
771
+
772
+ save_formatted_btn.click(
773
+ fn=save_formatted,
774
+ outputs=[file_output, results_display]
775
+ )
776
+
777
+ # JavaScript to capture arrow keys and pass to Gradio.
778
+ js = """
779
+ <script>
780
+ document.addEventListener('keydown', function(e) {
781
+ if (['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) {
782
+ e.preventDefault();
783
+ const el = document.querySelector('#key_press textarea');
784
+ if (el) {
785
+ el.value = e.key;
786
+ el.dispatchEvent(new Event('input'));
787
+ setTimeout(() => {
788
+ el.value = '';
789
+ el.dispatchEvent(new Event('input'));
790
+ }, 100);
791
+ }
792
+ }
793
+ });
794
+ </script>
795
+ """
796
+ gr.HTML(js)
797
+
798
+ print("Interface created successfully")
799
+ return interface
800
+
801
+
802
+ if __name__ == "__main__":
803
+ try:
804
+ print("Starting application...")
805
+ interface = create_interface()
806
+ print("Launching interface...")
807
+ interface.queue()
808
+ interface.launch(
809
+ server_name="0.0.0.0",
810
+ server_port=7860,
811
+ share=True,
812
+ debug=True,
813
+ show_error=True,
814
+ quiet=False
815
+ )
816
+ except Exception as e:
817
+ print(f"Error launching application: {str(e)}")
818
+ logger.error(f"Error launching application: {str(e)}")
819
+ logger.error(traceback.format_exc())
820
+ raise e
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio
2
+ numpy
3
+ pandas
4
+ opencv-python
5
+ pydicom
6
+ openpyxl