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 +1 -1
- microlive/gui/app.py +397 -30
- microlive/microscopy.py +5 -3
- {microlive-1.0.14.dist-info → microlive-1.0.16.dist-info}/METADATA +1 -1
- {microlive-1.0.14.dist-info → microlive-1.0.16.dist-info}/RECORD +9 -9
- {microlive-1.0.14.dist-info → microlive-1.0.16.dist-info}/WHEEL +0 -0
- {microlive-1.0.14.dist-info → microlive-1.0.16.dist-info}/entry_points.txt +0 -0
- {microlive-1.0.14.dist-info → microlive-1.0.16.dist-info}/licenses/LICENSE +0 -0
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12586
|
-
|
|
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
|
-
#
|
|
12688
|
-
#
|
|
12689
|
-
|
|
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
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12700
|
-
|
|
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
|
|
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
|
|
12747
|
-
|
|
12748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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] =
|
|
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] =
|
|
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.
|
|
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=
|
|
1
|
+
microlive/__init__.py,sha256=p_eGEVyBH_4sOqlUkHXgU0D7B2P-B79xdox8lMW2cbY,1385
|
|
2
2
|
microlive/imports.py,sha256=VAAMavSLIKO0LooadTXfCdZiv8LQbV_wITeIv8IHwxM,7531
|
|
3
|
-
microlive/microscopy.py,sha256=
|
|
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=
|
|
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
|
|
25
|
-
microlive-1.0.
|
|
26
|
-
microlive-1.0.
|
|
27
|
-
microlive-1.0.
|
|
28
|
-
microlive-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|