nettracer3d 0.4.4__tar.gz → 0.4.5__tar.gz
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-0.4.4/src/nettracer3d.egg-info → nettracer3d-0.4.5}/PKG-INFO +16 -2
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/pyproject.toml +28 -6
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/nettracer.py +47 -3
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/nettracer_gui.py +236 -42
- nettracer3d-0.4.5/src/nettracer3d/segmenter.py +693 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/smart_dilate.py +31 -3
- {nettracer3d-0.4.4 → nettracer3d-0.4.5/src/nettracer3d.egg-info}/PKG-INFO +16 -2
- nettracer3d-0.4.5/src/nettracer3d.egg-info/requires.txt +44 -0
- nettracer3d-0.4.4/src/nettracer3d/segmenter.py +0 -290
- nettracer3d-0.4.4/src/nettracer3d.egg-info/requires.txt +0 -23
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/LICENSE +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/README.md +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/setup.cfg +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/hub_getter.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.4.4 → nettracer3d-0.4.5}/src/nettracer3d.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Scripts for intializing and analyzing networks from segmentations of three dimensional images.
|
|
5
5
|
Author-email: Liam McLaughlin <boom2449@gmail.com>
|
|
6
6
|
Project-URL: User_Manual, https://drive.google.com/drive/folders/1fTkz3n4LN9_VxKRKC8lVQSlrz_wq0bVn?usp=drive_link
|
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.8
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: numpy==1.26.4
|
|
15
15
|
Requires-Dist: scipy
|
|
16
16
|
Requires-Dist: scikit-image
|
|
17
17
|
Requires-Dist: Pillow
|
|
@@ -25,12 +25,26 @@ Requires-Dist: python-louvain
|
|
|
25
25
|
Requires-Dist: tifffile
|
|
26
26
|
Requires-Dist: qtrangeslider
|
|
27
27
|
Requires-Dist: PyQt6
|
|
28
|
+
Requires-Dist: scikit-learn
|
|
28
29
|
Provides-Extra: cuda11
|
|
29
30
|
Requires-Dist: cupy-cuda11x; extra == "cuda11"
|
|
31
|
+
Requires-Dist: torch==2.2.0+cu118; extra == "cuda11"
|
|
32
|
+
Requires-Dist: torchvision==0.17.0+cu118; extra == "cuda11"
|
|
33
|
+
Requires-Dist: torchaudio==2.2.0+cu118; extra == "cuda11"
|
|
30
34
|
Provides-Extra: cuda12
|
|
31
35
|
Requires-Dist: cupy-cuda12x; extra == "cuda12"
|
|
36
|
+
Requires-Dist: torch==2.2.0+cu121; extra == "cuda12"
|
|
37
|
+
Requires-Dist: torchvision==0.17.0+cu121; extra == "cuda12"
|
|
38
|
+
Requires-Dist: torchaudio==2.2.0+cu121; extra == "cuda12"
|
|
32
39
|
Provides-Extra: cupy
|
|
33
40
|
Requires-Dist: cupy; extra == "cupy"
|
|
41
|
+
Requires-Dist: torch; extra == "cupy"
|
|
42
|
+
Requires-Dist: torchvision; extra == "cupy"
|
|
43
|
+
Requires-Dist: torchaudio; extra == "cupy"
|
|
44
|
+
Provides-Extra: gpu
|
|
45
|
+
Requires-Dist: cupy-cuda11x; (platform_system == "Linux" and platform_machine == "x86_64") and extra == "gpu"
|
|
46
|
+
Requires-Dist: cupy-cuda12x; platform_system == "Windows" and extra == "gpu"
|
|
47
|
+
Requires-Dist: cupy; platform_system == "Darwin" and extra == "gpu"
|
|
34
48
|
|
|
35
49
|
NetTracer3D is a python package developed for both 2D and 3D analysis of microscopic images in the .tif file format. It supports generation of 3D networks showing the relationships between objects (or nodes) in three dimensional space, either based on their own proximity or connectivity via connecting objects such as nerves or blood vessels. In addition to these functionalities are several advanced 3D data processing algorithms, such as labeling of branched structures or abstraction of branched structures into networks. Note that nettracer3d uses segmented data, which can be segmented from other softwares such as ImageJ and imported into NetTracer3D, although it does offer its own segmentation via intensity and volumetric thresholding, or random forest machine learning segmentation. NetTracer3D currently has a fully functional GUI. To use the GUI, after installing the nettracer3d package via pip, enter the command 'nettracer3d' in your command prompt:
|
|
36
50
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nettracer3d"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.5"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name="Liam McLaughlin", email="boom2449@gmail.com" },
|
|
6
6
|
]
|
|
7
7
|
description = "Scripts for intializing and analyzing networks from segmentations of three dimensional images."
|
|
8
8
|
dependencies = [
|
|
9
|
-
"numpy",
|
|
9
|
+
"numpy == 1.26.4",
|
|
10
10
|
"scipy",
|
|
11
11
|
"scikit-image",
|
|
12
12
|
"Pillow",
|
|
@@ -19,8 +19,10 @@ dependencies = [
|
|
|
19
19
|
"python-louvain",
|
|
20
20
|
"tifffile",
|
|
21
21
|
"qtrangeslider",
|
|
22
|
-
"PyQt6"
|
|
22
|
+
"PyQt6",
|
|
23
|
+
"scikit-learn"
|
|
23
24
|
]
|
|
25
|
+
|
|
24
26
|
readme = "README.md"
|
|
25
27
|
requires-python = ">=3.8"
|
|
26
28
|
classifiers = [
|
|
@@ -30,9 +32,29 @@ classifiers = [
|
|
|
30
32
|
]
|
|
31
33
|
|
|
32
34
|
[project.optional-dependencies]
|
|
33
|
-
CUDA11 = [
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
CUDA11 = [
|
|
36
|
+
"cupy-cuda11x",
|
|
37
|
+
"torch==2.2.0+cu118",
|
|
38
|
+
"torchvision==0.17.0+cu118",
|
|
39
|
+
"torchaudio==2.2.0+cu118"
|
|
40
|
+
]
|
|
41
|
+
CUDA12 = [
|
|
42
|
+
"cupy-cuda12x",
|
|
43
|
+
"torch==2.2.0+cu121",
|
|
44
|
+
"torchvision==0.17.0+cu121",
|
|
45
|
+
"torchaudio==2.2.0+cu121"
|
|
46
|
+
]
|
|
47
|
+
cupy = [
|
|
48
|
+
"cupy",
|
|
49
|
+
"torch", # Will get CPU version by default
|
|
50
|
+
"torchvision",
|
|
51
|
+
"torchaudio"
|
|
52
|
+
]
|
|
53
|
+
GPU = [ # Meta-dependency that user can choose based on their CUDA version
|
|
54
|
+
"cupy-cuda11x ; platform_system=='Linux' and platform_machine=='x86_64'",
|
|
55
|
+
"cupy-cuda12x ; platform_system=='Windows'",
|
|
56
|
+
"cupy ; platform_system=='Darwin'"
|
|
57
|
+
]
|
|
36
58
|
|
|
37
59
|
[project.scripts]
|
|
38
60
|
nettracer3d = "nettracer3d.run:main"
|
|
@@ -858,10 +858,26 @@ def hash_inners(search_region, inner_edges, GPU = True):
|
|
|
858
858
|
|
|
859
859
|
return inner_edges
|
|
860
860
|
|
|
861
|
+
|
|
862
|
+
def dilate_2D(array, search, scaling = 1):
|
|
863
|
+
|
|
864
|
+
inv = array < 1
|
|
865
|
+
|
|
866
|
+
inv = smart_dilate.compute_distance_transform_distance(inv)
|
|
867
|
+
|
|
868
|
+
inv = inv * scaling
|
|
869
|
+
|
|
870
|
+
inv = inv <= search
|
|
871
|
+
|
|
872
|
+
return inv
|
|
873
|
+
|
|
874
|
+
|
|
861
875
|
def dilate_3D(tiff_array, dilated_x, dilated_y, dilated_z):
|
|
862
876
|
"""Internal method to dilate an array in 3D. Dilation this way is much faster than using a distance transform although the latter is theoretically more accurate.
|
|
863
877
|
Arguments are an array, and the desired pixel dilation amounts in X, Y, Z."""
|
|
864
878
|
|
|
879
|
+
if tiff_array.shape[0] == 1:
|
|
880
|
+
return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
|
|
865
881
|
|
|
866
882
|
if dilated_x == 3 and dilated_y == 3 and dilated_z == 3:
|
|
867
883
|
|
|
@@ -996,6 +1012,8 @@ def dilate_3D_recursive(tiff_array, dilated_x, dilated_y, dilated_z, step_size=N
|
|
|
996
1012
|
|
|
997
1013
|
Each dilation parameter represents (n-1)/2 steps outward from the object.
|
|
998
1014
|
"""
|
|
1015
|
+
if tiff_array.shape[0] == 1:
|
|
1016
|
+
return dilate_2D(tiff_array, ((dilated_x - 1) / 2))
|
|
999
1017
|
# Calculate the smallest dimension of the array
|
|
1000
1018
|
min_dim = min(tiff_array.shape)
|
|
1001
1019
|
|
|
@@ -2414,6 +2432,7 @@ class Network_3D:
|
|
|
2414
2432
|
Can be called on a Network_3D object to save the node centroids properties to hard mem as a .xlsx file. It will save to the active directory if none is specified.
|
|
2415
2433
|
:param directory: (Optional - Val = None; String). The path to an indended directory to save the centroids to.
|
|
2416
2434
|
"""
|
|
2435
|
+
|
|
2417
2436
|
if self._node_centroids is not None:
|
|
2418
2437
|
if directory is None:
|
|
2419
2438
|
network_analysis._save_centroid_dictionary(self._node_centroids, 'node_centroids.xlsx')
|
|
@@ -2424,7 +2443,14 @@ class Network_3D:
|
|
|
2424
2443
|
print(f"Centroids saved to {directory}/node_centroids.xlsx")
|
|
2425
2444
|
|
|
2426
2445
|
if self._node_centroids is None:
|
|
2427
|
-
|
|
2446
|
+
if directory is None:
|
|
2447
|
+
network_analysis._save_centroid_dictionary({}, 'node_centroids.xlsx')
|
|
2448
|
+
print("Centroids saved to node_centroids.xlsx")
|
|
2449
|
+
|
|
2450
|
+
if directory is not None:
|
|
2451
|
+
network_analysis._save_centroid_dictionary({}, f'{directory}/node_centroids.xlsx')
|
|
2452
|
+
print(f"Centroids saved to {directory}/node_centroids.xlsx")
|
|
2453
|
+
|
|
2428
2454
|
|
|
2429
2455
|
def save_edge_centroids(self, directory = None):
|
|
2430
2456
|
"""
|
|
@@ -2442,6 +2468,13 @@ class Network_3D:
|
|
|
2442
2468
|
|
|
2443
2469
|
if self._edge_centroids is None:
|
|
2444
2470
|
print("Edge centroids attribute is empty, did not save...")
|
|
2471
|
+
if directory is None:
|
|
2472
|
+
network_analysis._save_centroid_dictionary({}, 'edge_centroids.xlsx', index = 'Edge ID')
|
|
2473
|
+
print("Centroids saved to edge_centroids.xlsx")
|
|
2474
|
+
|
|
2475
|
+
if directory is not None:
|
|
2476
|
+
network_analysis._save_centroid_dictionary({}, f'{directory}/edge_centroids.xlsx', index = 'Edge ID')
|
|
2477
|
+
print(f"Centroids saved to {directory}/edge_centroids.xlsx")
|
|
2445
2478
|
|
|
2446
2479
|
def save_search_region(self, directory = None):
|
|
2447
2480
|
"""
|
|
@@ -2494,7 +2527,13 @@ class Network_3D:
|
|
|
2494
2527
|
print(f"Node identities saved to {directory}/node_identities.xlsx")
|
|
2495
2528
|
|
|
2496
2529
|
if self._node_identities is None:
|
|
2497
|
-
|
|
2530
|
+
if directory is None:
|
|
2531
|
+
network_analysis.save_singval_dict({}, 'NodeID', 'Identity', 'node_identities.xlsx')
|
|
2532
|
+
print("Node identities saved to node_identities.xlsx")
|
|
2533
|
+
|
|
2534
|
+
if directory is not None:
|
|
2535
|
+
network_analysis.save_singval_dict({}, 'NodeID', 'Identity', f'{directory}/node_identities.xlsx')
|
|
2536
|
+
print(f"Node identities saved to {directory}/node_identities.xlsx")
|
|
2498
2537
|
|
|
2499
2538
|
def save_communities(self, directory = None):
|
|
2500
2539
|
"""
|
|
@@ -2511,8 +2550,13 @@ class Network_3D:
|
|
|
2511
2550
|
print(f"Communities saved to {directory}/node_communities.xlsx")
|
|
2512
2551
|
|
|
2513
2552
|
if self._communities is None:
|
|
2514
|
-
|
|
2553
|
+
if directory is None:
|
|
2554
|
+
network_analysis.save_singval_dict({}, 'NodeID', 'Community', 'node_communities.xlsx')
|
|
2555
|
+
print("Communities saved to node_communities.xlsx")
|
|
2515
2556
|
|
|
2557
|
+
if directory is not None:
|
|
2558
|
+
network_analysis.save_singval_dict({}, 'NodeID', 'Community', f'{directory}/node_communities.xlsx')
|
|
2559
|
+
print(f"Communities saved to {directory}/node_communities.xlsx")
|
|
2516
2560
|
|
|
2517
2561
|
def save_network_overlay(self, directory = None, filename = None):
|
|
2518
2562
|
|
|
@@ -5,7 +5,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
5
5
|
QFormLayout, QLineEdit, QPushButton, QFileDialog,
|
|
6
6
|
QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
|
|
7
7
|
QMenu, QTabWidget)
|
|
8
|
-
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer)
|
|
8
|
+
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal)
|
|
9
9
|
import numpy as np
|
|
10
10
|
import time
|
|
11
11
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
@@ -13,8 +13,8 @@ from matplotlib.figure import Figure
|
|
|
13
13
|
import matplotlib.pyplot as plt
|
|
14
14
|
from qtrangeslider import QRangeSlider
|
|
15
15
|
from nettracer3d import nettracer as n3d
|
|
16
|
-
from nettracer3d import smart_dilate as sdl
|
|
17
16
|
from nettracer3d import proximity as pxt
|
|
17
|
+
from nettracer3d import smart_dilate as sdl
|
|
18
18
|
from matplotlib.colors import LinearSegmentedColormap
|
|
19
19
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
20
20
|
import pandas as pd
|
|
@@ -27,6 +27,7 @@ from functools import partial
|
|
|
27
27
|
from nettracer3d import segmenter
|
|
28
28
|
|
|
29
29
|
|
|
30
|
+
|
|
30
31
|
class ImageViewerWindow(QMainWindow):
|
|
31
32
|
def __init__(self):
|
|
32
33
|
super().__init__()
|
|
@@ -178,7 +179,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
178
179
|
self.zoom_button.setCheckable(True)
|
|
179
180
|
self.zoom_button.setFixedSize(40, 40)
|
|
180
181
|
self.zoom_button.clicked.connect(self.toggle_zoom_mode)
|
|
181
|
-
|
|
182
|
+
buttons_layout.addWidget(self.zoom_button)
|
|
182
183
|
self.resizing = False
|
|
183
184
|
|
|
184
185
|
self.pan_button = QPushButton("✋")
|
|
@@ -187,6 +188,14 @@ class ImageViewerWindow(QMainWindow):
|
|
|
187
188
|
self.pan_button.clicked.connect(self.toggle_pan_mode)
|
|
188
189
|
buttons_layout.addWidget(self.pan_button)
|
|
189
190
|
|
|
191
|
+
self.high_button = QPushButton("👁️")
|
|
192
|
+
self.high_button.setCheckable(True)
|
|
193
|
+
self.high_button.setFixedSize(40, 40)
|
|
194
|
+
self.high_button.clicked.connect(self.toggle_highlight)
|
|
195
|
+
self.high_button.setChecked(True)
|
|
196
|
+
buttons_layout.addWidget(self.high_button)
|
|
197
|
+
self.highlight = True
|
|
198
|
+
|
|
190
199
|
control_layout.addWidget(buttons_widget)
|
|
191
200
|
|
|
192
201
|
self.preview = False #Whether in preview mode or not
|
|
@@ -1429,6 +1438,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1429
1438
|
|
|
1430
1439
|
|
|
1431
1440
|
|
|
1441
|
+
def toggle_highlight(self):
|
|
1442
|
+
self.highlight = self.high_button.isChecked()
|
|
1443
|
+
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1444
|
+
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1445
|
+
|
|
1446
|
+
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
1432
1447
|
|
|
1433
1448
|
|
|
1434
1449
|
def toggle_zoom_mode(self):
|
|
@@ -1512,6 +1527,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1512
1527
|
if self.machine_window is not None:
|
|
1513
1528
|
if event.key() == Qt.Key_A:
|
|
1514
1529
|
self.machine_window.switch_foreground()
|
|
1530
|
+
if event.key() == Qt.Key_X:
|
|
1531
|
+
self.high_button.click()
|
|
1515
1532
|
|
|
1516
1533
|
|
|
1517
1534
|
def update_brush_cursor(self):
|
|
@@ -1649,11 +1666,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1649
1666
|
x, y = int(event.xdata), int(event.ydata)
|
|
1650
1667
|
self.last_paint_pos = (x, y)
|
|
1651
1668
|
|
|
1652
|
-
|
|
1653
|
-
channel = 2
|
|
1654
|
-
else:
|
|
1655
|
-
channel = 3
|
|
1656
|
-
|
|
1669
|
+
channel = 2
|
|
1657
1670
|
|
|
1658
1671
|
# Paint at initial position
|
|
1659
1672
|
self.paint_at_position(x, y, self.erase, channel)
|
|
@@ -1692,8 +1705,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1692
1705
|
|
|
1693
1706
|
if erase:
|
|
1694
1707
|
val = 0
|
|
1708
|
+
elif self.foreground:
|
|
1709
|
+
val = 1
|
|
1695
1710
|
else:
|
|
1696
|
-
val =
|
|
1711
|
+
val = 2
|
|
1697
1712
|
|
|
1698
1713
|
height, width = self.channel_data[channel][self.current_slice].shape
|
|
1699
1714
|
radius = self.brush_size // 2
|
|
@@ -1774,10 +1789,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1774
1789
|
|
|
1775
1790
|
x, y = int(event.xdata), int(event.ydata)
|
|
1776
1791
|
|
|
1777
|
-
|
|
1778
|
-
channel = 2
|
|
1779
|
-
else:
|
|
1780
|
-
channel = 3
|
|
1792
|
+
channel = 2
|
|
1781
1793
|
|
|
1782
1794
|
|
|
1783
1795
|
if self.channel_data[2] is not None:
|
|
@@ -3313,26 +3325,41 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3313
3325
|
else:
|
|
3314
3326
|
normalized_image = np.clip((current_image - vmin) / (vmax - vmin), 0, 1)
|
|
3315
3327
|
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3328
|
+
if channel == 2 and self.machine_window is not None:
|
|
3329
|
+
custom_cmap = LinearSegmentedColormap.from_list(
|
|
3330
|
+
f'custom_{channel}',
|
|
3331
|
+
[(0, 0, 0, 0), # transparent for 0
|
|
3332
|
+
(0.5, 1, 0.5, 1), # light green for 1
|
|
3333
|
+
(1, 0.5, 0.5, 1)] # light red for 2
|
|
3334
|
+
)
|
|
3335
|
+
self.ax.imshow(current_image,
|
|
3336
|
+
cmap=custom_cmap,
|
|
3337
|
+
vmin=0,
|
|
3338
|
+
vmax=2,
|
|
3339
|
+
alpha=0.7,
|
|
3340
|
+
interpolation='nearest',
|
|
3341
|
+
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
3342
|
+
else:
|
|
3343
|
+
# Create custom colormap with higher intensity
|
|
3344
|
+
color = base_colors[channel]
|
|
3345
|
+
custom_cmap = LinearSegmentedColormap.from_list(
|
|
3346
|
+
f'custom_{channel}',
|
|
3347
|
+
[(0,0,0,0), (*color,1)]
|
|
3348
|
+
)
|
|
3349
|
+
|
|
3350
|
+
# Display the image with slightly higher alpha
|
|
3351
|
+
self.ax.imshow(normalized_image,
|
|
3352
|
+
alpha=0.7,
|
|
3353
|
+
cmap=custom_cmap,
|
|
3354
|
+
vmin=0,
|
|
3355
|
+
vmax=1,
|
|
3356
|
+
extent=(-0.5, min_width-0.5, min_height-0.5, -0.5))
|
|
3330
3357
|
|
|
3331
3358
|
if self.preview and not called:
|
|
3332
3359
|
self.create_highlight_overlay_slice(self.targs, bounds = self.bounds)
|
|
3333
3360
|
|
|
3334
3361
|
# Add highlight overlay if it exists
|
|
3335
|
-
if self.highlight_overlay is not None:
|
|
3362
|
+
if self.highlight_overlay is not None and self.highlight and self.machine_window is None:
|
|
3336
3363
|
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3337
3364
|
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
3338
3365
|
'highlight',
|
|
@@ -3341,6 +3368,19 @@ class ImageViewerWindow(QMainWindow):
|
|
|
3341
3368
|
self.ax.imshow(highlight_slice,
|
|
3342
3369
|
cmap=highlight_cmap,
|
|
3343
3370
|
alpha=0.5)
|
|
3371
|
+
elif self.highlight_overlay is not None and self.highlight:
|
|
3372
|
+
highlight_slice = self.highlight_overlay[self.current_slice]
|
|
3373
|
+
highlight_cmap = LinearSegmentedColormap.from_list(
|
|
3374
|
+
'highlight',
|
|
3375
|
+
[(0, 0, 0, 0), # transparent for 0
|
|
3376
|
+
(1, 1, 0, 1), # bright yellow for 1
|
|
3377
|
+
(0, 0.7, 1, 1)] # cool blue for 2
|
|
3378
|
+
)
|
|
3379
|
+
self.ax.imshow(highlight_slice,
|
|
3380
|
+
cmap=highlight_cmap,
|
|
3381
|
+
vmin=0,
|
|
3382
|
+
vmax=2, # Important: set vmax to 2 to accommodate both values
|
|
3383
|
+
alpha=0.5)
|
|
3344
3384
|
|
|
3345
3385
|
|
|
3346
3386
|
|
|
@@ -5781,13 +5821,13 @@ class MachineWindow(QMainWindow):
|
|
|
5781
5821
|
layout.addLayout(form_layout)
|
|
5782
5822
|
|
|
5783
5823
|
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
5824
|
if self.parent().active_channel == 0:
|
|
5787
5825
|
if self.parent().channel_data[0] is not None:
|
|
5788
5826
|
active_data = self.parent().channel_data[0]
|
|
5827
|
+
act_channel = 0
|
|
5789
5828
|
else:
|
|
5790
5829
|
active_data = self.parent().channel_data[1]
|
|
5830
|
+
act_channel = 1
|
|
5791
5831
|
|
|
5792
5832
|
|
|
5793
5833
|
array1 = np.zeros_like(active_data)
|
|
@@ -5795,12 +5835,18 @@ class MachineWindow(QMainWindow):
|
|
|
5795
5835
|
array3 = np.zeros_like(active_data)
|
|
5796
5836
|
self.parent().highlight_overlay = array3 #Clear this out for the segmenter to use
|
|
5797
5837
|
|
|
5798
|
-
self.parent().load_channel(2, array1, True)
|
|
5799
|
-
|
|
5838
|
+
self.parent().load_channel(2, array1, True)
|
|
5839
|
+
# Enable the channel button
|
|
5840
|
+
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
5841
|
+
if not self.parent().channel_buttons[2].isEnabled():
|
|
5842
|
+
self.parent().channel_buttons[2].setEnabled(True)
|
|
5843
|
+
self.parent().channel_buttons[2].click()
|
|
5844
|
+
self.parent().delete_buttons[2].setEnabled(True)
|
|
5800
5845
|
|
|
5846
|
+
self.parent().base_colors[act_channel] = self.parent().color_dictionary['WHITE']
|
|
5801
5847
|
self.parent().base_colors[2] = self.parent().color_dictionary['LIGHT_GREEN']
|
|
5802
|
-
self.parent().base_colors[3] = self.parent().color_dictionary['LIGHT_RED']
|
|
5803
5848
|
|
|
5849
|
+
self.parent().update_display()
|
|
5804
5850
|
|
|
5805
5851
|
# Set a reasonable default size
|
|
5806
5852
|
self.setMinimumWidth(400)
|
|
@@ -5830,20 +5876,39 @@ class MachineWindow(QMainWindow):
|
|
|
5830
5876
|
self.back_button.clicked.connect(self.toggle_background)
|
|
5831
5877
|
form_layout.addWidget(self.back_button)
|
|
5832
5878
|
|
|
5833
|
-
|
|
5834
|
-
|
|
5879
|
+
self.GPU = QPushButton("GPU")
|
|
5880
|
+
self.GPU.setCheckable(True)
|
|
5881
|
+
self.GPU.setChecked(False)
|
|
5882
|
+
self.GPU.clicked.connect(self.toggle_GPU)
|
|
5883
|
+
form_layout.addWidget(self.GPU)
|
|
5884
|
+
self.use_gpu = False
|
|
5885
|
+
|
|
5886
|
+
train_button = QPushButton("Train Quick Model")
|
|
5887
|
+
train_button.clicked.connect(lambda: self.train_model(speed = True))
|
|
5835
5888
|
form_layout.addRow(train_button)
|
|
5836
5889
|
|
|
5837
|
-
|
|
5838
|
-
|
|
5890
|
+
train_button = QPushButton("Train More Detailed Model")
|
|
5891
|
+
train_button.clicked.connect(lambda: self.train_model(speed = False))
|
|
5892
|
+
form_layout.addRow(train_button)
|
|
5893
|
+
|
|
5894
|
+
seg_button = QPushButton("Preview Segment")
|
|
5895
|
+
seg_button.clicked.connect(self.start_segmentation)
|
|
5839
5896
|
form_layout.addRow(seg_button)
|
|
5840
5897
|
|
|
5898
|
+
full_button = QPushButton("Segment All")
|
|
5899
|
+
full_button.clicked.connect(self.segment)
|
|
5900
|
+
form_layout.addRow(full_button)
|
|
5901
|
+
|
|
5841
5902
|
self.trained = False
|
|
5842
5903
|
|
|
5843
5904
|
|
|
5844
5905
|
self.segmenter = segmenter.InteractiveSegmenter(active_data, use_gpu=True)
|
|
5845
5906
|
|
|
5846
5907
|
|
|
5908
|
+
def toggle_GPU(self):
|
|
5909
|
+
|
|
5910
|
+
|
|
5911
|
+
self.use_gpu = self.GPU.isChecked()
|
|
5847
5912
|
|
|
5848
5913
|
|
|
5849
5914
|
def toggle_foreground(self):
|
|
@@ -5869,7 +5934,6 @@ class MachineWindow(QMainWindow):
|
|
|
5869
5934
|
self.fore_button.setChecked(True)
|
|
5870
5935
|
|
|
5871
5936
|
|
|
5872
|
-
|
|
5873
5937
|
def toggle_brush_mode(self):
|
|
5874
5938
|
"""Toggle brush mode on/off"""
|
|
5875
5939
|
self.parent().brush_mode = self.brush_button.isChecked()
|
|
@@ -5889,34 +5953,164 @@ class MachineWindow(QMainWindow):
|
|
|
5889
5953
|
|
|
5890
5954
|
self.brush_button.click()
|
|
5891
5955
|
|
|
5892
|
-
def train_model(self):
|
|
5956
|
+
def train_model(self, speed = True):
|
|
5893
5957
|
|
|
5894
|
-
self.
|
|
5958
|
+
self.kill_segmentation()
|
|
5959
|
+
# Wait a bit for cleanup
|
|
5960
|
+
time.sleep(0.1)
|
|
5961
|
+
self.segmenter.train_batch(self.parent().channel_data[2], speed = speed, use_gpu = self.use_gpu)
|
|
5895
5962
|
self.trained = True
|
|
5896
5963
|
|
|
5964
|
+
def start_segmentation(self):
|
|
5965
|
+
|
|
5966
|
+
self.kill_segmentation()
|
|
5967
|
+
print("Beginning new segmentation...")
|
|
5968
|
+
|
|
5969
|
+
if not self.trained:
|
|
5970
|
+
return
|
|
5971
|
+
else:
|
|
5972
|
+
self.segmentation_worker = SegmentationWorker(self.parent().highlight_overlay, self.segmenter, self.use_gpu)
|
|
5973
|
+
self.segmentation_worker.chunk_processed.connect(self.update_display) # Just update display
|
|
5974
|
+
self.segmentation_worker.finished.connect(self.segmentation_finished)
|
|
5975
|
+
current_xlim = self.parent().ax.get_xlim()
|
|
5976
|
+
current_ylim = self.parent().ax.get_ylim()
|
|
5977
|
+
self.segmenter.update_position(self.parent().current_slice, int((current_ylim[0] - current_ylim[1])/2), int((current_xlim[1] - current_xlim[0])/2))
|
|
5978
|
+
self.segmentation_worker.start()
|
|
5979
|
+
|
|
5980
|
+
|
|
5981
|
+
|
|
5982
|
+
def update_display(self):
|
|
5983
|
+
if not hasattr(self, '_last_update'):
|
|
5984
|
+
self._last_update = 0
|
|
5985
|
+
|
|
5986
|
+
current_time = time.time()
|
|
5987
|
+
if current_time - self._last_update >= 1: # Match worker's interval
|
|
5988
|
+
try:
|
|
5989
|
+
# Store current view state
|
|
5990
|
+
current_xlim = self.parent().ax.get_xlim()
|
|
5991
|
+
current_ylim = self.parent().ax.get_ylim()
|
|
5992
|
+
|
|
5993
|
+
self.segmenter.update_position(self.parent().current_slice, int((current_ylim[0] - current_ylim[1])/2), int((current_xlim[1] - current_xlim[0])/2))
|
|
5994
|
+
if not self.parent().painting:
|
|
5995
|
+
# Only update if view limits are valid
|
|
5996
|
+
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
5997
|
+
|
|
5998
|
+
|
|
5999
|
+
self._last_update = current_time
|
|
6000
|
+
except Exception as e:
|
|
6001
|
+
print(f"Display update error: {e}")
|
|
6002
|
+
|
|
6003
|
+
def segmentation_finished(self):
|
|
6004
|
+
print("Segmentation completed")
|
|
6005
|
+
current_xlim = self.parent().ax.get_xlim()
|
|
6006
|
+
current_ylim = self.parent().ax.get_ylim()
|
|
6007
|
+
self.parent().update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
6008
|
+
self.kill_segmentation()
|
|
6009
|
+
|
|
6010
|
+
def kill_segmentation(self):
|
|
6011
|
+
if hasattr(self, 'segmentation_worker'):
|
|
6012
|
+
self.segmentation_worker.stop()
|
|
6013
|
+
del self.segmentation_worker # Clean up reference
|
|
6014
|
+
|
|
6015
|
+
|
|
5897
6016
|
def segment(self):
|
|
5898
6017
|
|
|
5899
6018
|
if not self.trained:
|
|
5900
6019
|
return
|
|
5901
6020
|
else:
|
|
5902
|
-
|
|
6021
|
+
print("Segmenting entire volume with model...")
|
|
6022
|
+
foreground_coords, background_coords = self.segmenter.segment_volume(gpu = self.use_gpu)
|
|
5903
6023
|
|
|
5904
6024
|
# Clean up when done
|
|
5905
6025
|
self.segmenter.cleanup()
|
|
5906
6026
|
|
|
5907
6027
|
for z,y,x in foreground_coords:
|
|
5908
|
-
self.parent().highlight_overlay[z,y,x] =
|
|
6028
|
+
self.parent().highlight_overlay[z,y,x] = 255
|
|
6029
|
+
|
|
6030
|
+
self.parent().load_channel(3, self.parent().highlight_overlay, True)
|
|
6031
|
+
|
|
6032
|
+
# Not exactly sure why we need all this but the channel buttons weren't loading like they normally do when load_channel() is called:
|
|
6033
|
+
self.parent().channel_buttons[3].setEnabled(True)
|
|
6034
|
+
self.parent().channel_buttons[3].click()
|
|
6035
|
+
self.parent().delete_buttons[3].setEnabled(True)
|
|
6036
|
+
|
|
6037
|
+
self.parent().highlight_overlay = None
|
|
5909
6038
|
|
|
5910
6039
|
self.parent().update_display()
|
|
5911
6040
|
|
|
6041
|
+
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
6042
|
+
|
|
5912
6043
|
def closeEvent(self, event):
|
|
5913
6044
|
if self.brush_button.isChecked():
|
|
5914
6045
|
self.silence_button()
|
|
5915
6046
|
self.toggle_brush_mode()
|
|
5916
6047
|
self.parent().brush_mode = False
|
|
5917
6048
|
self.parent().machine_window = None
|
|
6049
|
+
self.kill_segmentation()
|
|
6050
|
+
|
|
5918
6051
|
|
|
5919
6052
|
|
|
6053
|
+
class SegmentationWorker(QThread):
|
|
6054
|
+
finished = pyqtSignal()
|
|
6055
|
+
chunk_processed = pyqtSignal()
|
|
6056
|
+
|
|
6057
|
+
def __init__(self, highlight_overlay, segmenter, use_gpu):
|
|
6058
|
+
super().__init__()
|
|
6059
|
+
self.overlay = highlight_overlay
|
|
6060
|
+
self.segmenter = segmenter
|
|
6061
|
+
self.use_gpu = use_gpu
|
|
6062
|
+
self._stop = False
|
|
6063
|
+
self.update_interval = 1 # Increased to 500ms
|
|
6064
|
+
self.chunks_since_update = 0
|
|
6065
|
+
self.chunks_per_update = 5 # Only update every 5 chunks
|
|
6066
|
+
self.last_update = time.time()
|
|
6067
|
+
|
|
6068
|
+
def stop(self):
|
|
6069
|
+
self._stop = True
|
|
6070
|
+
|
|
6071
|
+
def run(self):
|
|
6072
|
+
try:
|
|
6073
|
+
self.overlay.fill(False)
|
|
6074
|
+
|
|
6075
|
+
for foreground_coords, background_coords in self.segmenter.segment_volume_realtime(gpu = self.use_gpu):
|
|
6076
|
+
if self._stop:
|
|
6077
|
+
break
|
|
6078
|
+
|
|
6079
|
+
for z,y,x in foreground_coords:
|
|
6080
|
+
self.overlay[z,y,x] = 1
|
|
6081
|
+
for z,y,x in background_coords:
|
|
6082
|
+
self.overlay[z,y,x] = 2
|
|
6083
|
+
|
|
6084
|
+
# Update only after several chunks AND minimum time interval
|
|
6085
|
+
self.chunks_since_update += 1
|
|
6086
|
+
current_time = time.time()
|
|
6087
|
+
if (self.chunks_since_update >= self.chunks_per_update and
|
|
6088
|
+
current_time - self.last_update >= self.update_interval):
|
|
6089
|
+
self.chunk_processed.emit()
|
|
6090
|
+
self.chunks_since_update = 0
|
|
6091
|
+
self.last_update = current_time
|
|
6092
|
+
|
|
6093
|
+
self.finished.emit()
|
|
6094
|
+
|
|
6095
|
+
except Exception as e:
|
|
6096
|
+
print(f"Error in segmentation: {e}")
|
|
6097
|
+
raise
|
|
6098
|
+
|
|
6099
|
+
def run_batch(self):
|
|
6100
|
+
try:
|
|
6101
|
+
foreground_coords, _ = self.segmenter.segment_volume()
|
|
6102
|
+
|
|
6103
|
+
# Modify the array directly
|
|
6104
|
+
self.overlay.fill(False)
|
|
6105
|
+
for z,y,x in foreground_coords:
|
|
6106
|
+
self.overlay[z,y,x] = True
|
|
6107
|
+
|
|
6108
|
+
self.finished.emit()
|
|
6109
|
+
|
|
6110
|
+
except Exception as e:
|
|
6111
|
+
print(f"Error in segmentation: {e}")
|
|
6112
|
+
raise
|
|
6113
|
+
|
|
5920
6114
|
|
|
5921
6115
|
|
|
5922
6116
|
|