Update app.py
Browse files
app.py
CHANGED
|
@@ -62,7 +62,6 @@ class DicomAnalyzer:
|
|
| 62 |
self.max_pan_y = 0
|
| 63 |
self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
|
| 64 |
print("DicomAnalyzer initialized...")
|
| 65 |
-
|
| 66 |
def reset_all(self, image):
|
| 67 |
self.results = []
|
| 68 |
self.marks = []
|
|
@@ -115,6 +114,7 @@ class DicomAnalyzer:
|
|
| 115 |
except Exception as e:
|
| 116 |
print(f"Error normalizing image: {str(e)}")
|
| 117 |
return None
|
|
|
|
| 118 |
def reset_view(self):
|
| 119 |
self.zoom_factor = 1.0
|
| 120 |
self.pan_x = 0
|
|
@@ -152,6 +152,60 @@ class DicomAnalyzer:
|
|
| 152 |
print(f"Error handling keyboard input: {str(e)}")
|
| 153 |
return self.display_image
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
def analyze_roi(self, evt: gr.SelectData):
|
| 156 |
try:
|
| 157 |
if self.current_image is None:
|
|
@@ -220,96 +274,6 @@ class DicomAnalyzer:
|
|
| 220 |
print(f"Error analyzing ROI: {str(e)}")
|
| 221 |
return self.display_image, f"Error analyzing ROI: {str(e)}"
|
| 222 |
|
| 223 |
-
def update_display(self):
|
| 224 |
-
try:
|
| 225 |
-
if self.original_display is None:
|
| 226 |
-
return None
|
| 227 |
-
|
| 228 |
-
height, width = self.original_display.shape[:2]
|
| 229 |
-
new_height = int(height * self.zoom_factor)
|
| 230 |
-
new_width = int(width * self.zoom_factor)
|
| 231 |
-
|
| 232 |
-
zoomed = cv2.resize(self.original_display, (new_width, new_height),
|
| 233 |
-
interpolation=cv2.INTER_CUBIC)
|
| 234 |
-
|
| 235 |
-
zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
|
| 236 |
-
|
| 237 |
-
for x, y, diameter in self.marks:
|
| 238 |
-
zoomed_x = int(x * self.zoom_factor)
|
| 239 |
-
zoomed_y = int(y * self.zoom_factor)
|
| 240 |
-
zoomed_radius = int((diameter/2.0) * self.zoom_factor)
|
| 241 |
-
|
| 242 |
-
cv2.circle(zoomed_bgr,
|
| 243 |
-
(zoomed_x, zoomed_y),
|
| 244 |
-
zoomed_radius,
|
| 245 |
-
self.CIRCLE_COLOR,
|
| 246 |
-
1,
|
| 247 |
-
lineType=cv2.LINE_AA)
|
| 248 |
-
|
| 249 |
-
num_points = 8
|
| 250 |
-
for i in range(num_points):
|
| 251 |
-
angle = 2 * np.pi * i / num_points
|
| 252 |
-
point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
|
| 253 |
-
point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
|
| 254 |
-
cv2.circle(zoomed_bgr,
|
| 255 |
-
(point_x, point_y),
|
| 256 |
-
1,
|
| 257 |
-
self.CIRCLE_COLOR,
|
| 258 |
-
-1,
|
| 259 |
-
lineType=cv2.LINE_AA)
|
| 260 |
-
|
| 261 |
-
zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
|
| 262 |
-
|
| 263 |
-
self.max_pan_x = max(0, new_width - width)
|
| 264 |
-
self.max_pan_y = max(0, new_height - height)
|
| 265 |
-
self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
|
| 266 |
-
self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
|
| 267 |
-
|
| 268 |
-
visible = zoomed[
|
| 269 |
-
int(self.pan_y):int(self.pan_y + height),
|
| 270 |
-
int(self.pan_x):int(self.pan_x + width)
|
| 271 |
-
]
|
| 272 |
-
|
| 273 |
-
return visible
|
| 274 |
-
except Exception as e:
|
| 275 |
-
print(f"Error updating display: {str(e)}")
|
| 276 |
-
return self.original_display
|
| 277 |
-
|
| 278 |
-
def save_results(self):
|
| 279 |
-
"""
|
| 280 |
-
Basic save function for raw results with improved error handling and logging
|
| 281 |
-
"""
|
| 282 |
-
try:
|
| 283 |
-
if not self.results:
|
| 284 |
-
logger.warning("Attempted to save with no results")
|
| 285 |
-
return None, "No results to save"
|
| 286 |
-
|
| 287 |
-
df = pd.DataFrame(self.results)
|
| 288 |
-
columns_order = ['Area (mm²)', 'Mean', 'StdDev', 'Min', 'Max', 'Point']
|
| 289 |
-
df = df[columns_order]
|
| 290 |
-
|
| 291 |
-
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
| 292 |
-
output_file = f"analysis_results_{timestamp}.xlsx"
|
| 293 |
-
|
| 294 |
-
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
|
| 295 |
-
df.to_excel(writer, index=False, sheet_name='Results')
|
| 296 |
-
|
| 297 |
-
worksheet = writer.sheets['Results']
|
| 298 |
-
for idx, col in enumerate(df.columns):
|
| 299 |
-
max_length = max(
|
| 300 |
-
df[col].astype(str).apply(len).max(),
|
| 301 |
-
len(str(col))
|
| 302 |
-
) + 2
|
| 303 |
-
worksheet.column_dimensions[get_column_letter(idx + 1)].width = max_length
|
| 304 |
-
|
| 305 |
-
logger.info(f"Results saved successfully to {output_file}")
|
| 306 |
-
return output_file, f"Results saved successfully to {output_file}"
|
| 307 |
-
|
| 308 |
-
except Exception as e:
|
| 309 |
-
error_msg = f"Error saving results: {str(e)}"
|
| 310 |
-
logger.error(error_msg)
|
| 311 |
-
logger.error(traceback.format_exc())
|
| 312 |
-
return None, error_msg
|
| 313 |
def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
|
| 314 |
try:
|
| 315 |
base_col = col_group[1] # Mean column
|
|
@@ -317,16 +281,16 @@ class DicomAnalyzer:
|
|
| 317 |
|
| 318 |
row1, row2 = row_pair
|
| 319 |
|
| 320 |
-
# SNR Formula for first row
|
| 321 |
-
formula1 = f"={base_col}{row1}/{std_col}{row1}"
|
| 322 |
formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
|
| 323 |
cell1 = ws[f"{formula_col}{row1}"]
|
| 324 |
cell1.value = formula1
|
| 325 |
cell1.font = red_font
|
| 326 |
cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 327 |
|
| 328 |
-
# CNR Formula for second row with
|
| 329 |
-
formula2 = f"=
|
| 330 |
cell2 = ws[f"{formula_col}{row2}"]
|
| 331 |
cell2.value = formula2
|
| 332 |
cell2.font = red_font
|
|
@@ -458,32 +422,42 @@ class DicomAnalyzer:
|
|
| 458 |
row_number = row_pairs[i][1] # Second row for CNR
|
| 459 |
valid_cnr_refs = []
|
| 460 |
|
|
|
|
| 461 |
for cols in column_groups:
|
| 462 |
formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
|
| 463 |
mean_col = cols[1]
|
| 464 |
std_col = cols[2]
|
| 465 |
|
| 466 |
-
# Check
|
| 467 |
mean1 = ws[f"{mean_col}{row_pairs[i][0]}"].value
|
| 468 |
mean2 = ws[f"{mean_col}{row_pairs[i][1]}"].value
|
| 469 |
std2 = ws[f"{std_col}{row_pairs[i][1]}"].value
|
| 470 |
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
valid_cnr_refs.append(f"{formula_col}{row_number}")
|
| 475 |
|
| 476 |
# Add row to the averages table
|
| 477 |
size_cell = ws.cell(row=current_row, column=1, value=size)
|
| 478 |
size_cell.alignment = center_alignment
|
| 479 |
|
| 480 |
-
avg_cell = ws.cell(row=current_row, column=2)
|
| 481 |
if valid_cnr_refs:
|
| 482 |
-
# Create
|
| 483 |
refs = ",".join(valid_cnr_refs)
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
| 485 |
avg_cell.number_format = '0.000'
|
| 486 |
-
|
| 487 |
|
| 488 |
current_row += 1
|
| 489 |
|
|
@@ -499,8 +473,6 @@ class DicomAnalyzer:
|
|
| 499 |
except Exception as e:
|
| 500 |
logger.error(f"Error saving formatted results: {str(e)}")
|
| 501 |
return None, f"Error saving results: {str(e)}"
|
| 502 |
-
|
| 503 |
-
|
| 504 |
def _write_result_to_cells(self, ws, result, cols, row):
|
| 505 |
"""Helper method to write a single result to worksheet cells"""
|
| 506 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|
|
|
|
| 62 |
self.max_pan_y = 0
|
| 63 |
self.CIRCLE_COLOR = (0, 255, 255) # BGR Yellow
|
| 64 |
print("DicomAnalyzer initialized...")
|
|
|
|
| 65 |
def reset_all(self, image):
|
| 66 |
self.results = []
|
| 67 |
self.marks = []
|
|
|
|
| 114 |
except Exception as e:
|
| 115 |
print(f"Error normalizing image: {str(e)}")
|
| 116 |
return None
|
| 117 |
+
|
| 118 |
def reset_view(self):
|
| 119 |
self.zoom_factor = 1.0
|
| 120 |
self.pan_x = 0
|
|
|
|
| 152 |
print(f"Error handling keyboard input: {str(e)}")
|
| 153 |
return self.display_image
|
| 154 |
|
| 155 |
+
def update_display(self):
|
| 156 |
+
try:
|
| 157 |
+
if self.original_display is None:
|
| 158 |
+
return None
|
| 159 |
+
|
| 160 |
+
height, width = self.original_display.shape[:2]
|
| 161 |
+
new_height = int(height * self.zoom_factor)
|
| 162 |
+
new_width = int(width * self.zoom_factor)
|
| 163 |
+
|
| 164 |
+
zoomed = cv2.resize(self.original_display, (new_width, new_height),
|
| 165 |
+
interpolation=cv2.INTER_CUBIC)
|
| 166 |
+
|
| 167 |
+
zoomed_bgr = cv2.cvtColor(zoomed, cv2.COLOR_RGB2BGR)
|
| 168 |
+
|
| 169 |
+
for x, y, diameter in self.marks:
|
| 170 |
+
zoomed_x = int(x * self.zoom_factor)
|
| 171 |
+
zoomed_y = int(y * self.zoom_factor)
|
| 172 |
+
zoomed_radius = int((diameter/2.0) * self.zoom_factor)
|
| 173 |
+
|
| 174 |
+
cv2.circle(zoomed_bgr,
|
| 175 |
+
(zoomed_x, zoomed_y),
|
| 176 |
+
zoomed_radius,
|
| 177 |
+
self.CIRCLE_COLOR,
|
| 178 |
+
1,
|
| 179 |
+
lineType=cv2.LINE_AA)
|
| 180 |
+
|
| 181 |
+
num_points = 8
|
| 182 |
+
for i in range(num_points):
|
| 183 |
+
angle = 2 * np.pi * i / num_points
|
| 184 |
+
point_x = int(zoomed_x + zoomed_radius * np.cos(angle))
|
| 185 |
+
point_y = int(zoomed_y + zoomed_radius * np.sin(angle))
|
| 186 |
+
cv2.circle(zoomed_bgr,
|
| 187 |
+
(point_x, point_y),
|
| 188 |
+
1,
|
| 189 |
+
self.CIRCLE_COLOR,
|
| 190 |
+
-1,
|
| 191 |
+
lineType=cv2.LINE_AA)
|
| 192 |
+
|
| 193 |
+
zoomed = cv2.cvtColor(zoomed_bgr, cv2.COLOR_BGR2RGB)
|
| 194 |
+
|
| 195 |
+
self.max_pan_x = max(0, new_width - width)
|
| 196 |
+
self.max_pan_y = max(0, new_height - height)
|
| 197 |
+
self.pan_x = min(max(0, self.pan_x), self.max_pan_x)
|
| 198 |
+
self.pan_y = min(max(0, self.pan_y), self.max_pan_y)
|
| 199 |
+
|
| 200 |
+
visible = zoomed[
|
| 201 |
+
int(self.pan_y):int(self.pan_y + height),
|
| 202 |
+
int(self.pan_x):int(self.pan_x + width)
|
| 203 |
+
]
|
| 204 |
+
|
| 205 |
+
return visible
|
| 206 |
+
except Exception as e:
|
| 207 |
+
print(f"Error updating display: {str(e)}")
|
| 208 |
+
return self.original_display
|
| 209 |
def analyze_roi(self, evt: gr.SelectData):
|
| 210 |
try:
|
| 211 |
if self.current_image is None:
|
|
|
|
| 274 |
print(f"Error analyzing ROI: {str(e)}")
|
| 275 |
return self.display_image, f"Error analyzing ROI: {str(e)}"
|
| 276 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
def add_formulas_to_template(self, ws, row_pair, col_group, red_font):
|
| 278 |
try:
|
| 279 |
base_col = col_group[1] # Mean column
|
|
|
|
| 281 |
|
| 282 |
row1, row2 = row_pair
|
| 283 |
|
| 284 |
+
# SNR Formula for first row with error handling
|
| 285 |
+
formula1 = f"=IFERROR({base_col}{row1}/{std_col}{row1},\"\")"
|
| 286 |
formula_col = get_column_letter(column_index_from_string(col_group[-1]) + 1)
|
| 287 |
cell1 = ws[f"{formula_col}{row1}"]
|
| 288 |
cell1.value = formula1
|
| 289 |
cell1.font = red_font
|
| 290 |
cell1.alignment = openpyxl.styles.Alignment(horizontal='center')
|
| 291 |
|
| 292 |
+
# CNR Formula for second row with error handling
|
| 293 |
+
formula2 = f"=IFERROR(({base_col}{row1}-{base_col}{row2})/{std_col}{row2},\"\")"
|
| 294 |
cell2 = ws[f"{formula_col}{row2}"]
|
| 295 |
cell2.value = formula2
|
| 296 |
cell2.font = red_font
|
|
|
|
| 422 |
row_number = row_pairs[i][1] # Second row for CNR
|
| 423 |
valid_cnr_refs = []
|
| 424 |
|
| 425 |
+
# Get all CNR cells for this row
|
| 426 |
for cols in column_groups:
|
| 427 |
formula_col = get_column_letter(column_index_from_string(cols[-1]) + 1)
|
| 428 |
mean_col = cols[1]
|
| 429 |
std_col = cols[2]
|
| 430 |
|
| 431 |
+
# Check values used in CNR calculation
|
| 432 |
mean1 = ws[f"{mean_col}{row_pairs[i][0]}"].value
|
| 433 |
mean2 = ws[f"{mean_col}{row_pairs[i][1]}"].value
|
| 434 |
std2 = ws[f"{std_col}{row_pairs[i][1]}"].value
|
| 435 |
|
| 436 |
+
# Convert values to float if they're strings
|
| 437 |
+
try:
|
| 438 |
+
mean1 = float(mean1) if mean1 not in [None, ''] else 0
|
| 439 |
+
mean2 = float(mean2) if mean2 not in [None, ''] else 0
|
| 440 |
+
std2 = float(std2) if std2 not in [None, ''] else 0
|
| 441 |
+
except (ValueError, TypeError):
|
| 442 |
+
continue
|
| 443 |
+
|
| 444 |
+
# Only skip if ALL values are zero
|
| 445 |
+
if not (mean1 == 0 and mean2 == 0 and std2 == 0):
|
| 446 |
valid_cnr_refs.append(f"{formula_col}{row_number}")
|
| 447 |
|
| 448 |
# Add row to the averages table
|
| 449 |
size_cell = ws.cell(row=current_row, column=1, value=size)
|
| 450 |
size_cell.alignment = center_alignment
|
| 451 |
|
|
|
|
| 452 |
if valid_cnr_refs:
|
| 453 |
+
# Create array formula for average that handles errors
|
| 454 |
refs = ",".join(valid_cnr_refs)
|
| 455 |
+
array_formula = f'=IFERROR(AVERAGE(IF(ISNUMBER({refs}),{refs})),"")'
|
| 456 |
+
|
| 457 |
+
avg_cell = ws.cell(row=current_row, column=2)
|
| 458 |
+
avg_cell.value = array_formula
|
| 459 |
avg_cell.number_format = '0.000'
|
| 460 |
+
avg_cell.alignment = center_alignment
|
| 461 |
|
| 462 |
current_row += 1
|
| 463 |
|
|
|
|
| 473 |
except Exception as e:
|
| 474 |
logger.error(f"Error saving formatted results: {str(e)}")
|
| 475 |
return None, f"Error saving results: {str(e)}"
|
|
|
|
|
|
|
| 476 |
def _write_result_to_cells(self, ws, result, cols, row):
|
| 477 |
"""Helper method to write a single result to worksheet cells"""
|
| 478 |
center_alignment = openpyxl.styles.Alignment(horizontal='center')
|