microlive 1.0.22__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.22"
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:
@@ -6727,6 +6918,12 @@ class GUI(QMainWindow):
6727
6918
  if self.edit_mask_selector.count() <= 1:
6728
6919
  self.edit_status_label.setText("⚠ No masks available - run segmentation first")
6729
6920
  self.edit_status_label.setStyleSheet("color: #ff6666;")
6921
+ elif self.edit_mask_selector.count() == 2:
6922
+ # Single-Option Auto-Selection: only one mask available, select it automatically
6923
+ self.edit_mask_selector.setCurrentIndex(1)
6924
+ self.on_edit_mask_selector_changed(1)
6925
+ self.edit_status_label.setText("✓ Single mask auto-selected for editing")
6926
+ self.edit_status_label.setStyleSheet("color: #66ff66;")
6730
6927
  else:
6731
6928
  self.edit_status_label.setText("⚠ Select a mask to begin editing")
6732
6929
  self.edit_status_label.setStyleSheet("color: #ffcc00;")
@@ -7823,7 +8020,12 @@ class GUI(QMainWindow):
7823
8020
  self.update_threshold_histogram()
7824
8021
 
7825
8022
  def on_auto_threshold_clicked(self):
7826
- """Handle auto-threshold button click - calculate optimal threshold automatically."""
8023
+ """Handle auto-threshold button click - calculate optimal threshold automatically.
8024
+
8025
+ Calculates thresholds on multiple representative frames (beginning, middle, end)
8026
+ and averages them for more robust detection across temporal variability.
8027
+ Handles single-frame movies gracefully by using only the available frame.
8028
+ """
7827
8029
  if self.image_stack is None:
7828
8030
  self.statusBar().showMessage("No image loaded")
7829
8031
  return
@@ -7837,14 +8039,10 @@ class GUI(QMainWindow):
7837
8039
  return
7838
8040
 
7839
8041
  # Show progress
7840
- self.statusBar().showMessage("Calculating optimal threshold...")
8042
+ self.statusBar().showMessage("Calculating optimal threshold (multi-frame)...")
7841
8043
  QApplication.processEvents()
7842
8044
 
7843
8045
  try:
7844
- # Get current frame's image for this channel
7845
- # Shape: [Z, Y, X]
7846
- image_channel = image_to_use[self.current_frame, :, :, :, channel]
7847
-
7848
8046
  # Determine if using 3D or 2D mode
7849
8047
  use_3d = not self.use_maximum_projection
7850
8048
 
@@ -7856,20 +8054,64 @@ class GUI(QMainWindow):
7856
8054
  yx_spot_size = getattr(self, 'yx_spot_size_in_px', 5)
7857
8055
  z_spot_size = getattr(self, 'z_spot_size_in_px', 2)
7858
8056
 
7859
- # Calculate threshold using AutoThreshold class
7860
- auto_thresh = mi.AutoThreshold(
7861
- image=image_channel,
7862
- voxel_size_yx=voxel_yx,
7863
- voxel_size_z=voxel_z,
7864
- yx_spot_size_in_px=yx_spot_size,
7865
- z_spot_size_in_px=z_spot_size,
7866
- use_3d=use_3d
7867
- )
7868
- threshold_raw = auto_thresh.calculate()
7869
- 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
7870
8100
 
7871
- # Reduce threshold by 10% to improve spot coverage (auto-threshold tends to overestimate)
7872
- 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
7873
8115
 
7874
8116
  # Store per-channel
7875
8117
  self.auto_threshold_per_channel[channel] = threshold
@@ -7897,10 +8139,16 @@ class GUI(QMainWindow):
7897
8139
  # Auto-run single frame detection
7898
8140
  self.detect_spots_in_current_frame()
7899
8141
 
7900
- # Show result
7901
- self.statusBar().showMessage(
7902
- f"Auto-threshold Ch{channel}: {int(threshold)} (method: {method_used})"
7903
- )
8142
+ # Show result with frame count info
8143
+ if n_frames_used == 1:
8144
+ self.statusBar().showMessage(
8145
+ f"Auto-threshold Ch{channel}: {int(threshold)} (method: {method_used})"
8146
+ )
8147
+ else:
8148
+ self.statusBar().showMessage(
8149
+ f"Auto-threshold Ch{channel}: {int(threshold)} "
8150
+ f"(avg of {n_frames_used} frames, method: {method_used})"
8151
+ )
7904
8152
 
7905
8153
  except Exception as e:
7906
8154
  traceback.print_exc()
@@ -9257,6 +9505,7 @@ class GUI(QMainWindow):
9257
9505
  # Clear detection preview (not the multi-channel tracking data)
9258
9506
  self.detected_spots_frame = None
9259
9507
  self.reset_msd_tab()
9508
+ self.reset_colocalization_tab() # Clear stale colocalization results
9260
9509
  self.plot_tracking()
9261
9510
  # Get masks for tracking (supports both Cellpose and Segmentation)
9262
9511
  masks_complete, masks_nuc, masks_cyto_no_nuc = self._get_tracking_masks()
@@ -9911,7 +10160,10 @@ class GUI(QMainWindow):
9911
10160
  self.auto_threshold_btn = QPushButton("Auto")
9912
10161
  self.auto_threshold_btn.setFixedWidth(45)
9913
10162
  self.auto_threshold_btn.setFixedHeight(20)
9914
- self.auto_threshold_btn.setToolTip("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
+ )
9915
10167
  self.auto_threshold_btn.setStyleSheet("""
9916
10168
  QPushButton {
9917
10169
  background-color: #00d4aa;
@@ -12076,6 +12328,10 @@ class GUI(QMainWindow):
12076
12328
 
12077
12329
  # === Reset Manual Verify sub-tab ===
12078
12330
  self.reset_manual_colocalization()
12331
+
12332
+ # Reset sub-tab position to Visual (first tab)
12333
+ if hasattr(self, 'coloc_subtabs'):
12334
+ self.coloc_subtabs.setCurrentIndex(0)
12079
12335
 
12080
12336
  def extract_manual_colocalization_data(self, save_df=True):
12081
12337
  """Extract and optionally save manual colocalization data.
@@ -12662,10 +12918,6 @@ class GUI(QMainWindow):
12662
12918
  self.verify_visual_populate_button.clicked.connect(self.populate_verify_visual)
12663
12919
  top_bar.addWidget(self.verify_visual_populate_button)
12664
12920
 
12665
- self.verify_visual_sort_button = QPushButton("Sort")
12666
- self.verify_visual_sort_button.clicked.connect(self.sort_verify_visual)
12667
- top_bar.addWidget(self.verify_visual_sort_button)
12668
-
12669
12921
  self.verify_visual_cleanup_button = QPushButton("Cleanup")
12670
12922
  self.verify_visual_cleanup_button.clicked.connect(self.cleanup_verify_visual)
12671
12923
  top_bar.addWidget(self.verify_visual_cleanup_button)
@@ -12718,10 +12970,6 @@ class GUI(QMainWindow):
12718
12970
  self.verify_distance_populate_button.clicked.connect(self.populate_verify_distance)
12719
12971
  top_bar.addWidget(self.verify_distance_populate_button)
12720
12972
 
12721
- self.verify_distance_sort_button = QPushButton("Sort")
12722
- self.verify_distance_sort_button.clicked.connect(self.sort_verify_distance)
12723
- top_bar.addWidget(self.verify_distance_sort_button)
12724
-
12725
12973
  self.verify_distance_cleanup_button = QPushButton("Cleanup")
12726
12974
  self.verify_distance_cleanup_button.clicked.connect(self.cleanup_verify_distance)
12727
12975
  top_bar.addWidget(self.verify_distance_cleanup_button)
@@ -13525,7 +13773,10 @@ class GUI(QMainWindow):
13525
13773
  # === Verify Visual Subtab Methods ===
13526
13774
 
13527
13775
  def populate_verify_visual(self):
13528
- """Populate the Verify Visual subtab with Visual (ML/Intensity) colocalization results."""
13776
+ """Populate the Verify Visual subtab with Visual (ML/Intensity) colocalization results.
13777
+
13778
+ Automatically sorts spots by prediction value (uncertainty-first) for efficient review.
13779
+ """
13529
13780
  if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
13530
13781
  QMessageBox.warning(self, "No Results",
13531
13782
  "Please run Visual (ML/Intensity) colocalization first.")
@@ -13538,26 +13789,55 @@ class GUI(QMainWindow):
13538
13789
  crop_size = results.get('crop_size', 15)
13539
13790
  ch1 = results.get('ch1_index', 0)
13540
13791
  ch2 = results.get('ch2_index', 1)
13792
+ pred_values = results.get('prediction_values_vector')
13541
13793
 
13542
13794
  if flag_vector is None or mean_crop is None:
13543
13795
  QMessageBox.warning(self, "No Data", "Visual colocalization results are incomplete.")
13544
13796
  return
13545
13797
 
13546
- # 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)
13547
13830
  self._create_verification_crops(
13548
13831
  scroll_area=self.verify_visual_scroll_area,
13549
13832
  checkboxes_list_attr='verify_visual_checkboxes',
13550
- mean_crop=mean_crop,
13833
+ mean_crop=display_crop,
13551
13834
  crop_size=crop_size,
13552
- flag_vector=flag_vector,
13835
+ flag_vector=display_flags,
13553
13836
  stats_label=self.verify_visual_stats_label,
13554
13837
  num_channels=2,
13555
13838
  channels=(ch1, ch2)
13556
13839
  )
13557
13840
 
13558
- # Reset sorted flag so Sort button can be used
13559
- self._verify_visual_sorted = False
13560
-
13561
13841
  # Update stats label
13562
13842
  self._update_verify_visual_stats()
13563
13843
 
@@ -13574,78 +13854,6 @@ class GUI(QMainWindow):
13574
13854
  f"[{method}] Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
13575
13855
  )
13576
13856
 
13577
- def sort_verify_visual(self):
13578
- """Sort Verify Visual results by prediction value (lowest to highest for review)."""
13579
- if not hasattr(self, 'verify_visual_checkboxes') or len(self.verify_visual_checkboxes) == 0:
13580
- QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
13581
- return
13582
-
13583
- if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
13584
- QMessageBox.warning(self, "No Results", "No colocalization results available.")
13585
- return
13586
-
13587
- results = self.colocalization_results
13588
- values = results.get('prediction_values_vector')
13589
- mean_crop = results.get('mean_crop_filtered')
13590
- crop_size = results.get('crop_size', 15)
13591
- flag_vector = results.get('flag_vector')
13592
- ch1 = results.get('ch1_index', 0)
13593
- ch2 = results.get('ch2_index', 1)
13594
-
13595
- if values is None or len(values) == 0:
13596
- QMessageBox.information(self, "Cannot Sort", "No prediction values available for sorting.")
13597
- return
13598
-
13599
- if mean_crop is None:
13600
- QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
13601
- return
13602
-
13603
- # Check if already sorted (compare to original order)
13604
- if hasattr(self, '_verify_visual_sorted') and self._verify_visual_sorted:
13605
- QMessageBox.information(self, "Already Sorted", "Spots are already sorted by prediction value.")
13606
- return
13607
-
13608
- # Get current checkbox states before sorting
13609
- current_states = [chk.isChecked() for chk in self.verify_visual_checkboxes]
13610
-
13611
- # Create sorted indices (ascending by prediction value - uncertain first)
13612
- num_spots = len(values)
13613
- sorted_indices = np.argsort(values)
13614
-
13615
- # Re-order checkbox states to match new sort order
13616
- sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
13617
-
13618
- # Re-order crops - each spot is crop_size rows in the mean_crop array
13619
- num_crop_spots = mean_crop.shape[0] // crop_size
13620
- if num_crop_spots < num_spots:
13621
- num_spots = num_crop_spots
13622
- sorted_indices = sorted_indices[:num_spots]
13623
-
13624
- sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
13625
- for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
13626
- if old_idx < num_crop_spots:
13627
- sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
13628
- mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
13629
-
13630
- # Re-create verification crops with sorted data
13631
- self._create_verification_crops(
13632
- scroll_area=self.verify_visual_scroll_area,
13633
- checkboxes_list_attr='verify_visual_checkboxes',
13634
- mean_crop=sorted_crop,
13635
- crop_size=crop_size,
13636
- flag_vector=sorted_states, # Use previously checked states after reorder
13637
- stats_label=self.verify_visual_stats_label,
13638
- num_channels=2,
13639
- channels=(ch1, ch2)
13640
- )
13641
-
13642
- # Mark as sorted
13643
- self._verify_visual_sorted = True
13644
- self._verify_visual_sort_indices = sorted_indices
13645
-
13646
- # Update stats
13647
- self._update_verify_visual_stats()
13648
-
13649
13857
  def cleanup_verify_visual(self):
13650
13858
  """Clear all checkboxes in Verify Visual subtab."""
13651
13859
  if not hasattr(self, 'verify_visual_checkboxes'):
@@ -13870,26 +14078,54 @@ class GUI(QMainWindow):
13870
14078
  flag_vector.append(False)
13871
14079
  distance_values.append(threshold_px * 10.0)
13872
14080
 
13873
- # Store for later use (sorting, etc.)
14081
+ # Store for later use
13874
14082
  self.verify_distance_mean_crop = mean_crop
13875
14083
  self.verify_distance_crop_size = crop_size
13876
- self.verify_distance_values = np.array(distance_values) # 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
13877
14089
 
13878
- # 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)
13879
14118
  self._create_verification_crops(
13880
14119
  scroll_area=self.verify_distance_scroll_area,
13881
14120
  checkboxes_list_attr='verify_distance_checkboxes',
13882
- mean_crop=mean_crop,
14121
+ mean_crop=display_crop,
13883
14122
  crop_size=crop_size,
13884
- flag_vector=flag_vector,
14123
+ flag_vector=display_flags,
13885
14124
  stats_label=self.verify_distance_stats_label,
13886
14125
  num_channels=image.shape[-1] if image.ndim == 5 else 1,
13887
14126
  channels=(ch0, ch1)
13888
14127
  )
13889
14128
 
13890
- # Reset sorted flag so Sort button can be used
13891
- self._verify_distance_sorted = False
13892
-
13893
14129
  # Update stats label
13894
14130
  self._update_verify_distance_stats()
13895
14131
 
@@ -13913,90 +14149,6 @@ class GUI(QMainWindow):
13913
14149
  f"Total: {total} | Colocalized: {marked} ({pct:.1f}%)"
13914
14150
  )
13915
14151
 
13916
- def sort_verify_distance(self):
13917
- """Sort Verify Distance results by distance value (ascending - closest to threshold first).
13918
-
13919
- Similar to Visual method's certainty-based sorting, but uses the measured
13920
- distance to nearest partner. Spots with distances closest to the colocalization
13921
- threshold are shown first as they represent the most uncertain classifications.
13922
- """
13923
- if not hasattr(self, 'verify_distance_checkboxes') or len(self.verify_distance_checkboxes) == 0:
13924
- QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
13925
- return
13926
-
13927
- if not hasattr(self, 'verify_distance_mean_crop') or self.verify_distance_mean_crop is None:
13928
- QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
13929
- return
13930
-
13931
- # Check if distance values are available
13932
- if not hasattr(self, 'verify_distance_values') or self.verify_distance_values is None:
13933
- QMessageBox.warning(self, "No Distance Data",
13934
- "Distance values not available. Please re-run Populate.")
13935
- return
13936
-
13937
- # Check if already sorted
13938
- if hasattr(self, '_verify_distance_sorted') and self._verify_distance_sorted:
13939
- QMessageBox.information(self, "Already Sorted", "Spots are already sorted by distance value.")
13940
- return
13941
-
13942
- mean_crop = self.verify_distance_mean_crop
13943
- crop_size = self.verify_distance_crop_size
13944
- distance_values = self.verify_distance_values
13945
-
13946
- # Get current checkbox states before sorting
13947
- current_states = [chk.isChecked() for chk in self.verify_distance_checkboxes]
13948
- num_spots = len(current_states)
13949
-
13950
- # Sort ascending by distance (closest to threshold = most uncertain first)
13951
- # This matches the Visual method's approach of showing uncertain cases first
13952
- sorted_indices = np.argsort(distance_values)
13953
-
13954
- # Re-order states and distances
13955
- sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
13956
- sorted_distances = distance_values[sorted_indices]
13957
-
13958
- # Re-order crops
13959
- num_crop_spots = mean_crop.shape[0] // crop_size
13960
- if num_crop_spots < num_spots:
13961
- num_spots = num_crop_spots
13962
- sorted_indices = sorted_indices[:num_spots]
13963
-
13964
- sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
13965
- for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
13966
- if old_idx < num_crop_spots:
13967
- sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
13968
- mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
13969
-
13970
- # Get channels from distance results
13971
- results = self.distance_coloc_results if hasattr(self, 'distance_coloc_results') else {}
13972
- ch0 = results.get('channel_0', 0)
13973
- ch1 = results.get('channel_1', 1)
13974
- image = self.corrected_image if self.corrected_image is not None else self.image_stack
13975
- num_channels = image.shape[-1] if image is not None and image.ndim == 5 else 1
13976
-
13977
- # Re-create verification crops with sorted data
13978
- self._create_verification_crops(
13979
- scroll_area=self.verify_distance_scroll_area,
13980
- checkboxes_list_attr='verify_distance_checkboxes',
13981
- mean_crop=sorted_crop,
13982
- crop_size=crop_size,
13983
- flag_vector=sorted_states,
13984
- stats_label=self.verify_distance_stats_label,
13985
- num_channels=num_channels,
13986
- channels=(ch0, ch1)
13987
- )
13988
-
13989
- # Update stored data after sorting for consistency
13990
- self.verify_distance_mean_crop = sorted_crop
13991
- self.verify_distance_values = sorted_distances
13992
- self._verify_distance_sort_indices = sorted_indices # Store for reference
13993
-
13994
- # Mark as sorted
13995
- self._verify_distance_sorted = True
13996
-
13997
- # Update stats
13998
- self._update_verify_distance_stats()
13999
-
14000
14152
  def cleanup_verify_distance(self):
14001
14153
  """Clear all checkboxes in Verify Distance subtab."""
14002
14154
  if not hasattr(self, 'verify_distance_checkboxes'):
@@ -14111,8 +14263,8 @@ class GUI(QMainWindow):
14111
14263
  scroll_area.setWidget(container)
14112
14264
  setattr(self, checkboxes_list_attr, checkboxes)
14113
14265
 
14114
- # Note: sort_manual_colocalization() removed - replaced by sort_verify_visual()
14115
- # 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.
14116
14268
 
14117
14269
 
14118
14270
  # =============================================================================
@@ -16126,6 +16278,21 @@ class GUI(QMainWindow):
16126
16278
  self.edit_mask_selector.blockSignals(False)
16127
16279
  if hasattr(self, 'edit_instructions_group'):
16128
16280
  self.edit_instructions_group.setVisible(False)
16281
+
16282
+ # Reset Manual segmentation mode state
16283
+ if hasattr(self, 'cid_manual') and self.cid_manual is not None:
16284
+ try:
16285
+ self.canvas_segmentation.mpl_disconnect(self.cid_manual)
16286
+ except Exception:
16287
+ pass
16288
+ self.cid_manual = None
16289
+ # Clear polygon points
16290
+ self.manual_polygon_points = []
16291
+ if hasattr(self, 'btn_finish_polygon'):
16292
+ self.btn_finish_polygon.setEnabled(False)
16293
+ if hasattr(self, 'manual_status_label'):
16294
+ self.manual_status_label.setText("ℹ️ Click on image to place polygon vertices")
16295
+ self.manual_status_label.setStyleSheet("color: #888888; font-style: italic;")
16129
16296
 
16130
16297
  def reset_photobleaching_tab(self):
16131
16298
  self.figure_photobleaching.clear()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microlive
3
- Version: 1.0.22
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=CAGBPf0xGr2mMSTvHyNv3IvM7RMH4M9G2SLrlSbHt3U,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=bFqYMCAPD3lCD-n_HFdTYGfyOgt85dH4vGC4gncp1Yw,849379
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.22.dist-info/METADATA,sha256=LEehVwAK4XuGUIJuWJaYSHvrZOEsQ9rp5NWXs25dEnU,12462
24
- microlive-1.0.22.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
25
- microlive-1.0.22.dist-info/entry_points.txt,sha256=Zqp2vixyD8lngcfEmOi8fkCj7vPhesz5xlGBI-EubRw,54
26
- microlive-1.0.22.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
27
- microlive-1.0.22.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,,