nettracer3d 0.9.3__py3-none-any.whl → 0.9.4__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/excelotron.py +73 -19
- nettracer3d/neighborhoods.py +213 -17
- nettracer3d/nettracer.py +145 -15
- nettracer3d/nettracer_gui.py +21 -5
- {nettracer3d-0.9.3.dist-info → nettracer3d-0.9.4.dist-info}/METADATA +3 -3
- {nettracer3d-0.9.3.dist-info → nettracer3d-0.9.4.dist-info}/RECORD +10 -10
- {nettracer3d-0.9.3.dist-info → nettracer3d-0.9.4.dist-info}/WHEEL +0 -0
- {nettracer3d-0.9.3.dist-info → nettracer3d-0.9.4.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.9.3.dist-info → nettracer3d-0.9.4.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.9.3.dist-info → nettracer3d-0.9.4.dist-info}/top_level.txt +0 -0
nettracer3d/excelotron.py
CHANGED
|
@@ -4,7 +4,7 @@ import numpy as np
|
|
|
4
4
|
from PyQt6.QtWidgets import (QApplication, QMainWindow, QHBoxLayout, QVBoxLayout,
|
|
5
5
|
QWidget, QTableWidget, QTableWidgetItem, QPushButton,
|
|
6
6
|
QLabel, QLineEdit, QScrollArea, QFrame, QMessageBox,
|
|
7
|
-
QHeaderView, QAbstractItemView, QSplitter, QTabWidget)
|
|
7
|
+
QHeaderView, QAbstractItemView, QSplitter, QTabWidget, QCheckBox)
|
|
8
8
|
from PyQt6.QtCore import Qt, QMimeData, pyqtSignal
|
|
9
9
|
from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QDrag, QPainter, QPixmap
|
|
10
10
|
import os
|
|
@@ -502,6 +502,35 @@ class ClassifierGroupWidget(QFrame):
|
|
|
502
502
|
|
|
503
503
|
header_layout.addStretch()
|
|
504
504
|
|
|
505
|
+
# Hierarchical toggle
|
|
506
|
+
self.hierarchical_checkbox = QCheckBox("Hierarchical")
|
|
507
|
+
self.hierarchical_checkbox.setChecked(True) # Default to hierarchical
|
|
508
|
+
self.hierarchical_checkbox.setStyleSheet("""
|
|
509
|
+
QCheckBox {
|
|
510
|
+
font-weight: bold;
|
|
511
|
+
color: #007acc;
|
|
512
|
+
padding: 5px;
|
|
513
|
+
}
|
|
514
|
+
QCheckBox::indicator {
|
|
515
|
+
width: 18px;
|
|
516
|
+
height: 18px;
|
|
517
|
+
}
|
|
518
|
+
QCheckBox::indicator:unchecked {
|
|
519
|
+
border: 2px solid #007acc;
|
|
520
|
+
background-color: white;
|
|
521
|
+
border-radius: 3px;
|
|
522
|
+
}
|
|
523
|
+
QCheckBox::indicator:checked {
|
|
524
|
+
border: 2px solid #007acc;
|
|
525
|
+
background-color: #007acc;
|
|
526
|
+
border-radius: 3px;
|
|
527
|
+
}
|
|
528
|
+
QCheckBox::indicator:checked:hover {
|
|
529
|
+
background-color: #005a9e;
|
|
530
|
+
}
|
|
531
|
+
""")
|
|
532
|
+
header_layout.addWidget(self.hierarchical_checkbox)
|
|
533
|
+
|
|
505
534
|
# Add classifier button
|
|
506
535
|
add_btn = QPushButton("+ Add Classifier")
|
|
507
536
|
add_btn.setStyleSheet("""
|
|
@@ -559,7 +588,7 @@ class ClassifierGroupWidget(QFrame):
|
|
|
559
588
|
layout.addWidget(preview_btn)
|
|
560
589
|
|
|
561
590
|
self.setLayout(layout)
|
|
562
|
-
|
|
591
|
+
|
|
563
592
|
def add_classifier(self):
|
|
564
593
|
self.classifier_counter += 1
|
|
565
594
|
classifier_id = self.classifier_counter
|
|
@@ -597,25 +626,50 @@ class ClassifierGroupWidget(QFrame):
|
|
|
597
626
|
matched_identities = set()
|
|
598
627
|
classifier_usage = {classifier_id: 0 for classifier_id in classifier_ids} # Now classifier_ids is defined
|
|
599
628
|
|
|
629
|
+
|
|
600
630
|
for identity in original_identities:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
# Check classifiers in order
|
|
604
|
-
for classifier_id in classifier_ids:
|
|
605
|
-
classifier = self.classifiers[classifier_id]
|
|
606
|
-
|
|
607
|
-
if classifier.matches_identity(identity_str):
|
|
608
|
-
# This classifier matches
|
|
609
|
-
matched_identities.add(identity)
|
|
610
|
-
classifier_usage[classifier_id] += 1 # Track usage
|
|
631
|
+
identity_str = str(identity)
|
|
611
632
|
|
|
612
|
-
#
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
633
|
+
# Check classifiers in order
|
|
634
|
+
for classifier_id in classifier_ids:
|
|
635
|
+
classifier = self.classifiers[classifier_id]
|
|
636
|
+
|
|
637
|
+
if classifier.matches_identity(identity_str):
|
|
638
|
+
# This classifier matches
|
|
639
|
+
matched_identities.add(identity)
|
|
640
|
+
classifier_usage[classifier_id] += 1 # Track usage
|
|
641
|
+
|
|
642
|
+
# Set the new ID if provided
|
|
643
|
+
new_id = classifier.get_new_id()
|
|
644
|
+
if new_id and identity in self.identity_remap_widget.identity_mappings:
|
|
645
|
+
if self.hierarchical_checkbox.isChecked():
|
|
646
|
+
# Hierarchical mode - just set the new ID
|
|
647
|
+
self.identity_remap_widget.identity_mappings[identity]['new_edit'].setText(new_id)
|
|
648
|
+
else:
|
|
649
|
+
# Non-hierarchical mode - append to existing or create new
|
|
650
|
+
current_text = self.identity_remap_widget.identity_mappings[identity]['new_edit'].text().strip()
|
|
651
|
+
if current_text:
|
|
652
|
+
# Parse existing text to see if it's already a list
|
|
653
|
+
try:
|
|
654
|
+
existing_list = literal_eval(current_text)
|
|
655
|
+
if isinstance(existing_list, list):
|
|
656
|
+
existing_list.append(new_id)
|
|
657
|
+
new_text = str(existing_list)
|
|
658
|
+
else:
|
|
659
|
+
# Current text is a single value, make it a list
|
|
660
|
+
new_text = str([current_text, new_id])
|
|
661
|
+
except:
|
|
662
|
+
# If parsing fails, treat as single value
|
|
663
|
+
new_text = str([current_text, new_id])
|
|
664
|
+
else:
|
|
665
|
+
# No existing text, just set the new ID
|
|
666
|
+
new_text = new_id
|
|
667
|
+
|
|
668
|
+
self.identity_remap_widget.identity_mappings[identity]['new_edit'].setText(new_text)
|
|
669
|
+
|
|
670
|
+
# Only break if hierarchical mode (first match wins)
|
|
671
|
+
if self.hierarchical_checkbox.isChecked():
|
|
672
|
+
break
|
|
619
673
|
|
|
620
674
|
# Remove identities that didn't match any classifier
|
|
621
675
|
unmatched_identities = set(original_identities) - matched_identities
|
nettracer3d/neighborhoods.py
CHANGED
|
@@ -347,7 +347,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
347
347
|
id_dictionary: Optional[Dict[int, str]] = None,
|
|
348
348
|
graph_label = "Community ID",
|
|
349
349
|
title = 'UMAP Visualization of Community Compositions',
|
|
350
|
-
neighborhoods: Optional[Dict[int, int]] = None
|
|
350
|
+
neighborhoods: Optional[Dict[int, int]] = None,
|
|
351
|
+
draw_lines: bool = False):
|
|
351
352
|
"""
|
|
352
353
|
Convert cluster composition data to UMAP visualization.
|
|
353
354
|
|
|
@@ -370,6 +371,8 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
370
371
|
neighborhoods : dict, optional
|
|
371
372
|
Dictionary mapping node IDs to neighborhood IDs {node_id: neighborhood_id}.
|
|
372
373
|
If provided, points will be colored by neighborhood using community coloration methods.
|
|
374
|
+
draw_lines : bool
|
|
375
|
+
Whether to draw lines between nodes that share identities (default: False)
|
|
373
376
|
|
|
374
377
|
Returns:
|
|
375
378
|
--------
|
|
@@ -453,15 +456,111 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
453
456
|
plt.figure(figsize=(12, 8))
|
|
454
457
|
|
|
455
458
|
if n_components == 2:
|
|
456
|
-
if
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
459
|
+
# Draw scatter with different markers for multi-identity nodes if draw_lines is enabled
|
|
460
|
+
if draw_lines:
|
|
461
|
+
# Separate multi-identity and singleton nodes for different markers
|
|
462
|
+
singleton_indices = []
|
|
463
|
+
multi_indices = []
|
|
464
|
+
singleton_colors = []
|
|
465
|
+
multi_colors = []
|
|
466
|
+
|
|
467
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
468
|
+
vec = cluster_data[cluster_id]
|
|
469
|
+
if np.sum(vec) > 1: # Multi-identity
|
|
470
|
+
multi_indices.append(i)
|
|
471
|
+
multi_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
|
|
472
|
+
else: # Singleton
|
|
473
|
+
singleton_indices.append(i)
|
|
474
|
+
singleton_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
|
|
475
|
+
|
|
476
|
+
# Draw singleton nodes as circles
|
|
477
|
+
if singleton_indices:
|
|
478
|
+
if use_neighborhood_coloring or use_identity_coloring:
|
|
479
|
+
scatter1 = plt.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1],
|
|
480
|
+
c=singleton_colors, s=100, alpha=0.7, marker='o')
|
|
481
|
+
else:
|
|
482
|
+
scatter1 = plt.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1],
|
|
483
|
+
c=[point_colors[i] for i in singleton_indices], cmap='viridis', s=100, alpha=0.7, marker='o')
|
|
484
|
+
|
|
485
|
+
# Draw multi-identity nodes as squares
|
|
486
|
+
if multi_indices:
|
|
487
|
+
if use_neighborhood_coloring or use_identity_coloring:
|
|
488
|
+
scatter2 = plt.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1],
|
|
489
|
+
c=multi_colors, s=100, alpha=0.7, marker='s')
|
|
490
|
+
else:
|
|
491
|
+
scatter2 = plt.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1],
|
|
492
|
+
c=[point_colors[i] for i in multi_indices], cmap='viridis', s=100, alpha=0.7, marker='s')
|
|
493
|
+
scatter = scatter2 # For colorbar reference
|
|
494
|
+
else:
|
|
495
|
+
scatter = scatter1 if singleton_indices else None
|
|
462
496
|
else:
|
|
463
|
-
|
|
464
|
-
|
|
497
|
+
# Original behavior when draw_lines is False
|
|
498
|
+
if use_neighborhood_coloring:
|
|
499
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
500
|
+
c=point_colors, s=100, alpha=0.7)
|
|
501
|
+
elif use_identity_coloring:
|
|
502
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
503
|
+
c=point_colors, s=100, alpha=0.7)
|
|
504
|
+
else:
|
|
505
|
+
scatter = plt.scatter(embedding[:, 0], embedding[:, 1],
|
|
506
|
+
c=point_colors, cmap='viridis', s=100, alpha=0.7)
|
|
507
|
+
|
|
508
|
+
# Draw lines between nodes with shared identities (only if draw_lines=True)
|
|
509
|
+
if draw_lines:
|
|
510
|
+
# First pass: identify unique multi-identity configurations and their representatives
|
|
511
|
+
multi_config_map = {} # Maps tuple(config) -> {'count': int, 'representative_idx': int}
|
|
512
|
+
|
|
513
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
514
|
+
vec = cluster_data[cluster_id]
|
|
515
|
+
if np.sum(vec) > 1: # Multi-identity node
|
|
516
|
+
config = tuple(vec) # Convert to hashable tuple
|
|
517
|
+
if config not in multi_config_map:
|
|
518
|
+
multi_config_map[config] = {'count': 1, 'representative_idx': i}
|
|
519
|
+
else:
|
|
520
|
+
multi_config_map[config]['count'] += 1
|
|
521
|
+
|
|
522
|
+
# Second pass: draw lines for each unique configuration
|
|
523
|
+
for config, info in multi_config_map.items():
|
|
524
|
+
i = info['representative_idx']
|
|
525
|
+
count = info['count']
|
|
526
|
+
vec1 = np.array(config)
|
|
527
|
+
|
|
528
|
+
# For each identity this configuration has, find the closest representative
|
|
529
|
+
identity_indices = np.where(vec1 == 1)[0]
|
|
530
|
+
|
|
531
|
+
for identity_idx in identity_indices:
|
|
532
|
+
best_target = None
|
|
533
|
+
best_distance = float('inf')
|
|
534
|
+
backup_target = None
|
|
535
|
+
backup_distance = float('inf')
|
|
536
|
+
|
|
537
|
+
# Find closest node with this specific identity
|
|
538
|
+
for j, cluster_id2 in enumerate(cluster_ids):
|
|
539
|
+
if i != j: # Don't connect to self
|
|
540
|
+
vec2 = cluster_data[cluster_id2]
|
|
541
|
+
if vec2[identity_idx] == 1: # Shares this specific identity
|
|
542
|
+
distance = np.linalg.norm(embedding[i] - embedding[j])
|
|
543
|
+
|
|
544
|
+
# Prefer singleton nodes
|
|
545
|
+
if np.sum(vec2) == 1: # Singleton
|
|
546
|
+
if distance < best_distance:
|
|
547
|
+
best_distance = distance
|
|
548
|
+
best_target = j
|
|
549
|
+
else: # Multi-identity node (backup)
|
|
550
|
+
if distance < backup_distance:
|
|
551
|
+
backup_distance = distance
|
|
552
|
+
backup_target = j
|
|
553
|
+
|
|
554
|
+
# Draw line to best target (prefer singleton, fallback to multi)
|
|
555
|
+
target = best_target if best_target is not None else backup_target
|
|
556
|
+
if target is not None:
|
|
557
|
+
# Calculate relative line weight with reasonable cap
|
|
558
|
+
max_count = max(info['count'] for info in multi_config_map.values())
|
|
559
|
+
relative_weight = count / max_count # Normalize to 0-1
|
|
560
|
+
line_weight = 0.3 + relative_weight * 1.2 # Scale to 0.3-1.5 range
|
|
561
|
+
plt.plot([embedding[i, 0], embedding[target, 0]],
|
|
562
|
+
[embedding[i, 1], embedding[target, 1]],
|
|
563
|
+
alpha=0.3, color='gray', linewidth=line_weight)
|
|
465
564
|
|
|
466
565
|
if label:
|
|
467
566
|
# Add cluster ID labels
|
|
@@ -516,15 +615,112 @@ def visualize_cluster_composition_umap(cluster_data: Dict[int, np.ndarray],
|
|
|
516
615
|
fig = plt.figure(figsize=(14, 10))
|
|
517
616
|
ax = fig.add_subplot(111, projection='3d')
|
|
518
617
|
|
|
519
|
-
if
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
618
|
+
# Draw scatter with different markers for multi-identity nodes if draw_lines is enabled
|
|
619
|
+
if draw_lines:
|
|
620
|
+
# Separate multi-identity and singleton nodes for different markers
|
|
621
|
+
singleton_indices = []
|
|
622
|
+
multi_indices = []
|
|
623
|
+
singleton_colors = []
|
|
624
|
+
multi_colors = []
|
|
625
|
+
|
|
626
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
627
|
+
vec = cluster_data[cluster_id]
|
|
628
|
+
if np.sum(vec) > 1: # Multi-identity
|
|
629
|
+
multi_indices.append(i)
|
|
630
|
+
multi_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
|
|
631
|
+
else: # Singleton
|
|
632
|
+
singleton_indices.append(i)
|
|
633
|
+
singleton_colors.append(point_colors[i] if isinstance(point_colors, list) else point_colors)
|
|
634
|
+
|
|
635
|
+
# Draw singleton nodes as circles
|
|
636
|
+
if singleton_indices:
|
|
637
|
+
if use_neighborhood_coloring or use_identity_coloring:
|
|
638
|
+
scatter1 = ax.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1], embedding[singleton_indices, 2],
|
|
639
|
+
c=singleton_colors, s=100, alpha=0.7, marker='o')
|
|
640
|
+
else:
|
|
641
|
+
scatter1 = ax.scatter(embedding[singleton_indices, 0], embedding[singleton_indices, 1], embedding[singleton_indices, 2],
|
|
642
|
+
c=[point_colors[i] for i in singleton_indices], cmap='viridis', s=100, alpha=0.7, marker='o')
|
|
643
|
+
|
|
644
|
+
# Draw multi-identity nodes as squares
|
|
645
|
+
if multi_indices:
|
|
646
|
+
if use_neighborhood_coloring or use_identity_coloring:
|
|
647
|
+
scatter2 = ax.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1], embedding[multi_indices, 2],
|
|
648
|
+
c=multi_colors, s=100, alpha=0.7, marker='s')
|
|
649
|
+
else:
|
|
650
|
+
scatter2 = ax.scatter(embedding[multi_indices, 0], embedding[multi_indices, 1], embedding[multi_indices, 2],
|
|
651
|
+
c=[point_colors[i] for i in multi_indices], cmap='viridis', s=100, alpha=0.7, marker='s')
|
|
652
|
+
scatter = scatter2 # For colorbar reference
|
|
653
|
+
else:
|
|
654
|
+
scatter = scatter1 if singleton_indices else None
|
|
525
655
|
else:
|
|
526
|
-
|
|
527
|
-
|
|
656
|
+
# Original behavior when draw_lines is False
|
|
657
|
+
if use_neighborhood_coloring:
|
|
658
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
659
|
+
c=point_colors, s=100, alpha=0.7)
|
|
660
|
+
elif use_identity_coloring:
|
|
661
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
662
|
+
c=point_colors, s=100, alpha=0.7)
|
|
663
|
+
else:
|
|
664
|
+
scatter = ax.scatter(embedding[:, 0], embedding[:, 1], embedding[:, 2],
|
|
665
|
+
c=point_colors, cmap='viridis', s=100, alpha=0.7)
|
|
666
|
+
|
|
667
|
+
# Draw lines between nodes with shared identities (only if draw_lines=True)
|
|
668
|
+
if draw_lines:
|
|
669
|
+
# First pass: identify unique multi-identity configurations and their representatives
|
|
670
|
+
multi_config_map = {} # Maps tuple(config) -> {'count': int, 'representative_idx': int}
|
|
671
|
+
|
|
672
|
+
for i, cluster_id in enumerate(cluster_ids):
|
|
673
|
+
vec = cluster_data[cluster_id]
|
|
674
|
+
if np.sum(vec) > 1: # Multi-identity node
|
|
675
|
+
config = tuple(vec) # Convert to hashable tuple
|
|
676
|
+
if config not in multi_config_map:
|
|
677
|
+
multi_config_map[config] = {'count': 1, 'representative_idx': i}
|
|
678
|
+
else:
|
|
679
|
+
multi_config_map[config]['count'] += 1
|
|
680
|
+
|
|
681
|
+
# Second pass: draw lines for each unique configuration
|
|
682
|
+
for config, info in multi_config_map.items():
|
|
683
|
+
i = info['representative_idx']
|
|
684
|
+
count = info['count']
|
|
685
|
+
vec1 = np.array(config)
|
|
686
|
+
|
|
687
|
+
# For each identity this configuration has, find the closest representative
|
|
688
|
+
identity_indices = np.where(vec1 == 1)[0]
|
|
689
|
+
|
|
690
|
+
for identity_idx in identity_indices:
|
|
691
|
+
best_target = None
|
|
692
|
+
best_distance = float('inf')
|
|
693
|
+
backup_target = None
|
|
694
|
+
backup_distance = float('inf')
|
|
695
|
+
|
|
696
|
+
# Find closest node with this specific identity
|
|
697
|
+
for j, cluster_id2 in enumerate(cluster_ids):
|
|
698
|
+
if i != j: # Don't connect to self
|
|
699
|
+
vec2 = cluster_data[cluster_id2]
|
|
700
|
+
if vec2[identity_idx] == 1: # Shares this specific identity
|
|
701
|
+
distance = np.linalg.norm(embedding[i] - embedding[j])
|
|
702
|
+
|
|
703
|
+
# Prefer singleton nodes
|
|
704
|
+
if np.sum(vec2) == 1: # Singleton
|
|
705
|
+
if distance < best_distance:
|
|
706
|
+
best_distance = distance
|
|
707
|
+
best_target = j
|
|
708
|
+
else: # Multi-identity node (backup)
|
|
709
|
+
if distance < backup_distance:
|
|
710
|
+
backup_distance = distance
|
|
711
|
+
backup_target = j
|
|
712
|
+
|
|
713
|
+
# Draw line to best target (prefer singleton, fallback to multi)
|
|
714
|
+
target = best_target if best_target is not None else backup_target
|
|
715
|
+
if target is not None:
|
|
716
|
+
# Calculate relative line weight with reasonable cap
|
|
717
|
+
max_count = max(info['count'] for info in multi_config_map.values())
|
|
718
|
+
relative_weight = count / max_count # Normalize to 0-1
|
|
719
|
+
line_weight = 0.3 + relative_weight * 1.2 # Scale to 0.3-1.5 range
|
|
720
|
+
ax.plot([embedding[i, 0], embedding[target, 0]],
|
|
721
|
+
[embedding[i, 1], embedding[target, 1]],
|
|
722
|
+
[embedding[i, 2], embedding[target, 2]],
|
|
723
|
+
alpha=0.3, color='gray', linewidth=line_weight)
|
|
528
724
|
|
|
529
725
|
if label:
|
|
530
726
|
# Add cluster ID labels
|
nettracer3d/nettracer.py
CHANGED
|
@@ -4,6 +4,7 @@ import tifffile
|
|
|
4
4
|
from scipy import ndimage
|
|
5
5
|
from skimage import measure
|
|
6
6
|
import cv2
|
|
7
|
+
import ast
|
|
7
8
|
import concurrent.futures
|
|
8
9
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
9
10
|
from scipy.ndimage import zoom
|
|
@@ -382,6 +383,27 @@ def invert_dict(d):
|
|
|
382
383
|
inverted.setdefault(value, []).append(key)
|
|
383
384
|
return inverted
|
|
384
385
|
|
|
386
|
+
def invert_dict_special(d):
|
|
387
|
+
|
|
388
|
+
d = invert_dict(d)
|
|
389
|
+
|
|
390
|
+
new_dict = copy.deepcopy(d)
|
|
391
|
+
|
|
392
|
+
for key, vals in d.items():
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
idens = ast.literal_eval(key)
|
|
396
|
+
for iden in idens:
|
|
397
|
+
try:
|
|
398
|
+
new_dict[iden].extend(vals)
|
|
399
|
+
except:
|
|
400
|
+
new_dict[iden] = vals
|
|
401
|
+
del new_dict[key]
|
|
402
|
+
except:
|
|
403
|
+
pass
|
|
404
|
+
return new_dict
|
|
405
|
+
|
|
406
|
+
|
|
385
407
|
def invert_array(array):
|
|
386
408
|
"""Internal method used to flip node array indices. 0 becomes 255 and vice versa."""
|
|
387
409
|
inverted_array = np.where(array == 0, 255, 0).astype(np.uint8)
|
|
@@ -2117,7 +2139,19 @@ def erode(arrayimage, amount, xy_scale = 1, z_scale = 1, mode = 0):
|
|
|
2117
2139
|
|
|
2118
2140
|
return arrayimage
|
|
2119
2141
|
|
|
2142
|
+
def iden_set(idens):
|
|
2143
|
+
|
|
2144
|
+
idens = set(idens)
|
|
2145
|
+
real_iden_set = []
|
|
2146
|
+
for iden in idens:
|
|
2147
|
+
try:
|
|
2148
|
+
options = ast.literal_eval(iden)
|
|
2149
|
+
for opt in options:
|
|
2150
|
+
real_iden_set.append(opt)
|
|
2151
|
+
except:
|
|
2152
|
+
real_iden_set.append(iden)
|
|
2120
2153
|
|
|
2154
|
+
return set(real_iden_set)
|
|
2121
2155
|
|
|
2122
2156
|
|
|
2123
2157
|
|
|
@@ -5451,7 +5485,7 @@ class Network_3D:
|
|
|
5451
5485
|
|
|
5452
5486
|
community_dict = invert_dict(self.communities)
|
|
5453
5487
|
summation = 0
|
|
5454
|
-
id_set =
|
|
5488
|
+
id_set = iden_set(self.node_identities.values())
|
|
5455
5489
|
output = {sort: 0 for sort in id_set}
|
|
5456
5490
|
template = copy.deepcopy(output)
|
|
5457
5491
|
|
|
@@ -5463,7 +5497,12 @@ class Network_3D:
|
|
|
5463
5497
|
|
|
5464
5498
|
# Count identities in this community
|
|
5465
5499
|
for node in nodes:
|
|
5466
|
-
|
|
5500
|
+
try:
|
|
5501
|
+
idens = ast.literal_eval(self.node_identities[node])
|
|
5502
|
+
for iden in idens:
|
|
5503
|
+
counter[iden] += 1
|
|
5504
|
+
except:
|
|
5505
|
+
counter[self.node_identities[node]] += 1
|
|
5467
5506
|
|
|
5468
5507
|
# Convert to proportions within this community and weight by size
|
|
5469
5508
|
for sort in counter:
|
|
@@ -5492,11 +5531,60 @@ class Network_3D:
|
|
|
5492
5531
|
neighborhoods.visualize_cluster_composition_umap(self.node_centroids, None, id_dictionary = self.node_identities, graph_label = "Node ID", title = 'UMAP Visualization of Node Centroids')
|
|
5493
5532
|
|
|
5494
5533
|
|
|
5534
|
+
|
|
5535
|
+
def identity_umap(self):
|
|
5536
|
+
|
|
5537
|
+
try:
|
|
5538
|
+
|
|
5539
|
+
id_set = iden_set(self.node_identities.values())
|
|
5540
|
+
|
|
5541
|
+
template = np.zeros(len(id_set))
|
|
5542
|
+
|
|
5543
|
+
id_dict = {}
|
|
5544
|
+
for i, iden in enumerate(id_set):
|
|
5545
|
+
id_dict[iden] = i
|
|
5546
|
+
|
|
5547
|
+
umap_dict = {}
|
|
5548
|
+
|
|
5549
|
+
for node in self.node_identities.keys():
|
|
5550
|
+
umap_dict[node] = copy.deepcopy(template)
|
|
5551
|
+
try:
|
|
5552
|
+
idens = ast.literal_eval(self.node_identities[node])
|
|
5553
|
+
for iden in idens:
|
|
5554
|
+
index = id_dict[iden]
|
|
5555
|
+
ref = umap_dict[node]
|
|
5556
|
+
ref[index] = 1
|
|
5557
|
+
umap_dict[node] = ref
|
|
5558
|
+
except:
|
|
5559
|
+
index = id_dict[self.node_identities[node]]
|
|
5560
|
+
ref = umap_dict[node]
|
|
5561
|
+
ref[index] = 1
|
|
5562
|
+
umap_dict[node] = ref
|
|
5563
|
+
|
|
5564
|
+
neighbor_classes = {}
|
|
5565
|
+
import random
|
|
5566
|
+
|
|
5567
|
+
for node, iden in self.node_identities.items():
|
|
5568
|
+
try:
|
|
5569
|
+
idens = ast.literal_eval(iden)
|
|
5570
|
+
neighbor_classes[node] = random.choice(idens)
|
|
5571
|
+
except:
|
|
5572
|
+
neighbor_classes[node] = iden
|
|
5573
|
+
|
|
5574
|
+
|
|
5575
|
+
from . import neighborhoods
|
|
5576
|
+
|
|
5577
|
+
neighborhoods.visualize_cluster_composition_umap(umap_dict, None, id_dictionary = neighbor_classes, graph_label = "Node ID", title = 'UMAP Visualization of Node Identities', draw_lines = True)
|
|
5578
|
+
|
|
5579
|
+
except Exception as e:
|
|
5580
|
+
print(f"Error: {e}")
|
|
5581
|
+
|
|
5582
|
+
|
|
5495
5583
|
def community_id_info_per_com(self, umap = False, label = 0, limit = 0, proportional = False, neighbors = None):
|
|
5496
5584
|
|
|
5497
5585
|
community_dict = invert_dict(self.communities)
|
|
5498
5586
|
summation = 0
|
|
5499
|
-
id_set =
|
|
5587
|
+
id_set = iden_set(self.node_identities.values())
|
|
5500
5588
|
id_dict = {}
|
|
5501
5589
|
for i, iden in enumerate(id_set):
|
|
5502
5590
|
id_dict[iden] = i
|
|
@@ -5515,7 +5603,12 @@ class Network_3D:
|
|
|
5515
5603
|
|
|
5516
5604
|
# Count identities in this community
|
|
5517
5605
|
for node in nodes:
|
|
5518
|
-
|
|
5606
|
+
try:
|
|
5607
|
+
idens = ast.literal_eval(self.node_identities[node])
|
|
5608
|
+
for iden in idens:
|
|
5609
|
+
counter[id_dict[iden]] += 1
|
|
5610
|
+
except:
|
|
5611
|
+
counter[id_dict[self.node_identities[node]]] += 1 # Keep them as arrays
|
|
5519
5612
|
|
|
5520
5613
|
for i in range(len(counter)): # Translate them into proportions out of 1
|
|
5521
5614
|
|
|
@@ -5527,12 +5620,11 @@ class Network_3D:
|
|
|
5527
5620
|
umap_dict[community] = counter
|
|
5528
5621
|
|
|
5529
5622
|
else:
|
|
5530
|
-
idens =
|
|
5623
|
+
idens = invert_dict_special(self.node_identities)
|
|
5531
5624
|
iden_count = {}
|
|
5532
5625
|
template = {}
|
|
5533
5626
|
node_count = len(list(self.communities.keys()))
|
|
5534
5627
|
|
|
5535
|
-
|
|
5536
5628
|
for iden in id_set:
|
|
5537
5629
|
template[iden] = 0
|
|
5538
5630
|
|
|
@@ -5548,7 +5640,12 @@ class Network_3D:
|
|
|
5548
5640
|
counter = np.zeros(len(id_set))
|
|
5549
5641
|
|
|
5550
5642
|
for node in nodes:
|
|
5551
|
-
|
|
5643
|
+
try:
|
|
5644
|
+
idents = ast.literal_eval(self.node_identities[node])
|
|
5645
|
+
for iden in idents:
|
|
5646
|
+
iden_tracker[iden] += 1
|
|
5647
|
+
except:
|
|
5648
|
+
iden_tracker[self.node_identities[node]] += 1
|
|
5552
5649
|
|
|
5553
5650
|
i = 0
|
|
5554
5651
|
|
|
@@ -5819,7 +5916,7 @@ class Network_3D:
|
|
|
5819
5916
|
return heat_dict, overlay
|
|
5820
5917
|
|
|
5821
5918
|
|
|
5822
|
-
def merge_node_ids(self, path, data):
|
|
5919
|
+
def merge_node_ids(self, path, data, include = True):
|
|
5823
5920
|
|
|
5824
5921
|
if self.node_identities is None: # Prepare modular dict
|
|
5825
5922
|
|
|
@@ -5828,9 +5925,17 @@ class Network_3D:
|
|
|
5828
5925
|
nodes = list(np.unique(data))
|
|
5829
5926
|
if 0 in nodes:
|
|
5830
5927
|
del nodes[0]
|
|
5831
|
-
|
|
5832
5928
|
for node in nodes:
|
|
5833
|
-
|
|
5929
|
+
|
|
5930
|
+
self.node_identities[node] = [] # Assign to lists at first
|
|
5931
|
+
else:
|
|
5932
|
+
for node, iden in self.node_identities.items():
|
|
5933
|
+
try:
|
|
5934
|
+
self.node_identities[node] = ast.literal_eval(iden)
|
|
5935
|
+
except:
|
|
5936
|
+
self.node_identities[node] = [iden]
|
|
5937
|
+
|
|
5938
|
+
|
|
5834
5939
|
|
|
5835
5940
|
img_list = directory_info(path)
|
|
5836
5941
|
|
|
@@ -5840,9 +5945,12 @@ class Network_3D:
|
|
|
5840
5945
|
if len(np.unique(mask)) != 2:
|
|
5841
5946
|
|
|
5842
5947
|
mask = otsu_binarize(mask)
|
|
5948
|
+
else:
|
|
5949
|
+
mask = mask != 0
|
|
5843
5950
|
|
|
5844
5951
|
nodes = data * mask
|
|
5845
|
-
nodes =
|
|
5952
|
+
nodes = np.unique(nodes)
|
|
5953
|
+
nodes = nodes.tolist()
|
|
5846
5954
|
if 0 in nodes:
|
|
5847
5955
|
del nodes[0]
|
|
5848
5956
|
|
|
@@ -5853,21 +5961,43 @@ class Network_3D:
|
|
|
5853
5961
|
else:
|
|
5854
5962
|
base_name = img
|
|
5855
5963
|
|
|
5964
|
+
assigned = {}
|
|
5965
|
+
|
|
5966
|
+
|
|
5856
5967
|
for node in self.node_identities.keys():
|
|
5857
5968
|
|
|
5858
5969
|
try:
|
|
5859
5970
|
|
|
5860
|
-
if node in nodes:
|
|
5971
|
+
if int(node) in nodes:
|
|
5861
5972
|
|
|
5862
|
-
self.node_identities[node]
|
|
5973
|
+
self.node_identities[node].append(f'{base_name}+')
|
|
5863
5974
|
|
|
5864
|
-
|
|
5975
|
+
elif include:
|
|
5865
5976
|
|
|
5866
|
-
self.node_identities[node]
|
|
5977
|
+
self.node_identities[node].append(f'{base_name}-')
|
|
5867
5978
|
|
|
5868
5979
|
except:
|
|
5869
5980
|
pass
|
|
5870
5981
|
|
|
5982
|
+
modify_dict = copy.deepcopy(self.node_identities)
|
|
5983
|
+
|
|
5984
|
+
for node, iden in self.node_identities.items():
|
|
5985
|
+
|
|
5986
|
+
try:
|
|
5987
|
+
|
|
5988
|
+
if len(iden) == 1:
|
|
5989
|
+
|
|
5990
|
+
modify_dict[node] = str(iden[0]) # Singleton lists become bare strings
|
|
5991
|
+
elif len(iden) == 0:
|
|
5992
|
+
del modify_dict[node]
|
|
5993
|
+
else:
|
|
5994
|
+
modify_dict[node] = str(iden) # We hold multi element lists as strings for compatibility
|
|
5995
|
+
|
|
5996
|
+
except:
|
|
5997
|
+
pass
|
|
5998
|
+
|
|
5999
|
+
self.node_identities = modify_dict
|
|
6000
|
+
|
|
5871
6001
|
|
|
5872
6002
|
def nearest_neighbors_avg(self, root, targ, xy_scale = 1, z_scale = 1, num = 1, heatmap = False, threed = True, numpy = False, quant = False, centroids = True):
|
|
5873
6003
|
|
nettracer3d/nettracer_gui.py
CHANGED
|
@@ -4011,7 +4011,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
4011
4011
|
id_code_action = overlay_menu.addAction("Code Identities")
|
|
4012
4012
|
id_code_action.triggered.connect(lambda: self.show_code_dialog(sort = 'Identity'))
|
|
4013
4013
|
umap_action = overlay_menu.addAction("Centroid UMAP")
|
|
4014
|
-
umap_action.triggered.connect(self.
|
|
4014
|
+
umap_action.triggered.connect(self.handle_centroid_umap)
|
|
4015
|
+
iden_umap_action = overlay_menu.addAction("Identity UMAP (If any nodes were assigned multiple identities)")
|
|
4016
|
+
iden_umap_action.triggered.connect(self.handle_iden_umap)
|
|
4015
4017
|
|
|
4016
4018
|
rand_menu = analysis_menu.addMenu("Randomize")
|
|
4017
4019
|
random_action = rand_menu.addAction("Generate Equivalent Random Network")
|
|
@@ -5858,8 +5860,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
5858
5860
|
y_max = min(min_height, int(np.ceil(current_ylim[0] + 0.5)))
|
|
5859
5861
|
|
|
5860
5862
|
if self.pan_mode:
|
|
5861
|
-
box_len = (x_max - x_min)
|
|
5862
|
-
box_height = (y_max - y_min)
|
|
5863
|
+
box_len = int((x_max - x_min)/2)
|
|
5864
|
+
box_height = int((y_max - y_min)/2)
|
|
5863
5865
|
x_min = max(0, x_min - box_len)
|
|
5864
5866
|
x_max = min(self.shape[2], x_max + box_len)
|
|
5865
5867
|
y_min = max(0, y_min - box_height)
|
|
@@ -6195,13 +6197,20 @@ class ImageViewerWindow(QMainWindow):
|
|
|
6195
6197
|
dialog = CodeDialog(self, sort = sort)
|
|
6196
6198
|
dialog.exec()
|
|
6197
6199
|
|
|
6198
|
-
def
|
|
6200
|
+
def handle_centroid_umap(self):
|
|
6199
6201
|
|
|
6200
6202
|
if my_network.node_centroids is None:
|
|
6201
6203
|
self.show_centroid_dialog()
|
|
6202
6204
|
|
|
6203
6205
|
my_network.centroid_umap()
|
|
6204
6206
|
|
|
6207
|
+
def handle_iden_umap(self):
|
|
6208
|
+
|
|
6209
|
+
if my_network.node_identities is None:
|
|
6210
|
+
return
|
|
6211
|
+
|
|
6212
|
+
my_network.identity_umap()
|
|
6213
|
+
|
|
6205
6214
|
def closeEvent(self, event):
|
|
6206
6215
|
"""Override closeEvent to close all windows when main window closes"""
|
|
6207
6216
|
|
|
@@ -7468,6 +7477,12 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7468
7477
|
self.z_scale = QLineEdit(f"{my_network.z_scale}")
|
|
7469
7478
|
layout.addRow("z_scale:", self.z_scale)
|
|
7470
7479
|
|
|
7480
|
+
# Add Run button
|
|
7481
|
+
self.include = QPushButton("Include Negative Gates?")
|
|
7482
|
+
self.include.setCheckable(True)
|
|
7483
|
+
self.include.setChecked(True)
|
|
7484
|
+
layout.addWidget(self.include)
|
|
7485
|
+
|
|
7471
7486
|
# Add Run button
|
|
7472
7487
|
run_button = QPushButton("Get Directory")
|
|
7473
7488
|
run_button.clicked.connect(self.run)
|
|
@@ -7483,6 +7498,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7483
7498
|
|
|
7484
7499
|
|
|
7485
7500
|
data = self.parent().channel_data[0]
|
|
7501
|
+
include = self.include.isChecked()
|
|
7486
7502
|
|
|
7487
7503
|
if data is None:
|
|
7488
7504
|
return
|
|
@@ -7501,7 +7517,7 @@ class MergeNodeIdDialog(QDialog):
|
|
|
7501
7517
|
if search > 0:
|
|
7502
7518
|
data = sdl.smart_dilate(data, 1, 1, GPU = False, fast_dil = False, use_dt_dil_amount = search, xy_scale = xy_scale, z_scale = z_scale)
|
|
7503
7519
|
|
|
7504
|
-
my_network.merge_node_ids(selected_path, data)
|
|
7520
|
+
my_network.merge_node_ids(selected_path, data, include)
|
|
7505
7521
|
|
|
7506
7522
|
self.parent().format_for_upperright_table(my_network.node_identities, 'NodeID', 'Identity')
|
|
7507
7523
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.4
|
|
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/
|
|
@@ -110,6 +110,6 @@ McLaughlin, L., Zhang, B., Sharma, S. et al. Three dimensional multiscalar neuro
|
|
|
110
110
|
|
|
111
111
|
NetTracer3D was developed by Liam McLaughlin while working under Dr. Sanjay Jain at Washington University School of Medicine.
|
|
112
112
|
|
|
113
|
-
-- Version 0.9.
|
|
113
|
+
-- Version 0.9.4 Updates --
|
|
114
114
|
|
|
115
|
-
*
|
|
115
|
+
* Added some compatibility for nodes being assigned 'multiple identities'
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
nettracer3d/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
nettracer3d/cellpose_manager.py,sha256=NfRqW6Zl7yRU4qHCS_KjmR0R6QANSSgCO0_dr-eivxg,6694
|
|
3
3
|
nettracer3d/community_extractor.py,sha256=QG2v2y9lKb07LRU0zXCAlA_z-96BNqLHh5xP17KLmYA,28147
|
|
4
|
-
nettracer3d/excelotron.py,sha256=
|
|
4
|
+
nettracer3d/excelotron.py,sha256=aNof6k-DgMxVyFgsl3ltSCxG4vZW49cuvCBzfzhYhUY,75072
|
|
5
5
|
nettracer3d/modularity.py,sha256=pborVcDBvICB2-g8lNoSVZbIReIBlfeBmjFbPYmtq7Y,22443
|
|
6
6
|
nettracer3d/morphology.py,sha256=jyDjYzrZ4LvI5jOyw8DLsxmo-i5lpqHsejYpW7Tq7Mo,19786
|
|
7
|
-
nettracer3d/neighborhoods.py,sha256=
|
|
8
|
-
nettracer3d/nettracer.py,sha256=
|
|
9
|
-
nettracer3d/nettracer_gui.py,sha256=
|
|
7
|
+
nettracer3d/neighborhoods.py,sha256=2QzGKkQCSUerxigkcHF4iN7ys8kv-KHTbkD_9_2Krgo,58365
|
|
8
|
+
nettracer3d/nettracer.py,sha256=wraH1Ba61bA0awmBfH71u_hkKcytUG_pISkJX6iP-ic,268658
|
|
9
|
+
nettracer3d/nettracer_gui.py,sha256=tYg0wDV2p6b0czvozMrrpja7fbx3nSBgjR2ZV4b5vBE,601760
|
|
10
10
|
nettracer3d/network_analysis.py,sha256=kBzsVaq4dZkMe0k-VGvQIUvM-tK0ZZ8bvb-wtsugZRQ,46150
|
|
11
11
|
nettracer3d/network_draw.py,sha256=F7fw6Pcf4qWOhdKwLmhwqWdschbDlHzwCVolQC9imeU,14117
|
|
12
12
|
nettracer3d/node_draw.py,sha256=kZcR1PekLg0riioNeGcALIXQyZ5PtHA_9MT6z7Zovdk,10401
|
|
@@ -17,9 +17,9 @@ nettracer3d/segmenter.py,sha256=-Llkhp3TlAIBXZNhcfMFQRdg0vec1xtlOm0c4_bSU9U,7576
|
|
|
17
17
|
nettracer3d/segmenter_GPU.py,sha256=optCZ_zLIfe99rgqmyKWUZlWW5TF5jEC_C3keu1m7VQ,77771
|
|
18
18
|
nettracer3d/simple_network.py,sha256=dkG4jpc4zzdeuoaQobgGfL3PNo6N8dGKQ5hEEubFIvA,9947
|
|
19
19
|
nettracer3d/smart_dilate.py,sha256=TvRUh6B4q4zIdCO1BWH-xgTdND5OUNmo99eyxG9oIAU,27145
|
|
20
|
-
nettracer3d-0.9.
|
|
21
|
-
nettracer3d-0.9.
|
|
22
|
-
nettracer3d-0.9.
|
|
23
|
-
nettracer3d-0.9.
|
|
24
|
-
nettracer3d-0.9.
|
|
25
|
-
nettracer3d-0.9.
|
|
20
|
+
nettracer3d-0.9.4.dist-info/licenses/LICENSE,sha256=jnNT-yBeIAKAHpYthPvLeqCzJ6nSurgnKmloVnfsjCI,764
|
|
21
|
+
nettracer3d-0.9.4.dist-info/METADATA,sha256=-xNx22dCKedpkF7uLVduYpMYBCJcXv_2xssw34nEo4c,7048
|
|
22
|
+
nettracer3d-0.9.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
nettracer3d-0.9.4.dist-info/entry_points.txt,sha256=Nx1rr_0QhJXDBHAQg2vcqCzLMKBzSHfwy3xwGkueVyc,53
|
|
24
|
+
nettracer3d-0.9.4.dist-info/top_level.txt,sha256=zsYy9rZwirfCEOubolhee4TyzqBAL5gSUeFMzhFTX8c,12
|
|
25
|
+
nettracer3d-0.9.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|