nettracer3d 0.8.1__py3-none-any.whl → 0.8.3__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/cellpose_manager.py +161 -0
- nettracer3d/community_extractor.py +169 -23
- nettracer3d/neighborhoods.py +222 -23
- nettracer3d/nettracer.py +166 -68
- nettracer3d/nettracer_gui.py +584 -266
- nettracer3d/network_analysis.py +222 -230
- nettracer3d/proximity.py +191 -30
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/METADATA +44 -12
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/RECORD +13 -12
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/WHEEL +0 -0
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/entry_points.txt +0 -0
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/licenses/LICENSE +0 -0
- {nettracer3d-0.8.1.dist-info → nettracer3d-0.8.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from PyQt6.QtWidgets import QMessageBox, QWidget
|
|
6
|
+
|
|
7
|
+
class CellposeGUILauncher:
|
|
8
|
+
"""Simple launcher for cellpose GUI in PyQt6 applications."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, parent_widget=None):
|
|
11
|
+
"""
|
|
12
|
+
Initialize the launcher.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
parent_widget: PyQt6 widget for showing message boxes (optional)
|
|
16
|
+
"""
|
|
17
|
+
self.parent_widget = parent_widget
|
|
18
|
+
self.cellpose_process = None
|
|
19
|
+
|
|
20
|
+
def launch_cellpose_gui(self, image_path=None, working_directory=None):
|
|
21
|
+
"""
|
|
22
|
+
Launch cellpose GUI in a separate thread.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
image_path (str, optional): Path to image file to load automatically
|
|
26
|
+
working_directory (str, optional): Directory to start cellpose in
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
bool: True if launch was initiated successfully
|
|
30
|
+
"""
|
|
31
|
+
def run_cellpose():
|
|
32
|
+
"""Function to run in separate thread."""
|
|
33
|
+
try:
|
|
34
|
+
# Build command
|
|
35
|
+
cmd = [sys.executable, "-m", "cellpose"]
|
|
36
|
+
|
|
37
|
+
# Add image path if provided
|
|
38
|
+
if image_path and Path(image_path).exists():
|
|
39
|
+
cmd.extend(["--image_path", str(image_path)])
|
|
40
|
+
|
|
41
|
+
# Set working directory
|
|
42
|
+
cwd = working_directory if working_directory else None
|
|
43
|
+
|
|
44
|
+
# Launch cellpose GUI
|
|
45
|
+
self.cellpose_process = subprocess.Popen(
|
|
46
|
+
cmd,
|
|
47
|
+
cwd=cwd,
|
|
48
|
+
stdout=subprocess.PIPE,
|
|
49
|
+
stderr=subprocess.PIPE
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Optional: wait for process to complete
|
|
53
|
+
# self.cellpose_process.wait()
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
if self.parent_widget:
|
|
57
|
+
# Show error in main thread
|
|
58
|
+
self.show_error(f"Failed to launch cellpose GUI: {str(e)}")
|
|
59
|
+
else:
|
|
60
|
+
print(f"Failed to launch cellpose GUI: {str(e)}")
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
# Start cellpose in separate thread
|
|
64
|
+
thread = threading.Thread(target=run_cellpose, daemon=True)
|
|
65
|
+
thread.start()
|
|
66
|
+
|
|
67
|
+
if self.parent_widget:
|
|
68
|
+
self.show_info("Cellpose GUI launched!")
|
|
69
|
+
else:
|
|
70
|
+
print("Cellpose GUI launched!")
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
if self.parent_widget:
|
|
76
|
+
self.show_error(f"Failed to start cellpose thread: {str(e)}")
|
|
77
|
+
else:
|
|
78
|
+
print(f"Failed to start cellpose thread: {str(e)}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def launch_with_directory(self, directory_path):
|
|
82
|
+
"""
|
|
83
|
+
Launch cellpose GUI with a specific directory.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
directory_path (str): Directory containing images
|
|
87
|
+
"""
|
|
88
|
+
cmd_args = ["--dir", str(directory_path)]
|
|
89
|
+
return self.launch_cellpose_gui_with_args(cmd_args, working_directory=directory_path)
|
|
90
|
+
|
|
91
|
+
def launch_cellpose_gui_with_args(self, additional_args=None, working_directory=None):
|
|
92
|
+
"""
|
|
93
|
+
Launch cellpose GUI with custom arguments.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
additional_args (list): List of additional command line arguments
|
|
97
|
+
working_directory (str): Working directory for cellpose
|
|
98
|
+
"""
|
|
99
|
+
def run_cellpose_custom():
|
|
100
|
+
try:
|
|
101
|
+
cmd = [sys.executable, "-m", "cellpose"]
|
|
102
|
+
|
|
103
|
+
if additional_args:
|
|
104
|
+
cmd.extend(additional_args)
|
|
105
|
+
|
|
106
|
+
cwd = working_directory if working_directory else None
|
|
107
|
+
|
|
108
|
+
self.cellpose_process = subprocess.Popen(
|
|
109
|
+
cmd,
|
|
110
|
+
cwd=cwd,
|
|
111
|
+
stdout=subprocess.PIPE,
|
|
112
|
+
stderr=subprocess.PIPE
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
if self.parent_widget:
|
|
117
|
+
self.show_error(f"Failed to launch cellpose GUI: {str(e)}")
|
|
118
|
+
else:
|
|
119
|
+
print(f"Failed to launch cellpose GUI: {str(e)}")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
thread = threading.Thread(target=run_cellpose_custom, daemon=True)
|
|
123
|
+
thread.start()
|
|
124
|
+
return True
|
|
125
|
+
except Exception as e:
|
|
126
|
+
if self.parent_widget:
|
|
127
|
+
self.show_error(f"Failed to start cellpose: {str(e)}")
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def is_cellpose_running(self):
|
|
131
|
+
"""
|
|
132
|
+
Check if cellpose process is still running.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
bool: True if cellpose is still running
|
|
136
|
+
"""
|
|
137
|
+
if self.cellpose_process is None:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
return self.cellpose_process.poll() is None
|
|
141
|
+
|
|
142
|
+
def close_cellpose(self):
|
|
143
|
+
"""Terminate the cellpose process if running."""
|
|
144
|
+
if self.cellpose_process and self.is_cellpose_running():
|
|
145
|
+
try:
|
|
146
|
+
self.cellpose_process.terminate()
|
|
147
|
+
self.cellpose_process.wait(timeout=5) # Wait up to 5 seconds
|
|
148
|
+
except subprocess.TimeoutExpired:
|
|
149
|
+
self.cellpose_process.kill() # Force kill if it doesn't terminate
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"Error closing cellpose: {e}")
|
|
152
|
+
|
|
153
|
+
def show_info(self, message):
|
|
154
|
+
"""Show info message if parent widget available."""
|
|
155
|
+
if self.parent_widget:
|
|
156
|
+
QMessageBox.information(self.parent_widget, "Cellpose Launcher", message)
|
|
157
|
+
|
|
158
|
+
def show_error(self, message):
|
|
159
|
+
"""Show error message if parent widget available."""
|
|
160
|
+
if self.parent_widget:
|
|
161
|
+
QMessageBox.critical(self.parent_widget, "Cellpose Error", message)
|
|
@@ -393,28 +393,105 @@ def find_hub_nodes(G: nx.Graph, proportion: float = 0.1) -> List:
|
|
|
393
393
|
return output
|
|
394
394
|
|
|
395
395
|
def get_color_name_mapping():
|
|
396
|
-
"""Return a dictionary of
|
|
396
|
+
"""Return a dictionary of descriptive color names and their RGB values."""
|
|
397
397
|
return {
|
|
398
|
-
|
|
399
|
-
'
|
|
400
|
-
'
|
|
401
|
-
'
|
|
402
|
-
'
|
|
398
|
+
# Reds
|
|
399
|
+
'crimson_red': (220, 20, 60),
|
|
400
|
+
'bright_red': (255, 0, 0),
|
|
401
|
+
'dark_red': (139, 0, 0),
|
|
402
|
+
'coral_red': (255, 127, 80),
|
|
403
|
+
'rose_red': (255, 102, 102),
|
|
404
|
+
'burgundy': (128, 0, 32),
|
|
405
|
+
'cherry_red': (222, 49, 99),
|
|
406
|
+
|
|
407
|
+
# Greens
|
|
408
|
+
'forest_green': (34, 139, 34),
|
|
409
|
+
'lime_green': (50, 205, 50),
|
|
410
|
+
'bright_green': (0, 255, 0),
|
|
411
|
+
'dark_green': (0, 100, 0),
|
|
412
|
+
'mint_green': (152, 255, 152),
|
|
413
|
+
'sage_green': (159, 183, 121),
|
|
414
|
+
'emerald_green': (80, 200, 120),
|
|
415
|
+
'olive_green': (128, 128, 0),
|
|
416
|
+
|
|
417
|
+
# Blues
|
|
418
|
+
'royal_blue': (65, 105, 225),
|
|
419
|
+
'bright_blue': (0, 0, 255),
|
|
420
|
+
'navy_blue': (0, 0, 128),
|
|
421
|
+
'sky_blue': (135, 206, 235),
|
|
422
|
+
'steel_blue': (70, 130, 180),
|
|
423
|
+
'powder_blue': (176, 224, 230),
|
|
424
|
+
'midnight_blue': (25, 25, 112),
|
|
425
|
+
'cobalt_blue': (0, 71, 171),
|
|
426
|
+
|
|
427
|
+
# Purples
|
|
428
|
+
'deep_purple': (75, 0, 130),
|
|
429
|
+
'royal_purple': (120, 81, 169),
|
|
430
|
+
'lavender': (230, 230, 250),
|
|
431
|
+
'plum_purple': (221, 160, 221),
|
|
432
|
+
'violet_purple': (238, 130, 238),
|
|
403
433
|
'magenta': (255, 0, 255),
|
|
404
|
-
'
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
'
|
|
408
|
-
'
|
|
409
|
-
'
|
|
410
|
-
'
|
|
411
|
-
'
|
|
412
|
-
'
|
|
413
|
-
'
|
|
414
|
-
|
|
415
|
-
|
|
434
|
+
'orchid': (218, 112, 214),
|
|
435
|
+
|
|
436
|
+
# Yellows & Golds
|
|
437
|
+
'bright_yellow': (255, 255, 0),
|
|
438
|
+
'golden_yellow': (255, 215, 0),
|
|
439
|
+
'lemon_yellow': (255, 247, 0),
|
|
440
|
+
'amber': (255, 191, 0),
|
|
441
|
+
'mustard_yellow': (255, 219, 88),
|
|
442
|
+
'cream': (255, 253, 208),
|
|
443
|
+
'wheat': (245, 222, 179),
|
|
444
|
+
|
|
445
|
+
# Oranges
|
|
446
|
+
'bright_orange': (255, 165, 0),
|
|
447
|
+
'burnt_orange': (204, 85, 0),
|
|
448
|
+
'peach': (255, 218, 185),
|
|
449
|
+
'tangerine': (255, 163, 67),
|
|
450
|
+
'pumpkin_orange': (255, 117, 24),
|
|
451
|
+
'apricot': (251, 206, 177),
|
|
452
|
+
|
|
453
|
+
# Pinks
|
|
454
|
+
'hot_pink': (255, 105, 180),
|
|
455
|
+
'light_pink': (255, 192, 203),
|
|
456
|
+
'deep_pink': (255, 20, 147),
|
|
457
|
+
'salmon_pink': (250, 128, 114),
|
|
458
|
+
'blush_pink': (255, 182, 193),
|
|
459
|
+
'fuchsia': (255, 0, 255),
|
|
460
|
+
|
|
461
|
+
# Cyans & Teals
|
|
462
|
+
'bright_cyan': (0, 255, 255),
|
|
463
|
+
'dark_teal': (0, 128, 128),
|
|
416
464
|
'turquoise': (64, 224, 208),
|
|
417
|
-
'
|
|
465
|
+
'aqua': (0, 255, 255),
|
|
466
|
+
'seafoam': (159, 226, 191),
|
|
467
|
+
'teal_blue': (54, 117, 136),
|
|
468
|
+
|
|
469
|
+
# Browns & Earth Tones
|
|
470
|
+
'chocolate_brown': (210, 105, 30),
|
|
471
|
+
'saddle_brown': (139, 69, 19),
|
|
472
|
+
'light_brown': (205, 133, 63),
|
|
473
|
+
'tan': (210, 180, 140),
|
|
474
|
+
'beige': (245, 245, 220),
|
|
475
|
+
'coffee_brown': (111, 78, 55),
|
|
476
|
+
'rust_brown': (183, 65, 14),
|
|
477
|
+
|
|
478
|
+
# Grays & Neutrals
|
|
479
|
+
'charcoal_gray': (54, 69, 79),
|
|
480
|
+
'light_gray': (211, 211, 211),
|
|
481
|
+
'silver': (192, 192, 192),
|
|
482
|
+
'slate_gray': (112, 128, 144),
|
|
483
|
+
'ash_gray': (178, 190, 181),
|
|
484
|
+
'smoke_gray': (152, 152, 152),
|
|
485
|
+
|
|
486
|
+
# Additional Distinctive Colors
|
|
487
|
+
'lime_yellow': (191, 255, 0),
|
|
488
|
+
'electric_blue': (125, 249, 255),
|
|
489
|
+
'neon_green': (57, 255, 20),
|
|
490
|
+
'wine_red': (114, 47, 55),
|
|
491
|
+
'copper': (184, 115, 51),
|
|
492
|
+
'ivory': (255, 255, 240),
|
|
493
|
+
'periwinkle': (204, 204, 255),
|
|
494
|
+
'mint': (189, 252, 201)
|
|
418
495
|
}
|
|
419
496
|
|
|
420
497
|
def rgb_to_color_name(rgb: Tuple[int, int, int]) -> str:
|
|
@@ -440,21 +517,90 @@ def rgb_to_color_name(rgb: Tuple[int, int, int]) -> str:
|
|
|
440
517
|
distance = np.sqrt(np.sum((rgb_array - np.array(color_rgb)) ** 2))
|
|
441
518
|
if distance < min_distance:
|
|
442
519
|
min_distance = distance
|
|
520
|
+
#closest_color = color_name + f" {str(rgb_array)}" # <- if we want RGB names
|
|
443
521
|
closest_color = color_name
|
|
444
|
-
|
|
522
|
+
|
|
445
523
|
return closest_color
|
|
446
524
|
|
|
447
|
-
def convert_node_colors_to_names(node_to_color: Dict[int, Tuple[int, int, int]]
|
|
525
|
+
def convert_node_colors_to_names(node_to_color: Dict[int, Tuple[int, int, int]],
|
|
526
|
+
show_legend: bool = True,
|
|
527
|
+
figsize: Tuple[int, int] = (10, 8),
|
|
528
|
+
save_path: str = None) -> Dict[int, str]:
|
|
448
529
|
"""
|
|
449
530
|
Convert a dictionary of node-to-RGB mappings to node-to-color-name mappings.
|
|
531
|
+
Optionally displays a matplotlib legend showing the mappings.
|
|
450
532
|
|
|
451
533
|
Args:
|
|
452
534
|
node_to_color: Dictionary mapping node IDs to RGB tuples
|
|
535
|
+
show_legend: Whether to display the color legend plot
|
|
536
|
+
figsize: Figure size as (width, height) for the legend
|
|
537
|
+
save_path: Optional path to save the legend figure
|
|
453
538
|
|
|
454
539
|
Returns:
|
|
455
540
|
Dictionary mapping node IDs to color names
|
|
456
541
|
"""
|
|
457
|
-
|
|
542
|
+
# Convert colors to names
|
|
543
|
+
node_to_names = {node: rgb_to_color_name(color) for node, color in node_to_color.items()}
|
|
544
|
+
|
|
545
|
+
# Create legend if requested
|
|
546
|
+
if show_legend:
|
|
547
|
+
import matplotlib.pyplot as plt
|
|
548
|
+
from matplotlib.patches import Rectangle
|
|
549
|
+
|
|
550
|
+
num_entries = len(node_to_color)
|
|
551
|
+
|
|
552
|
+
# Calculate dynamic spacing based on number of entries
|
|
553
|
+
entry_height = 0.8
|
|
554
|
+
total_height = num_entries * entry_height + 1.5 # Extra space for title and margins
|
|
555
|
+
|
|
556
|
+
# Create figure and axis with proper scaling
|
|
557
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
558
|
+
ax.set_xlim(0, 10)
|
|
559
|
+
ax.set_ylim(0, total_height)
|
|
560
|
+
ax.axis('off')
|
|
561
|
+
|
|
562
|
+
# Title
|
|
563
|
+
ax.text(5, total_height - 0.5, 'Color Legend',
|
|
564
|
+
fontsize=16, fontweight='bold', ha='center')
|
|
565
|
+
|
|
566
|
+
# Sort nodes for consistent display
|
|
567
|
+
sorted_nodes = sorted(node_to_color.keys())
|
|
568
|
+
|
|
569
|
+
# Create legend entries
|
|
570
|
+
for i, node in enumerate(sorted_nodes):
|
|
571
|
+
y_pos = total_height - (i + 1) * entry_height - 0.8
|
|
572
|
+
rgb = node_to_color[node]
|
|
573
|
+
color_name = node_to_names[node]
|
|
574
|
+
|
|
575
|
+
# Normalize RGB values for matplotlib (0-1 range)
|
|
576
|
+
norm_rgb = tuple(c/255.0 for c in rgb)
|
|
577
|
+
|
|
578
|
+
# Draw color swatch (using actual RGB values)
|
|
579
|
+
swatch = Rectangle((1.0, y_pos - 0.15), 0.8, 0.3,
|
|
580
|
+
facecolor=norm_rgb, edgecolor='black', linewidth=1)
|
|
581
|
+
ax.add_patch(swatch)
|
|
582
|
+
|
|
583
|
+
# Node ID (exactly as it appears in dict keys)
|
|
584
|
+
ax.text(0.2, y_pos, str(node), fontsize=12, fontweight='bold',
|
|
585
|
+
va='center', ha='left')
|
|
586
|
+
|
|
587
|
+
# Color name (mapped name, nicely formatted)
|
|
588
|
+
ax.text(2.2, y_pos, color_name.replace('_', ' ').title(),
|
|
589
|
+
fontsize=11, va='center', ha='left')
|
|
590
|
+
|
|
591
|
+
# Add border around the legend
|
|
592
|
+
border = Rectangle((0.1, 0.1), 9.8, total_height - 0.2,
|
|
593
|
+
fill=False, edgecolor='gray', linewidth=2)
|
|
594
|
+
ax.add_patch(border)
|
|
595
|
+
|
|
596
|
+
plt.tight_layout()
|
|
597
|
+
|
|
598
|
+
if save_path:
|
|
599
|
+
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
|
600
|
+
|
|
601
|
+
plt.show()
|
|
602
|
+
|
|
603
|
+
return node_to_names
|
|
458
604
|
|
|
459
605
|
def generate_distinct_colors(n_colors: int) -> List[Tuple[int, int, int]]:
|
|
460
606
|
"""
|
|
@@ -519,7 +665,7 @@ def assign_node_colors(node_list: List[int], labeled_array: np.ndarray) -> Tuple
|
|
|
519
665
|
|
|
520
666
|
# Convert colors for naming
|
|
521
667
|
node_to_color_rgb = {k: tuple(v[:3]) for k, v in node_to_color.items()}
|
|
522
|
-
node_to_color_names = convert_node_colors_to_names(node_to_color_rgb)
|
|
668
|
+
node_to_color_names = convert_node_colors_to_names(node_to_color_rgb, show_legend = False)
|
|
523
669
|
|
|
524
670
|
return rgba_array, node_to_color_names
|
|
525
671
|
|