microlive 1.0.15__tar.gz → 1.0.16__tar.gz

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.
Files changed (28) hide show
  1. {microlive-1.0.15 → microlive-1.0.16}/PKG-INFO +1 -1
  2. {microlive-1.0.15 → microlive-1.0.16}/microlive/__init__.py +1 -1
  3. {microlive-1.0.15 → microlive-1.0.16}/microlive/gui/app.py +107 -3
  4. {microlive-1.0.15 → microlive-1.0.16}/.gitignore +0 -0
  5. {microlive-1.0.15 → microlive-1.0.16}/LICENSE +0 -0
  6. {microlive-1.0.15 → microlive-1.0.16}/README.md +0 -0
  7. {microlive-1.0.15 → microlive-1.0.16}/microlive/data/__init__.py +0 -0
  8. {microlive-1.0.15 → microlive-1.0.16}/microlive/data/icons/__init__.py +0 -0
  9. {microlive-1.0.15 → microlive-1.0.16}/microlive/data/icons/icon_micro.png +0 -0
  10. {microlive-1.0.15 → microlive-1.0.16}/microlive/data/models/__init__.py +0 -0
  11. {microlive-1.0.15 → microlive-1.0.16}/microlive/data/models/spot_detection_cnn.pth +0 -0
  12. {microlive-1.0.15 → microlive-1.0.16}/microlive/gui/__init__.py +0 -0
  13. {microlive-1.0.15 → microlive-1.0.16}/microlive/gui/main.py +0 -0
  14. {microlive-1.0.15 → microlive-1.0.16}/microlive/gui/micro_mac.command +0 -0
  15. {microlive-1.0.15 → microlive-1.0.16}/microlive/gui/micro_windows.bat +0 -0
  16. {microlive-1.0.15 → microlive-1.0.16}/microlive/imports.py +0 -0
  17. {microlive-1.0.15 → microlive-1.0.16}/microlive/microscopy.py +0 -0
  18. {microlive-1.0.15 → microlive-1.0.16}/microlive/ml_spot_detection.py +0 -0
  19. {microlive-1.0.15 → microlive-1.0.16}/microlive/pipelines/__init__.py +0 -0
  20. {microlive-1.0.15 → microlive-1.0.16}/microlive/pipelines/pipeline_FRAP.py +0 -0
  21. {microlive-1.0.15 → microlive-1.0.16}/microlive/pipelines/pipeline_folding_efficiency.py +0 -0
  22. {microlive-1.0.15 → microlive-1.0.16}/microlive/pipelines/pipeline_particle_tracking.py +0 -0
  23. {microlive-1.0.15 → microlive-1.0.16}/microlive/pipelines/pipeline_spot_detection_no_tracking.py +0 -0
  24. {microlive-1.0.15 → microlive-1.0.16}/microlive/utils/__init__.py +0 -0
  25. {microlive-1.0.15 → microlive-1.0.16}/microlive/utils/device.py +0 -0
  26. {microlive-1.0.15 → microlive-1.0.16}/microlive/utils/model_downloader.py +0 -0
  27. {microlive-1.0.15 → microlive-1.0.16}/microlive/utils/resources.py +0 -0
  28. {microlive-1.0.15 → microlive-1.0.16}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microlive
3
- Version: 1.0.15
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
@@ -23,7 +23,7 @@ Authors:
23
23
  Nathan L. Nowling, Brian Munsky, Ning Zhao
24
24
  """
25
25
 
26
- __version__ = "1.0.15"
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)
@@ -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()
@@ -13154,7 +13173,18 @@ class GUI(QMainWindow):
13154
13173
  spot_coord = (int(dfm.iloc[0]['y']), int(dfm.iloc[0]['x']))
13155
13174
  found_spot = True
13156
13175
  else:
13157
- 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)
13158
13188
  else:
13159
13189
  spot_coord = (0, 0)
13160
13190
  else:
@@ -13213,7 +13243,12 @@ class GUI(QMainWindow):
13213
13243
  else:
13214
13244
  main_img = norm_stack[selected_channelIndex]
13215
13245
  main_cmap = cmap_list_imagej[selected_channelIndex % len(cmap_list_imagej)]
13216
- 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)
13217
13252
  ax_main = fig.add_subplot(gs[0, 0])
13218
13253
 
13219
13254
  # Store main axes reference and recreate RectangleSelector
@@ -13229,8 +13264,15 @@ class GUI(QMainWindow):
13229
13264
  props=dict(facecolor='cyan', edgecolor='white', alpha=0.3, linewidth=2)
13230
13265
  )
13231
13266
 
13267
+ # 2D crop subgrid
13232
13268
  gs2 = gs[0, 1].subgridspec(C, 1, hspace=0.1)
13233
- 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)]
13234
13276
  # remove background if requested
13235
13277
  if hasattr(self, 'checkbox_remove_bg') and self.checkbox_remove_bg.isChecked():
13236
13278
  seg_mask = getattr(self, 'segmentation_mask', None)
@@ -13284,6 +13326,15 @@ class GUI(QMainWindow):
13284
13326
  rect = patches.Rectangle((x0, y0), crop_sz, crop_sz, edgecolor='white', facecolor='none', linewidth=2)
13285
13327
  ax_main.add_patch(rect)
13286
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
+
13287
13338
  for ci, ax in enumerate(axes_zoom):
13288
13339
  if found_spot:
13289
13340
  crop = norm_stack[ci, y0:y1, x0:x1]
@@ -13291,6 +13342,59 @@ class GUI(QMainWindow):
13291
13342
  crop = np.zeros((crop_sz, crop_sz))
13292
13343
  ax.imshow(crop, cmap=cmap_list_imagej[ci % len(cmap_list_imagej)], interpolation='nearest', vmin=0, vmax=1)
13293
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
+
13294
13398
  fig.tight_layout()
13295
13399
 
13296
13400
  # Add thin white frame border to main image
File without changes
File without changes
File without changes
File without changes