microlive 1.0.23__py3-none-any.whl → 1.0.24__py3-none-any.whl
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/__init__.py +1 -1
- microlive/gui/app.py +514 -351
- {microlive-1.0.23.dist-info → microlive-1.0.24.dist-info}/METADATA +1 -1
- {microlive-1.0.23.dist-info → microlive-1.0.24.dist-info}/RECORD +7 -7
- {microlive-1.0.23.dist-info → microlive-1.0.24.dist-info}/WHEEL +0 -0
- {microlive-1.0.23.dist-info → microlive-1.0.24.dist-info}/entry_points.txt +0 -0
- {microlive-1.0.23.dist-info → microlive-1.0.24.dist-info}/licenses/LICENSE +0 -0
microlive/__init__.py
CHANGED
|
@@ -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)
|
microlive/gui/app.py
CHANGED
|
@@ -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:
|
|
@@ -6728,9 +6919,11 @@ class GUI(QMainWindow):
|
|
|
6728
6919
|
self.edit_status_label.setText("⚠ No masks available - run segmentation first")
|
|
6729
6920
|
self.edit_status_label.setStyleSheet("color: #ff6666;")
|
|
6730
6921
|
elif self.edit_mask_selector.count() == 2:
|
|
6731
|
-
# Auto-
|
|
6922
|
+
# Single-Option Auto-Selection: only one mask available, select it automatically
|
|
6732
6923
|
self.edit_mask_selector.setCurrentIndex(1)
|
|
6733
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;")
|
|
6734
6927
|
else:
|
|
6735
6928
|
self.edit_status_label.setText("⚠ Select a mask to begin editing")
|
|
6736
6929
|
self.edit_status_label.setStyleSheet("color: #ffcc00;")
|
|
@@ -7827,7 +8020,12 @@ class GUI(QMainWindow):
|
|
|
7827
8020
|
self.update_threshold_histogram()
|
|
7828
8021
|
|
|
7829
8022
|
def on_auto_threshold_clicked(self):
|
|
7830
|
-
"""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
|
+
"""
|
|
7831
8029
|
if self.image_stack is None:
|
|
7832
8030
|
self.statusBar().showMessage("No image loaded")
|
|
7833
8031
|
return
|
|
@@ -7841,14 +8039,10 @@ class GUI(QMainWindow):
|
|
|
7841
8039
|
return
|
|
7842
8040
|
|
|
7843
8041
|
# Show progress
|
|
7844
|
-
self.statusBar().showMessage("Calculating optimal threshold...")
|
|
8042
|
+
self.statusBar().showMessage("Calculating optimal threshold (multi-frame)...")
|
|
7845
8043
|
QApplication.processEvents()
|
|
7846
8044
|
|
|
7847
8045
|
try:
|
|
7848
|
-
# Get current frame's image for this channel
|
|
7849
|
-
# Shape: [Z, Y, X]
|
|
7850
|
-
image_channel = image_to_use[self.current_frame, :, :, :, channel]
|
|
7851
|
-
|
|
7852
8046
|
# Determine if using 3D or 2D mode
|
|
7853
8047
|
use_3d = not self.use_maximum_projection
|
|
7854
8048
|
|
|
@@ -7860,20 +8054,64 @@ class GUI(QMainWindow):
|
|
|
7860
8054
|
yx_spot_size = getattr(self, 'yx_spot_size_in_px', 5)
|
|
7861
8055
|
z_spot_size = getattr(self, 'z_spot_size_in_px', 2)
|
|
7862
8056
|
|
|
7863
|
-
#
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
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
|
|
7874
8100
|
|
|
7875
|
-
#
|
|
7876
|
-
|
|
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
|
|
7877
8115
|
|
|
7878
8116
|
# Store per-channel
|
|
7879
8117
|
self.auto_threshold_per_channel[channel] = threshold
|
|
@@ -7901,10 +8139,16 @@ class GUI(QMainWindow):
|
|
|
7901
8139
|
# Auto-run single frame detection
|
|
7902
8140
|
self.detect_spots_in_current_frame()
|
|
7903
8141
|
|
|
7904
|
-
# Show result
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
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
|
+
)
|
|
7908
8152
|
|
|
7909
8153
|
except Exception as e:
|
|
7910
8154
|
traceback.print_exc()
|
|
@@ -9261,6 +9505,7 @@ class GUI(QMainWindow):
|
|
|
9261
9505
|
# Clear detection preview (not the multi-channel tracking data)
|
|
9262
9506
|
self.detected_spots_frame = None
|
|
9263
9507
|
self.reset_msd_tab()
|
|
9508
|
+
self.reset_colocalization_tab() # Clear stale colocalization results
|
|
9264
9509
|
self.plot_tracking()
|
|
9265
9510
|
# Get masks for tracking (supports both Cellpose and Segmentation)
|
|
9266
9511
|
masks_complete, masks_nuc, masks_cyto_no_nuc = self._get_tracking_masks()
|
|
@@ -9915,7 +10160,10 @@ class GUI(QMainWindow):
|
|
|
9915
10160
|
self.auto_threshold_btn = QPushButton("Auto")
|
|
9916
10161
|
self.auto_threshold_btn.setFixedWidth(45)
|
|
9917
10162
|
self.auto_threshold_btn.setFixedHeight(20)
|
|
9918
|
-
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
|
+
)
|
|
9919
10167
|
self.auto_threshold_btn.setStyleSheet("""
|
|
9920
10168
|
QPushButton {
|
|
9921
10169
|
background-color: #00d4aa;
|
|
@@ -12080,6 +12328,10 @@ class GUI(QMainWindow):
|
|
|
12080
12328
|
|
|
12081
12329
|
# === Reset Manual Verify sub-tab ===
|
|
12082
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)
|
|
12083
12335
|
|
|
12084
12336
|
def extract_manual_colocalization_data(self, save_df=True):
|
|
12085
12337
|
"""Extract and optionally save manual colocalization data.
|
|
@@ -12666,10 +12918,6 @@ class GUI(QMainWindow):
|
|
|
12666
12918
|
self.verify_visual_populate_button.clicked.connect(self.populate_verify_visual)
|
|
12667
12919
|
top_bar.addWidget(self.verify_visual_populate_button)
|
|
12668
12920
|
|
|
12669
|
-
self.verify_visual_sort_button = QPushButton("Sort")
|
|
12670
|
-
self.verify_visual_sort_button.clicked.connect(self.sort_verify_visual)
|
|
12671
|
-
top_bar.addWidget(self.verify_visual_sort_button)
|
|
12672
|
-
|
|
12673
12921
|
self.verify_visual_cleanup_button = QPushButton("Cleanup")
|
|
12674
12922
|
self.verify_visual_cleanup_button.clicked.connect(self.cleanup_verify_visual)
|
|
12675
12923
|
top_bar.addWidget(self.verify_visual_cleanup_button)
|
|
@@ -12722,10 +12970,6 @@ class GUI(QMainWindow):
|
|
|
12722
12970
|
self.verify_distance_populate_button.clicked.connect(self.populate_verify_distance)
|
|
12723
12971
|
top_bar.addWidget(self.verify_distance_populate_button)
|
|
12724
12972
|
|
|
12725
|
-
self.verify_distance_sort_button = QPushButton("Sort")
|
|
12726
|
-
self.verify_distance_sort_button.clicked.connect(self.sort_verify_distance)
|
|
12727
|
-
top_bar.addWidget(self.verify_distance_sort_button)
|
|
12728
|
-
|
|
12729
12973
|
self.verify_distance_cleanup_button = QPushButton("Cleanup")
|
|
12730
12974
|
self.verify_distance_cleanup_button.clicked.connect(self.cleanup_verify_distance)
|
|
12731
12975
|
top_bar.addWidget(self.verify_distance_cleanup_button)
|
|
@@ -13529,7 +13773,10 @@ class GUI(QMainWindow):
|
|
|
13529
13773
|
# === Verify Visual Subtab Methods ===
|
|
13530
13774
|
|
|
13531
13775
|
def populate_verify_visual(self):
|
|
13532
|
-
"""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
|
+
"""
|
|
13533
13780
|
if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
|
|
13534
13781
|
QMessageBox.warning(self, "No Results",
|
|
13535
13782
|
"Please run Visual (ML/Intensity) colocalization first.")
|
|
@@ -13542,26 +13789,55 @@ class GUI(QMainWindow):
|
|
|
13542
13789
|
crop_size = results.get('crop_size', 15)
|
|
13543
13790
|
ch1 = results.get('ch1_index', 0)
|
|
13544
13791
|
ch2 = results.get('ch2_index', 1)
|
|
13792
|
+
pred_values = results.get('prediction_values_vector')
|
|
13545
13793
|
|
|
13546
13794
|
if flag_vector is None or mean_crop is None:
|
|
13547
13795
|
QMessageBox.warning(self, "No Data", "Visual colocalization results are incomplete.")
|
|
13548
13796
|
return
|
|
13549
13797
|
|
|
13550
|
-
#
|
|
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)
|
|
13551
13830
|
self._create_verification_crops(
|
|
13552
13831
|
scroll_area=self.verify_visual_scroll_area,
|
|
13553
13832
|
checkboxes_list_attr='verify_visual_checkboxes',
|
|
13554
|
-
mean_crop=
|
|
13833
|
+
mean_crop=display_crop,
|
|
13555
13834
|
crop_size=crop_size,
|
|
13556
|
-
flag_vector=
|
|
13835
|
+
flag_vector=display_flags,
|
|
13557
13836
|
stats_label=self.verify_visual_stats_label,
|
|
13558
13837
|
num_channels=2,
|
|
13559
13838
|
channels=(ch1, ch2)
|
|
13560
13839
|
)
|
|
13561
13840
|
|
|
13562
|
-
# Reset sorted flag so Sort button can be used
|
|
13563
|
-
self._verify_visual_sorted = False
|
|
13564
|
-
|
|
13565
13841
|
# Update stats label
|
|
13566
13842
|
self._update_verify_visual_stats()
|
|
13567
13843
|
|
|
@@ -13578,78 +13854,6 @@ class GUI(QMainWindow):
|
|
|
13578
13854
|
f"[{method}] Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
|
|
13579
13855
|
)
|
|
13580
13856
|
|
|
13581
|
-
def sort_verify_visual(self):
|
|
13582
|
-
"""Sort Verify Visual results by prediction value (lowest to highest for review)."""
|
|
13583
|
-
if not hasattr(self, 'verify_visual_checkboxes') or len(self.verify_visual_checkboxes) == 0:
|
|
13584
|
-
QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
|
|
13585
|
-
return
|
|
13586
|
-
|
|
13587
|
-
if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
|
|
13588
|
-
QMessageBox.warning(self, "No Results", "No colocalization results available.")
|
|
13589
|
-
return
|
|
13590
|
-
|
|
13591
|
-
results = self.colocalization_results
|
|
13592
|
-
values = results.get('prediction_values_vector')
|
|
13593
|
-
mean_crop = results.get('mean_crop_filtered')
|
|
13594
|
-
crop_size = results.get('crop_size', 15)
|
|
13595
|
-
flag_vector = results.get('flag_vector')
|
|
13596
|
-
ch1 = results.get('ch1_index', 0)
|
|
13597
|
-
ch2 = results.get('ch2_index', 1)
|
|
13598
|
-
|
|
13599
|
-
if values is None or len(values) == 0:
|
|
13600
|
-
QMessageBox.information(self, "Cannot Sort", "No prediction values available for sorting.")
|
|
13601
|
-
return
|
|
13602
|
-
|
|
13603
|
-
if mean_crop is None:
|
|
13604
|
-
QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
|
|
13605
|
-
return
|
|
13606
|
-
|
|
13607
|
-
# Check if already sorted (compare to original order)
|
|
13608
|
-
if hasattr(self, '_verify_visual_sorted') and self._verify_visual_sorted:
|
|
13609
|
-
QMessageBox.information(self, "Already Sorted", "Spots are already sorted by prediction value.")
|
|
13610
|
-
return
|
|
13611
|
-
|
|
13612
|
-
# Get current checkbox states before sorting
|
|
13613
|
-
current_states = [chk.isChecked() for chk in self.verify_visual_checkboxes]
|
|
13614
|
-
|
|
13615
|
-
# Create sorted indices (ascending by prediction value - uncertain first)
|
|
13616
|
-
num_spots = len(values)
|
|
13617
|
-
sorted_indices = np.argsort(values)
|
|
13618
|
-
|
|
13619
|
-
# Re-order checkbox states to match new sort order
|
|
13620
|
-
sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
|
|
13621
|
-
|
|
13622
|
-
# Re-order crops - each spot is crop_size rows in the mean_crop array
|
|
13623
|
-
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
13624
|
-
if num_crop_spots < num_spots:
|
|
13625
|
-
num_spots = num_crop_spots
|
|
13626
|
-
sorted_indices = sorted_indices[:num_spots]
|
|
13627
|
-
|
|
13628
|
-
sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
|
|
13629
|
-
for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
|
|
13630
|
-
if old_idx < num_crop_spots:
|
|
13631
|
-
sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
|
|
13632
|
-
mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
|
|
13633
|
-
|
|
13634
|
-
# Re-create verification crops with sorted data
|
|
13635
|
-
self._create_verification_crops(
|
|
13636
|
-
scroll_area=self.verify_visual_scroll_area,
|
|
13637
|
-
checkboxes_list_attr='verify_visual_checkboxes',
|
|
13638
|
-
mean_crop=sorted_crop,
|
|
13639
|
-
crop_size=crop_size,
|
|
13640
|
-
flag_vector=sorted_states, # Use previously checked states after reorder
|
|
13641
|
-
stats_label=self.verify_visual_stats_label,
|
|
13642
|
-
num_channels=2,
|
|
13643
|
-
channels=(ch1, ch2)
|
|
13644
|
-
)
|
|
13645
|
-
|
|
13646
|
-
# Mark as sorted
|
|
13647
|
-
self._verify_visual_sorted = True
|
|
13648
|
-
self._verify_visual_sort_indices = sorted_indices
|
|
13649
|
-
|
|
13650
|
-
# Update stats
|
|
13651
|
-
self._update_verify_visual_stats()
|
|
13652
|
-
|
|
13653
13857
|
def cleanup_verify_visual(self):
|
|
13654
13858
|
"""Clear all checkboxes in Verify Visual subtab."""
|
|
13655
13859
|
if not hasattr(self, 'verify_visual_checkboxes'):
|
|
@@ -13874,26 +14078,54 @@ class GUI(QMainWindow):
|
|
|
13874
14078
|
flag_vector.append(False)
|
|
13875
14079
|
distance_values.append(threshold_px * 10.0)
|
|
13876
14080
|
|
|
13877
|
-
# Store for later use
|
|
14081
|
+
# Store for later use
|
|
13878
14082
|
self.verify_distance_mean_crop = mean_crop
|
|
13879
14083
|
self.verify_distance_crop_size = crop_size
|
|
13880
|
-
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
|
|
13881
14089
|
|
|
13882
|
-
|
|
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)
|
|
13883
14118
|
self._create_verification_crops(
|
|
13884
14119
|
scroll_area=self.verify_distance_scroll_area,
|
|
13885
14120
|
checkboxes_list_attr='verify_distance_checkboxes',
|
|
13886
|
-
mean_crop=
|
|
14121
|
+
mean_crop=display_crop,
|
|
13887
14122
|
crop_size=crop_size,
|
|
13888
|
-
flag_vector=
|
|
14123
|
+
flag_vector=display_flags,
|
|
13889
14124
|
stats_label=self.verify_distance_stats_label,
|
|
13890
14125
|
num_channels=image.shape[-1] if image.ndim == 5 else 1,
|
|
13891
14126
|
channels=(ch0, ch1)
|
|
13892
14127
|
)
|
|
13893
14128
|
|
|
13894
|
-
# Reset sorted flag so Sort button can be used
|
|
13895
|
-
self._verify_distance_sorted = False
|
|
13896
|
-
|
|
13897
14129
|
# Update stats label
|
|
13898
14130
|
self._update_verify_distance_stats()
|
|
13899
14131
|
|
|
@@ -13917,90 +14149,6 @@ class GUI(QMainWindow):
|
|
|
13917
14149
|
f"Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
|
|
13918
14150
|
)
|
|
13919
14151
|
|
|
13920
|
-
def sort_verify_distance(self):
|
|
13921
|
-
"""Sort Verify Distance results by distance value (ascending - closest to threshold first).
|
|
13922
|
-
|
|
13923
|
-
Similar to Visual method's certainty-based sorting, but uses the measured
|
|
13924
|
-
distance to nearest partner. Spots with distances closest to the colocalization
|
|
13925
|
-
threshold are shown first as they represent the most uncertain classifications.
|
|
13926
|
-
"""
|
|
13927
|
-
if not hasattr(self, 'verify_distance_checkboxes') or len(self.verify_distance_checkboxes) == 0:
|
|
13928
|
-
QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
|
|
13929
|
-
return
|
|
13930
|
-
|
|
13931
|
-
if not hasattr(self, 'verify_distance_mean_crop') or self.verify_distance_mean_crop is None:
|
|
13932
|
-
QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
|
|
13933
|
-
return
|
|
13934
|
-
|
|
13935
|
-
# Check if distance values are available
|
|
13936
|
-
if not hasattr(self, 'verify_distance_values') or self.verify_distance_values is None:
|
|
13937
|
-
QMessageBox.warning(self, "No Distance Data",
|
|
13938
|
-
"Distance values not available. Please re-run Populate.")
|
|
13939
|
-
return
|
|
13940
|
-
|
|
13941
|
-
# Check if already sorted
|
|
13942
|
-
if hasattr(self, '_verify_distance_sorted') and self._verify_distance_sorted:
|
|
13943
|
-
QMessageBox.information(self, "Already Sorted", "Spots are already sorted by distance value.")
|
|
13944
|
-
return
|
|
13945
|
-
|
|
13946
|
-
mean_crop = self.verify_distance_mean_crop
|
|
13947
|
-
crop_size = self.verify_distance_crop_size
|
|
13948
|
-
distance_values = self.verify_distance_values
|
|
13949
|
-
|
|
13950
|
-
# Get current checkbox states before sorting
|
|
13951
|
-
current_states = [chk.isChecked() for chk in self.verify_distance_checkboxes]
|
|
13952
|
-
num_spots = len(current_states)
|
|
13953
|
-
|
|
13954
|
-
# Sort ascending by distance (closest to threshold = most uncertain first)
|
|
13955
|
-
# This matches the Visual method's approach of showing uncertain cases first
|
|
13956
|
-
sorted_indices = np.argsort(distance_values)
|
|
13957
|
-
|
|
13958
|
-
# Re-order states and distances
|
|
13959
|
-
sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
|
|
13960
|
-
sorted_distances = distance_values[sorted_indices]
|
|
13961
|
-
|
|
13962
|
-
# Re-order crops
|
|
13963
|
-
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
13964
|
-
if num_crop_spots < num_spots:
|
|
13965
|
-
num_spots = num_crop_spots
|
|
13966
|
-
sorted_indices = sorted_indices[:num_spots]
|
|
13967
|
-
|
|
13968
|
-
sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
|
|
13969
|
-
for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
|
|
13970
|
-
if old_idx < num_crop_spots:
|
|
13971
|
-
sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
|
|
13972
|
-
mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
|
|
13973
|
-
|
|
13974
|
-
# Get channels from distance results
|
|
13975
|
-
results = self.distance_coloc_results if hasattr(self, 'distance_coloc_results') else {}
|
|
13976
|
-
ch0 = results.get('channel_0', 0)
|
|
13977
|
-
ch1 = results.get('channel_1', 1)
|
|
13978
|
-
image = self.corrected_image if self.corrected_image is not None else self.image_stack
|
|
13979
|
-
num_channels = image.shape[-1] if image is not None and image.ndim == 5 else 1
|
|
13980
|
-
|
|
13981
|
-
# Re-create verification crops with sorted data
|
|
13982
|
-
self._create_verification_crops(
|
|
13983
|
-
scroll_area=self.verify_distance_scroll_area,
|
|
13984
|
-
checkboxes_list_attr='verify_distance_checkboxes',
|
|
13985
|
-
mean_crop=sorted_crop,
|
|
13986
|
-
crop_size=crop_size,
|
|
13987
|
-
flag_vector=sorted_states,
|
|
13988
|
-
stats_label=self.verify_distance_stats_label,
|
|
13989
|
-
num_channels=num_channels,
|
|
13990
|
-
channels=(ch0, ch1)
|
|
13991
|
-
)
|
|
13992
|
-
|
|
13993
|
-
# Update stored data after sorting for consistency
|
|
13994
|
-
self.verify_distance_mean_crop = sorted_crop
|
|
13995
|
-
self.verify_distance_values = sorted_distances
|
|
13996
|
-
self._verify_distance_sort_indices = sorted_indices # Store for reference
|
|
13997
|
-
|
|
13998
|
-
# Mark as sorted
|
|
13999
|
-
self._verify_distance_sorted = True
|
|
14000
|
-
|
|
14001
|
-
# Update stats
|
|
14002
|
-
self._update_verify_distance_stats()
|
|
14003
|
-
|
|
14004
14152
|
def cleanup_verify_distance(self):
|
|
14005
14153
|
"""Clear all checkboxes in Verify Distance subtab."""
|
|
14006
14154
|
if not hasattr(self, 'verify_distance_checkboxes'):
|
|
@@ -14115,8 +14263,8 @@ class GUI(QMainWindow):
|
|
|
14115
14263
|
scroll_area.setWidget(container)
|
|
14116
14264
|
setattr(self, checkboxes_list_attr, checkboxes)
|
|
14117
14265
|
|
|
14118
|
-
# Note: sort_manual_colocalization() removed
|
|
14119
|
-
#
|
|
14266
|
+
# Note: sort_manual_colocalization() removed. sort_verify_visual() and
|
|
14267
|
+
# sort_verify_distance() merged into their respective populate_*() functions.
|
|
14120
14268
|
|
|
14121
14269
|
|
|
14122
14270
|
# =============================================================================
|
|
@@ -16130,6 +16278,21 @@ class GUI(QMainWindow):
|
|
|
16130
16278
|
self.edit_mask_selector.blockSignals(False)
|
|
16131
16279
|
if hasattr(self, 'edit_instructions_group'):
|
|
16132
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;")
|
|
16133
16296
|
|
|
16134
16297
|
def reset_photobleaching_tab(self):
|
|
16135
16298
|
self.figure_photobleaching.clear()
|
|
@@ -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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
microlive/__init__.py,sha256=-
|
|
1
|
+
microlive/__init__.py,sha256=-BqBR-Le9SXhvk8fhrUAhTe47nWfrmKCJCDz57tPXYA,1385
|
|
2
2
|
microlive/imports.py,sha256=wMJNmtG06joCJNPryktCwEKz1HCJhfGcm3et3boINuc,7676
|
|
3
3
|
microlive/microscopy.py,sha256=OFqf0JXJW4-2cLHvXnwwp_SfMFsUXwp5lDKbkCRR4ok,710841
|
|
4
4
|
microlive/ml_spot_detection.py,sha256=pVbOSGNJ0WWMuPRML42rFwvjKVZ0B1fJux1179OIbAg,10603
|
|
@@ -7,7 +7,7 @@ microlive/data/icons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
7
7
|
microlive/data/icons/icon_micro.png,sha256=b5tFv4E6vUmLwYmYeM4PJuxLV_XqEzN14ueolekTFW0,370236
|
|
8
8
|
microlive/data/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
microlive/gui/__init__.py,sha256=tB-CdDC7x5OwYFAQxLOUvfVnUThaXKXVRsB68YP0Y6Q,28
|
|
10
|
-
microlive/gui/app.py,sha256=
|
|
10
|
+
microlive/gui/app.py,sha256=bwNg8bSW3a56i8BEVMr8zOx-lzYcP89SxBV9qXV15fk,855606
|
|
11
11
|
microlive/gui/main.py,sha256=b66W_2V-pclGKOozfs75pwrCGbL_jkVU3kFt8RFMZIc,2520
|
|
12
12
|
microlive/gui/micro_mac.command,sha256=TkxYOO_5A2AiNJMz3_--1geBYfl77THpOLFZnV4J2ac,444
|
|
13
13
|
microlive/gui/micro_windows.bat,sha256=DJUKPhDbCO4HToLwSMT-QTYRe9Kr1wn5A2Ijy2klIrw,773
|
|
@@ -20,8 +20,8 @@ microlive/utils/device.py,sha256=tcPMU8UiXL-DuGwhudUgrbjW1lgIK_EUKIOeOn0U6q4,253
|
|
|
20
20
|
microlive/utils/model_downloader.py,sha256=EruviTEh75YBekpznn1RZ1Nj8lnDmeC4TKEnFLOow6Y,9448
|
|
21
21
|
microlive/utils/resources.py,sha256=Jz7kPI75xMLCBJMyX7Y_3ixKi_UgydfQkF0BlFtLCKs,1753
|
|
22
22
|
microlive/data/models/spot_detection_cnn.pth,sha256=Np7vpPJIbKQmuKY0Hx-4IkeEDsnks_QEgs7TqaYgZmI,8468580
|
|
23
|
-
microlive-1.0.
|
|
24
|
-
microlive-1.0.
|
|
25
|
-
microlive-1.0.
|
|
26
|
-
microlive-1.0.
|
|
27
|
-
microlive-1.0.
|
|
23
|
+
microlive-1.0.24.dist-info/METADATA,sha256=xmdVi_Pg0extJsEQ3psI4A1etnH_0w_jO1GalN2IJYI,12462
|
|
24
|
+
microlive-1.0.24.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
25
|
+
microlive-1.0.24.dist-info/entry_points.txt,sha256=Zqp2vixyD8lngcfEmOi8fkCj7vPhesz5xlGBI-EubRw,54
|
|
26
|
+
microlive-1.0.24.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
27
|
+
microlive-1.0.24.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|