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 CHANGED
@@ -23,7 +23,7 @@ Authors:
23
23
  Nathan L. Nowling, Brian Munsky, Ning Zhao
24
24
  """
25
25
 
26
- __version__ = "1.0.23"
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
- Enter manual segmentation mode:
3381
- - Display the current frame (or max‐proj) with filtering and clipping
3382
- - Clear any old manual mask
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
- print("No image loaded")
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 = self.image_stack[fr, :, :, :, ch]
3395
- img = np.max(image_channel, axis=0)
3396
- # smooth and clip for display
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
- fr = self.segmentation_current_frame
3427
- image_channel = self.image_stack[fr, :, :, :, ch]
3428
- max_proj = np.max(image_channel, axis=0)
3429
- max_proj = gaussian_filter(max_proj, sigma=2)
3430
- max_proj = np.clip(max_proj,
3431
- np.percentile(max_proj, 0.5),
3432
- np.percentile(max_proj, 99.))
3433
- self.ax_segmentation.clear()
3434
- self.ax_segmentation.imshow(max_proj, cmap='Spectral')
3435
- self.ax_segmentation.axis('off')
3436
- if len(self.selected_points) > 1:
3437
- polygon = np.array(self.selected_points)
3438
- self.ax_segmentation.plot(polygon[:, 0], polygon[:, 1], 'k-', lw=2)
3439
- self.ax_segmentation.plot(
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 finish_segmentation(self):
3448
- """
3449
- Terminate manual segmentation by disconnecting the click callback.
3450
- """
3451
- if hasattr(self, 'selected_points') and self.selected_points:
3452
- fr = self.segmentation_current_frame
3453
- ch = self.segmentation_current_channel
3454
- image_channel = self.image_stack[fr, :, :, :, ch]
3455
- # Apply Z selection
3456
- current_z = getattr(self, 'segmentation_current_z', -1)
3457
- if current_z == -1:
3458
- # Max Z-projection
3459
- max_proj = np.max(image_channel, axis=0)
3460
- self.segmentation_z_used_for_mask = -1
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
- # Specific Z-slice
3463
- z_idx = min(current_z, image_channel.shape[0] - 1)
3464
- max_proj = image_channel[z_idx, :, :]
3465
- self.segmentation_z_used_for_mask = z_idx
3466
- max_proj = gaussian_filter(max_proj, sigma=1)
3467
- max_proj = np.clip(max_proj, np.percentile(max_proj, 0.01), np.percentile(max_proj, 99.95))
3468
- # Create labeled mask with cell ID = 1 (not 255 which was incorrect)
3469
- # Use int32 dtype for proper labeled mask compatibility with tracking
3470
- mask = np.zeros(max_proj.shape[:2], dtype=np.int32)
3471
- polygon = np.array([self.selected_points], dtype=np.int32)
3472
- cv2.fillPoly(mask, polygon, 1) # Fill with cell ID 1, not 255
3473
- self.segmentation_mask = mask
3474
- self._active_mask_source = 'segmentation'
3475
- # Clear Cellpose/imported masks since we're using manual segmentation now
3476
- self.cellpose_masks_cyto = None
3477
- self.cellpose_masks_nuc = None
3478
- self.cellpose_masks_cyto_tyx = None
3479
- self.cellpose_masks_nuc_tyx = None
3480
- self.use_tyx_masks = False
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
- print("No points selected")
3499
- if hasattr(self, 'cid'):
3500
- try:
3501
- self.canvas_segmentation.mpl_disconnect(self.cid)
3502
- except Exception:
3503
- pass
3504
- del self.cid
3505
- self.selected_points = []
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 contours for segmentation mask
5323
+ # Draw mask overlay (semi-transparent, like Edit tab)
5218
5324
  if self.segmentation_mask is not None:
5219
- self.ax_segmentation.contour(self.segmentation_mask, levels=[0.5], colors='white', linewidths=1)
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
- segmentation_button (QPushButton)
5870
- finish_segmentation_button (QPushButton)
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
- manual_segmentation
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 label
6020
- manual_instructions = QLabel(
6021
- "Draw a polygon by clicking points on the image.\n"
6022
- "Click 'Manual Segmentation' to start, then click to add vertices.\n"
6023
- "Click 'Finish Segmentation' to complete the polygon."
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
- manual_instructions.setWordWrap(True)
6026
- manual_instructions.setStyleSheet("color: gray; font-size: 11px;")
6027
- manual_tab_layout.addWidget(manual_instructions)
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 for manual segmentation
6163
+ # Button row
6030
6164
  button_layout = QHBoxLayout()
6031
- self.segmentation_button = QPushButton("Manual Segmentation", self)
6032
- self.segmentation_button.clicked.connect(self.manual_segmentation)
6033
- button_layout.addWidget(self.segmentation_button)
6034
- self.finish_segmentation_button = QPushButton("Finish Segmentation", self)
6035
- self.finish_segmentation_button.clicked.connect(self.finish_segmentation)
6036
- button_layout.addWidget(self.finish_segmentation_button)
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
- self.edit_mask_selector.addItem(
6694
- f"Watershed Mask ({n_cells} cells, {shape[1]}×{shape[0]})",
6695
- "watershed"
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-select when only one mask is available
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
- # Calculate threshold using AutoThreshold class
7864
- auto_thresh = mi.AutoThreshold(
7865
- image=image_channel,
7866
- voxel_size_yx=voxel_yx,
7867
- voxel_size_z=voxel_z,
7868
- yx_spot_size_in_px=yx_spot_size,
7869
- z_spot_size_in_px=z_spot_size,
7870
- use_3d=use_3d
7871
- )
7872
- threshold_raw = auto_thresh.calculate()
7873
- method_used = auto_thresh.method_used
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
- # Reduce threshold by 10% to improve spot coverage (auto-threshold tends to overestimate)
7876
- threshold = threshold_raw * 0.9
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
- self.statusBar().showMessage(
7906
- f"Auto-threshold Ch{channel}: {int(threshold)} (method: {method_used})"
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("Auto-detect optimal threshold")
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
- # Create spot crops with checkboxes
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=mean_crop,
13833
+ mean_crop=display_crop,
13555
13834
  crop_size=crop_size,
13556
- flag_vector=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 (sorting, etc.)
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) # For sorting by distance
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
- # Create spot crops with checkboxes
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=mean_crop,
14121
+ mean_crop=display_crop,
13887
14122
  crop_size=crop_size,
13888
- flag_vector=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 - replaced by sort_verify_visual()
14119
- # and sort_verify_distance() in the separate verification subtabs
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.23
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=-MekCGFRKOy0VcMLrE5HZAa1NfGzKNvqxaubUKbq04w,1385
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=U8tFSMjgfoV5nXpOi_CdSIJ0wZOpGQwMe_4x4zBANZk,849593
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.23.dist-info/METADATA,sha256=6Obqn4Mns-cByOvPnVmbghxnROs0SC8Atao6nRYQGzA,12462
24
- microlive-1.0.23.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
- microlive-1.0.23.dist-info/entry_points.txt,sha256=Zqp2vixyD8lngcfEmOi8fkCj7vPhesz5xlGBI-EubRw,54
26
- microlive-1.0.23.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
27
- microlive-1.0.23.dist-info/RECORD,,
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,,