microlive 1.0.22__tar.gz → 1.0.24__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {microlive-1.0.22 → microlive-1.0.24}/PKG-INFO +1 -1
- {microlive-1.0.22 → microlive-1.0.24}/microlive/__init__.py +1 -1
- {microlive-1.0.22 → microlive-1.0.24}/microlive/gui/app.py +517 -350
- {microlive-1.0.22 → microlive-1.0.24}/.gitignore +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/LICENSE +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/README.md +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/data/__init__.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/data/icons/__init__.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/data/icons/icon_micro.png +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/data/models/__init__.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/data/models/spot_detection_cnn.pth +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/gui/__init__.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/gui/main.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/gui/micro_mac.command +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/gui/micro_windows.bat +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/imports.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/microscopy.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/ml_spot_detection.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/pipelines/__init__.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/pipelines/pipeline_FRAP.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/pipelines/pipeline_folding_efficiency.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/pipelines/pipeline_particle_tracking.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/utils/__init__.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/utils/device.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/utils/model_downloader.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/microlive/utils/resources.py +0 -0
- {microlive-1.0.22 → microlive-1.0.24}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: microlive
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.24
|
|
4
4
|
Summary: Live-cell microscopy image analysis and single-molecule measurements
|
|
5
5
|
Project-URL: Homepage, https://github.com/ningzhaoAnschutz/microlive
|
|
6
6
|
Project-URL: Documentation, https://github.com/ningzhaoAnschutz/microlive/blob/main/docs/user_guide.md
|
|
@@ -23,7 +23,7 @@ Authors:
|
|
|
23
23
|
Nathan L. Nowling, Brian Munsky, Ning Zhao
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
|
-
__version__ = "1.0.
|
|
26
|
+
__version__ = "1.0.24"
|
|
27
27
|
__author__ = "Luis U. Aguilera, William S. Raymond, Rhiannon M. Sears, Nathan L. Nowling, Brian Munsky, Ning Zhao"
|
|
28
28
|
|
|
29
29
|
# Package name (for backward compatibility)
|
|
@@ -1949,9 +1949,6 @@ class GUI(QMainWindow):
|
|
|
1949
1949
|
# Reset segmentation tab to Watershed (index 0) since Edit tab is only relevant after segmentation
|
|
1950
1950
|
if hasattr(self, 'segmentation_method_tabs'):
|
|
1951
1951
|
self.segmentation_method_tabs.setCurrentIndex(0)
|
|
1952
|
-
# Reset colocalization tab to Visual (index 0) since Verify tabs are only relevant after colocalization
|
|
1953
|
-
if hasattr(self, 'coloc_subtabs'):
|
|
1954
|
-
self.coloc_subtabs.setCurrentIndex(0)
|
|
1955
1952
|
|
|
1956
1953
|
self.plot_image()
|
|
1957
1954
|
self.plot_tracking()
|
|
@@ -3376,133 +3373,242 @@ class GUI(QMainWindow):
|
|
|
3376
3373
|
# =============================================================================
|
|
3377
3374
|
|
|
3378
3375
|
def manual_segmentation(self):
|
|
3379
|
-
"""
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
- Reset selected points
|
|
3384
|
-
- Connect a single click handler
|
|
3376
|
+
"""Activate manual segmentation mode with polygon drawing workflow.
|
|
3377
|
+
|
|
3378
|
+
This method is called when the Manual tab is selected.
|
|
3379
|
+
It sets up the click handler for placing polygon vertices.
|
|
3385
3380
|
"""
|
|
3386
3381
|
if self.image_stack is None:
|
|
3387
|
-
|
|
3382
|
+
if hasattr(self, 'manual_status_label'):
|
|
3383
|
+
self.manual_status_label.setText("⚠️ No image loaded")
|
|
3384
|
+
self.manual_status_label.setStyleSheet("color: #ffc107;")
|
|
3385
|
+
return
|
|
3386
|
+
|
|
3387
|
+
# Enter manual mode
|
|
3388
|
+
self.segmentation_mode = "manual"
|
|
3389
|
+
|
|
3390
|
+
# Connect click handler
|
|
3391
|
+
self._enter_manual_mode()
|
|
3392
|
+
|
|
3393
|
+
# Refresh display
|
|
3394
|
+
self._update_polygon_display()
|
|
3395
|
+
|
|
3396
|
+
# Update status based on current state
|
|
3397
|
+
self._update_polygon_status()
|
|
3398
|
+
|
|
3399
|
+
def _enter_manual_mode(self):
|
|
3400
|
+
"""Connect manual click handler when Manual tab is selected."""
|
|
3401
|
+
# Disconnect any existing handler first
|
|
3402
|
+
if hasattr(self, 'cid_manual') and self.cid_manual is not None:
|
|
3403
|
+
try:
|
|
3404
|
+
self.canvas_segmentation.mpl_disconnect(self.cid_manual)
|
|
3405
|
+
except Exception:
|
|
3406
|
+
pass
|
|
3407
|
+
|
|
3408
|
+
# Connect click handler for polygon vertices
|
|
3409
|
+
self.cid_manual = self.canvas_segmentation.mpl_connect(
|
|
3410
|
+
'button_press_event', self.on_polygon_click
|
|
3411
|
+
)
|
|
3412
|
+
|
|
3413
|
+
# Initialize polygon points list if not exists
|
|
3414
|
+
if not hasattr(self, 'manual_polygon_points'):
|
|
3415
|
+
self.manual_polygon_points = []
|
|
3416
|
+
|
|
3417
|
+
def _exit_manual_mode(self):
|
|
3418
|
+
"""Disconnect manual click handler when leaving Manual tab."""
|
|
3419
|
+
if hasattr(self, 'cid_manual') and self.cid_manual is not None:
|
|
3420
|
+
try:
|
|
3421
|
+
self.canvas_segmentation.mpl_disconnect(self.cid_manual)
|
|
3422
|
+
except Exception:
|
|
3423
|
+
pass
|
|
3424
|
+
self.cid_manual = None
|
|
3425
|
+
|
|
3426
|
+
def on_polygon_click(self, event):
|
|
3427
|
+
"""Handle click in manual segmentation mode - add polygon vertex.
|
|
3428
|
+
|
|
3429
|
+
Each click adds a vertex to the polygon. Points are connected with lines.
|
|
3430
|
+
When finished, the polygon is closed and filled to create the mask.
|
|
3431
|
+
"""
|
|
3432
|
+
if event.inaxes != self.ax_segmentation:
|
|
3433
|
+
return
|
|
3434
|
+
if event.xdata is None or event.ydata is None:
|
|
3435
|
+
return
|
|
3436
|
+
if event.button != 1: # Left click only
|
|
3437
|
+
return
|
|
3438
|
+
|
|
3439
|
+
x, y = event.xdata, event.ydata
|
|
3440
|
+
|
|
3441
|
+
# Get image dimensions to validate
|
|
3442
|
+
image_to_use = self.get_current_image_source()
|
|
3443
|
+
if image_to_use is None:
|
|
3388
3444
|
return
|
|
3445
|
+
|
|
3389
3446
|
ch = self.segmentation_current_channel
|
|
3390
3447
|
if self.use_max_proj_for_segmentation and self.segmentation_maxproj is not None:
|
|
3391
3448
|
img = self.segmentation_maxproj[..., ch]
|
|
3392
3449
|
else:
|
|
3393
3450
|
fr = self.segmentation_current_frame
|
|
3394
|
-
image_channel =
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
img_filtered = gaussian_filter(img, sigma=2)
|
|
3398
|
-
lo, hi = np.percentile(img_filtered, [0.5, 99.0])
|
|
3399
|
-
img_clipped = np.clip(img_filtered, lo, hi)
|
|
3400
|
-
# redraw segmentation canvas
|
|
3401
|
-
self.figure_segmentation.clear()
|
|
3402
|
-
self.ax_segmentation = self.figure_segmentation.add_subplot(111)
|
|
3403
|
-
self.ax_segmentation.imshow(img_clipped, cmap='Spectral')
|
|
3404
|
-
self.ax_segmentation.axis('off')
|
|
3405
|
-
self.figure_segmentation.tight_layout()
|
|
3406
|
-
self.canvas_segmentation.draw()
|
|
3407
|
-
# clear any previous manual mask
|
|
3408
|
-
if hasattr(self, 'manual_segmentation_mask'):
|
|
3409
|
-
del self.manual_segmentation_mask
|
|
3410
|
-
# enter manual mode
|
|
3411
|
-
self.selected_points = []
|
|
3412
|
-
self.segmentation_mode = "manual"
|
|
3413
|
-
# connect click handler exactly once
|
|
3414
|
-
self.cid = self.canvas_segmentation.mpl_connect(
|
|
3415
|
-
'button_press_event',
|
|
3416
|
-
self.on_click_segmentation)
|
|
3417
|
-
def on_click_segmentation(self, event):
|
|
3418
|
-
if event.inaxes != self.ax_segmentation:
|
|
3419
|
-
return
|
|
3420
|
-
if event.xdata is not None and event.ydata is not None:
|
|
3421
|
-
self.selected_points.append([int(event.xdata), int(event.ydata)])
|
|
3422
|
-
ch = self.segmentation_current_channel
|
|
3423
|
-
if self.use_max_proj_for_segmentation:
|
|
3424
|
-
max_proj = np.max(self.image_stack, axis=(0, 1))[..., ch]
|
|
3451
|
+
image_channel = image_to_use[fr, :, :, :, ch]
|
|
3452
|
+
if getattr(self, 'segmentation_current_z', -1) == -1:
|
|
3453
|
+
img = np.max(image_channel, axis=0)
|
|
3425
3454
|
else:
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
[p[0] for p in self.selected_points],
|
|
3441
|
-
[p[1] for p in self.selected_points],
|
|
3442
|
-
'bo', markersize=6,
|
|
3443
|
-
)
|
|
3444
|
-
self.canvas_segmentation.draw()
|
|
3455
|
+
z_idx = min(self.segmentation_current_z, image_channel.shape[0] - 1)
|
|
3456
|
+
img = image_channel[z_idx, :, :]
|
|
3457
|
+
|
|
3458
|
+
# Validate click is within bounds
|
|
3459
|
+
height, width = img.shape[:2]
|
|
3460
|
+
if x < 0 or x >= width or y < 0 or y >= height:
|
|
3461
|
+
return
|
|
3462
|
+
|
|
3463
|
+
# Add point to polygon
|
|
3464
|
+
self.manual_polygon_points.append((x, y))
|
|
3465
|
+
|
|
3466
|
+
# Update display
|
|
3467
|
+
self._update_polygon_display()
|
|
3468
|
+
self._update_polygon_status()
|
|
3445
3469
|
|
|
3470
|
+
def _update_polygon_display(self):
|
|
3471
|
+
"""Update the display to show current polygon points and lines."""
|
|
3472
|
+
# First draw the base image using plot_segmentation
|
|
3473
|
+
self.plot_segmentation()
|
|
3474
|
+
|
|
3475
|
+
# Then overlay polygon points and lines
|
|
3476
|
+
if hasattr(self, 'manual_polygon_points') and len(self.manual_polygon_points) > 0:
|
|
3477
|
+
points = self.manual_polygon_points
|
|
3478
|
+
|
|
3479
|
+
# Draw lines between consecutive points
|
|
3480
|
+
if len(points) > 1:
|
|
3481
|
+
xs = [p[0] for p in points]
|
|
3482
|
+
ys = [p[1] for p in points]
|
|
3483
|
+
self.ax_segmentation.plot(xs, ys, 'c-', linewidth=2, alpha=0.8)
|
|
3484
|
+
|
|
3485
|
+
# Draw points as markers
|
|
3486
|
+
for i, (px, py) in enumerate(points):
|
|
3487
|
+
if i == 0:
|
|
3488
|
+
# First point is special (green)
|
|
3489
|
+
self.ax_segmentation.plot(px, py, 'go', markersize=10, markeredgecolor='white', markeredgewidth=2)
|
|
3490
|
+
else:
|
|
3491
|
+
# Other points (cyan)
|
|
3492
|
+
self.ax_segmentation.plot(px, py, 'co', markersize=8, markeredgecolor='white', markeredgewidth=1.5)
|
|
3493
|
+
|
|
3494
|
+
self.canvas_segmentation.draw()
|
|
3446
3495
|
|
|
3447
|
-
def
|
|
3448
|
-
"""
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
if
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
self.
|
|
3496
|
+
def _update_polygon_status(self):
|
|
3497
|
+
"""Update status label and button states based on polygon drawing state."""
|
|
3498
|
+
n_points = len(getattr(self, 'manual_polygon_points', []))
|
|
3499
|
+
|
|
3500
|
+
# Update Finish button state
|
|
3501
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
3502
|
+
self.btn_finish_polygon.setEnabled(n_points >= 3)
|
|
3503
|
+
|
|
3504
|
+
# Update status label
|
|
3505
|
+
if hasattr(self, 'manual_status_label'):
|
|
3506
|
+
if self.segmentation_mask is not None and n_points == 0:
|
|
3507
|
+
n_pixels = np.sum(self.segmentation_mask > 0)
|
|
3508
|
+
self.manual_status_label.setText(f"✓ Mask active: {n_pixels:,} pixels")
|
|
3509
|
+
self.manual_status_label.setStyleSheet("color: #00cc66; font-weight: bold;")
|
|
3510
|
+
elif n_points == 0:
|
|
3511
|
+
self.manual_status_label.setText("ℹ️ Click on image to place polygon vertices")
|
|
3512
|
+
self.manual_status_label.setStyleSheet("color: #888888; font-style: italic;")
|
|
3513
|
+
elif n_points < 3:
|
|
3514
|
+
self.manual_status_label.setText(f"🔷 {n_points} point(s) - need at least 3 for polygon")
|
|
3515
|
+
self.manual_status_label.setStyleSheet("color: #17a2b8;")
|
|
3461
3516
|
else:
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
self.
|
|
3481
|
-
self.masks_imported = False
|
|
3482
|
-
# Reset import status labels
|
|
3483
|
-
if hasattr(self, 'label_cyto_mask_status'):
|
|
3484
|
-
self.label_cyto_mask_status.setText("No cytosol mask loaded")
|
|
3485
|
-
self.label_cyto_mask_status.setStyleSheet("color: gray;")
|
|
3486
|
-
if hasattr(self, 'label_nuc_mask_status'):
|
|
3487
|
-
self.label_nuc_mask_status.setText("No nucleus mask loaded")
|
|
3488
|
-
self.label_nuc_mask_status.setStyleSheet("color: gray;")
|
|
3489
|
-
self.ax_segmentation.clear()
|
|
3490
|
-
cmap_imagej = cmap_list_imagej[ch % len(cmap_list_imagej)]
|
|
3491
|
-
self.ax_segmentation.imshow(max_proj, cmap=cmap_imagej)
|
|
3492
|
-
self.ax_segmentation.contour(self.segmentation_mask, levels=[0.5], colors='white', linewidths=1)
|
|
3493
|
-
self.ax_segmentation.axis('off')
|
|
3494
|
-
self.canvas_segmentation.draw()
|
|
3495
|
-
self.photobleaching_calculated = False
|
|
3496
|
-
self.segmentation_mode = "manual"
|
|
3517
|
+
self.manual_status_label.setText(f"🔷 {n_points} points - click 'Finish Polygon' to create mask")
|
|
3518
|
+
self.manual_status_label.setStyleSheet("color: #17a2b8; font-weight: bold;")
|
|
3519
|
+
|
|
3520
|
+
def finish_manual_polygon(self):
|
|
3521
|
+
"""Close the polygon and create the mask from the drawn points."""
|
|
3522
|
+
if not hasattr(self, 'manual_polygon_points') or len(self.manual_polygon_points) < 3:
|
|
3523
|
+
if hasattr(self, 'manual_status_label'):
|
|
3524
|
+
self.manual_status_label.setText("⚠️ Need at least 3 points to create polygon")
|
|
3525
|
+
self.manual_status_label.setStyleSheet("color: #ffc107;")
|
|
3526
|
+
return
|
|
3527
|
+
|
|
3528
|
+
# Get image dimensions
|
|
3529
|
+
image_to_use = self.get_current_image_source()
|
|
3530
|
+
if image_to_use is None:
|
|
3531
|
+
return
|
|
3532
|
+
|
|
3533
|
+
ch = self.segmentation_current_channel
|
|
3534
|
+
if self.use_max_proj_for_segmentation and self.segmentation_maxproj is not None:
|
|
3535
|
+
img = self.segmentation_maxproj[..., ch]
|
|
3497
3536
|
else:
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3537
|
+
fr = self.segmentation_current_frame
|
|
3538
|
+
image_channel = image_to_use[fr, :, :, :, ch]
|
|
3539
|
+
if getattr(self, 'segmentation_current_z', -1) == -1:
|
|
3540
|
+
img = np.max(image_channel, axis=0)
|
|
3541
|
+
else:
|
|
3542
|
+
z_idx = min(self.segmentation_current_z, image_channel.shape[0] - 1)
|
|
3543
|
+
img = image_channel[z_idx, :, :]
|
|
3544
|
+
|
|
3545
|
+
height, width = img.shape[:2]
|
|
3546
|
+
|
|
3547
|
+
# Create mask using cv2.fillPoly
|
|
3548
|
+
mask = np.zeros((height, width), dtype=np.int32)
|
|
3549
|
+
pts = np.array([[int(round(x)), int(round(y))] for x, y in self.manual_polygon_points], dtype=np.int32)
|
|
3550
|
+
cv2.fillPoly(mask, [pts], 1)
|
|
3551
|
+
|
|
3552
|
+
# Set as active mask
|
|
3553
|
+
self.segmentation_mask = mask
|
|
3554
|
+
self._active_mask_source = 'segmentation'
|
|
3555
|
+
|
|
3556
|
+
# Clear Cellpose/imported masks
|
|
3557
|
+
self.cellpose_masks_cyto = None
|
|
3558
|
+
self.cellpose_masks_nuc = None
|
|
3559
|
+
self.cellpose_masks_cyto_tyx = None
|
|
3560
|
+
self.cellpose_masks_nuc_tyx = None
|
|
3561
|
+
self.use_tyx_masks = False
|
|
3562
|
+
self.masks_imported = False
|
|
3563
|
+
|
|
3564
|
+
if hasattr(self, 'label_cyto_mask_status'):
|
|
3565
|
+
self.label_cyto_mask_status.setText("No cytosol mask loaded")
|
|
3566
|
+
self.label_cyto_mask_status.setStyleSheet("color: gray;")
|
|
3567
|
+
if hasattr(self, 'label_nuc_mask_status'):
|
|
3568
|
+
self.label_nuc_mask_status.setText("No nucleus mask loaded")
|
|
3569
|
+
self.label_nuc_mask_status.setStyleSheet("color: gray;")
|
|
3570
|
+
|
|
3571
|
+
# Clear photobleaching
|
|
3572
|
+
self.photobleaching_calculated = False
|
|
3573
|
+
|
|
3574
|
+
# Clear polygon points (mask created)
|
|
3575
|
+
n_pixels = np.sum(mask > 0)
|
|
3576
|
+
self.manual_polygon_points = []
|
|
3577
|
+
|
|
3578
|
+
# Update display
|
|
3579
|
+
self.plot_segmentation()
|
|
3580
|
+
|
|
3581
|
+
# Update status
|
|
3582
|
+
if hasattr(self, 'manual_status_label'):
|
|
3583
|
+
self.manual_status_label.setText(f"✓ Mask created: {n_pixels:,} pixels")
|
|
3584
|
+
self.manual_status_label.setStyleSheet("color: #00cc66; font-weight: bold;")
|
|
3585
|
+
|
|
3586
|
+
# Disable finish button
|
|
3587
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
3588
|
+
self.btn_finish_polygon.setEnabled(False)
|
|
3589
|
+
|
|
3590
|
+
def clear_manual_mask(self):
|
|
3591
|
+
"""Clear the current polygon points and mask, reset to ready state."""
|
|
3592
|
+
# Clear polygon points
|
|
3593
|
+
self.manual_polygon_points = []
|
|
3594
|
+
|
|
3595
|
+
# Clear mask
|
|
3596
|
+
self.segmentation_mask = None
|
|
3597
|
+
self._active_mask_source = 'none'
|
|
3598
|
+
|
|
3599
|
+
# Clear photobleaching
|
|
3600
|
+
self.photobleaching_calculated = False
|
|
3601
|
+
|
|
3602
|
+
# Update display
|
|
3603
|
+
self.plot_segmentation()
|
|
3604
|
+
|
|
3605
|
+
# Update status
|
|
3606
|
+
self._update_polygon_status()
|
|
3607
|
+
|
|
3608
|
+
# Disable finish button
|
|
3609
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
3610
|
+
self.btn_finish_polygon.setEnabled(False)
|
|
3611
|
+
|
|
3506
3612
|
|
|
3507
3613
|
def next_frame(self):
|
|
3508
3614
|
if getattr(self, 'total_frames', 0) == 0:
|
|
@@ -5214,9 +5320,19 @@ class GUI(QMainWindow):
|
|
|
5214
5320
|
cmap_used = cmap_list_imagej[ch % len(cmap_list_imagej)]
|
|
5215
5321
|
self.ax_segmentation.imshow(normalized_image[..., 0], cmap=cmap_used, vmin=0, vmax=1)
|
|
5216
5322
|
|
|
5217
|
-
# Draw
|
|
5323
|
+
# Draw mask overlay (semi-transparent, like Edit tab)
|
|
5218
5324
|
if self.segmentation_mask is not None:
|
|
5219
|
-
|
|
5325
|
+
# Create RGBA overlay with 30% opacity
|
|
5326
|
+
mask_rgba = np.zeros((*self.segmentation_mask.shape, 4), dtype=np.float32)
|
|
5327
|
+
mask_region = self.segmentation_mask > 0
|
|
5328
|
+
mask_rgba[mask_region, 0] = 0.0 # R
|
|
5329
|
+
mask_rgba[mask_region, 1] = 0.8 # G (cyan)
|
|
5330
|
+
mask_rgba[mask_region, 2] = 0.8 # B
|
|
5331
|
+
mask_rgba[mask_region, 3] = 0.3 # Alpha (30% opacity)
|
|
5332
|
+
self.ax_segmentation.imshow(mask_rgba, interpolation='nearest')
|
|
5333
|
+
|
|
5334
|
+
# Draw white contour for clear boundary
|
|
5335
|
+
self.ax_segmentation.contour(self.segmentation_mask, levels=[0.5], colors='white', linewidths=1.5)
|
|
5220
5336
|
|
|
5221
5337
|
# Add axis labels in pixels (helpful for Cellpose diameter estimation)
|
|
5222
5338
|
height, width = image_to_display.shape[:2]
|
|
@@ -5866,8 +5982,11 @@ class GUI(QMainWindow):
|
|
|
5866
5982
|
segmentation_method_tabs (QTabWidget)
|
|
5867
5983
|
use_max_proj_checkbox (QCheckBox)
|
|
5868
5984
|
max_proj_status_label (QLabel)
|
|
5869
|
-
|
|
5870
|
-
|
|
5985
|
+
# Manual segmentation tab widgets:
|
|
5986
|
+
btn_clear_manual_mask (QPushButton): Clear the manual mask
|
|
5987
|
+
manual_status_label (QLabel): Shows current manual mode status
|
|
5988
|
+
manual_instructions_label (QLabel): Rich text instructions
|
|
5989
|
+
cid_manual (int): Matplotlib click handler connection ID
|
|
5871
5990
|
watershed_threshold_slider (QSlider)
|
|
5872
5991
|
watershed_threshold_label (QLabel)
|
|
5873
5992
|
watershed_size_slider (QSlider)
|
|
@@ -5891,8 +6010,7 @@ class GUI(QMainWindow):
|
|
|
5891
6010
|
export_segmentation_image
|
|
5892
6011
|
export_mask_as_tiff
|
|
5893
6012
|
update_segmentation_source
|
|
5894
|
-
|
|
5895
|
-
finish_segmentation
|
|
6013
|
+
clear_manual_mask
|
|
5896
6014
|
update_watershed_threshold_factor
|
|
5897
6015
|
run_watershed_segmentation
|
|
5898
6016
|
_on_segmentation_subtab_changed
|
|
@@ -6016,25 +6134,87 @@ class GUI(QMainWindow):
|
|
|
6016
6134
|
manual_tab_layout.setContentsMargins(10, 10, 10, 10)
|
|
6017
6135
|
manual_tab_layout.setSpacing(10)
|
|
6018
6136
|
|
|
6019
|
-
# Instructions
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6137
|
+
# Instructions panel with rich styling
|
|
6138
|
+
instructions_frame = QFrame()
|
|
6139
|
+
instructions_frame.setStyleSheet("""
|
|
6140
|
+
QFrame {
|
|
6141
|
+
background-color: #2a2a3a;
|
|
6142
|
+
border: 1px solid #444;
|
|
6143
|
+
border-radius: 6px;
|
|
6144
|
+
padding: 8px;
|
|
6145
|
+
}
|
|
6146
|
+
""")
|
|
6147
|
+
instructions_layout = QVBoxLayout(instructions_frame)
|
|
6148
|
+
instructions_layout.setContentsMargins(10, 8, 10, 8)
|
|
6149
|
+
instructions_layout.setSpacing(4)
|
|
6150
|
+
|
|
6151
|
+
self.manual_instructions_label = QLabel(
|
|
6152
|
+
"<b>🔷 Polygon Drawing Workflow:</b><br>"
|
|
6153
|
+
"1. Click on the image to place polygon vertices<br>"
|
|
6154
|
+
"2. Click 'Finish Polygon' to close and fill the mask<br>"
|
|
6155
|
+
"3. Use 'Clear' to start over"
|
|
6024
6156
|
)
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6157
|
+
self.manual_instructions_label.setTextFormat(Qt.RichText)
|
|
6158
|
+
self.manual_instructions_label.setWordWrap(True)
|
|
6159
|
+
self.manual_instructions_label.setStyleSheet("color: #cccccc; font-size: 11px;")
|
|
6160
|
+
instructions_layout.addWidget(self.manual_instructions_label)
|
|
6161
|
+
manual_tab_layout.addWidget(instructions_frame)
|
|
6028
6162
|
|
|
6029
|
-
# Button row
|
|
6163
|
+
# Button row
|
|
6030
6164
|
button_layout = QHBoxLayout()
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
self.
|
|
6035
|
-
self.
|
|
6036
|
-
|
|
6165
|
+
|
|
6166
|
+
# Finish Polygon button
|
|
6167
|
+
self.btn_finish_polygon = QPushButton("✓ Finish Polygon")
|
|
6168
|
+
self.btn_finish_polygon.setToolTip("Close the polygon and create the mask")
|
|
6169
|
+
self.btn_finish_polygon.setStyleSheet("""
|
|
6170
|
+
QPushButton {
|
|
6171
|
+
background-color: #28a745;
|
|
6172
|
+
color: white;
|
|
6173
|
+
border: none;
|
|
6174
|
+
border-radius: 4px;
|
|
6175
|
+
padding: 8px 16px;
|
|
6176
|
+
font-weight: bold;
|
|
6177
|
+
}
|
|
6178
|
+
QPushButton:hover {
|
|
6179
|
+
background-color: #218838;
|
|
6180
|
+
}
|
|
6181
|
+
QPushButton:disabled {
|
|
6182
|
+
background-color: #6c757d;
|
|
6183
|
+
}
|
|
6184
|
+
""")
|
|
6185
|
+
self.btn_finish_polygon.clicked.connect(self.finish_manual_polygon)
|
|
6186
|
+
self.btn_finish_polygon.setEnabled(False) # Disabled until at least 3 points
|
|
6187
|
+
button_layout.addWidget(self.btn_finish_polygon)
|
|
6188
|
+
|
|
6189
|
+
# Clear button
|
|
6190
|
+
self.btn_clear_manual_mask = QPushButton("🗑️ Clear")
|
|
6191
|
+
self.btn_clear_manual_mask.setToolTip("Clear all points and the mask")
|
|
6192
|
+
self.btn_clear_manual_mask.setStyleSheet("""
|
|
6193
|
+
QPushButton {
|
|
6194
|
+
background-color: #dc3545;
|
|
6195
|
+
color: white;
|
|
6196
|
+
border: none;
|
|
6197
|
+
border-radius: 4px;
|
|
6198
|
+
padding: 8px 16px;
|
|
6199
|
+
font-weight: bold;
|
|
6200
|
+
}
|
|
6201
|
+
QPushButton:hover {
|
|
6202
|
+
background-color: #c82333;
|
|
6203
|
+
}
|
|
6204
|
+
""")
|
|
6205
|
+
self.btn_clear_manual_mask.clicked.connect(self.clear_manual_mask)
|
|
6206
|
+
button_layout.addWidget(self.btn_clear_manual_mask)
|
|
6207
|
+
|
|
6037
6208
|
manual_tab_layout.addLayout(button_layout)
|
|
6209
|
+
|
|
6210
|
+
# Status label
|
|
6211
|
+
self.manual_status_label = QLabel("ℹ️ Click on image to place polygon vertices")
|
|
6212
|
+
self.manual_status_label.setStyleSheet("color: #888888; font-size: 11px; font-style: italic;")
|
|
6213
|
+
manual_tab_layout.addWidget(self.manual_status_label)
|
|
6214
|
+
|
|
6215
|
+
# Initialize polygon drawing state
|
|
6216
|
+
self.manual_polygon_points = [] # List of (x, y) tuples
|
|
6217
|
+
|
|
6038
6218
|
manual_tab_layout.addStretch()
|
|
6039
6219
|
|
|
6040
6220
|
# Manual tab will be added last (after Cellpose)
|
|
@@ -6617,16 +6797,24 @@ class GUI(QMainWindow):
|
|
|
6617
6797
|
Tab indices:
|
|
6618
6798
|
0 = Watershed
|
|
6619
6799
|
1 = Cellpose
|
|
6620
|
-
2 = Manual
|
|
6800
|
+
2 = Manual (polygon drawing workflow)
|
|
6621
6801
|
3 = Import (uses Cellpose-style display)
|
|
6622
6802
|
4 = Edit (edit existing masks)
|
|
6623
6803
|
"""
|
|
6624
6804
|
if index == 4: # Edit sub-tab
|
|
6805
|
+
self._exit_manual_mode()
|
|
6625
6806
|
self.enter_edit_mode()
|
|
6807
|
+
elif index == 2: # Manual sub-tab - polygon drawing
|
|
6808
|
+
self.exit_edit_mode()
|
|
6809
|
+
self._enter_manual_mode()
|
|
6810
|
+
self._update_polygon_display()
|
|
6811
|
+
self._update_polygon_status()
|
|
6626
6812
|
elif index == 1 or index == 3: # Cellpose or Import sub-tab
|
|
6813
|
+
self._exit_manual_mode()
|
|
6627
6814
|
self.exit_edit_mode()
|
|
6628
6815
|
self.plot_cellpose_results()
|
|
6629
|
-
else:
|
|
6816
|
+
else: # Watershed (index 0)
|
|
6817
|
+
self._exit_manual_mode()
|
|
6630
6818
|
self.exit_edit_mode()
|
|
6631
6819
|
self.plot_segmentation()
|
|
6632
6820
|
|
|
@@ -6686,14 +6874,17 @@ class GUI(QMainWindow):
|
|
|
6686
6874
|
self.edit_mask_selector.clear()
|
|
6687
6875
|
self.edit_mask_selector.addItem("-- Select Mask --")
|
|
6688
6876
|
|
|
6689
|
-
# Check for Watershed/Manual mask
|
|
6877
|
+
# Check for Watershed/Manual mask - label based on segmentation_mode
|
|
6690
6878
|
if self.segmentation_mask is not None:
|
|
6691
6879
|
n_cells = len(np.unique(self.segmentation_mask)) - 1 # Exclude 0
|
|
6692
6880
|
shape = self.segmentation_mask.shape
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6881
|
+
# Check segmentation_mode to label correctly
|
|
6882
|
+
mode = getattr(self, 'segmentation_mode', 'watershed')
|
|
6883
|
+
if mode == 'manual':
|
|
6884
|
+
label = f"Manual Mask ({n_cells} cells, {shape[1]}×{shape[0]})"
|
|
6885
|
+
else:
|
|
6886
|
+
label = f"Watershed Mask ({n_cells} cells, {shape[1]}×{shape[0]})"
|
|
6887
|
+
self.edit_mask_selector.addItem(label, "watershed")
|
|
6697
6888
|
|
|
6698
6889
|
# Check for Cellpose cytosol mask
|
|
6699
6890
|
if self.cellpose_masks_cyto is not None:
|
|
@@ -6727,6 +6918,12 @@ class GUI(QMainWindow):
|
|
|
6727
6918
|
if self.edit_mask_selector.count() <= 1:
|
|
6728
6919
|
self.edit_status_label.setText("⚠ No masks available - run segmentation first")
|
|
6729
6920
|
self.edit_status_label.setStyleSheet("color: #ff6666;")
|
|
6921
|
+
elif self.edit_mask_selector.count() == 2:
|
|
6922
|
+
# Single-Option Auto-Selection: only one mask available, select it automatically
|
|
6923
|
+
self.edit_mask_selector.setCurrentIndex(1)
|
|
6924
|
+
self.on_edit_mask_selector_changed(1)
|
|
6925
|
+
self.edit_status_label.setText("✓ Single mask auto-selected for editing")
|
|
6926
|
+
self.edit_status_label.setStyleSheet("color: #66ff66;")
|
|
6730
6927
|
else:
|
|
6731
6928
|
self.edit_status_label.setText("⚠ Select a mask to begin editing")
|
|
6732
6929
|
self.edit_status_label.setStyleSheet("color: #ffcc00;")
|
|
@@ -7823,7 +8020,12 @@ class GUI(QMainWindow):
|
|
|
7823
8020
|
self.update_threshold_histogram()
|
|
7824
8021
|
|
|
7825
8022
|
def on_auto_threshold_clicked(self):
|
|
7826
|
-
"""Handle auto-threshold button click - calculate optimal threshold automatically.
|
|
8023
|
+
"""Handle auto-threshold button click - calculate optimal threshold automatically.
|
|
8024
|
+
|
|
8025
|
+
Calculates thresholds on multiple representative frames (beginning, middle, end)
|
|
8026
|
+
and averages them for more robust detection across temporal variability.
|
|
8027
|
+
Handles single-frame movies gracefully by using only the available frame.
|
|
8028
|
+
"""
|
|
7827
8029
|
if self.image_stack is None:
|
|
7828
8030
|
self.statusBar().showMessage("No image loaded")
|
|
7829
8031
|
return
|
|
@@ -7837,14 +8039,10 @@ class GUI(QMainWindow):
|
|
|
7837
8039
|
return
|
|
7838
8040
|
|
|
7839
8041
|
# Show progress
|
|
7840
|
-
self.statusBar().showMessage("Calculating optimal threshold...")
|
|
8042
|
+
self.statusBar().showMessage("Calculating optimal threshold (multi-frame)...")
|
|
7841
8043
|
QApplication.processEvents()
|
|
7842
8044
|
|
|
7843
8045
|
try:
|
|
7844
|
-
# Get current frame's image for this channel
|
|
7845
|
-
# Shape: [Z, Y, X]
|
|
7846
|
-
image_channel = image_to_use[self.current_frame, :, :, :, channel]
|
|
7847
|
-
|
|
7848
8046
|
# Determine if using 3D or 2D mode
|
|
7849
8047
|
use_3d = not self.use_maximum_projection
|
|
7850
8048
|
|
|
@@ -7856,20 +8054,64 @@ class GUI(QMainWindow):
|
|
|
7856
8054
|
yx_spot_size = getattr(self, 'yx_spot_size_in_px', 5)
|
|
7857
8055
|
z_spot_size = getattr(self, 'z_spot_size_in_px', 2)
|
|
7858
8056
|
|
|
7859
|
-
#
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
8057
|
+
# --- Multi-frame threshold calculation ---
|
|
8058
|
+
# Select representative frames: beginning, middle, end
|
|
8059
|
+
total_frames = image_to_use.shape[0]
|
|
8060
|
+
if total_frames <= 1:
|
|
8061
|
+
frame_indices = [0]
|
|
8062
|
+
elif total_frames == 2:
|
|
8063
|
+
frame_indices = [0, 1]
|
|
8064
|
+
else:
|
|
8065
|
+
frame_indices = [0, total_frames // 2, total_frames - 1]
|
|
8066
|
+
|
|
8067
|
+
# Calculate threshold for each representative frame
|
|
8068
|
+
thresholds = []
|
|
8069
|
+
methods = []
|
|
8070
|
+
|
|
8071
|
+
for frame_idx in frame_indices:
|
|
8072
|
+
try:
|
|
8073
|
+
# Extract image for this frame and channel [Z, Y, X]
|
|
8074
|
+
image_channel = image_to_use[frame_idx, :, :, :, channel]
|
|
8075
|
+
|
|
8076
|
+
# Calculate threshold using AutoThreshold class
|
|
8077
|
+
auto_thresh = mi.AutoThreshold(
|
|
8078
|
+
image=image_channel,
|
|
8079
|
+
voxel_size_yx=voxel_yx,
|
|
8080
|
+
voxel_size_z=voxel_z,
|
|
8081
|
+
yx_spot_size_in_px=yx_spot_size,
|
|
8082
|
+
z_spot_size_in_px=z_spot_size,
|
|
8083
|
+
use_3d=use_3d
|
|
8084
|
+
)
|
|
8085
|
+
thresh = auto_thresh.calculate()
|
|
8086
|
+
|
|
8087
|
+
# Only include valid thresholds
|
|
8088
|
+
if thresh is not None and thresh > 0:
|
|
8089
|
+
thresholds.append(thresh)
|
|
8090
|
+
methods.append(auto_thresh.method_used)
|
|
8091
|
+
except Exception as frame_error:
|
|
8092
|
+
# Log but continue with other frames
|
|
8093
|
+
logging.debug(f"Auto-threshold failed for frame {frame_idx}: {frame_error}")
|
|
8094
|
+
continue
|
|
8095
|
+
|
|
8096
|
+
# Check if any valid thresholds were calculated
|
|
8097
|
+
if not thresholds:
|
|
8098
|
+
self.statusBar().showMessage("Auto-threshold failed: no valid frames")
|
|
8099
|
+
return
|
|
7870
8100
|
|
|
7871
|
-
#
|
|
7872
|
-
|
|
8101
|
+
# Compute average threshold across all valid frames
|
|
8102
|
+
threshold_raw = np.mean(thresholds)
|
|
8103
|
+
n_frames_used = len(thresholds)
|
|
8104
|
+
|
|
8105
|
+
# Determine method string for reporting
|
|
8106
|
+
unique_methods = set(methods)
|
|
8107
|
+
if len(unique_methods) == 1:
|
|
8108
|
+
method_used = methods[0]
|
|
8109
|
+
else:
|
|
8110
|
+
# Mixed methods across frames
|
|
8111
|
+
method_used = "multi-frame"
|
|
8112
|
+
|
|
8113
|
+
# Reduce threshold by 30% to improve spot coverage (auto-threshold tends to overestimate)
|
|
8114
|
+
threshold = threshold_raw * 0.7
|
|
7873
8115
|
|
|
7874
8116
|
# Store per-channel
|
|
7875
8117
|
self.auto_threshold_per_channel[channel] = threshold
|
|
@@ -7897,10 +8139,16 @@ class GUI(QMainWindow):
|
|
|
7897
8139
|
# Auto-run single frame detection
|
|
7898
8140
|
self.detect_spots_in_current_frame()
|
|
7899
8141
|
|
|
7900
|
-
# Show result
|
|
7901
|
-
|
|
7902
|
-
|
|
7903
|
-
|
|
8142
|
+
# Show result with frame count info
|
|
8143
|
+
if n_frames_used == 1:
|
|
8144
|
+
self.statusBar().showMessage(
|
|
8145
|
+
f"Auto-threshold Ch{channel}: {int(threshold)} (method: {method_used})"
|
|
8146
|
+
)
|
|
8147
|
+
else:
|
|
8148
|
+
self.statusBar().showMessage(
|
|
8149
|
+
f"Auto-threshold Ch{channel}: {int(threshold)} "
|
|
8150
|
+
f"(avg of {n_frames_used} frames, method: {method_used})"
|
|
8151
|
+
)
|
|
7904
8152
|
|
|
7905
8153
|
except Exception as e:
|
|
7906
8154
|
traceback.print_exc()
|
|
@@ -9257,6 +9505,7 @@ class GUI(QMainWindow):
|
|
|
9257
9505
|
# Clear detection preview (not the multi-channel tracking data)
|
|
9258
9506
|
self.detected_spots_frame = None
|
|
9259
9507
|
self.reset_msd_tab()
|
|
9508
|
+
self.reset_colocalization_tab() # Clear stale colocalization results
|
|
9260
9509
|
self.plot_tracking()
|
|
9261
9510
|
# Get masks for tracking (supports both Cellpose and Segmentation)
|
|
9262
9511
|
masks_complete, masks_nuc, masks_cyto_no_nuc = self._get_tracking_masks()
|
|
@@ -9911,7 +10160,10 @@ class GUI(QMainWindow):
|
|
|
9911
10160
|
self.auto_threshold_btn = QPushButton("Auto")
|
|
9912
10161
|
self.auto_threshold_btn.setFixedWidth(45)
|
|
9913
10162
|
self.auto_threshold_btn.setFixedHeight(20)
|
|
9914
|
-
self.auto_threshold_btn.setToolTip(
|
|
10163
|
+
self.auto_threshold_btn.setToolTip(
|
|
10164
|
+
"Auto-detect optimal threshold\\n"
|
|
10165
|
+
"(averages across beginning, middle, and end frames)"
|
|
10166
|
+
)
|
|
9915
10167
|
self.auto_threshold_btn.setStyleSheet("""
|
|
9916
10168
|
QPushButton {
|
|
9917
10169
|
background-color: #00d4aa;
|
|
@@ -12076,6 +12328,10 @@ class GUI(QMainWindow):
|
|
|
12076
12328
|
|
|
12077
12329
|
# === Reset Manual Verify sub-tab ===
|
|
12078
12330
|
self.reset_manual_colocalization()
|
|
12331
|
+
|
|
12332
|
+
# Reset sub-tab position to Visual (first tab)
|
|
12333
|
+
if hasattr(self, 'coloc_subtabs'):
|
|
12334
|
+
self.coloc_subtabs.setCurrentIndex(0)
|
|
12079
12335
|
|
|
12080
12336
|
def extract_manual_colocalization_data(self, save_df=True):
|
|
12081
12337
|
"""Extract and optionally save manual colocalization data.
|
|
@@ -12662,10 +12918,6 @@ class GUI(QMainWindow):
|
|
|
12662
12918
|
self.verify_visual_populate_button.clicked.connect(self.populate_verify_visual)
|
|
12663
12919
|
top_bar.addWidget(self.verify_visual_populate_button)
|
|
12664
12920
|
|
|
12665
|
-
self.verify_visual_sort_button = QPushButton("Sort")
|
|
12666
|
-
self.verify_visual_sort_button.clicked.connect(self.sort_verify_visual)
|
|
12667
|
-
top_bar.addWidget(self.verify_visual_sort_button)
|
|
12668
|
-
|
|
12669
12921
|
self.verify_visual_cleanup_button = QPushButton("Cleanup")
|
|
12670
12922
|
self.verify_visual_cleanup_button.clicked.connect(self.cleanup_verify_visual)
|
|
12671
12923
|
top_bar.addWidget(self.verify_visual_cleanup_button)
|
|
@@ -12718,10 +12970,6 @@ class GUI(QMainWindow):
|
|
|
12718
12970
|
self.verify_distance_populate_button.clicked.connect(self.populate_verify_distance)
|
|
12719
12971
|
top_bar.addWidget(self.verify_distance_populate_button)
|
|
12720
12972
|
|
|
12721
|
-
self.verify_distance_sort_button = QPushButton("Sort")
|
|
12722
|
-
self.verify_distance_sort_button.clicked.connect(self.sort_verify_distance)
|
|
12723
|
-
top_bar.addWidget(self.verify_distance_sort_button)
|
|
12724
|
-
|
|
12725
12973
|
self.verify_distance_cleanup_button = QPushButton("Cleanup")
|
|
12726
12974
|
self.verify_distance_cleanup_button.clicked.connect(self.cleanup_verify_distance)
|
|
12727
12975
|
top_bar.addWidget(self.verify_distance_cleanup_button)
|
|
@@ -13525,7 +13773,10 @@ class GUI(QMainWindow):
|
|
|
13525
13773
|
# === Verify Visual Subtab Methods ===
|
|
13526
13774
|
|
|
13527
13775
|
def populate_verify_visual(self):
|
|
13528
|
-
"""Populate the Verify Visual subtab with Visual (ML/Intensity) colocalization results.
|
|
13776
|
+
"""Populate the Verify Visual subtab with Visual (ML/Intensity) colocalization results.
|
|
13777
|
+
|
|
13778
|
+
Automatically sorts spots by prediction value (uncertainty-first) for efficient review.
|
|
13779
|
+
"""
|
|
13529
13780
|
if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
|
|
13530
13781
|
QMessageBox.warning(self, "No Results",
|
|
13531
13782
|
"Please run Visual (ML/Intensity) colocalization first.")
|
|
@@ -13538,26 +13789,55 @@ class GUI(QMainWindow):
|
|
|
13538
13789
|
crop_size = results.get('crop_size', 15)
|
|
13539
13790
|
ch1 = results.get('ch1_index', 0)
|
|
13540
13791
|
ch2 = results.get('ch2_index', 1)
|
|
13792
|
+
pred_values = results.get('prediction_values_vector')
|
|
13541
13793
|
|
|
13542
13794
|
if flag_vector is None or mean_crop is None:
|
|
13543
13795
|
QMessageBox.warning(self, "No Data", "Visual colocalization results are incomplete.")
|
|
13544
13796
|
return
|
|
13545
13797
|
|
|
13546
|
-
#
|
|
13798
|
+
# === AUTO-SORT BY PREDICTION VALUE (uncertainty-first) ===
|
|
13799
|
+
num_spots = len(flag_vector)
|
|
13800
|
+
display_crop = mean_crop
|
|
13801
|
+
display_flags = flag_vector
|
|
13802
|
+
|
|
13803
|
+
if pred_values is not None and len(pred_values) == num_spots and num_spots > 0:
|
|
13804
|
+
# Sort ascending by prediction value (lower = more uncertain = review first)
|
|
13805
|
+
sorted_indices = np.argsort(pred_values)
|
|
13806
|
+
|
|
13807
|
+
# Re-order flag vector
|
|
13808
|
+
sorted_flags = np.array([flag_vector[i] for i in sorted_indices])
|
|
13809
|
+
|
|
13810
|
+
# Re-order crops - each spot is crop_size rows in the mean_crop array
|
|
13811
|
+
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
13812
|
+
if num_crop_spots >= num_spots:
|
|
13813
|
+
sorted_crop = np.zeros_like(mean_crop[:num_spots * crop_size])
|
|
13814
|
+
for new_idx, old_idx in enumerate(sorted_indices):
|
|
13815
|
+
if old_idx < num_crop_spots:
|
|
13816
|
+
sorted_crop[new_idx * crop_size:(new_idx + 1) * crop_size] = \
|
|
13817
|
+
mean_crop[old_idx * crop_size:(old_idx + 1) * crop_size]
|
|
13818
|
+
display_crop = sorted_crop
|
|
13819
|
+
display_flags = sorted_flags
|
|
13820
|
+
|
|
13821
|
+
# Store sorted indices for potential export mapping
|
|
13822
|
+
self._verify_visual_sort_indices = sorted_indices
|
|
13823
|
+
self._verify_visual_sorted = True
|
|
13824
|
+
else:
|
|
13825
|
+
# No prediction values available - keep original order
|
|
13826
|
+
self._verify_visual_sorted = False
|
|
13827
|
+
self._verify_visual_sort_indices = None
|
|
13828
|
+
|
|
13829
|
+
# Create spot crops with checkboxes (now in sorted order)
|
|
13547
13830
|
self._create_verification_crops(
|
|
13548
13831
|
scroll_area=self.verify_visual_scroll_area,
|
|
13549
13832
|
checkboxes_list_attr='verify_visual_checkboxes',
|
|
13550
|
-
mean_crop=
|
|
13833
|
+
mean_crop=display_crop,
|
|
13551
13834
|
crop_size=crop_size,
|
|
13552
|
-
flag_vector=
|
|
13835
|
+
flag_vector=display_flags,
|
|
13553
13836
|
stats_label=self.verify_visual_stats_label,
|
|
13554
13837
|
num_channels=2,
|
|
13555
13838
|
channels=(ch1, ch2)
|
|
13556
13839
|
)
|
|
13557
13840
|
|
|
13558
|
-
# Reset sorted flag so Sort button can be used
|
|
13559
|
-
self._verify_visual_sorted = False
|
|
13560
|
-
|
|
13561
13841
|
# Update stats label
|
|
13562
13842
|
self._update_verify_visual_stats()
|
|
13563
13843
|
|
|
@@ -13574,78 +13854,6 @@ class GUI(QMainWindow):
|
|
|
13574
13854
|
f"[{method}] Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
|
|
13575
13855
|
)
|
|
13576
13856
|
|
|
13577
|
-
def sort_verify_visual(self):
|
|
13578
|
-
"""Sort Verify Visual results by prediction value (lowest to highest for review)."""
|
|
13579
|
-
if not hasattr(self, 'verify_visual_checkboxes') or len(self.verify_visual_checkboxes) == 0:
|
|
13580
|
-
QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
|
|
13581
|
-
return
|
|
13582
|
-
|
|
13583
|
-
if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
|
|
13584
|
-
QMessageBox.warning(self, "No Results", "No colocalization results available.")
|
|
13585
|
-
return
|
|
13586
|
-
|
|
13587
|
-
results = self.colocalization_results
|
|
13588
|
-
values = results.get('prediction_values_vector')
|
|
13589
|
-
mean_crop = results.get('mean_crop_filtered')
|
|
13590
|
-
crop_size = results.get('crop_size', 15)
|
|
13591
|
-
flag_vector = results.get('flag_vector')
|
|
13592
|
-
ch1 = results.get('ch1_index', 0)
|
|
13593
|
-
ch2 = results.get('ch2_index', 1)
|
|
13594
|
-
|
|
13595
|
-
if values is None or len(values) == 0:
|
|
13596
|
-
QMessageBox.information(self, "Cannot Sort", "No prediction values available for sorting.")
|
|
13597
|
-
return
|
|
13598
|
-
|
|
13599
|
-
if mean_crop is None:
|
|
13600
|
-
QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
|
|
13601
|
-
return
|
|
13602
|
-
|
|
13603
|
-
# Check if already sorted (compare to original order)
|
|
13604
|
-
if hasattr(self, '_verify_visual_sorted') and self._verify_visual_sorted:
|
|
13605
|
-
QMessageBox.information(self, "Already Sorted", "Spots are already sorted by prediction value.")
|
|
13606
|
-
return
|
|
13607
|
-
|
|
13608
|
-
# Get current checkbox states before sorting
|
|
13609
|
-
current_states = [chk.isChecked() for chk in self.verify_visual_checkboxes]
|
|
13610
|
-
|
|
13611
|
-
# Create sorted indices (ascending by prediction value - uncertain first)
|
|
13612
|
-
num_spots = len(values)
|
|
13613
|
-
sorted_indices = np.argsort(values)
|
|
13614
|
-
|
|
13615
|
-
# Re-order checkbox states to match new sort order
|
|
13616
|
-
sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
|
|
13617
|
-
|
|
13618
|
-
# Re-order crops - each spot is crop_size rows in the mean_crop array
|
|
13619
|
-
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
13620
|
-
if num_crop_spots < num_spots:
|
|
13621
|
-
num_spots = num_crop_spots
|
|
13622
|
-
sorted_indices = sorted_indices[:num_spots]
|
|
13623
|
-
|
|
13624
|
-
sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
|
|
13625
|
-
for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
|
|
13626
|
-
if old_idx < num_crop_spots:
|
|
13627
|
-
sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
|
|
13628
|
-
mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
|
|
13629
|
-
|
|
13630
|
-
# Re-create verification crops with sorted data
|
|
13631
|
-
self._create_verification_crops(
|
|
13632
|
-
scroll_area=self.verify_visual_scroll_area,
|
|
13633
|
-
checkboxes_list_attr='verify_visual_checkboxes',
|
|
13634
|
-
mean_crop=sorted_crop,
|
|
13635
|
-
crop_size=crop_size,
|
|
13636
|
-
flag_vector=sorted_states, # Use previously checked states after reorder
|
|
13637
|
-
stats_label=self.verify_visual_stats_label,
|
|
13638
|
-
num_channels=2,
|
|
13639
|
-
channels=(ch1, ch2)
|
|
13640
|
-
)
|
|
13641
|
-
|
|
13642
|
-
# Mark as sorted
|
|
13643
|
-
self._verify_visual_sorted = True
|
|
13644
|
-
self._verify_visual_sort_indices = sorted_indices
|
|
13645
|
-
|
|
13646
|
-
# Update stats
|
|
13647
|
-
self._update_verify_visual_stats()
|
|
13648
|
-
|
|
13649
13857
|
def cleanup_verify_visual(self):
|
|
13650
13858
|
"""Clear all checkboxes in Verify Visual subtab."""
|
|
13651
13859
|
if not hasattr(self, 'verify_visual_checkboxes'):
|
|
@@ -13870,26 +14078,54 @@ class GUI(QMainWindow):
|
|
|
13870
14078
|
flag_vector.append(False)
|
|
13871
14079
|
distance_values.append(threshold_px * 10.0)
|
|
13872
14080
|
|
|
13873
|
-
# Store for later use
|
|
14081
|
+
# Store for later use
|
|
13874
14082
|
self.verify_distance_mean_crop = mean_crop
|
|
13875
14083
|
self.verify_distance_crop_size = crop_size
|
|
13876
|
-
self.verify_distance_values = np.array(distance_values)
|
|
14084
|
+
self.verify_distance_values = np.array(distance_values)
|
|
14085
|
+
|
|
14086
|
+
# === AUTO-SORT BY DISTANCE VALUE (closest to threshold = most uncertain first) ===
|
|
14087
|
+
display_crop = mean_crop
|
|
14088
|
+
display_flags = flag_vector
|
|
13877
14089
|
|
|
13878
|
-
|
|
14090
|
+
if len(distance_values) == num_spots and num_spots > 0:
|
|
14091
|
+
# Sort ascending by distance (closest to threshold = most uncertain = review first)
|
|
14092
|
+
sorted_indices = np.argsort(distance_values)
|
|
14093
|
+
|
|
14094
|
+
# Re-order flag vector
|
|
14095
|
+
sorted_flags = [flag_vector[i] for i in sorted_indices]
|
|
14096
|
+
|
|
14097
|
+
# Re-order crops
|
|
14098
|
+
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
14099
|
+
if num_crop_spots >= num_spots:
|
|
14100
|
+
sorted_crop = np.zeros_like(mean_crop[:num_spots * crop_size])
|
|
14101
|
+
for new_idx, old_idx in enumerate(sorted_indices):
|
|
14102
|
+
if old_idx < num_crop_spots:
|
|
14103
|
+
sorted_crop[new_idx * crop_size:(new_idx + 1) * crop_size] = \
|
|
14104
|
+
mean_crop[old_idx * crop_size:(old_idx + 1) * crop_size]
|
|
14105
|
+
display_crop = sorted_crop
|
|
14106
|
+
display_flags = sorted_flags
|
|
14107
|
+
|
|
14108
|
+
# Update stored values to match sorted order
|
|
14109
|
+
self.verify_distance_mean_crop = display_crop
|
|
14110
|
+
self.verify_distance_values = np.array(distance_values)[sorted_indices]
|
|
14111
|
+
self._verify_distance_sort_indices = sorted_indices
|
|
14112
|
+
self._verify_distance_sorted = True
|
|
14113
|
+
else:
|
|
14114
|
+
self._verify_distance_sorted = False
|
|
14115
|
+
self._verify_distance_sort_indices = None
|
|
14116
|
+
|
|
14117
|
+
# Create spot crops with checkboxes (now in sorted order)
|
|
13879
14118
|
self._create_verification_crops(
|
|
13880
14119
|
scroll_area=self.verify_distance_scroll_area,
|
|
13881
14120
|
checkboxes_list_attr='verify_distance_checkboxes',
|
|
13882
|
-
mean_crop=
|
|
14121
|
+
mean_crop=display_crop,
|
|
13883
14122
|
crop_size=crop_size,
|
|
13884
|
-
flag_vector=
|
|
14123
|
+
flag_vector=display_flags,
|
|
13885
14124
|
stats_label=self.verify_distance_stats_label,
|
|
13886
14125
|
num_channels=image.shape[-1] if image.ndim == 5 else 1,
|
|
13887
14126
|
channels=(ch0, ch1)
|
|
13888
14127
|
)
|
|
13889
14128
|
|
|
13890
|
-
# Reset sorted flag so Sort button can be used
|
|
13891
|
-
self._verify_distance_sorted = False
|
|
13892
|
-
|
|
13893
14129
|
# Update stats label
|
|
13894
14130
|
self._update_verify_distance_stats()
|
|
13895
14131
|
|
|
@@ -13913,90 +14149,6 @@ class GUI(QMainWindow):
|
|
|
13913
14149
|
f"Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
|
|
13914
14150
|
)
|
|
13915
14151
|
|
|
13916
|
-
def sort_verify_distance(self):
|
|
13917
|
-
"""Sort Verify Distance results by distance value (ascending - closest to threshold first).
|
|
13918
|
-
|
|
13919
|
-
Similar to Visual method's certainty-based sorting, but uses the measured
|
|
13920
|
-
distance to nearest partner. Spots with distances closest to the colocalization
|
|
13921
|
-
threshold are shown first as they represent the most uncertain classifications.
|
|
13922
|
-
"""
|
|
13923
|
-
if not hasattr(self, 'verify_distance_checkboxes') or len(self.verify_distance_checkboxes) == 0:
|
|
13924
|
-
QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
|
|
13925
|
-
return
|
|
13926
|
-
|
|
13927
|
-
if not hasattr(self, 'verify_distance_mean_crop') or self.verify_distance_mean_crop is None:
|
|
13928
|
-
QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
|
|
13929
|
-
return
|
|
13930
|
-
|
|
13931
|
-
# Check if distance values are available
|
|
13932
|
-
if not hasattr(self, 'verify_distance_values') or self.verify_distance_values is None:
|
|
13933
|
-
QMessageBox.warning(self, "No Distance Data",
|
|
13934
|
-
"Distance values not available. Please re-run Populate.")
|
|
13935
|
-
return
|
|
13936
|
-
|
|
13937
|
-
# Check if already sorted
|
|
13938
|
-
if hasattr(self, '_verify_distance_sorted') and self._verify_distance_sorted:
|
|
13939
|
-
QMessageBox.information(self, "Already Sorted", "Spots are already sorted by distance value.")
|
|
13940
|
-
return
|
|
13941
|
-
|
|
13942
|
-
mean_crop = self.verify_distance_mean_crop
|
|
13943
|
-
crop_size = self.verify_distance_crop_size
|
|
13944
|
-
distance_values = self.verify_distance_values
|
|
13945
|
-
|
|
13946
|
-
# Get current checkbox states before sorting
|
|
13947
|
-
current_states = [chk.isChecked() for chk in self.verify_distance_checkboxes]
|
|
13948
|
-
num_spots = len(current_states)
|
|
13949
|
-
|
|
13950
|
-
# Sort ascending by distance (closest to threshold = most uncertain first)
|
|
13951
|
-
# This matches the Visual method's approach of showing uncertain cases first
|
|
13952
|
-
sorted_indices = np.argsort(distance_values)
|
|
13953
|
-
|
|
13954
|
-
# Re-order states and distances
|
|
13955
|
-
sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
|
|
13956
|
-
sorted_distances = distance_values[sorted_indices]
|
|
13957
|
-
|
|
13958
|
-
# Re-order crops
|
|
13959
|
-
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
13960
|
-
if num_crop_spots < num_spots:
|
|
13961
|
-
num_spots = num_crop_spots
|
|
13962
|
-
sorted_indices = sorted_indices[:num_spots]
|
|
13963
|
-
|
|
13964
|
-
sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
|
|
13965
|
-
for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
|
|
13966
|
-
if old_idx < num_crop_spots:
|
|
13967
|
-
sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
|
|
13968
|
-
mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
|
|
13969
|
-
|
|
13970
|
-
# Get channels from distance results
|
|
13971
|
-
results = self.distance_coloc_results if hasattr(self, 'distance_coloc_results') else {}
|
|
13972
|
-
ch0 = results.get('channel_0', 0)
|
|
13973
|
-
ch1 = results.get('channel_1', 1)
|
|
13974
|
-
image = self.corrected_image if self.corrected_image is not None else self.image_stack
|
|
13975
|
-
num_channels = image.shape[-1] if image is not None and image.ndim == 5 else 1
|
|
13976
|
-
|
|
13977
|
-
# Re-create verification crops with sorted data
|
|
13978
|
-
self._create_verification_crops(
|
|
13979
|
-
scroll_area=self.verify_distance_scroll_area,
|
|
13980
|
-
checkboxes_list_attr='verify_distance_checkboxes',
|
|
13981
|
-
mean_crop=sorted_crop,
|
|
13982
|
-
crop_size=crop_size,
|
|
13983
|
-
flag_vector=sorted_states,
|
|
13984
|
-
stats_label=self.verify_distance_stats_label,
|
|
13985
|
-
num_channels=num_channels,
|
|
13986
|
-
channels=(ch0, ch1)
|
|
13987
|
-
)
|
|
13988
|
-
|
|
13989
|
-
# Update stored data after sorting for consistency
|
|
13990
|
-
self.verify_distance_mean_crop = sorted_crop
|
|
13991
|
-
self.verify_distance_values = sorted_distances
|
|
13992
|
-
self._verify_distance_sort_indices = sorted_indices # Store for reference
|
|
13993
|
-
|
|
13994
|
-
# Mark as sorted
|
|
13995
|
-
self._verify_distance_sorted = True
|
|
13996
|
-
|
|
13997
|
-
# Update stats
|
|
13998
|
-
self._update_verify_distance_stats()
|
|
13999
|
-
|
|
14000
14152
|
def cleanup_verify_distance(self):
|
|
14001
14153
|
"""Clear all checkboxes in Verify Distance subtab."""
|
|
14002
14154
|
if not hasattr(self, 'verify_distance_checkboxes'):
|
|
@@ -14111,8 +14263,8 @@ class GUI(QMainWindow):
|
|
|
14111
14263
|
scroll_area.setWidget(container)
|
|
14112
14264
|
setattr(self, checkboxes_list_attr, checkboxes)
|
|
14113
14265
|
|
|
14114
|
-
# Note: sort_manual_colocalization() removed
|
|
14115
|
-
#
|
|
14266
|
+
# Note: sort_manual_colocalization() removed. sort_verify_visual() and
|
|
14267
|
+
# sort_verify_distance() merged into their respective populate_*() functions.
|
|
14116
14268
|
|
|
14117
14269
|
|
|
14118
14270
|
# =============================================================================
|
|
@@ -16126,6 +16278,21 @@ class GUI(QMainWindow):
|
|
|
16126
16278
|
self.edit_mask_selector.blockSignals(False)
|
|
16127
16279
|
if hasattr(self, 'edit_instructions_group'):
|
|
16128
16280
|
self.edit_instructions_group.setVisible(False)
|
|
16281
|
+
|
|
16282
|
+
# Reset Manual segmentation mode state
|
|
16283
|
+
if hasattr(self, 'cid_manual') and self.cid_manual is not None:
|
|
16284
|
+
try:
|
|
16285
|
+
self.canvas_segmentation.mpl_disconnect(self.cid_manual)
|
|
16286
|
+
except Exception:
|
|
16287
|
+
pass
|
|
16288
|
+
self.cid_manual = None
|
|
16289
|
+
# Clear polygon points
|
|
16290
|
+
self.manual_polygon_points = []
|
|
16291
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
16292
|
+
self.btn_finish_polygon.setEnabled(False)
|
|
16293
|
+
if hasattr(self, 'manual_status_label'):
|
|
16294
|
+
self.manual_status_label.setText("ℹ️ Click on image to place polygon vertices")
|
|
16295
|
+
self.manual_status_label.setStyleSheet("color: #888888; font-style: italic;")
|
|
16129
16296
|
|
|
16130
16297
|
def reset_photobleaching_tab(self):
|
|
16131
16298
|
self.figure_photobleaching.clear()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|