microlive 1.0.23__py3-none-any.whl → 1.0.25__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 +548 -361
- {microlive-1.0.23.dist-info → microlive-1.0.25.dist-info}/METADATA +1 -1
- {microlive-1.0.23.dist-info → microlive-1.0.25.dist-info}/RECORD +7 -7
- {microlive-1.0.23.dist-info → microlive-1.0.25.dist-info}/WHEEL +0 -0
- {microlive-1.0.23.dist-info → microlive-1.0.25.dist-info}/entry_points.txt +0 -0
- {microlive-1.0.23.dist-info → microlive-1.0.25.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.25"
|
|
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,249 @@ 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
|
+
# Clear any existing mask from other segmentation methods (e.g., watershed)
|
|
3409
|
+
# This ensures the user starts with a clean canvas for manual drawing
|
|
3410
|
+
current_mode = getattr(self, 'segmentation_mode', None)
|
|
3411
|
+
if current_mode != 'manual' and self.segmentation_mask is not None:
|
|
3412
|
+
self.segmentation_mask = None
|
|
3413
|
+
self._original_watershed_mask = None # Also clear the watershed backup
|
|
3414
|
+
|
|
3415
|
+
# Connect click handler for polygon vertices
|
|
3416
|
+
self.cid_manual = self.canvas_segmentation.mpl_connect(
|
|
3417
|
+
'button_press_event', self.on_polygon_click
|
|
3418
|
+
)
|
|
3419
|
+
|
|
3420
|
+
# Initialize polygon points list if not exists
|
|
3421
|
+
if not hasattr(self, 'manual_polygon_points'):
|
|
3422
|
+
self.manual_polygon_points = []
|
|
3423
|
+
|
|
3424
|
+
def _exit_manual_mode(self):
|
|
3425
|
+
"""Disconnect manual click handler when leaving Manual tab."""
|
|
3426
|
+
if hasattr(self, 'cid_manual') and self.cid_manual is not None:
|
|
3427
|
+
try:
|
|
3428
|
+
self.canvas_segmentation.mpl_disconnect(self.cid_manual)
|
|
3429
|
+
except Exception:
|
|
3430
|
+
pass
|
|
3431
|
+
self.cid_manual = None
|
|
3432
|
+
|
|
3433
|
+
def on_polygon_click(self, event):
|
|
3434
|
+
"""Handle click in manual segmentation mode - add polygon vertex.
|
|
3435
|
+
|
|
3436
|
+
Each click adds a vertex to the polygon. Points are connected with lines.
|
|
3437
|
+
When finished, the polygon is closed and filled to create the mask.
|
|
3438
|
+
"""
|
|
3439
|
+
if event.inaxes != self.ax_segmentation:
|
|
3388
3440
|
return
|
|
3441
|
+
if event.xdata is None or event.ydata is None:
|
|
3442
|
+
return
|
|
3443
|
+
if event.button != 1: # Left click only
|
|
3444
|
+
return
|
|
3445
|
+
|
|
3446
|
+
x, y = event.xdata, event.ydata
|
|
3447
|
+
|
|
3448
|
+
# Get image dimensions to validate
|
|
3449
|
+
image_to_use = self.get_current_image_source()
|
|
3450
|
+
if image_to_use is None:
|
|
3451
|
+
return
|
|
3452
|
+
|
|
3389
3453
|
ch = self.segmentation_current_channel
|
|
3390
3454
|
if self.use_max_proj_for_segmentation and self.segmentation_maxproj is not None:
|
|
3391
3455
|
img = self.segmentation_maxproj[..., ch]
|
|
3392
3456
|
else:
|
|
3393
3457
|
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]
|
|
3458
|
+
image_channel = image_to_use[fr, :, :, :, ch]
|
|
3459
|
+
if getattr(self, 'segmentation_current_z', -1) == -1:
|
|
3460
|
+
img = np.max(image_channel, axis=0)
|
|
3425
3461
|
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()
|
|
3462
|
+
z_idx = min(self.segmentation_current_z, image_channel.shape[0] - 1)
|
|
3463
|
+
img = image_channel[z_idx, :, :]
|
|
3464
|
+
|
|
3465
|
+
# Validate click is within bounds
|
|
3466
|
+
height, width = img.shape[:2]
|
|
3467
|
+
if x < 0 or x >= width or y < 0 or y >= height:
|
|
3468
|
+
return
|
|
3469
|
+
|
|
3470
|
+
# Add point to polygon
|
|
3471
|
+
self.manual_polygon_points.append((x, y))
|
|
3472
|
+
|
|
3473
|
+
# Update display
|
|
3474
|
+
self._update_polygon_display()
|
|
3475
|
+
self._update_polygon_status()
|
|
3445
3476
|
|
|
3477
|
+
def _update_polygon_display(self):
|
|
3478
|
+
"""Update the display to show current polygon points and lines."""
|
|
3479
|
+
# First draw the base image using plot_segmentation
|
|
3480
|
+
self.plot_segmentation()
|
|
3481
|
+
|
|
3482
|
+
# Then overlay polygon points and lines
|
|
3483
|
+
if hasattr(self, 'manual_polygon_points') and len(self.manual_polygon_points) > 0:
|
|
3484
|
+
points = self.manual_polygon_points
|
|
3485
|
+
|
|
3486
|
+
# Draw lines between consecutive points
|
|
3487
|
+
if len(points) > 1:
|
|
3488
|
+
xs = [p[0] for p in points]
|
|
3489
|
+
ys = [p[1] for p in points]
|
|
3490
|
+
self.ax_segmentation.plot(xs, ys, 'c-', linewidth=2, alpha=0.8)
|
|
3491
|
+
|
|
3492
|
+
# Draw points as markers
|
|
3493
|
+
for i, (px, py) in enumerate(points):
|
|
3494
|
+
if i == 0:
|
|
3495
|
+
# First point is special (green)
|
|
3496
|
+
self.ax_segmentation.plot(px, py, 'go', markersize=10, markeredgecolor='white', markeredgewidth=2)
|
|
3497
|
+
else:
|
|
3498
|
+
# Other points (cyan)
|
|
3499
|
+
self.ax_segmentation.plot(px, py, 'co', markersize=8, markeredgecolor='white', markeredgewidth=1.5)
|
|
3500
|
+
|
|
3501
|
+
self.canvas_segmentation.draw()
|
|
3446
3502
|
|
|
3447
|
-
def
|
|
3448
|
-
"""
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
if
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
self.
|
|
3503
|
+
def _update_polygon_status(self):
|
|
3504
|
+
"""Update status label and button states based on polygon drawing state."""
|
|
3505
|
+
n_points = len(getattr(self, 'manual_polygon_points', []))
|
|
3506
|
+
|
|
3507
|
+
# Update Finish button state
|
|
3508
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
3509
|
+
self.btn_finish_polygon.setEnabled(n_points >= 3)
|
|
3510
|
+
|
|
3511
|
+
# Update status label
|
|
3512
|
+
if hasattr(self, 'manual_status_label'):
|
|
3513
|
+
if self.segmentation_mask is not None and n_points == 0:
|
|
3514
|
+
n_pixels = np.sum(self.segmentation_mask > 0)
|
|
3515
|
+
self.manual_status_label.setText(f"✓ Mask active: {n_pixels:,} pixels")
|
|
3516
|
+
self.manual_status_label.setStyleSheet("color: #00cc66; font-weight: bold;")
|
|
3517
|
+
elif n_points == 0:
|
|
3518
|
+
self.manual_status_label.setText("ℹ️ Click on image to place polygon vertices")
|
|
3519
|
+
self.manual_status_label.setStyleSheet("color: #888888; font-style: italic;")
|
|
3520
|
+
elif n_points < 3:
|
|
3521
|
+
self.manual_status_label.setText(f"🔷 {n_points} point(s) - need at least 3 for polygon")
|
|
3522
|
+
self.manual_status_label.setStyleSheet("color: #17a2b8;")
|
|
3461
3523
|
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"
|
|
3524
|
+
self.manual_status_label.setText(f"🔷 {n_points} points - click 'Finish Polygon' to create mask")
|
|
3525
|
+
self.manual_status_label.setStyleSheet("color: #17a2b8; font-weight: bold;")
|
|
3526
|
+
|
|
3527
|
+
def finish_manual_polygon(self):
|
|
3528
|
+
"""Close the polygon and create the mask from the drawn points."""
|
|
3529
|
+
if not hasattr(self, 'manual_polygon_points') or len(self.manual_polygon_points) < 3:
|
|
3530
|
+
if hasattr(self, 'manual_status_label'):
|
|
3531
|
+
self.manual_status_label.setText("⚠️ Need at least 3 points to create polygon")
|
|
3532
|
+
self.manual_status_label.setStyleSheet("color: #ffc107;")
|
|
3533
|
+
return
|
|
3534
|
+
|
|
3535
|
+
# Get image dimensions
|
|
3536
|
+
image_to_use = self.get_current_image_source()
|
|
3537
|
+
if image_to_use is None:
|
|
3538
|
+
return
|
|
3539
|
+
|
|
3540
|
+
ch = self.segmentation_current_channel
|
|
3541
|
+
if self.use_max_proj_for_segmentation and self.segmentation_maxproj is not None:
|
|
3542
|
+
img = self.segmentation_maxproj[..., ch]
|
|
3497
3543
|
else:
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3544
|
+
fr = self.segmentation_current_frame
|
|
3545
|
+
image_channel = image_to_use[fr, :, :, :, ch]
|
|
3546
|
+
if getattr(self, 'segmentation_current_z', -1) == -1:
|
|
3547
|
+
img = np.max(image_channel, axis=0)
|
|
3548
|
+
else:
|
|
3549
|
+
z_idx = min(self.segmentation_current_z, image_channel.shape[0] - 1)
|
|
3550
|
+
img = image_channel[z_idx, :, :]
|
|
3551
|
+
|
|
3552
|
+
height, width = img.shape[:2]
|
|
3553
|
+
|
|
3554
|
+
# Create mask using cv2.fillPoly
|
|
3555
|
+
mask = np.zeros((height, width), dtype=np.int32)
|
|
3556
|
+
pts = np.array([[int(round(x)), int(round(y))] for x, y in self.manual_polygon_points], dtype=np.int32)
|
|
3557
|
+
cv2.fillPoly(mask, [pts], 1)
|
|
3558
|
+
|
|
3559
|
+
# Set as active mask
|
|
3560
|
+
self.segmentation_mask = mask
|
|
3561
|
+
self._active_mask_source = 'segmentation'
|
|
3562
|
+
|
|
3563
|
+
# Clear Cellpose/imported masks
|
|
3564
|
+
self.cellpose_masks_cyto = None
|
|
3565
|
+
self.cellpose_masks_nuc = None
|
|
3566
|
+
self.cellpose_masks_cyto_tyx = None
|
|
3567
|
+
self.cellpose_masks_nuc_tyx = None
|
|
3568
|
+
self.use_tyx_masks = False
|
|
3569
|
+
self.masks_imported = False
|
|
3570
|
+
|
|
3571
|
+
if hasattr(self, 'label_cyto_mask_status'):
|
|
3572
|
+
self.label_cyto_mask_status.setText("No cytosol mask loaded")
|
|
3573
|
+
self.label_cyto_mask_status.setStyleSheet("color: gray;")
|
|
3574
|
+
if hasattr(self, 'label_nuc_mask_status'):
|
|
3575
|
+
self.label_nuc_mask_status.setText("No nucleus mask loaded")
|
|
3576
|
+
self.label_nuc_mask_status.setStyleSheet("color: gray;")
|
|
3577
|
+
|
|
3578
|
+
# Clear photobleaching
|
|
3579
|
+
self.photobleaching_calculated = False
|
|
3580
|
+
|
|
3581
|
+
# Clear polygon points (mask created)
|
|
3582
|
+
n_pixels = np.sum(mask > 0)
|
|
3583
|
+
self.manual_polygon_points = []
|
|
3584
|
+
|
|
3585
|
+
# Update display
|
|
3586
|
+
self.plot_segmentation()
|
|
3587
|
+
|
|
3588
|
+
# Update status
|
|
3589
|
+
if hasattr(self, 'manual_status_label'):
|
|
3590
|
+
self.manual_status_label.setText(f"✓ Mask created: {n_pixels:,} pixels")
|
|
3591
|
+
self.manual_status_label.setStyleSheet("color: #00cc66; font-weight: bold;")
|
|
3592
|
+
|
|
3593
|
+
# Disable finish button
|
|
3594
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
3595
|
+
self.btn_finish_polygon.setEnabled(False)
|
|
3596
|
+
|
|
3597
|
+
def clear_manual_mask(self):
|
|
3598
|
+
"""Clear the current polygon points and mask, reset to ready state."""
|
|
3599
|
+
# Clear polygon points
|
|
3600
|
+
self.manual_polygon_points = []
|
|
3601
|
+
|
|
3602
|
+
# Clear mask
|
|
3603
|
+
self.segmentation_mask = None
|
|
3604
|
+
self._active_mask_source = 'none'
|
|
3605
|
+
|
|
3606
|
+
# Clear photobleaching
|
|
3607
|
+
self.photobleaching_calculated = False
|
|
3608
|
+
|
|
3609
|
+
# Update display
|
|
3610
|
+
self.plot_segmentation()
|
|
3611
|
+
|
|
3612
|
+
# Update status
|
|
3613
|
+
self._update_polygon_status()
|
|
3614
|
+
|
|
3615
|
+
# Disable finish button
|
|
3616
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
3617
|
+
self.btn_finish_polygon.setEnabled(False)
|
|
3618
|
+
|
|
3506
3619
|
|
|
3507
3620
|
def next_frame(self):
|
|
3508
3621
|
if getattr(self, 'total_frames', 0) == 0:
|
|
@@ -5214,9 +5327,19 @@ class GUI(QMainWindow):
|
|
|
5214
5327
|
cmap_used = cmap_list_imagej[ch % len(cmap_list_imagej)]
|
|
5215
5328
|
self.ax_segmentation.imshow(normalized_image[..., 0], cmap=cmap_used, vmin=0, vmax=1)
|
|
5216
5329
|
|
|
5217
|
-
# Draw
|
|
5330
|
+
# Draw mask overlay (semi-transparent, like Edit tab)
|
|
5218
5331
|
if self.segmentation_mask is not None:
|
|
5219
|
-
|
|
5332
|
+
# Create RGBA overlay with 30% opacity
|
|
5333
|
+
mask_rgba = np.zeros((*self.segmentation_mask.shape, 4), dtype=np.float32)
|
|
5334
|
+
mask_region = self.segmentation_mask > 0
|
|
5335
|
+
mask_rgba[mask_region, 0] = 0.0 # R
|
|
5336
|
+
mask_rgba[mask_region, 1] = 0.8 # G (cyan)
|
|
5337
|
+
mask_rgba[mask_region, 2] = 0.8 # B
|
|
5338
|
+
mask_rgba[mask_region, 3] = 0.3 # Alpha (30% opacity)
|
|
5339
|
+
self.ax_segmentation.imshow(mask_rgba, interpolation='nearest')
|
|
5340
|
+
|
|
5341
|
+
# Draw white contour for clear boundary
|
|
5342
|
+
self.ax_segmentation.contour(self.segmentation_mask, levels=[0.5], colors='white', linewidths=1.5)
|
|
5220
5343
|
|
|
5221
5344
|
# Add axis labels in pixels (helpful for Cellpose diameter estimation)
|
|
5222
5345
|
height, width = image_to_display.shape[:2]
|
|
@@ -5866,8 +5989,11 @@ class GUI(QMainWindow):
|
|
|
5866
5989
|
segmentation_method_tabs (QTabWidget)
|
|
5867
5990
|
use_max_proj_checkbox (QCheckBox)
|
|
5868
5991
|
max_proj_status_label (QLabel)
|
|
5869
|
-
|
|
5870
|
-
|
|
5992
|
+
# Manual segmentation tab widgets:
|
|
5993
|
+
btn_clear_manual_mask (QPushButton): Clear the manual mask
|
|
5994
|
+
manual_status_label (QLabel): Shows current manual mode status
|
|
5995
|
+
manual_instructions_label (QLabel): Rich text instructions
|
|
5996
|
+
cid_manual (int): Matplotlib click handler connection ID
|
|
5871
5997
|
watershed_threshold_slider (QSlider)
|
|
5872
5998
|
watershed_threshold_label (QLabel)
|
|
5873
5999
|
watershed_size_slider (QSlider)
|
|
@@ -5891,8 +6017,7 @@ class GUI(QMainWindow):
|
|
|
5891
6017
|
export_segmentation_image
|
|
5892
6018
|
export_mask_as_tiff
|
|
5893
6019
|
update_segmentation_source
|
|
5894
|
-
|
|
5895
|
-
finish_segmentation
|
|
6020
|
+
clear_manual_mask
|
|
5896
6021
|
update_watershed_threshold_factor
|
|
5897
6022
|
run_watershed_segmentation
|
|
5898
6023
|
_on_segmentation_subtab_changed
|
|
@@ -6016,25 +6141,87 @@ class GUI(QMainWindow):
|
|
|
6016
6141
|
manual_tab_layout.setContentsMargins(10, 10, 10, 10)
|
|
6017
6142
|
manual_tab_layout.setSpacing(10)
|
|
6018
6143
|
|
|
6019
|
-
# Instructions
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6144
|
+
# Instructions panel with rich styling
|
|
6145
|
+
instructions_frame = QFrame()
|
|
6146
|
+
instructions_frame.setStyleSheet("""
|
|
6147
|
+
QFrame {
|
|
6148
|
+
background-color: #2a2a3a;
|
|
6149
|
+
border: 1px solid #444;
|
|
6150
|
+
border-radius: 6px;
|
|
6151
|
+
padding: 8px;
|
|
6152
|
+
}
|
|
6153
|
+
""")
|
|
6154
|
+
instructions_layout = QVBoxLayout(instructions_frame)
|
|
6155
|
+
instructions_layout.setContentsMargins(10, 8, 10, 8)
|
|
6156
|
+
instructions_layout.setSpacing(4)
|
|
6157
|
+
|
|
6158
|
+
self.manual_instructions_label = QLabel(
|
|
6159
|
+
"<b>🔷 Polygon Drawing Workflow:</b><br>"
|
|
6160
|
+
"1. Click on the image to place polygon vertices<br>"
|
|
6161
|
+
"2. Click 'Finish Polygon' to close and fill the mask<br>"
|
|
6162
|
+
"3. Use 'Clear' to start over"
|
|
6024
6163
|
)
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6164
|
+
self.manual_instructions_label.setTextFormat(Qt.RichText)
|
|
6165
|
+
self.manual_instructions_label.setWordWrap(True)
|
|
6166
|
+
self.manual_instructions_label.setStyleSheet("color: #cccccc; font-size: 11px;")
|
|
6167
|
+
instructions_layout.addWidget(self.manual_instructions_label)
|
|
6168
|
+
manual_tab_layout.addWidget(instructions_frame)
|
|
6028
6169
|
|
|
6029
|
-
# Button row
|
|
6170
|
+
# Button row
|
|
6030
6171
|
button_layout = QHBoxLayout()
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
self.
|
|
6035
|
-
self.
|
|
6036
|
-
|
|
6172
|
+
|
|
6173
|
+
# Finish Polygon button
|
|
6174
|
+
self.btn_finish_polygon = QPushButton("✓ Finish Polygon")
|
|
6175
|
+
self.btn_finish_polygon.setToolTip("Close the polygon and create the mask")
|
|
6176
|
+
self.btn_finish_polygon.setStyleSheet("""
|
|
6177
|
+
QPushButton {
|
|
6178
|
+
background-color: #28a745;
|
|
6179
|
+
color: white;
|
|
6180
|
+
border: none;
|
|
6181
|
+
border-radius: 4px;
|
|
6182
|
+
padding: 8px 16px;
|
|
6183
|
+
font-weight: bold;
|
|
6184
|
+
}
|
|
6185
|
+
QPushButton:hover {
|
|
6186
|
+
background-color: #218838;
|
|
6187
|
+
}
|
|
6188
|
+
QPushButton:disabled {
|
|
6189
|
+
background-color: #6c757d;
|
|
6190
|
+
}
|
|
6191
|
+
""")
|
|
6192
|
+
self.btn_finish_polygon.clicked.connect(self.finish_manual_polygon)
|
|
6193
|
+
self.btn_finish_polygon.setEnabled(False) # Disabled until at least 3 points
|
|
6194
|
+
button_layout.addWidget(self.btn_finish_polygon)
|
|
6195
|
+
|
|
6196
|
+
# Clear button
|
|
6197
|
+
self.btn_clear_manual_mask = QPushButton("🗑️ Clear")
|
|
6198
|
+
self.btn_clear_manual_mask.setToolTip("Clear all points and the mask")
|
|
6199
|
+
self.btn_clear_manual_mask.setStyleSheet("""
|
|
6200
|
+
QPushButton {
|
|
6201
|
+
background-color: #dc3545;
|
|
6202
|
+
color: white;
|
|
6203
|
+
border: none;
|
|
6204
|
+
border-radius: 4px;
|
|
6205
|
+
padding: 8px 16px;
|
|
6206
|
+
font-weight: bold;
|
|
6207
|
+
}
|
|
6208
|
+
QPushButton:hover {
|
|
6209
|
+
background-color: #c82333;
|
|
6210
|
+
}
|
|
6211
|
+
""")
|
|
6212
|
+
self.btn_clear_manual_mask.clicked.connect(self.clear_manual_mask)
|
|
6213
|
+
button_layout.addWidget(self.btn_clear_manual_mask)
|
|
6214
|
+
|
|
6037
6215
|
manual_tab_layout.addLayout(button_layout)
|
|
6216
|
+
|
|
6217
|
+
# Status label
|
|
6218
|
+
self.manual_status_label = QLabel("ℹ️ Click on image to place polygon vertices")
|
|
6219
|
+
self.manual_status_label.setStyleSheet("color: #888888; font-size: 11px; font-style: italic;")
|
|
6220
|
+
manual_tab_layout.addWidget(self.manual_status_label)
|
|
6221
|
+
|
|
6222
|
+
# Initialize polygon drawing state
|
|
6223
|
+
self.manual_polygon_points = [] # List of (x, y) tuples
|
|
6224
|
+
|
|
6038
6225
|
manual_tab_layout.addStretch()
|
|
6039
6226
|
|
|
6040
6227
|
# Manual tab will be added last (after Cellpose)
|
|
@@ -6200,7 +6387,7 @@ class GUI(QMainWindow):
|
|
|
6200
6387
|
|
|
6201
6388
|
self.cellpose_cyto_diameter_input = QDoubleSpinBox()
|
|
6202
6389
|
self.cellpose_cyto_diameter_input.setRange(0, 1000)
|
|
6203
|
-
self.cellpose_cyto_diameter_input.setValue(
|
|
6390
|
+
self.cellpose_cyto_diameter_input.setValue(350)
|
|
6204
6391
|
cyto_layout.addRow("Diameter (px):", self.cellpose_cyto_diameter_input)
|
|
6205
6392
|
|
|
6206
6393
|
self.chk_optimize_cyto = QCheckBox("Optimize Parameters")
|
|
@@ -6617,16 +6804,24 @@ class GUI(QMainWindow):
|
|
|
6617
6804
|
Tab indices:
|
|
6618
6805
|
0 = Watershed
|
|
6619
6806
|
1 = Cellpose
|
|
6620
|
-
2 = Manual
|
|
6807
|
+
2 = Manual (polygon drawing workflow)
|
|
6621
6808
|
3 = Import (uses Cellpose-style display)
|
|
6622
6809
|
4 = Edit (edit existing masks)
|
|
6623
6810
|
"""
|
|
6624
6811
|
if index == 4: # Edit sub-tab
|
|
6812
|
+
self._exit_manual_mode()
|
|
6625
6813
|
self.enter_edit_mode()
|
|
6814
|
+
elif index == 2: # Manual sub-tab - polygon drawing
|
|
6815
|
+
self.exit_edit_mode()
|
|
6816
|
+
self._enter_manual_mode()
|
|
6817
|
+
self._update_polygon_display()
|
|
6818
|
+
self._update_polygon_status()
|
|
6626
6819
|
elif index == 1 or index == 3: # Cellpose or Import sub-tab
|
|
6820
|
+
self._exit_manual_mode()
|
|
6627
6821
|
self.exit_edit_mode()
|
|
6628
6822
|
self.plot_cellpose_results()
|
|
6629
|
-
else:
|
|
6823
|
+
else: # Watershed (index 0)
|
|
6824
|
+
self._exit_manual_mode()
|
|
6630
6825
|
self.exit_edit_mode()
|
|
6631
6826
|
self.plot_segmentation()
|
|
6632
6827
|
|
|
@@ -6686,14 +6881,17 @@ class GUI(QMainWindow):
|
|
|
6686
6881
|
self.edit_mask_selector.clear()
|
|
6687
6882
|
self.edit_mask_selector.addItem("-- Select Mask --")
|
|
6688
6883
|
|
|
6689
|
-
# Check for Watershed/Manual mask
|
|
6884
|
+
# Check for Watershed/Manual mask - label based on segmentation_mode
|
|
6690
6885
|
if self.segmentation_mask is not None:
|
|
6691
6886
|
n_cells = len(np.unique(self.segmentation_mask)) - 1 # Exclude 0
|
|
6692
6887
|
shape = self.segmentation_mask.shape
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6888
|
+
# Check segmentation_mode to label correctly
|
|
6889
|
+
mode = getattr(self, 'segmentation_mode', 'watershed')
|
|
6890
|
+
if mode == 'manual':
|
|
6891
|
+
label = f"Manual Mask ({n_cells} cells, {shape[1]}×{shape[0]})"
|
|
6892
|
+
else:
|
|
6893
|
+
label = f"Watershed Mask ({n_cells} cells, {shape[1]}×{shape[0]})"
|
|
6894
|
+
self.edit_mask_selector.addItem(label, "watershed")
|
|
6697
6895
|
|
|
6698
6896
|
# Check for Cellpose cytosol mask
|
|
6699
6897
|
if self.cellpose_masks_cyto is not None:
|
|
@@ -6728,9 +6926,11 @@ class GUI(QMainWindow):
|
|
|
6728
6926
|
self.edit_status_label.setText("⚠ No masks available - run segmentation first")
|
|
6729
6927
|
self.edit_status_label.setStyleSheet("color: #ff6666;")
|
|
6730
6928
|
elif self.edit_mask_selector.count() == 2:
|
|
6731
|
-
# Auto-
|
|
6929
|
+
# Single-Option Auto-Selection: only one mask available, select it automatically
|
|
6732
6930
|
self.edit_mask_selector.setCurrentIndex(1)
|
|
6733
6931
|
self.on_edit_mask_selector_changed(1)
|
|
6932
|
+
self.edit_status_label.setText("✓ Single mask auto-selected for editing")
|
|
6933
|
+
self.edit_status_label.setStyleSheet("color: #66ff66;")
|
|
6734
6934
|
else:
|
|
6735
6935
|
self.edit_status_label.setText("⚠ Select a mask to begin editing")
|
|
6736
6936
|
self.edit_status_label.setStyleSheet("color: #ffcc00;")
|
|
@@ -7827,7 +8027,12 @@ class GUI(QMainWindow):
|
|
|
7827
8027
|
self.update_threshold_histogram()
|
|
7828
8028
|
|
|
7829
8029
|
def on_auto_threshold_clicked(self):
|
|
7830
|
-
"""Handle auto-threshold button click - calculate optimal threshold automatically.
|
|
8030
|
+
"""Handle auto-threshold button click - calculate optimal threshold automatically.
|
|
8031
|
+
|
|
8032
|
+
Calculates thresholds on multiple representative frames (beginning, middle, end)
|
|
8033
|
+
and averages them for more robust detection across temporal variability.
|
|
8034
|
+
Handles single-frame movies gracefully by using only the available frame.
|
|
8035
|
+
"""
|
|
7831
8036
|
if self.image_stack is None:
|
|
7832
8037
|
self.statusBar().showMessage("No image loaded")
|
|
7833
8038
|
return
|
|
@@ -7841,14 +8046,10 @@ class GUI(QMainWindow):
|
|
|
7841
8046
|
return
|
|
7842
8047
|
|
|
7843
8048
|
# Show progress
|
|
7844
|
-
self.statusBar().showMessage("Calculating optimal threshold...")
|
|
8049
|
+
self.statusBar().showMessage("Calculating optimal threshold (multi-frame)...")
|
|
7845
8050
|
QApplication.processEvents()
|
|
7846
8051
|
|
|
7847
8052
|
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
8053
|
# Determine if using 3D or 2D mode
|
|
7853
8054
|
use_3d = not self.use_maximum_projection
|
|
7854
8055
|
|
|
@@ -7860,20 +8061,64 @@ class GUI(QMainWindow):
|
|
|
7860
8061
|
yx_spot_size = getattr(self, 'yx_spot_size_in_px', 5)
|
|
7861
8062
|
z_spot_size = getattr(self, 'z_spot_size_in_px', 2)
|
|
7862
8063
|
|
|
7863
|
-
#
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
threshold_raw = auto_thresh.calculate()
|
|
7873
|
-
method_used = auto_thresh.method_used
|
|
8064
|
+
# --- Multi-frame threshold calculation ---
|
|
8065
|
+
# Select representative frames: beginning, middle, end
|
|
8066
|
+
total_frames = image_to_use.shape[0]
|
|
8067
|
+
if total_frames <= 1:
|
|
8068
|
+
frame_indices = [0]
|
|
8069
|
+
elif total_frames == 2:
|
|
8070
|
+
frame_indices = [0, 1]
|
|
8071
|
+
else:
|
|
8072
|
+
frame_indices = [0, total_frames // 2, total_frames - 1]
|
|
7874
8073
|
|
|
7875
|
-
#
|
|
7876
|
-
|
|
8074
|
+
# Calculate threshold for each representative frame
|
|
8075
|
+
thresholds = []
|
|
8076
|
+
methods = []
|
|
8077
|
+
|
|
8078
|
+
for frame_idx in frame_indices:
|
|
8079
|
+
try:
|
|
8080
|
+
# Extract image for this frame and channel [Z, Y, X]
|
|
8081
|
+
image_channel = image_to_use[frame_idx, :, :, :, channel]
|
|
8082
|
+
|
|
8083
|
+
# Calculate threshold using AutoThreshold class
|
|
8084
|
+
auto_thresh = mi.AutoThreshold(
|
|
8085
|
+
image=image_channel,
|
|
8086
|
+
voxel_size_yx=voxel_yx,
|
|
8087
|
+
voxel_size_z=voxel_z,
|
|
8088
|
+
yx_spot_size_in_px=yx_spot_size,
|
|
8089
|
+
z_spot_size_in_px=z_spot_size,
|
|
8090
|
+
use_3d=use_3d
|
|
8091
|
+
)
|
|
8092
|
+
thresh = auto_thresh.calculate()
|
|
8093
|
+
|
|
8094
|
+
# Only include valid thresholds
|
|
8095
|
+
if thresh is not None and thresh > 0:
|
|
8096
|
+
thresholds.append(thresh)
|
|
8097
|
+
methods.append(auto_thresh.method_used)
|
|
8098
|
+
except Exception as frame_error:
|
|
8099
|
+
# Log but continue with other frames
|
|
8100
|
+
logging.debug(f"Auto-threshold failed for frame {frame_idx}: {frame_error}")
|
|
8101
|
+
continue
|
|
8102
|
+
|
|
8103
|
+
# Check if any valid thresholds were calculated
|
|
8104
|
+
if not thresholds:
|
|
8105
|
+
self.statusBar().showMessage("Auto-threshold failed: no valid frames")
|
|
8106
|
+
return
|
|
8107
|
+
|
|
8108
|
+
# Compute average threshold across all valid frames
|
|
8109
|
+
threshold_raw = np.mean(thresholds)
|
|
8110
|
+
n_frames_used = len(thresholds)
|
|
8111
|
+
|
|
8112
|
+
# Determine method string for reporting
|
|
8113
|
+
unique_methods = set(methods)
|
|
8114
|
+
if len(unique_methods) == 1:
|
|
8115
|
+
method_used = methods[0]
|
|
8116
|
+
else:
|
|
8117
|
+
# Mixed methods across frames
|
|
8118
|
+
method_used = "multi-frame"
|
|
8119
|
+
|
|
8120
|
+
# Reduce threshold by 30% to improve spot coverage (auto-threshold tends to overestimate)
|
|
8121
|
+
threshold = threshold_raw * 0.7
|
|
7877
8122
|
|
|
7878
8123
|
# Store per-channel
|
|
7879
8124
|
self.auto_threshold_per_channel[channel] = threshold
|
|
@@ -7901,10 +8146,16 @@ class GUI(QMainWindow):
|
|
|
7901
8146
|
# Auto-run single frame detection
|
|
7902
8147
|
self.detect_spots_in_current_frame()
|
|
7903
8148
|
|
|
7904
|
-
# Show result
|
|
7905
|
-
|
|
7906
|
-
|
|
7907
|
-
|
|
8149
|
+
# Show result with frame count info
|
|
8150
|
+
if n_frames_used == 1:
|
|
8151
|
+
self.statusBar().showMessage(
|
|
8152
|
+
f"Auto-threshold Ch{channel}: {int(threshold)} (method: {method_used})"
|
|
8153
|
+
)
|
|
8154
|
+
else:
|
|
8155
|
+
self.statusBar().showMessage(
|
|
8156
|
+
f"Auto-threshold Ch{channel}: {int(threshold)} "
|
|
8157
|
+
f"(avg of {n_frames_used} frames, method: {method_used})"
|
|
8158
|
+
)
|
|
7908
8159
|
|
|
7909
8160
|
except Exception as e:
|
|
7910
8161
|
traceback.print_exc()
|
|
@@ -9261,6 +9512,7 @@ class GUI(QMainWindow):
|
|
|
9261
9512
|
# Clear detection preview (not the multi-channel tracking data)
|
|
9262
9513
|
self.detected_spots_frame = None
|
|
9263
9514
|
self.reset_msd_tab()
|
|
9515
|
+
self.reset_colocalization_tab() # Clear stale colocalization results
|
|
9264
9516
|
self.plot_tracking()
|
|
9265
9517
|
# Get masks for tracking (supports both Cellpose and Segmentation)
|
|
9266
9518
|
masks_complete, masks_nuc, masks_cyto_no_nuc = self._get_tracking_masks()
|
|
@@ -9420,11 +9672,8 @@ class GUI(QMainWindow):
|
|
|
9420
9672
|
self.display_correlation_plot()
|
|
9421
9673
|
self.channels_spots = [self.current_channel]
|
|
9422
9674
|
self.populate_colocalization_channels()
|
|
9423
|
-
#
|
|
9424
|
-
|
|
9425
|
-
self.verify_visual_scroll_area.setWidget(QWidget())
|
|
9426
|
-
if hasattr(self, 'verify_distance_scroll_area'):
|
|
9427
|
-
self.verify_distance_scroll_area.setWidget(QWidget())
|
|
9675
|
+
# Note: Verification subtabs (Verify Visual, Verify Distance) are already
|
|
9676
|
+
# reset at the start of perform_particle_tracking() via reset_colocalization_tab()
|
|
9428
9677
|
self.MIN_FRAMES_MSD = 20
|
|
9429
9678
|
self.MIN_PARTICLES_MSD = 10
|
|
9430
9679
|
|
|
@@ -9915,7 +10164,10 @@ class GUI(QMainWindow):
|
|
|
9915
10164
|
self.auto_threshold_btn = QPushButton("Auto")
|
|
9916
10165
|
self.auto_threshold_btn.setFixedWidth(45)
|
|
9917
10166
|
self.auto_threshold_btn.setFixedHeight(20)
|
|
9918
|
-
self.auto_threshold_btn.setToolTip(
|
|
10167
|
+
self.auto_threshold_btn.setToolTip(
|
|
10168
|
+
"Auto-detect optimal threshold\\n"
|
|
10169
|
+
"(averages across beginning, middle, and end frames)"
|
|
10170
|
+
)
|
|
9919
10171
|
self.auto_threshold_btn.setStyleSheet("""
|
|
9920
10172
|
QPushButton {
|
|
9921
10173
|
background-color: #00d4aa;
|
|
@@ -11530,7 +11782,7 @@ class GUI(QMainWindow):
|
|
|
11530
11782
|
num_z = image.shape[1]
|
|
11531
11783
|
max_proj = np.max(image, axis=1, keepdims=True)
|
|
11532
11784
|
image = np.repeat(max_proj, num_z, axis=1)
|
|
11533
|
-
crop_size = int(self.yx_spot_size_in_px) +
|
|
11785
|
+
crop_size = int(self.yx_spot_size_in_px) + 7
|
|
11534
11786
|
if crop_size % 2 == 0:
|
|
11535
11787
|
crop_size += 1
|
|
11536
11788
|
|
|
@@ -12078,8 +12330,28 @@ class GUI(QMainWindow):
|
|
|
12078
12330
|
self.dist_coloc_zoom_label.setText("🔍 Full View")
|
|
12079
12331
|
self.dist_coloc_zoom_label.setStyleSheet("color: #888888; font-size: 10px;")
|
|
12080
12332
|
|
|
12333
|
+
# === Reset Verify Visual sub-tab ===
|
|
12334
|
+
if hasattr(self, 'verify_visual_scroll_area'):
|
|
12335
|
+
self.verify_visual_scroll_area.setWidget(QWidget())
|
|
12336
|
+
if hasattr(self, 'verify_visual_checkboxes'):
|
|
12337
|
+
self.verify_visual_checkboxes = []
|
|
12338
|
+
if hasattr(self, 'verify_visual_stats_label'):
|
|
12339
|
+
self.verify_visual_stats_label.setText("Run Visual colocalization first, then click Populate")
|
|
12340
|
+
|
|
12341
|
+
# === Reset Verify Distance sub-tab ===
|
|
12342
|
+
if hasattr(self, 'verify_distance_scroll_area'):
|
|
12343
|
+
self.verify_distance_scroll_area.setWidget(QWidget())
|
|
12344
|
+
if hasattr(self, 'verify_distance_checkboxes'):
|
|
12345
|
+
self.verify_distance_checkboxes = []
|
|
12346
|
+
if hasattr(self, 'verify_distance_stats_label'):
|
|
12347
|
+
self.verify_distance_stats_label.setText("Run Distance colocalization first, then click Populate")
|
|
12348
|
+
|
|
12081
12349
|
# === Reset Manual Verify sub-tab ===
|
|
12082
12350
|
self.reset_manual_colocalization()
|
|
12351
|
+
|
|
12352
|
+
# Reset sub-tab position to Visual (first tab)
|
|
12353
|
+
if hasattr(self, 'coloc_subtabs'):
|
|
12354
|
+
self.coloc_subtabs.setCurrentIndex(0)
|
|
12083
12355
|
|
|
12084
12356
|
def extract_manual_colocalization_data(self, save_df=True):
|
|
12085
12357
|
"""Extract and optionally save manual colocalization data.
|
|
@@ -12666,10 +12938,6 @@ class GUI(QMainWindow):
|
|
|
12666
12938
|
self.verify_visual_populate_button.clicked.connect(self.populate_verify_visual)
|
|
12667
12939
|
top_bar.addWidget(self.verify_visual_populate_button)
|
|
12668
12940
|
|
|
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
12941
|
self.verify_visual_cleanup_button = QPushButton("Cleanup")
|
|
12674
12942
|
self.verify_visual_cleanup_button.clicked.connect(self.cleanup_verify_visual)
|
|
12675
12943
|
top_bar.addWidget(self.verify_visual_cleanup_button)
|
|
@@ -12722,10 +12990,6 @@ class GUI(QMainWindow):
|
|
|
12722
12990
|
self.verify_distance_populate_button.clicked.connect(self.populate_verify_distance)
|
|
12723
12991
|
top_bar.addWidget(self.verify_distance_populate_button)
|
|
12724
12992
|
|
|
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
12993
|
self.verify_distance_cleanup_button = QPushButton("Cleanup")
|
|
12730
12994
|
self.verify_distance_cleanup_button.clicked.connect(self.cleanup_verify_distance)
|
|
12731
12995
|
top_bar.addWidget(self.verify_distance_cleanup_button)
|
|
@@ -13529,7 +13793,10 @@ class GUI(QMainWindow):
|
|
|
13529
13793
|
# === Verify Visual Subtab Methods ===
|
|
13530
13794
|
|
|
13531
13795
|
def populate_verify_visual(self):
|
|
13532
|
-
"""Populate the Verify Visual subtab with Visual (ML/Intensity) colocalization results.
|
|
13796
|
+
"""Populate the Verify Visual subtab with Visual (ML/Intensity) colocalization results.
|
|
13797
|
+
|
|
13798
|
+
Automatically sorts spots by prediction value (uncertainty-first) for efficient review.
|
|
13799
|
+
"""
|
|
13533
13800
|
if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
|
|
13534
13801
|
QMessageBox.warning(self, "No Results",
|
|
13535
13802
|
"Please run Visual (ML/Intensity) colocalization first.")
|
|
@@ -13542,26 +13809,55 @@ class GUI(QMainWindow):
|
|
|
13542
13809
|
crop_size = results.get('crop_size', 15)
|
|
13543
13810
|
ch1 = results.get('ch1_index', 0)
|
|
13544
13811
|
ch2 = results.get('ch2_index', 1)
|
|
13812
|
+
pred_values = results.get('prediction_values_vector')
|
|
13545
13813
|
|
|
13546
13814
|
if flag_vector is None or mean_crop is None:
|
|
13547
13815
|
QMessageBox.warning(self, "No Data", "Visual colocalization results are incomplete.")
|
|
13548
13816
|
return
|
|
13549
13817
|
|
|
13550
|
-
#
|
|
13818
|
+
# === AUTO-SORT BY PREDICTION VALUE (uncertainty-first) ===
|
|
13819
|
+
num_spots = len(flag_vector)
|
|
13820
|
+
display_crop = mean_crop
|
|
13821
|
+
display_flags = flag_vector
|
|
13822
|
+
|
|
13823
|
+
if pred_values is not None and len(pred_values) == num_spots and num_spots > 0:
|
|
13824
|
+
# Sort ascending by prediction value (lower = more uncertain = review first)
|
|
13825
|
+
sorted_indices = np.argsort(pred_values)
|
|
13826
|
+
|
|
13827
|
+
# Re-order flag vector
|
|
13828
|
+
sorted_flags = np.array([flag_vector[i] for i in sorted_indices])
|
|
13829
|
+
|
|
13830
|
+
# Re-order crops - each spot is crop_size rows in the mean_crop array
|
|
13831
|
+
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
13832
|
+
if num_crop_spots >= num_spots:
|
|
13833
|
+
sorted_crop = np.zeros_like(mean_crop[:num_spots * crop_size])
|
|
13834
|
+
for new_idx, old_idx in enumerate(sorted_indices):
|
|
13835
|
+
if old_idx < num_crop_spots:
|
|
13836
|
+
sorted_crop[new_idx * crop_size:(new_idx + 1) * crop_size] = \
|
|
13837
|
+
mean_crop[old_idx * crop_size:(old_idx + 1) * crop_size]
|
|
13838
|
+
display_crop = sorted_crop
|
|
13839
|
+
display_flags = sorted_flags
|
|
13840
|
+
|
|
13841
|
+
# Store sorted indices for potential export mapping
|
|
13842
|
+
self._verify_visual_sort_indices = sorted_indices
|
|
13843
|
+
self._verify_visual_sorted = True
|
|
13844
|
+
else:
|
|
13845
|
+
# No prediction values available - keep original order
|
|
13846
|
+
self._verify_visual_sorted = False
|
|
13847
|
+
self._verify_visual_sort_indices = None
|
|
13848
|
+
|
|
13849
|
+
# Create spot crops with checkboxes (now in sorted order)
|
|
13551
13850
|
self._create_verification_crops(
|
|
13552
13851
|
scroll_area=self.verify_visual_scroll_area,
|
|
13553
13852
|
checkboxes_list_attr='verify_visual_checkboxes',
|
|
13554
|
-
mean_crop=
|
|
13853
|
+
mean_crop=display_crop,
|
|
13555
13854
|
crop_size=crop_size,
|
|
13556
|
-
flag_vector=
|
|
13855
|
+
flag_vector=display_flags,
|
|
13557
13856
|
stats_label=self.verify_visual_stats_label,
|
|
13558
13857
|
num_channels=2,
|
|
13559
13858
|
channels=(ch1, ch2)
|
|
13560
13859
|
)
|
|
13561
13860
|
|
|
13562
|
-
# Reset sorted flag so Sort button can be used
|
|
13563
|
-
self._verify_visual_sorted = False
|
|
13564
|
-
|
|
13565
13861
|
# Update stats label
|
|
13566
13862
|
self._update_verify_visual_stats()
|
|
13567
13863
|
|
|
@@ -13578,78 +13874,6 @@ class GUI(QMainWindow):
|
|
|
13578
13874
|
f"[{method}] Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
|
|
13579
13875
|
)
|
|
13580
13876
|
|
|
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
13877
|
def cleanup_verify_visual(self):
|
|
13654
13878
|
"""Clear all checkboxes in Verify Visual subtab."""
|
|
13655
13879
|
if not hasattr(self, 'verify_visual_checkboxes'):
|
|
@@ -13733,7 +13957,7 @@ class GUI(QMainWindow):
|
|
|
13733
13957
|
return
|
|
13734
13958
|
|
|
13735
13959
|
# Create crops and determine colocalization status
|
|
13736
|
-
crop_size = int(getattr(self, 'yx_spot_size_in_px', 5)) +
|
|
13960
|
+
crop_size = int(getattr(self, 'yx_spot_size_in_px', 5)) + 7
|
|
13737
13961
|
if crop_size % 2 == 0:
|
|
13738
13962
|
crop_size += 1
|
|
13739
13963
|
|
|
@@ -13874,26 +14098,54 @@ class GUI(QMainWindow):
|
|
|
13874
14098
|
flag_vector.append(False)
|
|
13875
14099
|
distance_values.append(threshold_px * 10.0)
|
|
13876
14100
|
|
|
13877
|
-
# Store for later use
|
|
14101
|
+
# Store for later use
|
|
13878
14102
|
self.verify_distance_mean_crop = mean_crop
|
|
13879
14103
|
self.verify_distance_crop_size = crop_size
|
|
13880
|
-
self.verify_distance_values = np.array(distance_values)
|
|
14104
|
+
self.verify_distance_values = np.array(distance_values)
|
|
14105
|
+
|
|
14106
|
+
# === AUTO-SORT BY DISTANCE VALUE (closest to threshold = most uncertain first) ===
|
|
14107
|
+
display_crop = mean_crop
|
|
14108
|
+
display_flags = flag_vector
|
|
14109
|
+
|
|
14110
|
+
if len(distance_values) == num_spots and num_spots > 0:
|
|
14111
|
+
# Sort ascending by distance (closest to threshold = most uncertain = review first)
|
|
14112
|
+
sorted_indices = np.argsort(distance_values)
|
|
14113
|
+
|
|
14114
|
+
# Re-order flag vector
|
|
14115
|
+
sorted_flags = [flag_vector[i] for i in sorted_indices]
|
|
14116
|
+
|
|
14117
|
+
# Re-order crops
|
|
14118
|
+
num_crop_spots = mean_crop.shape[0] // crop_size
|
|
14119
|
+
if num_crop_spots >= num_spots:
|
|
14120
|
+
sorted_crop = np.zeros_like(mean_crop[:num_spots * crop_size])
|
|
14121
|
+
for new_idx, old_idx in enumerate(sorted_indices):
|
|
14122
|
+
if old_idx < num_crop_spots:
|
|
14123
|
+
sorted_crop[new_idx * crop_size:(new_idx + 1) * crop_size] = \
|
|
14124
|
+
mean_crop[old_idx * crop_size:(old_idx + 1) * crop_size]
|
|
14125
|
+
display_crop = sorted_crop
|
|
14126
|
+
display_flags = sorted_flags
|
|
14127
|
+
|
|
14128
|
+
# Update stored values to match sorted order
|
|
14129
|
+
self.verify_distance_mean_crop = display_crop
|
|
14130
|
+
self.verify_distance_values = np.array(distance_values)[sorted_indices]
|
|
14131
|
+
self._verify_distance_sort_indices = sorted_indices
|
|
14132
|
+
self._verify_distance_sorted = True
|
|
14133
|
+
else:
|
|
14134
|
+
self._verify_distance_sorted = False
|
|
14135
|
+
self._verify_distance_sort_indices = None
|
|
13881
14136
|
|
|
13882
|
-
# Create spot crops with checkboxes
|
|
14137
|
+
# Create spot crops with checkboxes (now in sorted order)
|
|
13883
14138
|
self._create_verification_crops(
|
|
13884
14139
|
scroll_area=self.verify_distance_scroll_area,
|
|
13885
14140
|
checkboxes_list_attr='verify_distance_checkboxes',
|
|
13886
|
-
mean_crop=
|
|
14141
|
+
mean_crop=display_crop,
|
|
13887
14142
|
crop_size=crop_size,
|
|
13888
|
-
flag_vector=
|
|
14143
|
+
flag_vector=display_flags,
|
|
13889
14144
|
stats_label=self.verify_distance_stats_label,
|
|
13890
14145
|
num_channels=image.shape[-1] if image.ndim == 5 else 1,
|
|
13891
14146
|
channels=(ch0, ch1)
|
|
13892
14147
|
)
|
|
13893
14148
|
|
|
13894
|
-
# Reset sorted flag so Sort button can be used
|
|
13895
|
-
self._verify_distance_sorted = False
|
|
13896
|
-
|
|
13897
14149
|
# Update stats label
|
|
13898
14150
|
self._update_verify_distance_stats()
|
|
13899
14151
|
|
|
@@ -13917,90 +14169,6 @@ class GUI(QMainWindow):
|
|
|
13917
14169
|
f"Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
|
|
13918
14170
|
)
|
|
13919
14171
|
|
|
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
14172
|
def cleanup_verify_distance(self):
|
|
14005
14173
|
"""Clear all checkboxes in Verify Distance subtab."""
|
|
14006
14174
|
if not hasattr(self, 'verify_distance_checkboxes'):
|
|
@@ -14072,9 +14240,13 @@ class GUI(QMainWindow):
|
|
|
14072
14240
|
for ch_idx, ch in enumerate(channels[:2]):
|
|
14073
14241
|
if ch < crop_block.shape[-1]:
|
|
14074
14242
|
channel_crop = crop_block[:, :, ch]
|
|
14075
|
-
|
|
14243
|
+
# Use 1st-99th percentile for normalization to reduce noise amplification
|
|
14244
|
+
cmin = np.nanpercentile(channel_crop, 1)
|
|
14245
|
+
cmax = np.nanpercentile(channel_crop, 99)
|
|
14076
14246
|
if cmax > cmin:
|
|
14077
|
-
|
|
14247
|
+
# Clip values outside the percentile range
|
|
14248
|
+
clipped = np.clip(channel_crop, cmin, cmax)
|
|
14249
|
+
norm = ((clipped - cmin) / (cmax - cmin) * 255).astype(np.uint8)
|
|
14078
14250
|
else:
|
|
14079
14251
|
norm = np.zeros_like(channel_crop, np.uint8)
|
|
14080
14252
|
h, w = norm.shape
|
|
@@ -14115,8 +14287,8 @@ class GUI(QMainWindow):
|
|
|
14115
14287
|
scroll_area.setWidget(container)
|
|
14116
14288
|
setattr(self, checkboxes_list_attr, checkboxes)
|
|
14117
14289
|
|
|
14118
|
-
# Note: sort_manual_colocalization() removed
|
|
14119
|
-
#
|
|
14290
|
+
# Note: sort_manual_colocalization() removed. sort_verify_visual() and
|
|
14291
|
+
# sort_verify_distance() merged into their respective populate_*() functions.
|
|
14120
14292
|
|
|
14121
14293
|
|
|
14122
14294
|
# =============================================================================
|
|
@@ -16130,6 +16302,21 @@ class GUI(QMainWindow):
|
|
|
16130
16302
|
self.edit_mask_selector.blockSignals(False)
|
|
16131
16303
|
if hasattr(self, 'edit_instructions_group'):
|
|
16132
16304
|
self.edit_instructions_group.setVisible(False)
|
|
16305
|
+
|
|
16306
|
+
# Reset Manual segmentation mode state
|
|
16307
|
+
if hasattr(self, 'cid_manual') and self.cid_manual is not None:
|
|
16308
|
+
try:
|
|
16309
|
+
self.canvas_segmentation.mpl_disconnect(self.cid_manual)
|
|
16310
|
+
except Exception:
|
|
16311
|
+
pass
|
|
16312
|
+
self.cid_manual = None
|
|
16313
|
+
# Clear polygon points
|
|
16314
|
+
self.manual_polygon_points = []
|
|
16315
|
+
if hasattr(self, 'btn_finish_polygon'):
|
|
16316
|
+
self.btn_finish_polygon.setEnabled(False)
|
|
16317
|
+
if hasattr(self, 'manual_status_label'):
|
|
16318
|
+
self.manual_status_label.setText("ℹ️ Click on image to place polygon vertices")
|
|
16319
|
+
self.manual_status_label.setStyleSheet("color: #888888; font-style: italic;")
|
|
16133
16320
|
|
|
16134
16321
|
def reset_photobleaching_tab(self):
|
|
16135
16322
|
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.25
|
|
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=JW-O7uWXCYdBnH59QPD7UhRi3lSkuOw9v5_K2std8rA,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=SiEC2BdCg35H1sSxUeFTzzSIKBATSKv-iQcIUQ4L5KQ,857080
|
|
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.25.dist-info/METADATA,sha256=JlvkcS0Phlf6NBugGZTlqUaFYNSZYUj2V-lA9vfeuiE,12462
|
|
24
|
+
microlive-1.0.25.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
25
|
+
microlive-1.0.25.dist-info/entry_points.txt,sha256=Zqp2vixyD8lngcfEmOi8fkCj7vPhesz5xlGBI-EubRw,54
|
|
26
|
+
microlive-1.0.25.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
27
|
+
microlive-1.0.25.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|