microlive 1.0.14__py3-none-any.whl → 1.0.16__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.14"
26
+ __version__ = "1.0.16"
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
@@ -101,6 +101,7 @@ from matplotlib.backends.backend_qt5agg import (
101
101
  FigureCanvasQTAgg as FigureCanvas,
102
102
  NavigationToolbar2QT as NavigationToolbar,)
103
103
  from mpl_toolkits.axes_grid1.inset_locator import inset_axes
104
+ from mpl_toolkits.mplot3d import Axes3D # For 3D intensity profile visualization
104
105
  from functools import partial
105
106
  from scipy.optimize import curve_fit
106
107
  from scipy.ndimage import gaussian_filter, label, center_of_mass, distance_transform_edt
@@ -2841,12 +2842,21 @@ class GUI(QMainWindow):
2841
2842
  if hasattr(self, 'time_slider_display'):
2842
2843
  self.time_slider_display.setEnabled(False)
2843
2844
  self.time_slider_display.setValue(0)
2845
+ self.time_slider_display.setMaximum(0)
2846
+ if hasattr(self, 'frame_label_display'):
2847
+ self.frame_label_display.setText("0/0")
2844
2848
  if hasattr(self, 'play_button_display'):
2845
2849
  self.play_button_display.setEnabled(False)
2846
2850
  if hasattr(self, 'time_slider_tracking'):
2847
2851
  self.time_slider_tracking.setValue(0)
2852
+ self.time_slider_tracking.setMaximum(0)
2853
+ if hasattr(self, 'frame_label_tracking'):
2854
+ self.frame_label_tracking.setText("0/0")
2848
2855
  if hasattr(self, 'time_slider_tracking_vis'):
2849
2856
  self.time_slider_tracking_vis.setValue(0)
2857
+ self.time_slider_tracking_vis.setMaximum(0)
2858
+ if hasattr(self, 'frame_label_tracking_vis'):
2859
+ self.frame_label_tracking_vis.setText("0/0")
2850
2860
 
2851
2861
  # Stop any playing timers
2852
2862
  self.stop_all_playback()
@@ -2937,12 +2947,21 @@ class GUI(QMainWindow):
2937
2947
  if hasattr(self, 'time_slider_display'):
2938
2948
  self.time_slider_display.setEnabled(False)
2939
2949
  self.time_slider_display.setValue(0)
2950
+ self.time_slider_display.setMaximum(0)
2951
+ if hasattr(self, 'frame_label_display'):
2952
+ self.frame_label_display.setText("0/0")
2940
2953
  if hasattr(self, 'play_button_display'):
2941
2954
  self.play_button_display.setEnabled(False)
2942
2955
  if hasattr(self, 'time_slider_tracking'):
2943
2956
  self.time_slider_tracking.setValue(0)
2957
+ self.time_slider_tracking.setMaximum(0)
2958
+ if hasattr(self, 'frame_label_tracking'):
2959
+ self.frame_label_tracking.setText("0/0")
2944
2960
  if hasattr(self, 'time_slider_tracking_vis'):
2945
2961
  self.time_slider_tracking_vis.setValue(0)
2962
+ self.time_slider_tracking_vis.setMaximum(0)
2963
+ if hasattr(self, 'frame_label_tracking_vis'):
2964
+ self.frame_label_tracking_vis.setText("0/0")
2946
2965
 
2947
2966
  # Stop any playing timers
2948
2967
  self.stop_all_playback()
@@ -9054,7 +9073,7 @@ class GUI(QMainWindow):
9054
9073
  # Cluster radius
9055
9074
  self.cluster_radius_input = QSpinBox()
9056
9075
  self.cluster_radius_input.setMinimum(100)
9057
- self.cluster_radius_input.setMaximum(2000)
9076
+ self.cluster_radius_input.setMaximum(6000)
9058
9077
  self.cluster_radius_input.setValue(self.cluster_radius_nm)
9059
9078
  self.cluster_radius_input.valueChanged.connect(self.update_cluster_radius)
9060
9079
  params_layout.addRow("Cluster radius (nm):", self.cluster_radius_input)
@@ -11701,9 +11720,13 @@ class GUI(QMainWindow):
11701
11720
  layout = QVBoxLayout(self.coloc_verify_distance_widget)
11702
11721
  layout.setContentsMargins(10, 5, 10, 5)
11703
11722
 
11704
- # Info label
11705
- info_label = QLabel("Review and correct Distance-based colocalization results:")
11723
+ # Info label explaining what is displayed
11724
+ info_label = QLabel(
11725
+ "Review unique particle tracks. Each row shows a time-averaged crop. "
11726
+ "A track is marked colocalized (✓) if ANY frame is within the distance threshold."
11727
+ )
11706
11728
  info_label.setStyleSheet("font-style: italic; color: #999;")
11729
+ info_label.setWordWrap(True)
11707
11730
  layout.addWidget(info_label)
11708
11731
 
11709
11732
  # Top bar with stats and buttons
@@ -12553,6 +12576,9 @@ class GUI(QMainWindow):
12553
12576
  channels=(ch1, ch2)
12554
12577
  )
12555
12578
 
12579
+ # Reset sorted flag so Sort button can be used
12580
+ self._verify_visual_sorted = False
12581
+
12556
12582
  # Update stats label
12557
12583
  self._update_verify_visual_stats()
12558
12584
 
@@ -12570,20 +12596,76 @@ class GUI(QMainWindow):
12570
12596
  )
12571
12597
 
12572
12598
  def sort_verify_visual(self):
12573
- """Sort Verify Visual results by prediction value (lowest to highest)."""
12599
+ """Sort Verify Visual results by prediction value (lowest to highest for review)."""
12574
12600
  if not hasattr(self, 'verify_visual_checkboxes') or len(self.verify_visual_checkboxes) == 0:
12601
+ QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
12602
+ return
12603
+
12604
+ if not hasattr(self, 'colocalization_results') or not self.colocalization_results:
12605
+ QMessageBox.warning(self, "No Results", "No colocalization results available.")
12575
12606
  return
12576
12607
 
12577
- values = self.colocalization_results.get('prediction_values_vector') if hasattr(self, 'colocalization_results') else None
12608
+ results = self.colocalization_results
12609
+ values = results.get('prediction_values_vector')
12610
+ mean_crop = results.get('mean_crop_filtered')
12611
+ crop_size = results.get('crop_size', 15)
12612
+ flag_vector = results.get('flag_vector')
12613
+ ch1 = results.get('ch1_index', 0)
12614
+ ch2 = results.get('ch2_index', 1)
12615
+
12578
12616
  if values is None or len(values) == 0:
12579
12617
  QMessageBox.information(self, "Cannot Sort", "No prediction values available for sorting.")
12580
12618
  return
12581
12619
 
12582
- # Re-populate with sorted order would require rebuilding crops
12583
- # For now, show a message that sorting is based on visual arrangement
12584
- QMessageBox.information(self, "Sort",
12585
- "Spots are already displayed in their original detection order. "
12586
- "Lower prediction values indicate uncertain colocalization.")
12620
+ if mean_crop is None:
12621
+ QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
12622
+ return
12623
+
12624
+ # Check if already sorted (compare to original order)
12625
+ if hasattr(self, '_verify_visual_sorted') and self._verify_visual_sorted:
12626
+ QMessageBox.information(self, "Already Sorted", "Spots are already sorted by prediction value.")
12627
+ return
12628
+
12629
+ # Get current checkbox states before sorting
12630
+ current_states = [chk.isChecked() for chk in self.verify_visual_checkboxes]
12631
+
12632
+ # Create sorted indices (ascending by prediction value - uncertain first)
12633
+ num_spots = len(values)
12634
+ sorted_indices = np.argsort(values)
12635
+
12636
+ # Re-order checkbox states to match new sort order
12637
+ sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
12638
+
12639
+ # Re-order crops - each spot is crop_size rows in the mean_crop array
12640
+ num_crop_spots = mean_crop.shape[0] // crop_size
12641
+ if num_crop_spots < num_spots:
12642
+ num_spots = num_crop_spots
12643
+ sorted_indices = sorted_indices[:num_spots]
12644
+
12645
+ sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
12646
+ for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
12647
+ if old_idx < num_crop_spots:
12648
+ sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
12649
+ mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
12650
+
12651
+ # Re-create verification crops with sorted data
12652
+ self._create_verification_crops(
12653
+ scroll_area=self.verify_visual_scroll_area,
12654
+ checkboxes_list_attr='verify_visual_checkboxes',
12655
+ mean_crop=sorted_crop,
12656
+ crop_size=crop_size,
12657
+ flag_vector=sorted_states, # Use previously checked states after reorder
12658
+ stats_label=self.verify_visual_stats_label,
12659
+ num_channels=2,
12660
+ channels=(ch1, ch2)
12661
+ )
12662
+
12663
+ # Mark as sorted
12664
+ self._verify_visual_sorted = True
12665
+ self._verify_visual_sort_indices = sorted_indices
12666
+
12667
+ # Update stats
12668
+ self._update_verify_visual_stats()
12587
12669
 
12588
12670
  def cleanup_verify_visual(self):
12589
12671
  """Clear all checkboxes in Verify Visual subtab."""
@@ -12630,7 +12712,11 @@ class GUI(QMainWindow):
12630
12712
  # === Verify Distance Subtab Methods ===
12631
12713
 
12632
12714
  def populate_verify_distance(self):
12633
- """Populate the Verify Distance subtab with Distance colocalization results."""
12715
+ """Populate the Verify Distance subtab with Distance colocalization results.
12716
+
12717
+ Calculates and stores the minimum distance from each reference channel spot
12718
+ to its nearest partner in the target channel for sorting purposes.
12719
+ """
12634
12720
  if not hasattr(self, 'distance_coloc_results') or not self.distance_coloc_results:
12635
12721
  QMessageBox.warning(self, "No Results",
12636
12722
  "Please run Distance colocalization first.")
@@ -12641,8 +12727,10 @@ class GUI(QMainWindow):
12641
12727
  ch0 = results.get('channel_0', 0)
12642
12728
  ch1 = results.get('channel_1', 1)
12643
12729
  df_coloc = results.get('df_colocalized', pd.DataFrame())
12730
+ df_ch1_all = results.get('df_ch1_all', pd.DataFrame())
12644
12731
  threshold_px = results.get('threshold_distance_px', 2.0)
12645
12732
  threshold_nm = results.get('threshold_distance_nm', 130.0)
12733
+ use_3d = results.get('use_3d', False)
12646
12734
 
12647
12735
  # We need to create crops from tracking data
12648
12736
  if not hasattr(self, 'df_tracking') or self.df_tracking.empty:
@@ -12684,28 +12772,129 @@ class GUI(QMainWindow):
12684
12772
 
12685
12773
  num_spots = mean_crop.shape[0] // crop_size
12686
12774
 
12687
- # Create flag vector based on distance colocalization
12688
- # Mark spots as colocalized if their coordinates match
12689
- coloc_coords = set()
12775
+ # Build set of colocalized coordinates for matching
12776
+ # Use a tolerance-based approach instead of exact coordinate matching
12777
+ coloc_coords_array = np.empty((0, 4)) # z, y, x, cell_id
12690
12778
  if not df_coloc.empty:
12691
- for _, row in df_coloc.iterrows():
12692
- coord = (round(row.get('x', 0), 1), round(row.get('y', 0), 1))
12693
- coloc_coords.add(coord)
12694
-
12779
+ if 'z' in df_coloc.columns and use_3d:
12780
+ coloc_coords_array = df_coloc[['z', 'y', 'x', 'cell_id']].values
12781
+ else:
12782
+ # Add dummy z=0 for 2D matching
12783
+ coloc_coords_array = np.column_stack([
12784
+ np.zeros(len(df_coloc)),
12785
+ df_coloc['y'].values,
12786
+ df_coloc['x'].values,
12787
+ df_coloc['cell_id'].values
12788
+ ])
12789
+
12790
+ # Calculate minimum distances for each spot in ch0 to nearest spot in ch1
12791
+ # This will be used for sorting (ascending = closest to threshold = most uncertain)
12792
+ distance_values = []
12695
12793
  flag_vector = []
12696
- for i, (_, row) in enumerate(df_ch0.drop_duplicates(subset=['particle']).iterrows()):
12794
+
12795
+ # Get ch1 coordinates for distance calculation
12796
+ ch1_coords = None
12797
+ if not df_ch1_all.empty and 'x' in df_ch1_all.columns and 'y' in df_ch1_all.columns:
12798
+ if use_3d and 'z' in df_ch1_all.columns:
12799
+ ch1_coords = df_ch1_all[['z', 'y', 'x']].values
12800
+ else:
12801
+ ch1_coords = df_ch1_all[['y', 'x']].values
12802
+
12803
+ # Get anisotropic scaling for 3D
12804
+ voxel_z_nm = results.get('voxel_z_nm', 300.0)
12805
+ voxel_xy_nm = results.get('voxel_xy_nm', 130.0)
12806
+ z_scale = voxel_z_nm / voxel_xy_nm if use_3d and voxel_xy_nm > 0 else 1.0
12807
+
12808
+ # Use the same particle column identification as CropArray
12809
+ # This ensures our iteration matches the crop order
12810
+ df_ch0_copy = df_ch0.copy()
12811
+ if 'unique_particle' in df_ch0_copy.columns:
12812
+ particle_col = 'unique_particle'
12813
+ elif 'cell_id' in df_ch0_copy.columns:
12814
+ if 'spot_type' in df_ch0_copy.columns:
12815
+ df_ch0_copy['unique_particle'] = (
12816
+ df_ch0_copy['cell_id'].astype(str) + '_' +
12817
+ df_ch0_copy['spot_type'].astype(str) + '_' +
12818
+ df_ch0_copy['particle'].astype(str)
12819
+ )
12820
+ else:
12821
+ df_ch0_copy['unique_particle'] = (
12822
+ df_ch0_copy['cell_id'].astype(str) + '_' +
12823
+ df_ch0_copy['particle'].astype(str)
12824
+ )
12825
+ particle_col = 'unique_particle'
12826
+ else:
12827
+ particle_col = 'particle'
12828
+
12829
+ # Helper function to check if a spot coordinate is in the colocalized set
12830
+ def is_coord_colocalized(z, y, x, cell_id, coloc_arr, tolerance=1.0):
12831
+ """Check if a spot is in the colocalized set using coordinate tolerance."""
12832
+ if len(coloc_arr) == 0:
12833
+ return False
12834
+ # Filter by cell_id first for efficiency
12835
+ cell_mask = coloc_arr[:, 3].astype(int) == int(cell_id)
12836
+ cell_coloc = coloc_arr[cell_mask]
12837
+ if len(cell_coloc) == 0:
12838
+ return False
12839
+ # Check distance to each colocalized spot
12840
+ for cz, cy, cx, _ in cell_coloc:
12841
+ dist_xy = np.sqrt((x - cx)**2 + (y - cy)**2)
12842
+ dist_z = abs(z - cz) if use_3d else 0
12843
+ if dist_xy <= tolerance and dist_z <= tolerance:
12844
+ return True
12845
+ return False
12846
+
12847
+ # Iterate unique particles in the same order as CropArray
12848
+ unique_particles = df_ch0_copy[particle_col].unique()
12849
+
12850
+ for i, particle_id in enumerate(unique_particles):
12697
12851
  if i >= num_spots:
12698
12852
  break
12699
- coord = (round(row.get('x', 0), 1), round(row.get('y', 0), 1))
12700
- flag_vector.append(coord in coloc_coords)
12853
+
12854
+ df_particle = df_ch0_copy[df_ch0_copy[particle_col] == particle_id]
12855
+
12856
+ # Check if ANY observation of this particle is colocalized
12857
+ is_coloc = False
12858
+ min_dist_all = threshold_px * 10.0 # Large default
12859
+
12860
+ for _, row in df_particle.iterrows():
12861
+ x_val, y_val = row['x'], row['y']
12862
+ z_val = row.get('z', 0)
12863
+ cell_id = row.get('cell_id', 0)
12864
+
12865
+ # Check if this observation is in the colocalized set
12866
+ if len(coloc_coords_array) > 0:
12867
+ if is_coord_colocalized(z_val, y_val, x_val, cell_id, coloc_coords_array, tolerance=1.0):
12868
+ is_coloc = True
12869
+
12870
+ # Calculate minimum distance to any ch1 spot for this observation
12871
+ if ch1_coords is not None and len(ch1_coords) > 0:
12872
+ if use_3d and ch1_coords.shape[1] == 3:
12873
+ spot_coord = np.array([[z_val * z_scale, y_val, x_val]])
12874
+ ch1_scaled = ch1_coords.copy().astype(float)
12875
+ ch1_scaled[:, 0] = ch1_scaled[:, 0] * z_scale # Scale Z
12876
+ else:
12877
+ spot_coord = np.array([[y_val, x_val]])
12878
+ ch1_scaled = ch1_coords
12879
+
12880
+ from scipy.spatial.distance import cdist
12881
+ distances = cdist(spot_coord, ch1_scaled, metric='euclidean')
12882
+ obs_min_dist = float(np.min(distances))
12883
+ if obs_min_dist < min_dist_all:
12884
+ min_dist_all = obs_min_dist
12885
+
12886
+ flag_vector.append(is_coloc)
12887
+ distance_values.append(min_dist_all)
12701
12888
 
12702
- # Pad flag_vector if needed
12889
+ # Pad vectors if needed (shouldn't happen, but just in case)
12703
12890
  while len(flag_vector) < num_spots:
12704
12891
  flag_vector.append(False)
12892
+ distance_values.append(threshold_px * 10.0)
12705
12893
 
12706
- # Store for later use
12894
+ # Store for later use (sorting, etc.)
12707
12895
  self.verify_distance_mean_crop = mean_crop
12708
12896
  self.verify_distance_crop_size = crop_size
12897
+ self.verify_distance_values = np.array(distance_values) # For sorting by distance
12709
12898
 
12710
12899
  # Create spot crops with checkboxes
12711
12900
  self._create_verification_crops(
@@ -12719,6 +12908,9 @@ class GUI(QMainWindow):
12719
12908
  channels=(ch0, ch1)
12720
12909
  )
12721
12910
 
12911
+ # Reset sorted flag so Sort button can be used
12912
+ self._verify_distance_sorted = False
12913
+
12722
12914
  # Update stats label
12723
12915
  self._update_verify_distance_stats()
12724
12916
 
@@ -12743,9 +12935,88 @@ class GUI(QMainWindow):
12743
12935
  )
12744
12936
 
12745
12937
  def sort_verify_distance(self):
12746
- """Sort Verify Distance results (by cell ID or coordinate)."""
12747
- QMessageBox.information(self, "Sort",
12748
- "Distance colocalization spots are displayed in detection order.")
12938
+ """Sort Verify Distance results by distance value (ascending - closest to threshold first).
12939
+
12940
+ Similar to Visual method's certainty-based sorting, but uses the measured
12941
+ distance to nearest partner. Spots with distances closest to the colocalization
12942
+ threshold are shown first as they represent the most uncertain classifications.
12943
+ """
12944
+ if not hasattr(self, 'verify_distance_checkboxes') or len(self.verify_distance_checkboxes) == 0:
12945
+ QMessageBox.information(self, "No Data", "No spots to sort. Please click Populate first.")
12946
+ return
12947
+
12948
+ if not hasattr(self, 'verify_distance_mean_crop') or self.verify_distance_mean_crop is None:
12949
+ QMessageBox.warning(self, "No Data", "Crop data not available for sorting.")
12950
+ return
12951
+
12952
+ # Check if distance values are available
12953
+ if not hasattr(self, 'verify_distance_values') or self.verify_distance_values is None:
12954
+ QMessageBox.warning(self, "No Distance Data",
12955
+ "Distance values not available. Please re-run Populate.")
12956
+ return
12957
+
12958
+ # Check if already sorted
12959
+ if hasattr(self, '_verify_distance_sorted') and self._verify_distance_sorted:
12960
+ QMessageBox.information(self, "Already Sorted", "Spots are already sorted by distance value.")
12961
+ return
12962
+
12963
+ mean_crop = self.verify_distance_mean_crop
12964
+ crop_size = self.verify_distance_crop_size
12965
+ distance_values = self.verify_distance_values
12966
+
12967
+ # Get current checkbox states before sorting
12968
+ current_states = [chk.isChecked() for chk in self.verify_distance_checkboxes]
12969
+ num_spots = len(current_states)
12970
+
12971
+ # Sort ascending by distance (closest to threshold = most uncertain first)
12972
+ # This matches the Visual method's approach of showing uncertain cases first
12973
+ sorted_indices = np.argsort(distance_values)
12974
+
12975
+ # Re-order states and distances
12976
+ sorted_states = [current_states[i] if i < len(current_states) else False for i in sorted_indices]
12977
+ sorted_distances = distance_values[sorted_indices]
12978
+
12979
+ # Re-order crops
12980
+ num_crop_spots = mean_crop.shape[0] // crop_size
12981
+ if num_crop_spots < num_spots:
12982
+ num_spots = num_crop_spots
12983
+ sorted_indices = sorted_indices[:num_spots]
12984
+
12985
+ sorted_crop = np.zeros_like(mean_crop[:num_spots*crop_size])
12986
+ for new_idx, old_idx in enumerate(sorted_indices[:num_spots]):
12987
+ if old_idx < num_crop_spots:
12988
+ sorted_crop[new_idx*crop_size:(new_idx+1)*crop_size] = \
12989
+ mean_crop[old_idx*crop_size:(old_idx+1)*crop_size]
12990
+
12991
+ # Get channels from distance results
12992
+ results = self.distance_coloc_results if hasattr(self, 'distance_coloc_results') else {}
12993
+ ch0 = results.get('channel_0', 0)
12994
+ ch1 = results.get('channel_1', 1)
12995
+ image = self.corrected_image if self.corrected_image is not None else self.image_stack
12996
+ num_channels = image.shape[-1] if image is not None and image.ndim == 5 else 1
12997
+
12998
+ # Re-create verification crops with sorted data
12999
+ self._create_verification_crops(
13000
+ scroll_area=self.verify_distance_scroll_area,
13001
+ checkboxes_list_attr='verify_distance_checkboxes',
13002
+ mean_crop=sorted_crop,
13003
+ crop_size=crop_size,
13004
+ flag_vector=sorted_states,
13005
+ stats_label=self.verify_distance_stats_label,
13006
+ num_channels=num_channels,
13007
+ channels=(ch0, ch1)
13008
+ )
13009
+
13010
+ # Update stored data after sorting for consistency
13011
+ self.verify_distance_mean_crop = sorted_crop
13012
+ self.verify_distance_values = sorted_distances
13013
+ self._verify_distance_sort_indices = sorted_indices # Store for reference
13014
+
13015
+ # Mark as sorted
13016
+ self._verify_distance_sorted = True
13017
+
13018
+ # Update stats
13019
+ self._update_verify_distance_stats()
12749
13020
 
12750
13021
  def cleanup_verify_distance(self):
12751
13022
  """Clear all checkboxes in Verify Distance subtab."""
@@ -12833,7 +13104,12 @@ class GUI(QMainWindow):
12833
13104
 
12834
13105
  # Checkbox
12835
13106
  chk = QCheckBox(f"Spot {i+1}")
12836
- chk.setChecked(bool(flag_vector[i]) if i < len(flag_vector) else False)
13107
+ # Safely get the flag value (handle numpy arrays, lists, etc.)
13108
+ try:
13109
+ flag_val = bool(flag_vector[i]) if i < len(flag_vector) else False
13110
+ except (TypeError, IndexError):
13111
+ flag_val = False
13112
+ chk.setChecked(flag_val)
12837
13113
  chk.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
12838
13114
 
12839
13115
  # Connect to stats update
@@ -12897,7 +13173,18 @@ class GUI(QMainWindow):
12897
13173
  spot_coord = (int(dfm.iloc[0]['y']), int(dfm.iloc[0]['x']))
12898
13174
  found_spot = True
12899
13175
  else:
12900
- spot_coord = (0, 0)
13176
+ # Particle not in current frame - jump to first valid frame for this particle
13177
+ df_particle = self.df_tracking[self.df_tracking[particle_col] == pid]
13178
+ if not df_particle.empty:
13179
+ first_frame = int(df_particle['frame'].min())
13180
+ self.current_frame = first_frame
13181
+ if hasattr(self, 'time_slider_tracking_vis'):
13182
+ self.time_slider_tracking_vis.setValue(first_frame)
13183
+ dfm = df_particle[df_particle['frame'] == first_frame]
13184
+ spot_coord = (int(dfm.iloc[0]['y']), int(dfm.iloc[0]['x']))
13185
+ found_spot = True
13186
+ else:
13187
+ spot_coord = (0, 0)
12901
13188
  else:
12902
13189
  spot_coord = (0, 0)
12903
13190
  else:
@@ -12956,7 +13243,12 @@ class GUI(QMainWindow):
12956
13243
  else:
12957
13244
  main_img = norm_stack[selected_channelIndex]
12958
13245
  main_cmap = cmap_list_imagej[selected_channelIndex % len(cmap_list_imagej)]
12959
- gs = fig.add_gridspec(1, 2, width_ratios=[3, 2], hspace=0.1, wspace=0.1)
13246
+
13247
+ # Always show 3D intensity profile for spot quality assessment
13248
+ show_3d_profile = True
13249
+
13250
+ # 3-column layout: main image + 2D crops + 3D surfaces
13251
+ gs = fig.add_gridspec(1, 3, width_ratios=[3, 1, 1.5], hspace=0.1, wspace=0.15)
12960
13252
  ax_main = fig.add_subplot(gs[0, 0])
12961
13253
 
12962
13254
  # Store main axes reference and recreate RectangleSelector
@@ -12972,8 +13264,15 @@ class GUI(QMainWindow):
12972
13264
  props=dict(facecolor='cyan', edgecolor='white', alpha=0.3, linewidth=2)
12973
13265
  )
12974
13266
 
13267
+ # 2D crop subgrid
12975
13268
  gs2 = gs[0, 1].subgridspec(C, 1, hspace=0.1)
12976
- axes_zoom = [fig.add_subplot(gs2[i, 0]) for i in range(C)]
13269
+ axes_zoom = [fig.add_subplot(gs2[i, 0]) for i in range(C)]
13270
+
13271
+ # 3D profile subgrid (only create if enabled)
13272
+ axes_3d = []
13273
+ if show_3d_profile:
13274
+ gs3 = gs[0, 2].subgridspec(C, 1, hspace=0.15)
13275
+ axes_3d = [fig.add_subplot(gs3[i, 0], projection='3d') for i in range(C)]
12977
13276
  # remove background if requested
12978
13277
  if hasattr(self, 'checkbox_remove_bg') and self.checkbox_remove_bg.isChecked():
12979
13278
  seg_mask = getattr(self, 'segmentation_mask', None)
@@ -13027,6 +13326,15 @@ class GUI(QMainWindow):
13027
13326
  rect = patches.Rectangle((x0, y0), crop_sz, crop_sz, edgecolor='white', facecolor='none', linewidth=2)
13028
13327
  ax_main.add_patch(rect)
13029
13328
  ax_main.axis('off')
13329
+
13330
+ # Add thin border to show image boundaries (matching Tracking tab style)
13331
+ # Use -0.5 origin and full size to place border outside the image
13332
+ if self.image_stack is not None:
13333
+ img_H, img_W = main_img.shape[:2]
13334
+ img_border = patches.Rectangle((-0.5, -0.5), img_W, img_H, linewidth=0.8,
13335
+ edgecolor='#555555', facecolor='none', linestyle='-')
13336
+ ax_main.add_patch(img_border)
13337
+
13030
13338
  for ci, ax in enumerate(axes_zoom):
13031
13339
  if found_spot:
13032
13340
  crop = norm_stack[ci, y0:y1, x0:x1]
@@ -13034,6 +13342,59 @@ class GUI(QMainWindow):
13034
13342
  crop = np.zeros((crop_sz, crop_sz))
13035
13343
  ax.imshow(crop, cmap=cmap_list_imagej[ci % len(cmap_list_imagej)], interpolation='nearest', vmin=0, vmax=1)
13036
13344
  ax.axis('off')
13345
+ # Add gray frame border to 2D crops using Rectangle (spines hidden by axis('off'))
13346
+ # Use -0.5 origin and full size to place border outside the image (matplotlib centers pixels at integers)
13347
+ crop_h, crop_w = crop.shape[:2]
13348
+ crop_border = patches.Rectangle((-0.5, -0.5), crop_w, crop_h, linewidth=0.8,
13349
+ edgecolor='#555555', facecolor='none', linestyle='-')
13350
+ ax.add_patch(crop_border)
13351
+
13352
+ # Render 3D intensity profiles if enabled
13353
+ if show_3d_profile and axes_3d:
13354
+ # Create meshgrid for surface plot (only once, using crop dimensions)
13355
+ crop_example = norm_stack[0, y0:y1, x0:x1] if found_spot else np.zeros((crop_sz, crop_sz))
13356
+ Y_grid, X_grid = np.meshgrid(np.arange(crop_example.shape[0]),
13357
+ np.arange(crop_example.shape[1]), indexing='ij')
13358
+
13359
+ for ci, ax3d in enumerate(axes_3d):
13360
+ if found_spot:
13361
+ crop = norm_stack[ci, y0:y1, x0:x1]
13362
+ else:
13363
+ crop = np.zeros((crop_sz, crop_sz))
13364
+
13365
+ # Get channel colormap
13366
+ cmap = cmap_list_imagej[ci % len(cmap_list_imagej)]
13367
+
13368
+ # Plot surface with matching colormap
13369
+ ax3d.plot_surface(X_grid, Y_grid, crop, cmap=cmap,
13370
+ edgecolor='none', alpha=0.9, antialiased=True)
13371
+
13372
+ # Style the 3D axes for dark theme
13373
+ ax3d.set_facecolor('black')
13374
+ ax3d.set_xlabel('X', fontsize=7, color='white', labelpad=-2)
13375
+ ax3d.set_ylabel('Y', fontsize=7, color='white', labelpad=-2)
13376
+ ax3d.set_zlabel('I', fontsize=7, color='white', labelpad=-2)
13377
+ ax3d.tick_params(axis='both', which='major', labelsize=5, colors='white', pad=0)
13378
+ ax3d.tick_params(axis='z', which='major', labelsize=5, colors='white', pad=0)
13379
+
13380
+ # Set consistent Z limits for comparison across channels
13381
+ ax3d.set_zlim(0, 1)
13382
+
13383
+ # Make pane and grid styling match dark theme
13384
+ ax3d.xaxis.pane.fill = False
13385
+ ax3d.yaxis.pane.fill = False
13386
+ ax3d.zaxis.pane.fill = False
13387
+ ax3d.xaxis.pane.set_edgecolor('gray')
13388
+ ax3d.yaxis.pane.set_edgecolor('gray')
13389
+ ax3d.zaxis.pane.set_edgecolor('gray')
13390
+ ax3d.grid(True, alpha=0.3, color='gray')
13391
+
13392
+ # Add channel label
13393
+ ax3d.set_title(f'Ch{ci}', fontsize=8, color='white', pad=-5)
13394
+
13395
+ # Set viewing angle for nice perspective
13396
+ ax3d.view_init(elev=25, azim=-45)
13397
+
13037
13398
  fig.tight_layout()
13038
13399
 
13039
13400
  # Add thin white frame border to main image
@@ -15475,6 +15836,7 @@ class GUI(QMainWindow):
15475
15836
  self.verify_visual_checkboxes = []
15476
15837
  if hasattr(self, 'verify_visual_stats_label'):
15477
15838
  self.verify_visual_stats_label.setText("Run Visual colocalization first, then click Populate")
15839
+ self._verify_visual_sorted = False
15478
15840
 
15479
15841
  # Reset Verify Distance
15480
15842
  if hasattr(self, 'verify_distance_scroll_area'):
@@ -15483,6 +15845,11 @@ class GUI(QMainWindow):
15483
15845
  self.verify_distance_checkboxes = []
15484
15846
  if hasattr(self, 'verify_distance_stats_label'):
15485
15847
  self.verify_distance_stats_label.setText("Run Distance colocalization first, then click Populate")
15848
+ # Reset stored distance data for sorting
15849
+ self.verify_distance_mean_crop = None
15850
+ self.verify_distance_crop_size = None
15851
+ self.verify_distance_values = None
15852
+ self._verify_distance_sorted = False
15486
15853
 
15487
15854
  def reset_cellpose_tab(self):
15488
15855
  """Reset Cellpose tab state, masks, and UI controls to defaults."""
microlive/microscopy.py CHANGED
@@ -3948,7 +3948,8 @@ class BigFISH():
3948
3948
 
3949
3949
  # Select isolated spots (cluster_id < 0) and set cluster_size to 1
3950
3950
  spots_no_clusters = clusters_and_spots_big_fish[clusters_and_spots_big_fish[:,-1] < 0].copy()
3951
- spots_no_clusters[:,-1] = 1 # Replace cluster_id with cluster_size=1
3951
+ if len(spots_no_clusters) > 0:
3952
+ spots_no_clusters[:,-1] = 1 # Replace cluster_id with cluster_size=1
3952
3953
 
3953
3954
  # Select cluster centroids with cluster_size > 1
3954
3955
  clusters_no_spots = clusters[clusters[:,-2] > 1]
@@ -5012,6 +5013,7 @@ class DataProcessing():
5012
5013
  self.fast_gaussian_fit = fast_gaussian_fit
5013
5014
  # This number represent the number of columns that doesnt change with the number of color channels in the image
5014
5015
  self.NUMBER_OF_CONSTANT_COLUMNS_IN_DATAFRAME = 18
5016
+
5015
5017
  def get_dataframe(self):
5016
5018
  '''
5017
5019
  This method extracts data from the class SpotDetection and returns the data as a dataframe.
@@ -5162,7 +5164,7 @@ class DataProcessing():
5162
5164
  array_spots_nuc[:,10:13] = spots_nuc[:,:3] # populating coord
5163
5165
  array_spots_nuc[:,13] = 1 # is_nuc
5164
5166
  array_spots_nuc[:,14] = 0 # is_cluster
5165
- array_spots_nuc[:,15] = 0 # cluster_size
5167
+ array_spots_nuc[:,15] = spots_nuc[:,3] # cluster_size (use actual detected value)
5166
5168
  array_spots_nuc[:,16] = spot_type # spot_type
5167
5169
  array_spots_nuc[:,17] = is_cell_in_border # is_cell_fragmented
5168
5170
 
@@ -5171,7 +5173,7 @@ class DataProcessing():
5171
5173
  array_spots_cytosol_only[:,10:13] = spots_cytosol_only[:,:3] # populating coord
5172
5174
  array_spots_cytosol_only[:,13] = 0 # is_nuc
5173
5175
  array_spots_cytosol_only[:,14] = 0 # is_cluster
5174
- array_spots_cytosol_only[:,15] = 1 # cluster_size
5176
+ array_spots_cytosol_only[:,15] = spots_cytosol_only[:,3] # cluster_size (use actual detected value)
5175
5177
  array_spots_cytosol_only[:,16] = spot_type # spot_type
5176
5178
  array_spots_cytosol_only[:,17] = is_cell_in_border # is_cell_fragmented
5177
5179
  if (detected_cyto_clusters == True): #(detected_cyto == True) and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microlive
3
- Version: 1.0.14
3
+ Version: 1.0.16
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,14 +1,13 @@
1
- microlive/__init__.py,sha256=ZAui2VsXaQAYA4fnc1FhYO1v3lfGGEFrmTJyZeBAY9E,1385
1
+ microlive/__init__.py,sha256=p_eGEVyBH_4sOqlUkHXgU0D7B2P-B79xdox8lMW2cbY,1385
2
2
  microlive/imports.py,sha256=VAAMavSLIKO0LooadTXfCdZiv8LQbV_wITeIv8IHwxM,7531
3
- microlive/microscopy.py,sha256=97T9tEOVwBhEbAZujlDSeC3jt5xSQSCGJ8kboI6ucho,710732
3
+ microlive/microscopy.py,sha256=OFqf0JXJW4-2cLHvXnwwp_SfMFsUXwp5lDKbkCRR4ok,710841
4
4
  microlive/ml_spot_detection.py,sha256=pVbOSGNJ0WWMuPRML42rFwvjKVZ0B1fJux1179OIbAg,10603
5
5
  microlive/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  microlive/data/icons/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
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
- microlive/data/models/spot_detection_cnn.pth,sha256=Np7vpPJIbKQmuKY0Hx-4IkeEDsnks_QEgs7TqaYgZmI,8468580
10
9
  microlive/gui/__init__.py,sha256=tB-CdDC7x5OwYFAQxLOUvfVnUThaXKXVRsB68YP0Y6Q,28
11
- microlive/gui/app.py,sha256=GTl2Iwe5uG603Ja6ykwfSG2kB7YaZJYQukLpC0DOurw,787890
10
+ microlive/gui/app.py,sha256=sUz8MBQ4bpiNsZAhyZ7lhneY_hF45KORgepXShCCFYE,805899
12
11
  microlive/gui/main.py,sha256=b66W_2V-pclGKOozfs75pwrCGbL_jkVU3kFt8RFMZIc,2520
13
12
  microlive/gui/micro_mac.command,sha256=TkxYOO_5A2AiNJMz3_--1geBYfl77THpOLFZnV4J2ac,444
14
13
  microlive/gui/micro_windows.bat,sha256=DJUKPhDbCO4HToLwSMT-QTYRe9Kr1wn5A2Ijy2klIrw,773
@@ -21,8 +20,9 @@ microlive/utils/__init__.py,sha256=metAf2zPS8w23d8dyM7-ld1ovrOKBdx3y3zu5IVrzIg,5
21
20
  microlive/utils/device.py,sha256=tcPMU8UiXL-DuGwhudUgrbjW1lgIK_EUKIOeOn0U6q4,2533
22
21
  microlive/utils/model_downloader.py,sha256=EruviTEh75YBekpznn1RZ1Nj8lnDmeC4TKEnFLOow6Y,9448
23
22
  microlive/utils/resources.py,sha256=Jz7kPI75xMLCBJMyX7Y_3ixKi_UgydfQkF0BlFtLCKs,1753
24
- microlive-1.0.14.dist-info/METADATA,sha256=mxn3h5atEOVG_u04F48N-H2ORlMJLC0c4aJQ9bGbB5c,12434
25
- microlive-1.0.14.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
- microlive-1.0.14.dist-info/entry_points.txt,sha256=Zqp2vixyD8lngcfEmOi8fkCj7vPhesz5xlGBI-EubRw,54
27
- microlive-1.0.14.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
28
- microlive-1.0.14.dist-info/RECORD,,
23
+ microlive/data/models/spot_detection_cnn.pth,sha256=Np7vpPJIbKQmuKY0Hx-4IkeEDsnks_QEgs7TqaYgZmI,8468580
24
+ microlive-1.0.16.dist-info/METADATA,sha256=fscI8jQ0VT1fs9gUE9HtKXtlkhdH39hQiMGPV5VRx_I,12434
25
+ microlive-1.0.16.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
26
+ microlive-1.0.16.dist-info/entry_points.txt,sha256=Zqp2vixyD8lngcfEmOi8fkCj7vPhesz5xlGBI-EubRw,54
27
+ microlive-1.0.16.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
28
+ microlive-1.0.16.dist-info/RECORD,,