microlive 1.0.23__py3-none-any.whl → 1.0.25__py3-none-any.whl

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