nettracer3d 0.8.4__py3-none-any.whl → 0.8.5__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.
Potentially problematic release.
This version of nettracer3d might be problematic. Click here for more details.
- nettracer3d/community_extractor.py +3 -2
- nettracer3d/neighborhoods.py +140 -31
- nettracer3d/nettracer.py +10 -3
- nettracer3d/nettracer_gui.py +467 -703
- nettracer3d/painting.py +373 -0
- nettracer3d/proximity.py +2 -2
- nettracer3d/segmenter.py +849 -851
- nettracer3d/segmenter_GPU.py +806 -658
- nettracer3d/smart_dilate.py +2 -2
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/METADATA +5 -2
- nettracer3d-0.8.5.dist-info/RECORD +25 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/licenses/LICENSE +2 -4
- nettracer3d-0.8.4.dist-info/RECORD +0 -24
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.4.dist-info → nettracer3d-0.8.5.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,7 @@ import random
|
|
|
12
12
|
from . import node_draw
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
def binarize(image):
|
|
16
17
|
"""Convert an array from numerical values to boolean mask"""
|
|
17
18
|
image = image != 0
|
|
@@ -654,7 +655,7 @@ def assign_node_colors(node_list: List[int], labeled_array: np.ndarray) -> Tuple
|
|
|
654
655
|
|
|
655
656
|
# Create lookup table
|
|
656
657
|
max_label = max(max(labeled_array.flat), max(node_list) if node_list else 0)
|
|
657
|
-
color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8) # Transparent by default
|
|
658
|
+
color_lut = np.zeros((int(max_label) + 1, 4), dtype=np.uint8) # Transparent by default
|
|
658
659
|
|
|
659
660
|
for node_id, color in node_to_color.items():
|
|
660
661
|
color_lut[node_id] = color
|
|
@@ -684,7 +685,7 @@ def assign_community_colors(community_dict: Dict[int, int], labeled_array: np.nd
|
|
|
684
685
|
|
|
685
686
|
# Create lookup table - this is the key optimization
|
|
686
687
|
max_label = max(max(labeled_array.flat), max(node_to_color.keys()) if node_to_color else 0)
|
|
687
|
-
color_lut = np.zeros((max_label + 1, 4), dtype=np.uint8) # Transparent by default
|
|
688
|
+
color_lut = np.zeros((int(max_label) + 1, 4), dtype=np.uint8) # Transparent by default
|
|
688
689
|
|
|
689
690
|
for node_id, color in node_to_color.items():
|
|
690
691
|
color_lut[node_id] = color
|
nettracer3d/neighborhoods.py
CHANGED
|
@@ -2,11 +2,13 @@ import numpy as np
|
|
|
2
2
|
from sklearn.cluster import KMeans
|
|
3
3
|
from sklearn.metrics import calinski_harabasz_score
|
|
4
4
|
import matplotlib.pyplot as plt
|
|
5
|
-
from typing import Dict, Set
|
|
5
|
+
from typing import Dict, Set, List, Tuple, Optional
|
|
6
6
|
import umap
|
|
7
7
|
from matplotlib.colors import LinearSegmentedColormap
|
|
8
8
|
from sklearn.cluster import DBSCAN
|
|
9
9
|
from sklearn.neighbors import NearestNeighbors
|
|
10
|
+
import matplotlib.colors as mcolors
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
import os
|
|
@@ -308,11 +310,43 @@ def plot_dict_heatmap(unsorted_data_dict, id_set, figsize=(12, 8), title="Neighb
|
|
|
308
310
|
|
|
309
311
|
|
|
310
312
|
|
|
313
|
+
def generate_distinct_colors(n_colors: int) -> List[str]:
|
|
314
|
+
"""
|
|
315
|
+
Generate visually distinct colors using matplotlib's tab and Set colormaps.
|
|
316
|
+
Falls back to HSV generation for large numbers of colors.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
n_colors: Number of distinct colors needed
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
List of color strings compatible with matplotlib
|
|
323
|
+
"""
|
|
324
|
+
if n_colors <= 10:
|
|
325
|
+
# Use tab10 colormap for up to 10 colors
|
|
326
|
+
colors = plt.cm.tab10(np.linspace(0, 1, min(n_colors, 10)))
|
|
327
|
+
return [mcolors.rgb2hex(color) for color in colors[:n_colors]]
|
|
328
|
+
elif n_colors <= 20:
|
|
329
|
+
# Use tab20 for up to 20 colors
|
|
330
|
+
colors = plt.cm.tab20(np.linspace(0, 1, min(n_colors, 20)))
|
|
331
|
+
return [mcolors.rgb2hex(color) for color in colors[:n_colors]]
|
|
332
|
+
else:
|
|
333
|
+
# For larger numbers, use HSV space
|
|
334
|
+
colors = []
|
|
335
|
+
for i in range(n_colors):
|
|
336
|
+
hue = i / n_colors
|
|
337
|
+
color = mcolors.hsv_to_rgb([hue, 0.8, 0.9]) # High saturation and value
|
|
338
|
+
colors.append(mcolors.rgb2hex(color))
|
|
339
|
+
return colors
|
|
340
|
+
|
|
341
|
+
|
|
311
342
|
def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
312
343
|
class_names: Set[str],
|
|
313
|
-
label = False,
|
|
344
|
+
label: bool = False,
|
|
314
345
|
n_components: int = 2,
|
|
315
|
-
random_state: int = 42
|
|
346
|
+
random_state: int = 42,
|
|
347
|
+
id_dictionary: Optional[Dict[int, str]] = None,
|
|
348
|
+
graph_label = "Community ID",
|
|
349
|
+
title = 'UMAP Visualization of Community Compositions'):
|
|
316
350
|
"""
|
|
317
351
|
Convert cluster composition data to UMAP visualization.
|
|
318
352
|
|
|
@@ -323,10 +357,15 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
323
357
|
representing the composition of each cluster
|
|
324
358
|
class_names : set
|
|
325
359
|
Set of strings representing the class names (order corresponds to array indices)
|
|
360
|
+
label : bool
|
|
361
|
+
Whether to show cluster ID labels on the plot
|
|
326
362
|
n_components : int
|
|
327
363
|
Number of UMAP components (default: 2 for 2D visualization)
|
|
328
364
|
random_state : int
|
|
329
365
|
Random state for reproducibility
|
|
366
|
+
id_dictionary : dict, optional
|
|
367
|
+
Dictionary mapping cluster IDs to identity names. If provided, colors will be
|
|
368
|
+
assigned by identity rather than cluster ID, and a legend will be shown.
|
|
330
369
|
|
|
331
370
|
Returns:
|
|
332
371
|
--------
|
|
@@ -335,7 +374,10 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
335
374
|
"""
|
|
336
375
|
|
|
337
376
|
# Convert set to sorted list for consistent ordering
|
|
338
|
-
|
|
377
|
+
try:
|
|
378
|
+
class_labels = sorted(list(class_names))
|
|
379
|
+
except:
|
|
380
|
+
class_labels = None
|
|
339
381
|
|
|
340
382
|
# Extract cluster IDs and compositions
|
|
341
383
|
cluster_ids = list(cluster_data.keys())
|
|
@@ -347,57 +389,124 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
347
389
|
# Fit and transform the composition data
|
|
348
390
|
embedding = reducer.fit_transform(compositions)
|
|
349
391
|
|
|
392
|
+
# Prepare colors and labels based on whether id_dictionary is provided
|
|
393
|
+
if id_dictionary is not None:
|
|
394
|
+
# Get unique identities and assign colors (group all unknown into single "Unknown" category)
|
|
395
|
+
identities = [id_dictionary.get(cluster_id, "Unknown") for cluster_id in cluster_ids]
|
|
396
|
+
unique_identities = sorted(list(set(identities)))
|
|
397
|
+
colors = generate_distinct_colors(len(unique_identities))
|
|
398
|
+
identity_to_color = {identity: colors[i] for i, identity in enumerate(unique_identities)}
|
|
399
|
+
point_colors = [identity_to_color[identity] for identity in identities]
|
|
400
|
+
use_identity_coloring = True
|
|
401
|
+
else:
|
|
402
|
+
# Use default cluster ID coloring
|
|
403
|
+
point_colors = cluster_ids
|
|
404
|
+
use_identity_coloring = False
|
|
405
|
+
|
|
350
406
|
# Create visualization
|
|
351
|
-
plt.figure(figsize=(
|
|
407
|
+
plt.figure(figsize=(12, 8))
|
|
352
408
|
|
|
353
409
|
if n_components == 2:
|
|
354
|
-
|
|
355
|
-
|
|
410
|
+
if use_identity_coloring:
|
|
411
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
412
|
+
c=point_colors, s=100, alpha=0.7)
|
|
413
|
+
else:
|
|
414
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
415
|
+
c=point_colors, cmap='viridis', s=100, alpha=0.7)
|
|
356
416
|
|
|
357
417
|
if label:
|
|
358
418
|
# Add cluster ID labels
|
|
359
419
|
for i, cluster_id in enumerate(cluster_ids):
|
|
360
|
-
|
|
420
|
+
display_label = f'{cluster_id}'
|
|
421
|
+
if id_dictionary is not None:
|
|
422
|
+
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
423
|
+
display_label = f'{cluster_id}\n({identity})'
|
|
424
|
+
|
|
425
|
+
plt.annotate(display_label,
|
|
361
426
|
(embedding[i, 0], embedding[i, 1]),
|
|
362
427
|
xytext=(5, 5), textcoords='offset points',
|
|
363
|
-
fontsize=9, alpha=0.8)
|
|
428
|
+
fontsize=9, alpha=0.8, ha='left')
|
|
429
|
+
|
|
430
|
+
# Add appropriate legend/colorbar
|
|
431
|
+
if use_identity_coloring:
|
|
432
|
+
# Create custom legend for identities
|
|
433
|
+
legend_elements = [plt.Line2D([0], [0], marker='o', color='w',
|
|
434
|
+
markerfacecolor=identity_to_color[identity],
|
|
435
|
+
markersize=10, label=identity)
|
|
436
|
+
for identity in unique_identities]
|
|
437
|
+
plt.legend(handles=legend_elements, title='Identity',
|
|
438
|
+
bbox_to_anchor=(1.05, 1), loc='upper left')
|
|
439
|
+
else:
|
|
440
|
+
plt.colorbar(scatter, label=graph_label)
|
|
364
441
|
|
|
365
|
-
plt.colorbar(scatter, label='Community ID')
|
|
366
442
|
plt.xlabel('UMAP Component 1')
|
|
367
443
|
plt.ylabel('UMAP Component 2')
|
|
368
|
-
|
|
444
|
+
if use_identity_coloring:
|
|
445
|
+
title += ' (Colored by Identity)'
|
|
446
|
+
plt.title(title)
|
|
369
447
|
|
|
370
448
|
elif n_components == 3:
|
|
371
|
-
fig = plt.figure(figsize=(
|
|
449
|
+
fig = plt.figure(figsize=(14, 10))
|
|
372
450
|
ax = fig.add_subplot(111, projection='3d')
|
|
373
|
-
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
374
|
-
c=cluster_ids, cmap='viridis', s=100, alpha=0.7)
|
|
375
451
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
452
|
+
if use_identity_coloring:
|
|
453
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
454
|
+
c=point_colors, s=100, alpha=0.7)
|
|
455
|
+
else:
|
|
456
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
457
|
+
c=point_colors, cmap='viridis', s=100, alpha=0.7)
|
|
458
|
+
|
|
459
|
+
if label:
|
|
460
|
+
# Add cluster ID labels
|
|
461
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
462
|
+
display_label = f'C{cluster_id}'
|
|
463
|
+
if id_dictionary is not None:
|
|
464
|
+
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
465
|
+
display_label = f'C{cluster_id}\n({identity})'
|
|
466
|
+
|
|
467
|
+
ax.text(embedding[i, 0], embedding[i, 1], embedding[i, 2],
|
|
468
|
+
display_label, fontsize=8)
|
|
380
469
|
|
|
381
470
|
ax.set_xlabel('UMAP Component 1')
|
|
382
471
|
ax.set_ylabel('UMAP Component 2')
|
|
383
472
|
ax.set_zlabel('UMAP Component 3')
|
|
384
|
-
|
|
385
|
-
|
|
473
|
+
title = '3D UMAP Visualization of Cluster Compositions'
|
|
474
|
+
if use_identity_coloring:
|
|
475
|
+
title += ' (Colored by Identity)'
|
|
476
|
+
ax.set_title(title)
|
|
477
|
+
|
|
478
|
+
# Add appropriate legend/colorbar
|
|
479
|
+
if use_identity_coloring:
|
|
480
|
+
# Create custom legend for identities
|
|
481
|
+
legend_elements = [plt.Line2D([0], [0], marker='o', color='w',
|
|
482
|
+
markerfacecolor=identity_to_color[identity],
|
|
483
|
+
markersize=10, label=identity)
|
|
484
|
+
for identity in unique_identities]
|
|
485
|
+
ax.legend(handles=legend_elements, title='Identity',
|
|
486
|
+
bbox_to_anchor=(1.05, 1), loc='upper left')
|
|
487
|
+
else:
|
|
488
|
+
plt.colorbar(scatter, label='Cluster ID')
|
|
386
489
|
|
|
387
490
|
plt.tight_layout()
|
|
388
491
|
plt.show()
|
|
389
492
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
493
|
+
if class_labels is not None:
|
|
494
|
+
# Print composition details
|
|
495
|
+
print("Cluster Compositions:")
|
|
496
|
+
print(f"Classes: {class_labels}")
|
|
497
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
498
|
+
composition = compositions[i]
|
|
499
|
+
identity_info = ""
|
|
500
|
+
if id_dictionary is not None:
|
|
501
|
+
identity = id_dictionary.get(cluster_id, "Unknown")
|
|
502
|
+
identity_info = f" (Identity: {identity})"
|
|
503
|
+
|
|
504
|
+
print(f"Cluster {cluster_id}{identity_info}: {composition}")
|
|
505
|
+
# Show which classes dominate this cluster
|
|
506
|
+
dominant_indices = np.argsort(composition)[::-1][:2] # Top 2
|
|
507
|
+
dominant_classes = [class_labels[idx] for idx in dominant_indices]
|
|
508
|
+
dominant_values = [composition[idx] for idx in dominant_indices]
|
|
509
|
+
print(f" Dominant: {dominant_classes[0]} ({dominant_values[0]:.3f}), {dominant_classes[1]} ({dominant_values[1]:.3f})")
|
|
401
510
|
|
|
402
511
|
return embedding
|
|
403
512
|
|
nettracer3d/nettracer.py
CHANGED
|
@@ -105,7 +105,7 @@ def process_label(args):
|
|
|
105
105
|
print(f"Processing node {label}")
|
|
106
106
|
|
|
107
107
|
# Get the pre-computed bounding box for this label
|
|
108
|
-
slice_obj = bounding_boxes[label-1] # -1 because label numbers start at 1
|
|
108
|
+
slice_obj = bounding_boxes[int(label)-1] # -1 because label numbers start at 1
|
|
109
109
|
if slice_obj is None:
|
|
110
110
|
return None, None, None
|
|
111
111
|
|
|
@@ -130,7 +130,7 @@ def create_node_dictionary(nodes, edges, num_nodes, dilate_xy, dilate_z):
|
|
|
130
130
|
with ThreadPoolExecutor(max_workers=mp.cpu_count()) as executor:
|
|
131
131
|
# Create args list with bounding_boxes included
|
|
132
132
|
args_list = [(nodes, edges, i, dilate_xy, dilate_z, array_shape, bounding_boxes)
|
|
133
|
-
for i in range(1, num_nodes + 1)]
|
|
133
|
+
for i in range(1, int(num_nodes) + 1)]
|
|
134
134
|
|
|
135
135
|
# Execute parallel tasks to process labels
|
|
136
136
|
results = executor.map(process_label, args_list)
|
|
@@ -5152,7 +5152,7 @@ class Network_3D:
|
|
|
5152
5152
|
search_x, search_z = dilation_length_to_pixels(self._xy_scale, self._z_scale, search, search)
|
|
5153
5153
|
|
|
5154
5154
|
|
|
5155
|
-
num_nodes = np.max(self._nodes)
|
|
5155
|
+
num_nodes = int(np.max(self._nodes))
|
|
5156
5156
|
|
|
5157
5157
|
my_dict = proximity.create_node_dictionary(self._nodes, num_nodes, search_x, search_z, targets = targets, fastdil = fastdil, xy_scale = self._xy_scale, z_scale = self._z_scale, search = search)
|
|
5158
5158
|
|
|
@@ -5299,6 +5299,13 @@ class Network_3D:
|
|
|
5299
5299
|
|
|
5300
5300
|
return output
|
|
5301
5301
|
|
|
5302
|
+
def centroid_umap(self):
|
|
5303
|
+
|
|
5304
|
+
from . import neighborhoods
|
|
5305
|
+
|
|
5306
|
+
neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
|
|
5307
|
+
|
|
5308
|
+
|
|
5302
5309
|
def community_id_info_per_com(self, umap = False, label = False, limit = 0, proportional = False):
|
|
5303
5310
|
|
|
5304
5311
|
community_dict = invert_dict(self.communities)
|