nettracer3d 0.6.9__py3-none-any.whl → 0.7.1__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.
- nettracer3d/morphology.py +2 -0
- nettracer3d/nettracer.py +176 -0
- nettracer3d/nettracer_gui.py +238 -5
- nettracer3d/proximity.py +327 -3
- nettracer3d/segmenter.py +86 -3
- nettracer3d/smart_dilate.py +4 -4
- {nettracer3d-0.6.9.dist-info → nettracer3d-0.7.1.dist-info}/METADATA +27 -25
- nettracer3d-0.7.1.dist-info/RECORD +20 -0
- {nettracer3d-0.6.9.dist-info → nettracer3d-0.7.1.dist-info}/WHEEL +1 -1
- nettracer3d-0.6.9.dist-info/RECORD +0 -20
- {nettracer3d-0.6.9.dist-info → nettracer3d-0.7.1.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.6.9.dist-info → nettracer3d-0.7.1.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.6.9.dist-info → nettracer3d-0.7.1.dist-info}/top_level.txt +0 -0
nettracer3d/morphology.py
CHANGED
|
@@ -518,6 +518,7 @@ def compute_distance_transform_distance_GPU(nodes, sampling = [1,1,1]):
|
|
|
518
518
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
519
519
|
if is_pseudo_3d:
|
|
520
520
|
nodes = cp.squeeze(nodes) # Convert to 2D for processing
|
|
521
|
+
del sampling[0]
|
|
521
522
|
|
|
522
523
|
# Compute the distance transform on the GPU
|
|
523
524
|
distance = cpx.distance_transform_edt(nodes, sampling = sampling)
|
|
@@ -533,6 +534,7 @@ def compute_distance_transform_distance(nodes, sampling = [1,1,1]):
|
|
|
533
534
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
534
535
|
if is_pseudo_3d:
|
|
535
536
|
nodes = np.squeeze(nodes) # Convert to 2D for processing
|
|
537
|
+
del sampling[0]
|
|
536
538
|
|
|
537
539
|
# Fallback to CPU if there's an issue with GPU computation
|
|
538
540
|
distance = ndimage.distance_transform_edt(nodes, sampling = sampling)
|
nettracer3d/nettracer.py
CHANGED
|
@@ -4431,6 +4431,101 @@ class Network_3D:
|
|
|
4431
4431
|
return neighborhood_dict, proportion_dict, title1, title2, densities
|
|
4432
4432
|
|
|
4433
4433
|
|
|
4434
|
+
def get_ripley(self, root = None, targ = None, distance = 1, edgecorrect = True, bounds = None, ignore_dims = False, proportion = 0.5):
|
|
4435
|
+
|
|
4436
|
+
|
|
4437
|
+
if root is None or targ is None: #Self clustering in this case
|
|
4438
|
+
roots = self._node_centroids.values()
|
|
4439
|
+
targs = self._node_centroids.values()
|
|
4440
|
+
else:
|
|
4441
|
+
roots = []
|
|
4442
|
+
targs = []
|
|
4443
|
+
|
|
4444
|
+
for node, nodeid in self.node_identities.items(): #Otherwise we need to pull out this info
|
|
4445
|
+
if nodeid == root:
|
|
4446
|
+
roots.append(self._node_centroids[node])
|
|
4447
|
+
elif nodeid == targ:
|
|
4448
|
+
targs.append(self._node_centroids[node])
|
|
4449
|
+
|
|
4450
|
+
rooties = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
|
|
4451
|
+
targs = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
|
|
4452
|
+
points_array = np.vstack((rooties, targs))
|
|
4453
|
+
del rooties
|
|
4454
|
+
|
|
4455
|
+
try:
|
|
4456
|
+
if self.nodes.shape[0] == 1:
|
|
4457
|
+
dim = 2
|
|
4458
|
+
else:
|
|
4459
|
+
dim = 3
|
|
4460
|
+
except:
|
|
4461
|
+
dim = 2
|
|
4462
|
+
for centroid in self.node_centroids.values():
|
|
4463
|
+
if centroid[0] != 0:
|
|
4464
|
+
dim = 3
|
|
4465
|
+
break
|
|
4466
|
+
|
|
4467
|
+
|
|
4468
|
+
if ignore_dims:
|
|
4469
|
+
|
|
4470
|
+
factor = 0.25
|
|
4471
|
+
|
|
4472
|
+
|
|
4473
|
+
if bounds is None:
|
|
4474
|
+
if dim == 2:
|
|
4475
|
+
min_coords = np.array([0,0])
|
|
4476
|
+
else:
|
|
4477
|
+
min_coords = np.array([0,0,0])
|
|
4478
|
+
max_coords = np.max(points_array, axis=0)
|
|
4479
|
+
max_coords = np.flip(max_coords)
|
|
4480
|
+
bounds = (min_coords, max_coords)
|
|
4481
|
+
else:
|
|
4482
|
+
min_coords, max_coords = bounds
|
|
4483
|
+
|
|
4484
|
+
dim_list = max_coords - min_coords
|
|
4485
|
+
|
|
4486
|
+
new_list = []
|
|
4487
|
+
|
|
4488
|
+
|
|
4489
|
+
if dim == 3:
|
|
4490
|
+
for centroid in roots:
|
|
4491
|
+
|
|
4492
|
+
if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor) and ((centroid[0] - min_coords[2]) > dim_list[2] * factor) and ((max_coords[2] - centroid[0]) > dim_list[2] * factor):
|
|
4493
|
+
new_list.append(centroid)
|
|
4494
|
+
#print(f"dim_list: {dim_list}, centroid: {centroid}, min_coords: {min_coords}, max_coords: {max_coords}")
|
|
4495
|
+
else:
|
|
4496
|
+
for centroid in roots:
|
|
4497
|
+
|
|
4498
|
+
if ((centroid[2] - min_coords[0]) > dim_list[0] * factor) and ((max_coords[0] - centroid[2]) > dim_list[0] * factor) and ((centroid[1] - min_coords[1]) > dim_list[1] * factor) and ((max_coords[1] - centroid[1]) > dim_list[1] * factor):
|
|
4499
|
+
new_list.append(centroid)
|
|
4500
|
+
|
|
4501
|
+
roots = new_list
|
|
4502
|
+
print(f"Utilizing {len(roots)} root points. Note that low n values are unstable.")
|
|
4503
|
+
is_subset = True
|
|
4504
|
+
else:
|
|
4505
|
+
is_subset = False
|
|
4506
|
+
|
|
4507
|
+
|
|
4508
|
+
|
|
4509
|
+
|
|
4510
|
+
roots = proximity.convert_centroids_to_array(roots, xy_scale = self.xy_scale, z_scale = self.z_scale)
|
|
4511
|
+
|
|
4512
|
+
|
|
4513
|
+
if dim == 2:
|
|
4514
|
+
roots = proximity.convert_augmented_array_to_points(roots)
|
|
4515
|
+
targs = proximity.convert_augmented_array_to_points(targs)
|
|
4516
|
+
|
|
4517
|
+
r_vals = proximity.generate_r_values(points_array, distance, bounds = bounds, dim = dim, max_proportion=proportion)
|
|
4518
|
+
|
|
4519
|
+
k_vals = proximity.optimized_ripleys_k(roots, targs, r_vals, bounds=bounds, edge_correction=edgecorrect, dim = dim, is_subset = is_subset)
|
|
4520
|
+
|
|
4521
|
+
h_vals = proximity.compute_ripleys_h(k_vals, r_vals, dim)
|
|
4522
|
+
|
|
4523
|
+
proximity.plot_ripley_functions(r_vals, k_vals, h_vals, dim)
|
|
4524
|
+
|
|
4525
|
+
return r_vals, k_vals, h_vals
|
|
4526
|
+
|
|
4527
|
+
|
|
4528
|
+
|
|
4434
4529
|
|
|
4435
4530
|
#Morphological stats or network linking:
|
|
4436
4531
|
|
|
@@ -4490,6 +4585,87 @@ class Network_3D:
|
|
|
4490
4585
|
|
|
4491
4586
|
return array
|
|
4492
4587
|
|
|
4588
|
+
|
|
4589
|
+
|
|
4590
|
+
def random_nodes(self, bounds = None, mask = None):
|
|
4591
|
+
|
|
4592
|
+
if self.nodes is not None:
|
|
4593
|
+
try:
|
|
4594
|
+
self.nodes = np.zeros_like(self.nodes)
|
|
4595
|
+
except:
|
|
4596
|
+
pass
|
|
4597
|
+
|
|
4598
|
+
|
|
4599
|
+
if mask is not None:
|
|
4600
|
+
coords = np.argwhere(mask != 0)
|
|
4601
|
+
else:
|
|
4602
|
+
if bounds is not None:
|
|
4603
|
+
(z1, y1, x1), (z2, y2, x2) = bounds
|
|
4604
|
+
z1, y1, x1 = int(z1), int(y1), int(x1)
|
|
4605
|
+
z2, y2, x2 = int(z2), int(y2), int(x2)
|
|
4606
|
+
z_range = np.arange(z1, z2 + 1)
|
|
4607
|
+
y_range = np.arange(y1, y2 + 1)
|
|
4608
|
+
x_range = np.arange(x1, x2 + 1)
|
|
4609
|
+
z_grid, y_grid, x_grid = np.meshgrid(z_range, y_range, x_range, indexing='ij')
|
|
4610
|
+
del z_range
|
|
4611
|
+
del y_range
|
|
4612
|
+
del x_range
|
|
4613
|
+
coords = np.stack([z_grid.flatten(), y_grid.flatten(), x_grid.flatten()], axis=1)
|
|
4614
|
+
del z_grid
|
|
4615
|
+
del y_grid
|
|
4616
|
+
del x_grid
|
|
4617
|
+
else:
|
|
4618
|
+
shape = ()
|
|
4619
|
+
try:
|
|
4620
|
+
shape = self.nodes.shape
|
|
4621
|
+
except:
|
|
4622
|
+
try:
|
|
4623
|
+
shape = self.edges.shape
|
|
4624
|
+
except:
|
|
4625
|
+
try:
|
|
4626
|
+
shape = self._network_overlay.shape
|
|
4627
|
+
except:
|
|
4628
|
+
try:
|
|
4629
|
+
shape = self._id_overlay.shape
|
|
4630
|
+
except:
|
|
4631
|
+
pass
|
|
4632
|
+
|
|
4633
|
+
ranges = [np.arange(s) for s in shape]
|
|
4634
|
+
|
|
4635
|
+
# Create meshgrid
|
|
4636
|
+
mesh = np.meshgrid(*ranges, indexing='ij')
|
|
4637
|
+
del ranges
|
|
4638
|
+
|
|
4639
|
+
# Stack and reshape
|
|
4640
|
+
coords = np.stack(mesh, axis=-1).reshape(-1, len(shape))
|
|
4641
|
+
del mesh
|
|
4642
|
+
|
|
4643
|
+
if len(coords) < len(self.node_centroids):
|
|
4644
|
+
print(f"Warning: Only {len(coords)} positions available for {len(self.node_centroids)} labels")
|
|
4645
|
+
|
|
4646
|
+
new_centroids = {}
|
|
4647
|
+
|
|
4648
|
+
# Generate random indices without replacement
|
|
4649
|
+
available_count = min(len(coords), len(self.node_centroids))
|
|
4650
|
+
rand_indices = np.random.choice(len(coords), available_count, replace=False)
|
|
4651
|
+
|
|
4652
|
+
# Assign random positions to labels
|
|
4653
|
+
for i, label in enumerate(self.node_centroids.keys()):
|
|
4654
|
+
if i < len(rand_indices):
|
|
4655
|
+
centroid = coords[rand_indices[i]]
|
|
4656
|
+
new_centroids[label] = centroid
|
|
4657
|
+
z, y, x = centroid
|
|
4658
|
+
try:
|
|
4659
|
+
self.nodes[z, y, x] = label
|
|
4660
|
+
except:
|
|
4661
|
+
pass
|
|
4662
|
+
|
|
4663
|
+
# Update the centroids dictionary
|
|
4664
|
+
self.node_centroids = new_centroids
|
|
4665
|
+
|
|
4666
|
+
return self.node_centroids, self._nodes
|
|
4667
|
+
|
|
4668
|
+
|
|
4493
4669
|
def community_id_info(self):
|
|
4494
4670
|
def invert_dict(d):
|
|
4495
4671
|
inverted = {}
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -1246,7 +1246,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1246
1246
|
except:
|
|
1247
1247
|
pass
|
|
1248
1248
|
|
|
1249
|
-
print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
|
|
1249
|
+
#print(f"Found {len(filtered_df)} direct connections between nodes of ID {sort} and their neighbors (of any ID)")
|
|
1250
1250
|
|
|
1251
1251
|
if self.channel_data[0].shape[0] * self.channel_data[0].shape[1] * self.channel_data[0].shape[2] > self.mini_thresh:
|
|
1252
1252
|
self.mini_overlay = True
|
|
@@ -2687,8 +2687,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2687
2687
|
degree_dist_action.triggered.connect(self.show_degree_dist_dialog)
|
|
2688
2688
|
neighbor_id_action = stats_menu.addAction("Identity Distribution of Neighbors")
|
|
2689
2689
|
neighbor_id_action.triggered.connect(self.show_neighbor_id_dialog)
|
|
2690
|
-
|
|
2691
|
-
|
|
2690
|
+
ripley_action = stats_menu.addAction("Clustering Analysis")
|
|
2691
|
+
ripley_action.triggered.connect(self.show_ripley_dialog)
|
|
2692
2692
|
vol_action = stats_menu.addAction("Calculate Volumes")
|
|
2693
2693
|
vol_action.triggered.connect(self.volumes)
|
|
2694
2694
|
rad_action = stats_menu.addAction("Calculate Radii")
|
|
@@ -2707,6 +2707,13 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2707
2707
|
id_code_action = overlay_menu.addAction("Code Identities")
|
|
2708
2708
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
2709
2709
|
|
|
2710
|
+
rand_menu = analysis_menu.addMenu("Randomize")
|
|
2711
|
+
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
2712
|
+
random_action.triggered.connect(self.show_random_dialog)
|
|
2713
|
+
random_nodes = rand_menu.addAction("Scramble Nodes (Centroids)")
|
|
2714
|
+
random_nodes.triggered.connect(self.show_randnode_dialog)
|
|
2715
|
+
|
|
2716
|
+
|
|
2710
2717
|
|
|
2711
2718
|
# Process menu
|
|
2712
2719
|
process_menu = menubar.addMenu("Process")
|
|
@@ -3501,8 +3508,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3501
3508
|
nii_img = nib.load(filename)
|
|
3502
3509
|
# Get data and transpose to match TIFF orientation
|
|
3503
3510
|
# If X needs to become Z, we move axis 2 (X) to position 0 (Z)
|
|
3504
|
-
|
|
3505
|
-
self.channel_data[channel_index] = np.transpose(
|
|
3511
|
+
arraydata = nii_img.get_fdata()
|
|
3512
|
+
self.channel_data[channel_index] = np.transpose(arraydata, (2, 1, 0))
|
|
3506
3513
|
|
|
3507
3514
|
elif file_extension in ['jpg', 'jpeg', 'png']:
|
|
3508
3515
|
from PIL import Image
|
|
@@ -4157,10 +4164,18 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4157
4164
|
dialog = NeighborIdentityDialog(self)
|
|
4158
4165
|
dialog.exec()
|
|
4159
4166
|
|
|
4167
|
+
def show_ripley_dialog(self):
|
|
4168
|
+
dialog = RipleyDialog(self)
|
|
4169
|
+
dialog.exec()
|
|
4170
|
+
|
|
4160
4171
|
def show_random_dialog(self):
|
|
4161
4172
|
dialog = RandomDialog(self)
|
|
4162
4173
|
dialog.exec()
|
|
4163
4174
|
|
|
4175
|
+
def show_randnode_dialog(self):
|
|
4176
|
+
dialog = RandNodeDialog(self)
|
|
4177
|
+
dialog.exec()
|
|
4178
|
+
|
|
4164
4179
|
def show_rad_dialog(self):
|
|
4165
4180
|
dialog = RadDialog(self)
|
|
4166
4181
|
dialog.exec()
|
|
@@ -5977,9 +5992,121 @@ class NeighborIdentityDialog(QDialog):
|
|
|
5977
5992
|
|
|
5978
5993
|
|
|
5979
5994
|
|
|
5995
|
+
class RipleyDialog(QDialog):
|
|
5996
|
+
|
|
5997
|
+
def __init__(self, parent=None):
|
|
5998
|
+
|
|
5999
|
+
super().__init__(parent)
|
|
6000
|
+
self.setWindowTitle(f"Find Ripley's H Function From Centroids")
|
|
6001
|
+
self.setModal(True)
|
|
6002
|
+
|
|
6003
|
+
layout = QFormLayout(self)
|
|
6004
|
+
|
|
6005
|
+
if my_network.node_identities is not None:
|
|
6006
|
+
self.root = QComboBox()
|
|
6007
|
+
self.root.addItems(list(set(my_network.node_identities.values())))
|
|
6008
|
+
self.root.setCurrentIndex(0)
|
|
6009
|
+
layout.addRow("Root Identity to Search for Neighbors", self.root)
|
|
6010
|
+
else:
|
|
6011
|
+
self.root = None
|
|
6012
|
+
|
|
6013
|
+
if my_network.node_identities is not None:
|
|
6014
|
+
self.targ = QComboBox()
|
|
6015
|
+
self.targ.addItems(list(set(my_network.node_identities.values())))
|
|
6016
|
+
self.targ.setCurrentIndex(0)
|
|
6017
|
+
layout.addRow("Targ Identity to be Searched For", self.targ)
|
|
6018
|
+
else:
|
|
6019
|
+
self.targ = None
|
|
6020
|
+
|
|
6021
|
+
self.distance = QLineEdit("5")
|
|
6022
|
+
layout.addRow("Bucket Distance for Searching For Clusters (automatically scaled by xy and z scales):", self.distance)
|
|
6023
|
+
|
|
6024
|
+
|
|
6025
|
+
self.proportion = QLineEdit("0.5")
|
|
6026
|
+
layout.addRow("Proportion of image to search? (0-1, high vals increase border artifacts): ", self.proportion)
|
|
6027
|
+
|
|
6028
|
+
self.edgecorrect = QPushButton("Border Correction")
|
|
6029
|
+
self.edgecorrect.setCheckable(True)
|
|
6030
|
+
self.edgecorrect.setChecked(False)
|
|
6031
|
+
layout.addRow("Use Border Correction (Extrapolate for points beyond the border):", self.edgecorrect)
|
|
6032
|
+
|
|
6033
|
+
self.ignore = QPushButton("Ignore Border Roots")
|
|
6034
|
+
self.ignore.setCheckable(True)
|
|
6035
|
+
self.ignore.setChecked(False)
|
|
6036
|
+
layout.addRow("Exclude Root Nodes Near Borders?:", self.ignore)
|
|
6037
|
+
|
|
6038
|
+
# Add Run button
|
|
6039
|
+
run_button = QPushButton("Get Ripley's H")
|
|
6040
|
+
run_button.clicked.connect(self.ripley)
|
|
6041
|
+
layout.addWidget(run_button)
|
|
6042
|
+
|
|
6043
|
+
def ripley(self):
|
|
6044
|
+
|
|
6045
|
+
try:
|
|
6046
|
+
|
|
6047
|
+
if my_network.node_centroids is None:
|
|
6048
|
+
self.parent().show_centroid_dialog()
|
|
6049
|
+
|
|
6050
|
+
try:
|
|
6051
|
+
root = self.root.currentText()
|
|
6052
|
+
except:
|
|
6053
|
+
root = None
|
|
6054
|
+
|
|
6055
|
+
try:
|
|
6056
|
+
targ = self.targ.currentText()
|
|
6057
|
+
except:
|
|
6058
|
+
targ = None
|
|
6059
|
+
|
|
6060
|
+
try:
|
|
6061
|
+
distance = float(self.distance.text())
|
|
6062
|
+
except:
|
|
6063
|
+
return
|
|
6064
|
+
|
|
6065
|
+
|
|
6066
|
+
try:
|
|
6067
|
+
proportion = abs(float(self.proportion.text()))
|
|
6068
|
+
except:
|
|
6069
|
+
proportion = 0.5
|
|
6070
|
+
|
|
6071
|
+
if proportion > 1 or proportion <= 0:
|
|
6072
|
+
print("Utilizing proportion = 0.5")
|
|
6073
|
+
proportion = 0.5
|
|
6074
|
+
|
|
6075
|
+
|
|
6076
|
+
edgecorrect = self.edgecorrect.isChecked()
|
|
6077
|
+
|
|
6078
|
+
ignore = self.ignore.isChecked()
|
|
6079
|
+
|
|
6080
|
+
if my_network.nodes is not None:
|
|
6081
|
+
|
|
6082
|
+
if my_network.nodes.shape[0] == 1:
|
|
6083
|
+
bounds = (np.array([0, 0]), np.array([my_network.nodes.shape[2], my_network.nodes.shape[1]]))
|
|
6084
|
+
else:
|
|
6085
|
+
bounds = (np.array([0, 0, 0]), np.array([my_network.nodes.shape[2], my_network.nodes.shape[1], my_network.nodes.shape[0]]))
|
|
6086
|
+
else:
|
|
6087
|
+
bounds = None
|
|
6088
|
+
|
|
6089
|
+
r_vals, k_vals, h_vals = my_network.get_ripley(root, targ, distance, edgecorrect, bounds, ignore, proportion)
|
|
6090
|
+
|
|
6091
|
+
k_dict = dict(zip(r_vals, k_vals))
|
|
6092
|
+
h_dict = dict(zip(r_vals, h_vals))
|
|
6093
|
+
|
|
6094
|
+
|
|
6095
|
+
self.parent().format_for_upperright_table(k_dict, metric='Radius (scaled)', value='L Value', title="Ripley's K")
|
|
6096
|
+
self.parent().format_for_upperright_table(h_dict, metric='Radius (scaled)', value='L Normed', title="Ripley's H")
|
|
5980
6097
|
|
|
5981
6098
|
|
|
6099
|
+
self.accept()
|
|
5982
6100
|
|
|
6101
|
+
except Exception as e:
|
|
6102
|
+
QMessageBox.critical(
|
|
6103
|
+
self,
|
|
6104
|
+
"Error:",
|
|
6105
|
+
f"Failed to preform cluster analysis: {str(e)}"
|
|
6106
|
+
)
|
|
6107
|
+
import traceback
|
|
6108
|
+
print(traceback.format_exc())
|
|
6109
|
+
print(f"Error: {e}")
|
|
5983
6110
|
|
|
5984
6111
|
class RandomDialog(QDialog):
|
|
5985
6112
|
|
|
@@ -6019,6 +6146,79 @@ class RandomDialog(QDialog):
|
|
|
6019
6146
|
|
|
6020
6147
|
self.accept()
|
|
6021
6148
|
|
|
6149
|
+
class RandNodeDialog(QDialog):
|
|
6150
|
+
|
|
6151
|
+
def __init__(self, parent=None):
|
|
6152
|
+
|
|
6153
|
+
super().__init__(parent)
|
|
6154
|
+
self.setWindowTitle("Random Node Parameters")
|
|
6155
|
+
self.setModal(True)
|
|
6156
|
+
layout = QFormLayout(self)
|
|
6157
|
+
|
|
6158
|
+
|
|
6159
|
+
self.mode = QComboBox()
|
|
6160
|
+
self.mode.addItems(["Anywhere", "Within Dimensional Bounds of Nodes", "Within Masked Bounds of Edges", "Within Masked Bounds of Overlay1", "Within Masked Bounds of Overlay2"])
|
|
6161
|
+
self.mode.setCurrentIndex(0)
|
|
6162
|
+
layout.addRow("Mode", self.mode)
|
|
6163
|
+
|
|
6164
|
+
# Add Run button
|
|
6165
|
+
run_button = QPushButton("Get Random Nodes (Will go in Nodes)")
|
|
6166
|
+
run_button.clicked.connect(self.random)
|
|
6167
|
+
layout.addWidget(run_button)
|
|
6168
|
+
|
|
6169
|
+
def random(self):
|
|
6170
|
+
|
|
6171
|
+
try:
|
|
6172
|
+
|
|
6173
|
+
if my_network.node_centroids is None:
|
|
6174
|
+
self.parent().show_centroid_dialog()
|
|
6175
|
+
|
|
6176
|
+
bounds = None
|
|
6177
|
+
mask = None
|
|
6178
|
+
|
|
6179
|
+
mode = self.mode.currentIndex()
|
|
6180
|
+
|
|
6181
|
+
if mode == 0 and not (my_network.nodes is None and my_network.edges is None and my_network.network_overlay is None and my_network.id_overlay is None):
|
|
6182
|
+
pass
|
|
6183
|
+
elif mode == 1 or (my_network.nodes is None and my_network.edges is None and my_network.network_overlay is None and my_network.id_overlay is None):
|
|
6184
|
+
print("HELLO")
|
|
6185
|
+
# Convert string labels to integers if necessary
|
|
6186
|
+
if any(isinstance(k, str) for k in my_network.node_centroids.keys()):
|
|
6187
|
+
label_map = {label: idx for idx, label in enumerate(my_network.node_centroids.keys())}
|
|
6188
|
+
my_network.node_centroids = {label_map[k]: v for k, v in my_network.node_centroids.items()}
|
|
6189
|
+
|
|
6190
|
+
# Convert centroids to array and keep track of labels
|
|
6191
|
+
labels = np.array(list(my_network.node_centroids.keys()), dtype=np.uint32)
|
|
6192
|
+
centroid_points = np.array([my_network.node_centroids[label] for label in labels])
|
|
6193
|
+
|
|
6194
|
+
# Calculate shape if not provided
|
|
6195
|
+
max_coords = centroid_points.max(axis=0)
|
|
6196
|
+
max_shape = tuple(max_coord + 1 for max_coord in max_coords)
|
|
6197
|
+
min_coords = centroid_points.min(axis=0)
|
|
6198
|
+
min_shape = tuple(min_coord + 1 for min_coord in min_coords)
|
|
6199
|
+
bounds = (min_shape, max_shape)
|
|
6200
|
+
else:
|
|
6201
|
+
mask = n3d.binarize(self.parent().channel_data[mode - 1])
|
|
6202
|
+
|
|
6203
|
+
centroids, array = my_network.random_nodes(bounds = bounds, mask = mask)
|
|
6204
|
+
|
|
6205
|
+
if my_network.nodes is not None:
|
|
6206
|
+
try:
|
|
6207
|
+
self.parent().load_channel(0, array, data = True)
|
|
6208
|
+
except:
|
|
6209
|
+
pass
|
|
6210
|
+
|
|
6211
|
+
self.parent().format_for_upperright_table(my_network.node_centroids, 'NodeID', ['Z', 'Y', 'X'], 'Node Centroids')
|
|
6212
|
+
|
|
6213
|
+
except Exception as e:
|
|
6214
|
+
QMessageBox.critical(
|
|
6215
|
+
self,
|
|
6216
|
+
"Error:",
|
|
6217
|
+
f"Failed to randomize: {str(e)}"
|
|
6218
|
+
)
|
|
6219
|
+
print(f"Error: {e}")
|
|
6220
|
+
|
|
6221
|
+
|
|
6022
6222
|
class RadDialog(QDialog):
|
|
6023
6223
|
|
|
6024
6224
|
def __init__(self, parent=None):
|
|
@@ -7208,8 +7408,14 @@ class MachineWindow(QMainWindow):
|
|
|
7208
7408
|
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
7209
7409
|
train_detailed = QPushButton("Train More Detailed Model")
|
|
7210
7410
|
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
7411
|
+
save = QPushButton("Save Model")
|
|
7412
|
+
save.clicked.connect(self.save_model)
|
|
7413
|
+
load = QPushButton("Load Model")
|
|
7414
|
+
load.clicked.connect(self.load_model)
|
|
7211
7415
|
training_layout.addWidget(train_quick)
|
|
7212
7416
|
training_layout.addWidget(train_detailed)
|
|
7417
|
+
training_layout.addWidget(save)
|
|
7418
|
+
training_layout.addWidget(load)
|
|
7213
7419
|
training_group.setLayout(training_layout)
|
|
7214
7420
|
|
|
7215
7421
|
# Group 4: Segmentation Options
|
|
@@ -7280,6 +7486,33 @@ class MachineWindow(QMainWindow):
|
|
|
7280
7486
|
except:
|
|
7281
7487
|
pass
|
|
7282
7488
|
|
|
7489
|
+
def save_model(self):
|
|
7490
|
+
|
|
7491
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
7492
|
+
self,
|
|
7493
|
+
f"Save Model As",
|
|
7494
|
+
"", # Default directory
|
|
7495
|
+
"numpy data (*.npz);;All Files (*)" # File type filter
|
|
7496
|
+
)
|
|
7497
|
+
|
|
7498
|
+
if filename: # Only proceed if user didn't cancel
|
|
7499
|
+
# If user didn't type an extension, add .tif
|
|
7500
|
+
if not filename.endswith(('.npz')):
|
|
7501
|
+
filename += '.npz'
|
|
7502
|
+
|
|
7503
|
+
self.segmenter.save_model(filename, self.parent().channel_data[2])
|
|
7504
|
+
|
|
7505
|
+
def load_model(self):
|
|
7506
|
+
|
|
7507
|
+
filename, _ = QFileDialog.getOpenFileName(
|
|
7508
|
+
self,
|
|
7509
|
+
f"Load Model",
|
|
7510
|
+
"",
|
|
7511
|
+
"numpy data (*.npz)"
|
|
7512
|
+
)
|
|
7513
|
+
|
|
7514
|
+
self.segmenter.load_model(filename)
|
|
7515
|
+
self.trained = True
|
|
7283
7516
|
|
|
7284
7517
|
def toggle_two(self):
|
|
7285
7518
|
if self.two.isChecked():
|
nettracer3d/proximity.py
CHANGED
|
@@ -7,6 +7,7 @@ from scipy import ndimage
|
|
|
7
7
|
import concurrent.futures
|
|
8
8
|
import multiprocessing as mp
|
|
9
9
|
import pandas as pd
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
10
11
|
from typing import Dict, Union, Tuple, List, Optional
|
|
11
12
|
|
|
12
13
|
|
|
@@ -288,7 +289,6 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
|
|
|
288
289
|
Returns:
|
|
289
290
|
3D numpy array where each cell contains the label of the closest centroid as uint32
|
|
290
291
|
"""
|
|
291
|
-
from scipy.spatial import cKDTree
|
|
292
292
|
|
|
293
293
|
# Convert string labels to integers if necessary
|
|
294
294
|
if any(isinstance(k, str) for k in centroids.keys()):
|
|
@@ -305,7 +305,7 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
|
|
|
305
305
|
shape = tuple(max_coord + 1 for max_coord in max_coords)
|
|
306
306
|
|
|
307
307
|
# Create KD-tree
|
|
308
|
-
tree =
|
|
308
|
+
tree = KDTree(centroid_points)
|
|
309
309
|
|
|
310
310
|
# Create coordinate arrays
|
|
311
311
|
coords = np.array(np.meshgrid(
|
|
@@ -322,4 +322,328 @@ def create_voronoi_3d_kdtree(centroids: Dict[Union[int, str], Union[Tuple[int, i
|
|
|
322
322
|
label_array = labels[indices].astype(np.uint32)
|
|
323
323
|
|
|
324
324
|
# Reshape to final shape
|
|
325
|
-
return label_array.reshape(shape)
|
|
325
|
+
return label_array.reshape(shape)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
#Ripley cluster analysis:
|
|
330
|
+
|
|
331
|
+
def convert_centroids_to_array(centroids_list, xy_scale = 1, z_scale = 1):
|
|
332
|
+
"""
|
|
333
|
+
Convert a dictionary of centroids to a numpy array suitable for Ripley's K calculation.
|
|
334
|
+
|
|
335
|
+
Parameters:
|
|
336
|
+
centroids_list: List of centroid coordinate arrays
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
numpy array of shape (n, d) where n is number of points and d is dimensionality
|
|
340
|
+
"""
|
|
341
|
+
# Determine how many centroids we have
|
|
342
|
+
n_points = len(centroids_list)
|
|
343
|
+
|
|
344
|
+
# Get dimensionality from the first centroid
|
|
345
|
+
dim = len(list(centroids_list)[0])
|
|
346
|
+
|
|
347
|
+
# Create empty array
|
|
348
|
+
points_array = np.zeros((n_points, dim))
|
|
349
|
+
|
|
350
|
+
# Fill array with coordinates
|
|
351
|
+
for i, coords in enumerate(centroids_list):
|
|
352
|
+
points_array[i] = coords
|
|
353
|
+
|
|
354
|
+
points_array[:, 1:] = points_array[:, 1:] * xy_scale #account for scaling
|
|
355
|
+
|
|
356
|
+
points_array[:, 0] = points_array[:, 0] * z_scale #account for scaling
|
|
357
|
+
|
|
358
|
+
return points_array
|
|
359
|
+
|
|
360
|
+
def generate_r_values(points_array, step_size, bounds = None, dim = 2, max_proportion=0.5):
|
|
361
|
+
"""
|
|
362
|
+
Generate an array of r values based on point distribution and step size.
|
|
363
|
+
|
|
364
|
+
Parameters:
|
|
365
|
+
points_array: numpy array of shape (n, d) with point coordinates
|
|
366
|
+
step_size: user-defined step size for r values
|
|
367
|
+
max_proportion: maximum proportion of the study area extent to use (default 0.5)
|
|
368
|
+
This prevents analyzing at distances where edge effects dominate
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
numpy array of r values
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
if bounds is None:
|
|
375
|
+
if dim == 2:
|
|
376
|
+
min_coords = np.array([0,0])
|
|
377
|
+
else:
|
|
378
|
+
min_coords = np.array([0,0,0])
|
|
379
|
+
max_coords = np.max(points_array, axis=0)
|
|
380
|
+
max_coords = np.flip(max_coords)
|
|
381
|
+
else:
|
|
382
|
+
min_coords, max_coords = bounds
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Calculate the longest dimension
|
|
386
|
+
dimensions = max_coords - min_coords
|
|
387
|
+
max_dimension = np.max(dimensions)
|
|
388
|
+
|
|
389
|
+
# Calculate maximum r value (typically half the shortest side for 2D,
|
|
390
|
+
# or scaled by max_proportion for general use)
|
|
391
|
+
max_r = max_dimension * max_proportion
|
|
392
|
+
|
|
393
|
+
# Generate r values from 0 to max_r with step_size increments
|
|
394
|
+
num_steps = int(max_r / step_size)
|
|
395
|
+
r_values = np.linspace(step_size, max_r, num_steps)
|
|
396
|
+
|
|
397
|
+
if r_values[0] == 0:
|
|
398
|
+
np.delete(r_values, 0)
|
|
399
|
+
|
|
400
|
+
return r_values
|
|
401
|
+
|
|
402
|
+
def convert_augmented_array_to_points(augmented_array):
|
|
403
|
+
"""
|
|
404
|
+
Convert an array where first column is 1 and remaining columns are coordinates.
|
|
405
|
+
|
|
406
|
+
Parameters:
|
|
407
|
+
augmented_array: 2D array where first column is 1 and rest are coordinates
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
numpy array with just the coordinate columns
|
|
411
|
+
"""
|
|
412
|
+
# Extract just the coordinate columns (all except first column)
|
|
413
|
+
return augmented_array[:, 1:]
|
|
414
|
+
|
|
415
|
+
def optimized_ripleys_k(reference_points, subset_points, r_values, bounds=None, edge_correction=True, dim = 2, is_subset = False):
|
|
416
|
+
"""
|
|
417
|
+
Optimized computation of Ripley's K function using KD-Tree with simplified but effective edge correction.
|
|
418
|
+
|
|
419
|
+
Parameters:
|
|
420
|
+
reference_points: numpy array of shape (n, d) containing coordinates (d=2 or d=3)
|
|
421
|
+
subset_points: numpy array of shape (m, d) containing coordinates
|
|
422
|
+
r_values: numpy array of distances at which to compute K
|
|
423
|
+
bounds: tuple of (min_coords, max_coords) defining the study area boundaries
|
|
424
|
+
edge_correction: Boolean indicating whether to apply edge correction
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
K_values: numpy array of K values corresponding to r_values
|
|
428
|
+
"""
|
|
429
|
+
n_ref = len(reference_points)
|
|
430
|
+
n_subset = len(subset_points)
|
|
431
|
+
|
|
432
|
+
# Determine bounds if not provided
|
|
433
|
+
if bounds is None:
|
|
434
|
+
min_coords = np.min(reference_points, axis=0)
|
|
435
|
+
max_coords = np.max(reference_points, axis=0)
|
|
436
|
+
bounds = (min_coords, max_coords)
|
|
437
|
+
|
|
438
|
+
# Calculate volume of study area
|
|
439
|
+
min_bounds, max_bounds = bounds
|
|
440
|
+
sides = max_bounds - min_bounds
|
|
441
|
+
volume = np.prod(sides)
|
|
442
|
+
|
|
443
|
+
# Point intensity (points per unit volume)
|
|
444
|
+
intensity = n_ref / volume
|
|
445
|
+
|
|
446
|
+
# Build KD-Tree for efficient nearest neighbor search
|
|
447
|
+
tree = KDTree(reference_points)
|
|
448
|
+
|
|
449
|
+
# Initialize K values
|
|
450
|
+
K_values = np.zeros(len(r_values))
|
|
451
|
+
|
|
452
|
+
# For each r value, compute cumulative counts
|
|
453
|
+
for i, r in enumerate(r_values):
|
|
454
|
+
total_count = 0
|
|
455
|
+
|
|
456
|
+
# Query the tree for all points within radius r of each subset point
|
|
457
|
+
for j, point in enumerate(subset_points):
|
|
458
|
+
# Find all reference points within radius r
|
|
459
|
+
indices = tree.query_ball_point(point, r)
|
|
460
|
+
count = len(indices)
|
|
461
|
+
|
|
462
|
+
# Apply edge correction if needed
|
|
463
|
+
if edge_correction:
|
|
464
|
+
# Calculate edge correction weight
|
|
465
|
+
weight = 1.0
|
|
466
|
+
|
|
467
|
+
if dim == 2:
|
|
468
|
+
# For 2D - check all four boundaries
|
|
469
|
+
x, y = point
|
|
470
|
+
|
|
471
|
+
# Distances to all boundaries
|
|
472
|
+
x_min_dist = x - min_bounds[0]
|
|
473
|
+
x_max_dist = max_bounds[0] - x
|
|
474
|
+
y_min_dist = y - min_bounds[1]
|
|
475
|
+
y_max_dist = max_bounds[1] - y
|
|
476
|
+
|
|
477
|
+
proportion_in = 1.0
|
|
478
|
+
# Apply correction for each boundary if needed
|
|
479
|
+
if x_min_dist < r:
|
|
480
|
+
proportion_in -= 0.5 * (1 - x_min_dist/r)
|
|
481
|
+
if x_max_dist < r:
|
|
482
|
+
proportion_in -= 0.5 * (1 - x_max_dist/r)
|
|
483
|
+
if y_min_dist < r:
|
|
484
|
+
proportion_in -= 0.5 * (1 - y_min_dist/r)
|
|
485
|
+
if y_max_dist < r:
|
|
486
|
+
proportion_in -= 0.5 * (1 - y_max_dist/r)
|
|
487
|
+
|
|
488
|
+
# Corner correction
|
|
489
|
+
if ((x_min_dist < r and y_min_dist < r) or
|
|
490
|
+
(x_min_dist < r and y_max_dist < r) or
|
|
491
|
+
(x_max_dist < r and y_min_dist < r) or
|
|
492
|
+
(x_max_dist < r and y_max_dist < r)):
|
|
493
|
+
proportion_in += 0.1 # Add a small boost for corners
|
|
494
|
+
|
|
495
|
+
elif dim == 3:
|
|
496
|
+
# For 3D - check all six boundaries
|
|
497
|
+
x, y, z = point
|
|
498
|
+
|
|
499
|
+
# Distances to all boundaries
|
|
500
|
+
x_min_dist = x - min_bounds[0]
|
|
501
|
+
x_max_dist = max_bounds[0] - x
|
|
502
|
+
y_min_dist = y - min_bounds[1]
|
|
503
|
+
y_max_dist = max_bounds[1] - y
|
|
504
|
+
z_min_dist = z - min_bounds[2]
|
|
505
|
+
z_max_dist = max_bounds[2] - z
|
|
506
|
+
|
|
507
|
+
proportion_in = 1.0
|
|
508
|
+
# Apply correction for each boundary if needed
|
|
509
|
+
if x_min_dist < r:
|
|
510
|
+
proportion_in -= 0.25 * (1 - x_min_dist/r)
|
|
511
|
+
if x_max_dist < r:
|
|
512
|
+
proportion_in -= 0.25 * (1 - x_max_dist/r)
|
|
513
|
+
if y_min_dist < r:
|
|
514
|
+
proportion_in -= 0.25 * (1 - y_min_dist/r)
|
|
515
|
+
if y_max_dist < r:
|
|
516
|
+
proportion_in -= 0.25 * (1 - y_max_dist/r)
|
|
517
|
+
if z_min_dist < r:
|
|
518
|
+
proportion_in -= 0.25 * (1 - z_min_dist/r)
|
|
519
|
+
if z_max_dist < r:
|
|
520
|
+
proportion_in -= 0.25 * (1 - z_max_dist/r)
|
|
521
|
+
|
|
522
|
+
# Corner correction for 3D (if point is near a corner)
|
|
523
|
+
num_close_edges = (
|
|
524
|
+
(x_min_dist < r) + (x_max_dist < r) +
|
|
525
|
+
(y_min_dist < r) + (y_max_dist < r) +
|
|
526
|
+
(z_min_dist < r) + (z_max_dist < r)
|
|
527
|
+
)
|
|
528
|
+
if num_close_edges >= 2:
|
|
529
|
+
proportion_in += 0.05 * num_close_edges # Stronger boost for more edges
|
|
530
|
+
|
|
531
|
+
# Ensure proportion_in stays within reasonable bounds
|
|
532
|
+
proportion_in = max(0.1, min(1.0, proportion_in))
|
|
533
|
+
weight = 1.0 / proportion_in
|
|
534
|
+
|
|
535
|
+
count *= weight
|
|
536
|
+
|
|
537
|
+
total_count += count
|
|
538
|
+
|
|
539
|
+
# Subtract self-counts if points appear in both sets
|
|
540
|
+
if is_subset or np.array_equal(reference_points, subset_points):
|
|
541
|
+
total_count -= n_ref # Subtract all self-counts
|
|
542
|
+
|
|
543
|
+
# Normalize
|
|
544
|
+
K_values[i] = total_count / (n_subset * intensity)
|
|
545
|
+
|
|
546
|
+
return K_values
|
|
547
|
+
|
|
548
|
+
def ripleys_h_function_3d(k_values, r_values):
|
|
549
|
+
"""
|
|
550
|
+
Convert K values to H values for 3D point patterns with edge correction.
|
|
551
|
+
|
|
552
|
+
Parameters:
|
|
553
|
+
k_values: numpy array of K function values
|
|
554
|
+
r_values: numpy array of distances at which K was computed
|
|
555
|
+
edge_weights: optional array of edge correction weights
|
|
556
|
+
|
|
557
|
+
Returns:
|
|
558
|
+
h_values: numpy array of H function values
|
|
559
|
+
"""
|
|
560
|
+
h_values = np.cbrt(k_values / (4/3 * np.pi)) - r_values
|
|
561
|
+
|
|
562
|
+
return h_values
|
|
563
|
+
|
|
564
|
+
def ripleys_h_function_2d(k_values, r_values):
|
|
565
|
+
"""
|
|
566
|
+
Convert K values to H values for 2D point patterns with edge correction.
|
|
567
|
+
|
|
568
|
+
Parameters:
|
|
569
|
+
k_values: numpy array of K function values
|
|
570
|
+
r_values: numpy array of distances at which K was computed
|
|
571
|
+
edge_weights: optional array of edge correction weights
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
h_values: numpy array of H function values
|
|
575
|
+
"""
|
|
576
|
+
h_values = np.sqrt(k_values / np.pi) - r_values
|
|
577
|
+
|
|
578
|
+
return h_values
|
|
579
|
+
|
|
580
|
+
def compute_ripleys_h(k_values, r_values, dimension=2):
|
|
581
|
+
"""
|
|
582
|
+
Compute Ripley's H function (normalized K) with edge correction.
|
|
583
|
+
|
|
584
|
+
Parameters:
|
|
585
|
+
k_values: numpy array of K function values
|
|
586
|
+
r_values: numpy array of distances at which K was computed
|
|
587
|
+
edge_weights: optional array of edge correction weights
|
|
588
|
+
dimension: dimensionality of the point pattern (2 for 2D, 3 for 3D)
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
h_values: numpy array of H function values
|
|
592
|
+
"""
|
|
593
|
+
if dimension == 2:
|
|
594
|
+
return ripleys_h_function_2d(k_values, r_values)
|
|
595
|
+
elif dimension == 3:
|
|
596
|
+
return ripleys_h_function_3d(k_values, r_values)
|
|
597
|
+
else:
|
|
598
|
+
raise ValueError("Dimension must be 2 or 3")
|
|
599
|
+
|
|
600
|
+
def plot_ripley_functions(r_values, k_values, h_values, dimension=2, figsize=(12, 5)):
|
|
601
|
+
"""
|
|
602
|
+
Plot Ripley's K and H functions with theoretical Poisson distribution references
|
|
603
|
+
adjusted for edge effects.
|
|
604
|
+
|
|
605
|
+
Parameters:
|
|
606
|
+
r_values: numpy array of distances at which K and H were computed
|
|
607
|
+
k_values: numpy array of K function values
|
|
608
|
+
h_values: numpy array of H function values (normalized K)
|
|
609
|
+
edge_weights: optional array of edge correction weights
|
|
610
|
+
dimension: dimensionality of the point pattern (2 for 2D, 3 for 3D)
|
|
611
|
+
figsize: tuple specifying figure size (width, height)
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
#plt.figure()
|
|
615
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=figsize)
|
|
616
|
+
|
|
617
|
+
# Theoretical values for complete spatial randomness (CSR)
|
|
618
|
+
if dimension == 2:
|
|
619
|
+
theo_k = np.pi * r_values**2 # πr² for 2D
|
|
620
|
+
elif dimension == 3:
|
|
621
|
+
theo_k = (4/3) * np.pi * r_values**3 # (4/3)πr³ for 3D
|
|
622
|
+
else:
|
|
623
|
+
raise ValueError("Dimension must be 2 or 3")
|
|
624
|
+
|
|
625
|
+
# Theoretical H values are always 0 for CSR
|
|
626
|
+
theo_h = np.zeros_like(r_values)
|
|
627
|
+
|
|
628
|
+
# Plot K function
|
|
629
|
+
ax1.plot(r_values, k_values, 'b-', label='Observed K(r)')
|
|
630
|
+
ax1.plot(r_values, theo_k, 'r--', label='Theoretical K(r) for CSR')
|
|
631
|
+
ax1.set_xlabel('Distance (r)')
|
|
632
|
+
ax1.set_ylabel('L(r)')
|
|
633
|
+
ax1.set_title("Ripley's K Function")
|
|
634
|
+
ax1.legend()
|
|
635
|
+
ax1.grid(True, alpha=0.3)
|
|
636
|
+
|
|
637
|
+
# Plot H function
|
|
638
|
+
ax2.plot(r_values, h_values, 'b-', label='Observed H(r)')
|
|
639
|
+
ax2.plot(r_values, theo_h, 'r--', label='Theoretical H(r) for CSR')
|
|
640
|
+
ax2.set_xlabel('Distance (r)')
|
|
641
|
+
ax2.set_ylabel('L(r) Normalized')
|
|
642
|
+
ax2.set_title("Ripley's H Function")
|
|
643
|
+
ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
|
|
644
|
+
ax2.legend()
|
|
645
|
+
ax2.grid(True, alpha=0.3)
|
|
646
|
+
|
|
647
|
+
plt.tight_layout()
|
|
648
|
+
plt.show()
|
|
649
|
+
#plt.clf()
|
nettracer3d/segmenter.py
CHANGED
|
@@ -75,6 +75,12 @@ class InteractiveSegmenter:
|
|
|
75
75
|
self.dogs = [(1, 2), (2, 4), (4, 8)]
|
|
76
76
|
self.master_chunk = 49
|
|
77
77
|
|
|
78
|
+
#Data when loading prev model:
|
|
79
|
+
self.previous_foreground = None
|
|
80
|
+
self.previous_background = None
|
|
81
|
+
self.previous_z_fore = None
|
|
82
|
+
self.previous_z_back = None
|
|
83
|
+
|
|
78
84
|
def segment_slice_chunked(self, slice_z, block_size = 49):
|
|
79
85
|
"""
|
|
80
86
|
A completely standalone method to segment a single z-slice in chunks
|
|
@@ -1757,10 +1763,11 @@ class InteractiveSegmenter:
|
|
|
1757
1763
|
except:
|
|
1758
1764
|
pass
|
|
1759
1765
|
|
|
1760
|
-
def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False):
|
|
1766
|
+
def train_batch(self, foreground_array, speed = True, use_gpu = False, use_two = False, mem_lock = False, saving = False):
|
|
1761
1767
|
"""Train directly on foreground and background arrays"""
|
|
1762
1768
|
|
|
1763
|
-
|
|
1769
|
+
if not saving:
|
|
1770
|
+
print("Training model...")
|
|
1764
1771
|
self.speed = speed
|
|
1765
1772
|
self.cur_gpu = use_gpu
|
|
1766
1773
|
if mem_lock != self.mem_lock:
|
|
@@ -1969,12 +1976,40 @@ class InteractiveSegmenter:
|
|
|
1969
1976
|
z_back, y_back, x_back = np.where(foreground_array == 2)
|
|
1970
1977
|
background_features = self.feature_cache[z_back, y_back, x_back]
|
|
1971
1978
|
except:
|
|
1972
|
-
|
|
1979
|
+
pass
|
|
1980
|
+
|
|
1981
|
+
|
|
1982
|
+
if self.previous_foreground is not None:
|
|
1983
|
+
failed = True
|
|
1984
|
+
try:
|
|
1985
|
+
foreground_features = np.vstack([self.previous_foreground, foreground_features])
|
|
1986
|
+
failed = False
|
|
1987
|
+
except:
|
|
1988
|
+
pass
|
|
1989
|
+
try:
|
|
1990
|
+
background_features = np.vstack([self.previous_background, background_features])
|
|
1991
|
+
failed = False
|
|
1992
|
+
except:
|
|
1993
|
+
pass
|
|
1994
|
+
try:
|
|
1995
|
+
z_fore = np.concatenate([self.previous_z_fore, z_fore])
|
|
1996
|
+
except:
|
|
1997
|
+
pass
|
|
1998
|
+
try:
|
|
1999
|
+
z_back = np.concatenate([self.previous_z_back, z_back])
|
|
2000
|
+
except:
|
|
2001
|
+
pass
|
|
2002
|
+
if failed:
|
|
2003
|
+
print("Could not combine new model with old loaded model. Perhaps you are trying to combine a quick model with a deep model? I cannot combine these...")
|
|
2004
|
+
|
|
2005
|
+
if saving:
|
|
1973
2006
|
|
|
2007
|
+
return foreground_features, background_features, z_fore, z_back
|
|
1974
2008
|
|
|
1975
2009
|
# Combine features and labels
|
|
1976
2010
|
X = np.vstack([foreground_features, background_features])
|
|
1977
2011
|
y = np.hstack([np.ones(len(z_fore)), np.zeros(len(z_back))])
|
|
2012
|
+
|
|
1978
2013
|
|
|
1979
2014
|
# Train the model
|
|
1980
2015
|
try:
|
|
@@ -1988,6 +2023,54 @@ class InteractiveSegmenter:
|
|
|
1988
2023
|
|
|
1989
2024
|
|
|
1990
2025
|
|
|
2026
|
+
print("Done")
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
def save_model(self, file_name, foreground_array):
|
|
2030
|
+
|
|
2031
|
+
print("Saving model data")
|
|
2032
|
+
|
|
2033
|
+
foreground_features, background_features, z_fore, z_back = self.train_batch(foreground_array, speed = self.speed, use_gpu = self.use_gpu, use_two = self.use_two, mem_lock = self.mem_lock, saving = True)
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
np.savez(file_name,
|
|
2037
|
+
foreground_features=foreground_features,
|
|
2038
|
+
background_features=background_features,
|
|
2039
|
+
z_fore=z_fore,
|
|
2040
|
+
z_back=z_back,
|
|
2041
|
+
speed=self.speed,
|
|
2042
|
+
use_gpu=self.use_gpu,
|
|
2043
|
+
use_two=self.use_two,
|
|
2044
|
+
mem_lock=self.mem_lock)
|
|
2045
|
+
|
|
2046
|
+
print(f"Model data saved to {file_name}")
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def load_model(self, file_name):
|
|
2050
|
+
|
|
2051
|
+
print("Loading model data")
|
|
2052
|
+
|
|
2053
|
+
data = np.load(file_name)
|
|
2054
|
+
|
|
2055
|
+
# Unpack the arrays
|
|
2056
|
+
self.previous_foreground = data['foreground_features']
|
|
2057
|
+
self.previous_background = data['background_features']
|
|
2058
|
+
self.previous_z_fore = data['z_fore']
|
|
2059
|
+
self.previous_z_back = data['z_back']
|
|
2060
|
+
self.speed = bool(data['speed'])
|
|
2061
|
+
self.use_gpu = bool(data['use_gpu'])
|
|
2062
|
+
self.use_two = bool(data['use_two'])
|
|
2063
|
+
self.mem_lock = bool(data['mem_lock'])
|
|
2064
|
+
|
|
2065
|
+
X = np.vstack([self.previous_foreground, self.previous_background])
|
|
2066
|
+
y = np.hstack([np.ones(len(self.previous_z_fore)), np.zeros(len(self.previous_z_back))])
|
|
2067
|
+
|
|
2068
|
+
try:
|
|
2069
|
+
self.model.fit(X, y)
|
|
2070
|
+
except:
|
|
2071
|
+
print(X)
|
|
2072
|
+
print(y)
|
|
2073
|
+
|
|
1991
2074
|
print("Done")
|
|
1992
2075
|
|
|
1993
2076
|
def get_feature_map_slice(self, z, speed, use_gpu):
|
nettracer3d/smart_dilate.py
CHANGED
|
@@ -188,9 +188,6 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, GPU = Fa
|
|
|
188
188
|
Returns:
|
|
189
189
|
Dilated 3D array
|
|
190
190
|
"""
|
|
191
|
-
if array.shape[0] == 1:
|
|
192
|
-
|
|
193
|
-
return nettracer.dilate_2D(array, search_distance, scaling = xy_scaling)
|
|
194
191
|
|
|
195
192
|
# Determine which dimension needs resampling. the moral of the story is read documentation before you do something unecessary.
|
|
196
193
|
"""
|
|
@@ -235,7 +232,6 @@ def dilate_3D_dt(array, search_distance, xy_scaling=1.0, z_scaling=1.0, GPU = Fa
|
|
|
235
232
|
# Threshold the distance transform to get dilated result
|
|
236
233
|
inv = inv <= search_distance
|
|
237
234
|
|
|
238
|
-
|
|
239
235
|
return inv.astype(np.uint8), indices, array
|
|
240
236
|
|
|
241
237
|
|
|
@@ -475,6 +471,7 @@ def compute_distance_transform_GPU(nodes, return_dists = False, sampling = [1, 1
|
|
|
475
471
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
476
472
|
if is_pseudo_3d:
|
|
477
473
|
nodes = np.squeeze(nodes) # Convert to 2D for processing
|
|
474
|
+
del sampling[0]
|
|
478
475
|
|
|
479
476
|
# Convert numpy array to CuPy array
|
|
480
477
|
nodes_cp = cp.asarray(nodes)
|
|
@@ -507,6 +504,7 @@ def compute_distance_transform(nodes, return_dists = False, sampling = [1, 1, 1]
|
|
|
507
504
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
508
505
|
if is_pseudo_3d:
|
|
509
506
|
nodes = np.squeeze(nodes) # Convert to 2D for processing
|
|
507
|
+
del sampling[0]
|
|
510
508
|
|
|
511
509
|
dists, nearest_label_indices = distance_transform_edt(nodes, return_indices=True, sampling = sampling)
|
|
512
510
|
|
|
@@ -533,6 +531,7 @@ def compute_distance_transform_distance_GPU(nodes, sampling = [1, 1, 1]):
|
|
|
533
531
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
534
532
|
if is_pseudo_3d:
|
|
535
533
|
nodes = np.squeeze(nodes) # Convert to 2D for processing
|
|
534
|
+
del sampling[0]
|
|
536
535
|
|
|
537
536
|
# Convert numpy array to CuPy array
|
|
538
537
|
nodes_cp = cp.asarray(nodes)
|
|
@@ -554,6 +553,7 @@ def compute_distance_transform_distance(nodes, sampling = [1, 1, 1]):
|
|
|
554
553
|
is_pseudo_3d = nodes.shape[0] == 1
|
|
555
554
|
if is_pseudo_3d:
|
|
556
555
|
nodes = np.squeeze(nodes) # Convert to 2D for processing
|
|
556
|
+
del sampling[0]
|
|
557
557
|
|
|
558
558
|
# Fallback to CPU if there's an issue with GPU computation
|
|
559
559
|
distance = distance_transform_edt(nodes, sampling = sampling)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <liamm@wustl.edu>
|
|
6
6
|
Project-URL: Documentation, https://nettracer3d.readthedocs.io/en/latest/
|
|
@@ -9,26 +9,26 @@ Project-URL: Reference_Citation_For_Use, https://doi.org/10.1101/2024.07.29.6056
|
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
10
|
Classifier: License :: Other/Proprietary License
|
|
11
11
|
Classifier: Operating System :: OS Independent
|
|
12
|
-
Requires-Python:
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: numpy
|
|
16
|
-
Requires-Dist: scipy
|
|
17
|
-
Requires-Dist: scikit-image
|
|
18
|
-
Requires-Dist: Pillow
|
|
19
|
-
Requires-Dist: matplotlib
|
|
20
|
-
Requires-Dist: networkx
|
|
21
|
-
Requires-Dist: opencv-python-headless
|
|
22
|
-
Requires-Dist: openpyxl
|
|
23
|
-
Requires-Dist: pandas
|
|
24
|
-
Requires-Dist: napari
|
|
25
|
-
Requires-Dist: python-louvain
|
|
26
|
-
Requires-Dist: tifffile
|
|
27
|
-
Requires-Dist: qtrangeslider
|
|
28
|
-
Requires-Dist: PyQt6
|
|
29
|
-
Requires-Dist: scikit-learn
|
|
30
|
-
Requires-Dist: nibabel
|
|
31
|
-
Requires-Dist: setuptools
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: scipy
|
|
17
|
+
Requires-Dist: scikit-image
|
|
18
|
+
Requires-Dist: Pillow
|
|
19
|
+
Requires-Dist: matplotlib
|
|
20
|
+
Requires-Dist: networkx
|
|
21
|
+
Requires-Dist: opencv-python-headless
|
|
22
|
+
Requires-Dist: openpyxl
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: napari
|
|
25
|
+
Requires-Dist: python-louvain
|
|
26
|
+
Requires-Dist: tifffile
|
|
27
|
+
Requires-Dist: qtrangeslider
|
|
28
|
+
Requires-Dist: PyQt6
|
|
29
|
+
Requires-Dist: scikit-learn
|
|
30
|
+
Requires-Dist: nibabel
|
|
31
|
+
Requires-Dist: setuptools
|
|
32
32
|
Provides-Extra: cuda11
|
|
33
33
|
Requires-Dist: cupy-cuda11x; extra == "cuda11"
|
|
34
34
|
Provides-Extra: cuda12
|
|
@@ -73,12 +73,14 @@ NetTracer3D is free to use/fork for academic/nonprofit use so long as citation i
|
|
|
73
73
|
|
|
74
74
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
75
75
|
|
|
76
|
-
-- Version 0.
|
|
76
|
+
-- Version 0.7.0 Updates --
|
|
77
77
|
|
|
78
|
-
1.
|
|
78
|
+
1. Added new function in 'Analyze -> Stats -> Cluster Analysis'
|
|
79
|
+
* This function allows the user to create a ripley's K or H function to compare the relative clustering of two types of nodes, or of one type of node vs itself.
|
|
79
80
|
|
|
80
|
-
2. Added new
|
|
81
|
+
2. Added new function in 'Analyze -> Randomize -> Scramble Nodes'
|
|
82
|
+
* This function randomly rearranges the node (centroids) for comparison with other centroid-using methods, as a possible way to demonstrate non-random behavior.
|
|
83
|
+
* The randomize menu is likewise new and the 'Generate Equivalent Random Network' method was moved there.
|
|
81
84
|
|
|
82
|
-
3.
|
|
83
|
-
|
|
84
|
-
4. Now specifies python 3.11.
|
|
85
|
+
3. Bug fixes.
|
|
86
|
+
* Importantly fixed a bug with dt-based dilation not working in 2D, which I had accidentally introduced recently.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
nettracer3d/community_extractor.py,sha256=5v9SCCLX3P1RX0fjPVKH5NHMFkMolZ5BTe0bR_a67xg,24479
|
|
3
|
+
nettracer3d/modularity.py,sha256=FH3GpTHorRNkdQULe-2DWgFE3i0_u__hrao7Nx_6Ge4,30249
|
|
4
|
+
nettracer3d/morphology.py,sha256=kEzuzBxHHpWt3o_LHMXYJGJT8-Z_dEP99QpZjOkavzE,19742
|
|
5
|
+
nettracer3d/nettracer.py,sha256=dnnWEr9xpzoxV-NqLJm4DAFmZJ25CSjhA85AR_LT8Qc,218452
|
|
6
|
+
nettracer3d/nettracer_gui.py,sha256=jeqsjaLGdaXdPcuy3Z3iEVnaOvyGDTEjD8uVkoNPFtk,426501
|
|
7
|
+
nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
|
|
8
|
+
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
9
|
+
nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
|
|
10
|
+
nettracer3d/proximity.py,sha256=2Fj1UTKDEFBf7r1SgWXhiSINOTMjP4G5_Egu58l2zTk,24756
|
|
11
|
+
nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
|
|
12
|
+
nettracer3d/segmenter.py,sha256=gJS2AXqHhnw29cbzIxAah2LsrE7_7XnzG7mYSAovZ4I,87847
|
|
13
|
+
nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
|
|
14
|
+
nettracer3d/smart_dilate.py,sha256=MNFz-7P56OFwkNx2N24SH4gV0kL3KwzmZvAxg-T7f3U,25781
|
|
15
|
+
nettracer3d-0.7.1.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
16
|
+
nettracer3d-0.7.1.dist-info/METADATA,sha256=-RaYqwgIm9jxvt5ihvTLlhR-TMlS2YH38SAUHvQWxUA,4823
|
|
17
|
+
nettracer3d-0.7.1.dist-info/WHEEL,sha256=SmOxYU7pzNKBqASvQJ7DjX3XGUF92lrGhMb3R6_iiqI,91
|
|
18
|
+
nettracer3d-0.7.1.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
19
|
+
nettracer3d-0.7.1.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
20
|
+
nettracer3d-0.7.1.dist-info/RECORD,,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
nettracer3d/community_extractor.py,sha256=5v9SCCLX3P1RX0fjPVKH5NHMFkMolZ5BTe0bR_a67xg,24479
|
|
3
|
-
nettracer3d/modularity.py,sha256=FH3GpTHorRNkdQULe-2DWgFE3i0_u__hrao7Nx_6Ge4,30249
|
|
4
|
-
nettracer3d/morphology.py,sha256=y6OXLvyRmIBFpF3snC7ZirOW3Hg9jsHE11wbe0HrzQM,19692
|
|
5
|
-
nettracer3d/nettracer.py,sha256=muv5Z33KOLKzzzbnNLDad_z1LYCKbuQUuKQLFQ0HAPY,211734
|
|
6
|
-
nettracer3d/nettracer_gui.py,sha256=UCls6Z8O3SaXJUyxzdJlPDxIug7w290VbrojSWQDKs8,417722
|
|
7
|
-
nettracer3d/network_analysis.py,sha256=q1q7lxtA3lebxitfC_jfiT9cnpYXJw4q0Oy2_-Aj8qE,48068
|
|
8
|
-
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
9
|
-
nettracer3d/node_draw.py,sha256=k3sCTfUCJs3aH1C1q1gTNxDz9EAQbBd1hsUIJajxRx8,9823
|
|
10
|
-
nettracer3d/proximity.py,sha256=-HoJseoD2O2Q1eLDBMMmM_UYPqM1vZg7bpG4_9Ime-o,12516
|
|
11
|
-
nettracer3d/run.py,sha256=xYeaAc8FCx8MuzTGyL3NR3mK7WZzffAYAH23bNRZYO4,127
|
|
12
|
-
nettracer3d/segmenter.py,sha256=GnQTVisQUKhackgjFxoldMlbArHEwHGiZPwv-sgPTj0,85072
|
|
13
|
-
nettracer3d/simple_network.py,sha256=fP1gkDdtQcHruEZpUdasKdZeVacoLOxKhR3bY0L1CAQ,15426
|
|
14
|
-
nettracer3d/smart_dilate.py,sha256=SKm5OSu7jcnA7eUKz58_LscadV-lCvY2ZzdVzCvTmkY,25796
|
|
15
|
-
nettracer3d-0.6.9.dist-info/licenses/LICENSE,sha256=gM207DhJjWrxLuEWXl0Qz5ISbtWDmADfjHp3yC2XISs,888
|
|
16
|
-
nettracer3d-0.6.9.dist-info/METADATA,sha256=5JFlDK6cwwRKV25onj8KGX6ylD4vFXMaoZM1lzZCGaw,4755
|
|
17
|
-
nettracer3d-0.6.9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
18
|
-
nettracer3d-0.6.9.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
19
|
-
nettracer3d-0.6.9.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
20
|
-
nettracer3d-0.6.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|