nettracer3d 0.4.8__tar.gz → 0.5.0__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.8/src/nettracer3d.egg-info → nettracer3d-0.5.0}/PKG-INFO +15 -15
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/pyproject.toml +15 -15
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/nettracer_gui.py +220 -88
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/segmenter.py +31 -28
- {nettracer3d-0.4.8 → nettracer3d-0.5.0/src/nettracer3d.egg-info}/PKG-INFO +15 -15
- nettracer3d-0.5.0/src/nettracer3d.egg-info/requires.txt +24 -0
- nettracer3d-0.4.8/src/nettracer3d.egg-info/requires.txt +0 -24
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/LICENSE +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/README.md +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/setup.cfg +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/__init__.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/community_extractor.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/hub_getter.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/modularity.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/morphology.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/nettracer.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/network_analysis.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/network_draw.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/node_draw.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/proximity.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/run.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/simple_network.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d/smart_dilate.py +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d.egg-info/SOURCES.txt +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d.egg-info/dependency_links.txt +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/src/nettracer3d.egg-info/entry_points.txt +0 -0
- {nettracer3d-0.4.8 → nettracer3d-0.5.0}/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.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -12,20 +12,20 @@ Requires-Python: >=3.8
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy==1.26.4
|
|
15
|
-
Requires-Dist: scipy
|
|
16
|
-
Requires-Dist: scikit-image
|
|
17
|
-
Requires-Dist: Pillow
|
|
18
|
-
Requires-Dist: matplotlib
|
|
19
|
-
Requires-Dist: networkx
|
|
20
|
-
Requires-Dist: opencv-python-headless
|
|
21
|
-
Requires-Dist: openpyxl
|
|
22
|
-
Requires-Dist: pandas
|
|
23
|
-
Requires-Dist: napari
|
|
24
|
-
Requires-Dist: python-louvain
|
|
25
|
-
Requires-Dist: tifffile
|
|
26
|
-
Requires-Dist: qtrangeslider
|
|
27
|
-
Requires-Dist: PyQt6
|
|
28
|
-
Requires-Dist: scikit-learn
|
|
15
|
+
Requires-Dist: scipy==1.14.1
|
|
16
|
+
Requires-Dist: scikit-image==0.25.0
|
|
17
|
+
Requires-Dist: Pillow==11.1.0
|
|
18
|
+
Requires-Dist: matplotlib==3.9.2
|
|
19
|
+
Requires-Dist: networkx==3.2.1
|
|
20
|
+
Requires-Dist: opencv-python-headless==4.10.0.84
|
|
21
|
+
Requires-Dist: openpyxl==3.1.2
|
|
22
|
+
Requires-Dist: pandas==2.2.0
|
|
23
|
+
Requires-Dist: napari==0.5.5
|
|
24
|
+
Requires-Dist: python-louvain==0.16
|
|
25
|
+
Requires-Dist: tifffile==2023.7.18
|
|
26
|
+
Requires-Dist: qtrangeslider==0.1.5
|
|
27
|
+
Requires-Dist: PyQt6==6.8.0
|
|
28
|
+
Requires-Dist: scikit-learn==1.6.1
|
|
29
29
|
Provides-Extra: cuda11
|
|
30
30
|
Requires-Dist: cupy-cuda11x; extra == "cuda11"
|
|
31
31
|
Provides-Extra: cuda12
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nettracer3d"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
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
9
|
"numpy == 1.26.4",
|
|
10
|
-
"scipy",
|
|
11
|
-
"scikit-image",
|
|
12
|
-
"Pillow",
|
|
13
|
-
"matplotlib",
|
|
14
|
-
"networkx",
|
|
15
|
-
"opencv-python-headless",
|
|
16
|
-
"openpyxl",
|
|
17
|
-
"pandas",
|
|
18
|
-
"napari",
|
|
19
|
-
"python-louvain",
|
|
20
|
-
"tifffile",
|
|
21
|
-
"qtrangeslider",
|
|
22
|
-
"PyQt6",
|
|
23
|
-
"scikit-learn"
|
|
10
|
+
"scipy == 1.14.1",
|
|
11
|
+
"scikit-image == 0.25.0",
|
|
12
|
+
"Pillow == 11.1.0",
|
|
13
|
+
"matplotlib == 3.9.2",
|
|
14
|
+
"networkx == 3.2.1",
|
|
15
|
+
"opencv-python-headless == 4.10.0.84",
|
|
16
|
+
"openpyxl == 3.1.2",
|
|
17
|
+
"pandas == 2.2.0",
|
|
18
|
+
"napari == 0.5.5",
|
|
19
|
+
"python-louvain == 0.16",
|
|
20
|
+
"tifffile == 2023.7.18",
|
|
21
|
+
"qtrangeslider == 0.1.5",
|
|
22
|
+
"PyQt6 == 6.8.0",
|
|
23
|
+
"scikit-learn == 1.6.1"
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
readme = "README.md"
|
|
@@ -4,7 +4,7 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
|
4
4
|
QHBoxLayout, QSlider, QMenuBar, QMenu, QDialog,
|
|
5
5
|
QFormLayout, QLineEdit, QPushButton, QFileDialog,
|
|
6
6
|
QLabel, QComboBox, QMessageBox, QTableView, QInputDialog,
|
|
7
|
-
QMenu, QTabWidget)
|
|
7
|
+
QMenu, QTabWidget, QGroupBox)
|
|
8
8
|
from PyQt6.QtCore import (QPoint, Qt, QAbstractTableModel, QTimer, QThread, pyqtSignal)
|
|
9
9
|
import numpy as np
|
|
10
10
|
import time
|
|
@@ -196,6 +196,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
196
196
|
buttons_layout.addWidget(self.high_button)
|
|
197
197
|
self.highlight = True
|
|
198
198
|
|
|
199
|
+
self.pen_button = QPushButton("🖊️")
|
|
200
|
+
self.pen_button.setCheckable(True)
|
|
201
|
+
self.pen_button.setFixedSize(40, 40)
|
|
202
|
+
self.pen_button.clicked.connect(self.toggle_brush_mode)
|
|
203
|
+
buttons_layout.addWidget(self.pen_button)
|
|
204
|
+
|
|
205
|
+
self.thresh_button = QPushButton("✏️")
|
|
206
|
+
self.thresh_button.setFixedSize(40, 40)
|
|
207
|
+
self.thresh_button.clicked.connect(self.show_thresh_dialog)
|
|
208
|
+
buttons_layout.addWidget(self.thresh_button)
|
|
209
|
+
|
|
199
210
|
control_layout.addWidget(buttons_widget)
|
|
200
211
|
|
|
201
212
|
self.preview = False #Whether in preview mode or not
|
|
@@ -1289,11 +1300,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1289
1300
|
my_network.nodes = my_network.nodes * mask
|
|
1290
1301
|
self.load_channel(0, my_network.nodes, True)
|
|
1291
1302
|
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1303
|
+
if my_network.network_lists is not None:
|
|
1304
|
+
for i in range(len(my_network.network_lists[0]) - 1, -1, -1):
|
|
1305
|
+
if my_network.network_lists[0][i] in self.clicked_values['nodes'] or my_network.network_lists[0][i] in self.clicked_values['nodes']:
|
|
1306
|
+
del my_network.network_lists[0][i]
|
|
1307
|
+
del my_network.network_lists[1][i]
|
|
1308
|
+
del my_network.network_lists[2][i]
|
|
1297
1309
|
|
|
1298
1310
|
|
|
1299
1311
|
|
|
@@ -1303,11 +1315,12 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1303
1315
|
my_network.edges = my_network.edges * mask
|
|
1304
1316
|
self.load_channel(1, my_network.edges, True)
|
|
1305
1317
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1318
|
+
if my_network.network_lists is not None:
|
|
1319
|
+
for i in range(len(my_network.network_lists[1]) - 1, -1, -1):
|
|
1320
|
+
if my_network.network_lists[2][i] in self.clicked_values['edges']:
|
|
1321
|
+
del my_network.network_lists[0][i]
|
|
1322
|
+
del my_network.network_lists[1][i]
|
|
1323
|
+
del my_network.network_lists[2][i]
|
|
1311
1324
|
|
|
1312
1325
|
my_network.network_lists = my_network.network_lists
|
|
1313
1326
|
|
|
@@ -1451,6 +1464,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1451
1464
|
self.zoom_mode = self.zoom_button.isChecked()
|
|
1452
1465
|
if self.zoom_mode:
|
|
1453
1466
|
self.pan_button.setChecked(False)
|
|
1467
|
+
self.pen_button.setChecked(False)
|
|
1454
1468
|
self.pan_mode = False
|
|
1455
1469
|
self.brush_mode = False
|
|
1456
1470
|
if self.machine_window is not None:
|
|
@@ -1468,6 +1482,7 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1468
1482
|
self.pan_mode = self.pan_button.isChecked()
|
|
1469
1483
|
if self.pan_mode:
|
|
1470
1484
|
self.zoom_button.setChecked(False)
|
|
1485
|
+
self.pen_button.setChecked(False)
|
|
1471
1486
|
self.zoom_mode = False
|
|
1472
1487
|
self.brush_mode = False
|
|
1473
1488
|
if self.machine_window is not None:
|
|
@@ -1479,6 +1494,17 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1479
1494
|
else:
|
|
1480
1495
|
self.machine_window.toggle_brush_button()
|
|
1481
1496
|
|
|
1497
|
+
def toggle_brush_mode(self):
|
|
1498
|
+
"""Toggle brush mode on/off"""
|
|
1499
|
+
self.brush_mode = self.pen_button.isChecked()
|
|
1500
|
+
if self.brush_mode:
|
|
1501
|
+
self.pan_button.setChecked(False)
|
|
1502
|
+
self.zoom_button.setChecked(False)
|
|
1503
|
+
self.pan_mode = False
|
|
1504
|
+
self.zoom_mode = False
|
|
1505
|
+
self.update_brush_cursor()
|
|
1506
|
+
else:
|
|
1507
|
+
self.canvas.setCursor(Qt.CursorShape.ArrowCursor)
|
|
1482
1508
|
|
|
1483
1509
|
|
|
1484
1510
|
def on_mpl_scroll(self, event):
|
|
@@ -1666,7 +1692,10 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1666
1692
|
x, y = int(event.xdata), int(event.ydata)
|
|
1667
1693
|
self.last_paint_pos = (x, y)
|
|
1668
1694
|
|
|
1669
|
-
|
|
1695
|
+
if self.pen_button.isChecked():
|
|
1696
|
+
channel = self.active_channel
|
|
1697
|
+
else:
|
|
1698
|
+
channel = 2
|
|
1670
1699
|
|
|
1671
1700
|
# Paint at initial position
|
|
1672
1701
|
self.paint_at_position(x, y, self.erase, channel)
|
|
@@ -1705,6 +1734,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1705
1734
|
|
|
1706
1735
|
if erase:
|
|
1707
1736
|
val = 0
|
|
1737
|
+
elif self.machine_window is None:
|
|
1738
|
+
val = 255
|
|
1708
1739
|
elif self.foreground:
|
|
1709
1740
|
val = 1
|
|
1710
1741
|
else:
|
|
@@ -1789,13 +1820,16 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1789
1820
|
|
|
1790
1821
|
x, y = int(event.xdata), int(event.ydata)
|
|
1791
1822
|
|
|
1792
|
-
|
|
1823
|
+
if self.pen_button.isChecked():
|
|
1824
|
+
channel = self.active_channel
|
|
1825
|
+
else:
|
|
1826
|
+
channel = 2
|
|
1793
1827
|
|
|
1794
1828
|
|
|
1795
|
-
if self.channel_data[
|
|
1829
|
+
if self.channel_data[channel] is not None:
|
|
1796
1830
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1797
1831
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1798
|
-
height, width = self.channel_data[
|
|
1832
|
+
height, width = self.channel_data[channel][self.current_slice].shape
|
|
1799
1833
|
|
|
1800
1834
|
if hasattr(self, 'last_paint_pos'):
|
|
1801
1835
|
last_x, last_y = self.last_paint_pos
|
|
@@ -1888,8 +1922,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
1888
1922
|
|
|
1889
1923
|
if self.brush_mode:
|
|
1890
1924
|
self.painting = False
|
|
1891
|
-
|
|
1892
|
-
|
|
1925
|
+
try:
|
|
1926
|
+
for i in self.restore_channels:
|
|
1927
|
+
self.channel_visible[i] = True
|
|
1928
|
+
except:
|
|
1929
|
+
pass
|
|
1893
1930
|
current_xlim = self.ax.get_xlim() if hasattr(self, 'ax') and self.ax.get_xlim() != (0, 1) else None
|
|
1894
1931
|
current_ylim = self.ax.get_ylim() if hasattr(self, 'ax') and self.ax.get_ylim() != (0, 1) else None
|
|
1895
1932
|
self.update_display(preserve_zoom=(current_xlim, current_ylim))
|
|
@@ -2223,6 +2260,8 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2223
2260
|
skeletonize_action.triggered.connect(self.show_skeletonize_dialog)
|
|
2224
2261
|
watershed_action = image_menu.addAction("Watershed")
|
|
2225
2262
|
watershed_action.triggered.connect(self.show_watershed_dialog)
|
|
2263
|
+
invert_action = image_menu.addAction("Invert")
|
|
2264
|
+
invert_action.triggered.connect(self.show_invert_dialog)
|
|
2226
2265
|
z_proj_action = image_menu.addAction("Z Project")
|
|
2227
2266
|
z_proj_action.triggered.connect(self.show_z_dialog)
|
|
2228
2267
|
|
|
@@ -2254,8 +2293,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2254
2293
|
idoverlay_action = overlay_menu.addAction("Create ID Overlay")
|
|
2255
2294
|
idoverlay_action.triggered.connect(self.show_idoverlay_dialog)
|
|
2256
2295
|
searchoverlay_action = overlay_menu.addAction("Show Search Regions")
|
|
2257
|
-
white_action = overlay_menu.addAction("White Background Overlay")
|
|
2258
|
-
white_action.triggered.connect(self.show_white_dialog)
|
|
2259
2296
|
searchoverlay_action.triggered.connect(self.show_search_dialog)
|
|
2260
2297
|
shuffle_action = overlay_menu.addAction("Shuffle")
|
|
2261
2298
|
shuffle_action.triggered.connect(self.show_shuffle_dialog)
|
|
@@ -2389,6 +2426,11 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2389
2426
|
dialog = WatershedDialog(self)
|
|
2390
2427
|
dialog.exec()
|
|
2391
2428
|
|
|
2429
|
+
def show_invert_dialog(self):
|
|
2430
|
+
"""Show the watershed parameter dialog."""
|
|
2431
|
+
dialog = InvertDialog(self)
|
|
2432
|
+
dialog.exec()
|
|
2433
|
+
|
|
2392
2434
|
def show_z_dialog(self):
|
|
2393
2435
|
"""Show the z-proj dialog."""
|
|
2394
2436
|
dialog = ZDialog(self)
|
|
@@ -2431,6 +2473,9 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2431
2473
|
|
|
2432
2474
|
def show_thresh_dialog(self):
|
|
2433
2475
|
"""Show threshold dialog"""
|
|
2476
|
+
if self.machine_window is not None:
|
|
2477
|
+
return
|
|
2478
|
+
|
|
2434
2479
|
dialog = ThresholdDialog(self)
|
|
2435
2480
|
dialog.exec()
|
|
2436
2481
|
|
|
@@ -2532,11 +2577,6 @@ class ImageViewerWindow(QMainWindow):
|
|
|
2532
2577
|
dialog = SearchOverlayDialog(self)
|
|
2533
2578
|
dialog.exec()
|
|
2534
2579
|
|
|
2535
|
-
def show_white_dialog(self):
|
|
2536
|
-
"""Show the white dialog"""
|
|
2537
|
-
dialog = WhiteDialog(self)
|
|
2538
|
-
dialog.exec()
|
|
2539
|
-
|
|
2540
2580
|
def show_shuffle_dialog(self):
|
|
2541
2581
|
"""Show the shuffle dialog"""
|
|
2542
2582
|
dialog = ShuffleDialog(self)
|
|
@@ -4622,40 +4662,6 @@ class IdOverlayDialog(QDialog):
|
|
|
4622
4662
|
|
|
4623
4663
|
self.accept()
|
|
4624
4664
|
|
|
4625
|
-
class WhiteDialog(QDialog):
|
|
4626
|
-
|
|
4627
|
-
def __init__(self, parent=None):
|
|
4628
|
-
|
|
4629
|
-
super().__init__(parent)
|
|
4630
|
-
self.setWindowTitle("Generate White Overlay?")
|
|
4631
|
-
self.setModal(True)
|
|
4632
|
-
|
|
4633
|
-
layout = QFormLayout(self)
|
|
4634
|
-
|
|
4635
|
-
# Add Run button
|
|
4636
|
-
run_button = QPushButton("Generate (Will go to Overlay 2)")
|
|
4637
|
-
run_button.clicked.connect(self.white_overlay)
|
|
4638
|
-
layout.addWidget(run_button)
|
|
4639
|
-
|
|
4640
|
-
def white_overlay(self):
|
|
4641
|
-
|
|
4642
|
-
try:
|
|
4643
|
-
if isinstance(my_network.nodes, np.ndarray) :
|
|
4644
|
-
overlay = np.ones_like(my_network.nodes).astype(np.uint8) * 255
|
|
4645
|
-
elif isinstance(my_network.edges, np.ndarray):
|
|
4646
|
-
overlay = np.ones_like(my_network.edges).astype(np.uint8) * 255
|
|
4647
|
-
elif isinstance(my_network.network_overlay, np.ndarray):
|
|
4648
|
-
overlay = np.ones_like(my_network.network_overlay).astype(np.uint8) * 255
|
|
4649
|
-
|
|
4650
|
-
my_network.id_overlay = overlay
|
|
4651
|
-
|
|
4652
|
-
self.parent().load_channel(3, channel_data = my_network.id_overlay, data = True)
|
|
4653
|
-
|
|
4654
|
-
self.accept()
|
|
4655
|
-
|
|
4656
|
-
except Exception as e:
|
|
4657
|
-
print(f"Error making white background: {e}")
|
|
4658
|
-
|
|
4659
4665
|
|
|
4660
4666
|
class ShuffleDialog(QDialog):
|
|
4661
4667
|
|
|
@@ -5820,6 +5826,11 @@ class MachineWindow(QMainWindow):
|
|
|
5820
5826
|
|
|
5821
5827
|
layout.addLayout(form_layout)
|
|
5822
5828
|
|
|
5829
|
+
if self.parent().pen_button.isChecked(): #Disable the pen mode if the user is in it because the segmenter pen forks it
|
|
5830
|
+
self.parent().pen_button.click()
|
|
5831
|
+
|
|
5832
|
+
self.parent().pen_button.setEnabled(False)
|
|
5833
|
+
|
|
5823
5834
|
|
|
5824
5835
|
if self.parent().active_channel == 0:
|
|
5825
5836
|
if self.parent().channel_data[0] is not None:
|
|
@@ -5848,56 +5859,85 @@ class MachineWindow(QMainWindow):
|
|
|
5848
5859
|
|
|
5849
5860
|
self.parent().update_display()
|
|
5850
5861
|
|
|
5851
|
-
# Set a reasonable default size
|
|
5852
|
-
self.setMinimumWidth(
|
|
5853
|
-
self.setMinimumHeight(
|
|
5862
|
+
# Set a reasonable default size for the window
|
|
5863
|
+
self.setMinimumWidth(600) # Increased to accommodate grouped buttons
|
|
5864
|
+
self.setMinimumHeight(500)
|
|
5854
5865
|
|
|
5855
|
-
# Create
|
|
5856
|
-
|
|
5857
|
-
|
|
5866
|
+
# Create main layout container
|
|
5867
|
+
main_widget = QWidget()
|
|
5868
|
+
main_layout = QVBoxLayout(main_widget)
|
|
5858
5869
|
|
|
5859
|
-
#
|
|
5870
|
+
# Group 1: Drawing tools (Brush + Foreground/Background)
|
|
5871
|
+
drawing_group = QGroupBox("Drawing Tools")
|
|
5872
|
+
drawing_layout = QHBoxLayout()
|
|
5873
|
+
|
|
5874
|
+
# Brush button
|
|
5860
5875
|
self.brush_button = QPushButton("🖌️")
|
|
5861
5876
|
self.brush_button.setCheckable(True)
|
|
5862
5877
|
self.brush_button.setFixedSize(40, 40)
|
|
5863
5878
|
self.brush_button.clicked.connect(self.toggle_brush_mode)
|
|
5864
|
-
form_layout.addWidget(self.brush_button)
|
|
5865
5879
|
self.brush_button.click()
|
|
5866
5880
|
|
|
5881
|
+
# Foreground/Background buttons in their own horizontal layout
|
|
5882
|
+
fb_layout = QHBoxLayout()
|
|
5867
5883
|
self.fore_button = QPushButton("Foreground")
|
|
5868
5884
|
self.fore_button.setCheckable(True)
|
|
5869
5885
|
self.fore_button.setChecked(True)
|
|
5870
5886
|
self.fore_button.clicked.connect(self.toggle_foreground)
|
|
5871
|
-
form_layout.addWidget(self.fore_button)
|
|
5872
5887
|
|
|
5873
5888
|
self.back_button = QPushButton("Background")
|
|
5874
5889
|
self.back_button.setCheckable(True)
|
|
5875
5890
|
self.back_button.setChecked(False)
|
|
5876
5891
|
self.back_button.clicked.connect(self.toggle_background)
|
|
5877
|
-
form_layout.addWidget(self.back_button)
|
|
5878
5892
|
|
|
5893
|
+
fb_layout.addWidget(self.fore_button)
|
|
5894
|
+
fb_layout.addWidget(self.back_button)
|
|
5895
|
+
|
|
5896
|
+
drawing_layout.addWidget(self.brush_button)
|
|
5897
|
+
drawing_layout.addLayout(fb_layout)
|
|
5898
|
+
drawing_group.setLayout(drawing_layout)
|
|
5899
|
+
|
|
5900
|
+
# Group 2: Processing Options (GPU)
|
|
5901
|
+
processing_group = QGroupBox("Processing Options")
|
|
5902
|
+
processing_layout = QHBoxLayout()
|
|
5879
5903
|
self.GPU = QPushButton("GPU")
|
|
5880
5904
|
self.GPU.setCheckable(True)
|
|
5881
5905
|
self.GPU.setChecked(False)
|
|
5882
5906
|
self.GPU.clicked.connect(self.toggle_GPU)
|
|
5883
|
-
form_layout.addWidget(self.GPU)
|
|
5884
5907
|
self.use_gpu = False
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5908
|
+
processing_layout.addWidget(self.GPU)
|
|
5909
|
+
processing_group.setLayout(processing_layout)
|
|
5910
|
+
|
|
5911
|
+
# Group 3: Training Options
|
|
5912
|
+
training_group = QGroupBox("Training")
|
|
5913
|
+
training_layout = QVBoxLayout()
|
|
5914
|
+
train_quick = QPushButton("Train Quick Model")
|
|
5915
|
+
train_quick.clicked.connect(lambda: self.train_model(speed=True))
|
|
5916
|
+
train_detailed = QPushButton("Train More Detailed Model")
|
|
5917
|
+
train_detailed.clicked.connect(lambda: self.train_model(speed=False))
|
|
5918
|
+
training_layout.addWidget(train_quick)
|
|
5919
|
+
training_layout.addWidget(train_detailed)
|
|
5920
|
+
training_group.setLayout(training_layout)
|
|
5921
|
+
|
|
5922
|
+
# Group 4: Segmentation Options
|
|
5923
|
+
segmentation_group = QGroupBox("Segmentation")
|
|
5924
|
+
segmentation_layout = QVBoxLayout()
|
|
5894
5925
|
seg_button = QPushButton("Preview Segment")
|
|
5895
5926
|
seg_button.clicked.connect(self.start_segmentation)
|
|
5896
|
-
form_layout.addRow(seg_button)
|
|
5897
|
-
|
|
5898
5927
|
full_button = QPushButton("Segment All")
|
|
5899
5928
|
full_button.clicked.connect(self.segment)
|
|
5900
|
-
|
|
5929
|
+
segmentation_layout.addWidget(seg_button)
|
|
5930
|
+
segmentation_layout.addWidget(full_button)
|
|
5931
|
+
segmentation_group.setLayout(segmentation_layout)
|
|
5932
|
+
|
|
5933
|
+
# Add all groups to main layout
|
|
5934
|
+
main_layout.addWidget(drawing_group)
|
|
5935
|
+
main_layout.addWidget(processing_group)
|
|
5936
|
+
main_layout.addWidget(training_group)
|
|
5937
|
+
main_layout.addWidget(segmentation_group)
|
|
5938
|
+
|
|
5939
|
+
# Set the main widget as the central widget
|
|
5940
|
+
self.setCentralWidget(main_widget)
|
|
5901
5941
|
|
|
5902
5942
|
self.trained = False
|
|
5903
5943
|
|
|
@@ -5988,7 +6028,25 @@ class MachineWindow(QMainWindow):
|
|
|
5988
6028
|
self.segmenter.update_position(self.parent().current_slice, int((current_ylim[0] - current_ylim[1])/2), int((current_xlim[1] - current_xlim[0])/2))
|
|
5989
6029
|
self.segmentation_worker.start()
|
|
5990
6030
|
|
|
6031
|
+
def confirm_seg_dialog(self):
|
|
6032
|
+
"""Shows a dialog asking user to confirm segment all"""
|
|
6033
|
+
msg = QMessageBox()
|
|
6034
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
6035
|
+
msg.setText("Alert")
|
|
6036
|
+
msg.setInformativeText("Segment Entire Image? (Window will freeze for processing)")
|
|
6037
|
+
msg.setWindowTitle("Confirm")
|
|
6038
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
6039
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5991
6040
|
|
|
6041
|
+
def confirm_close_dialog(self):
|
|
6042
|
+
"""Shows a dialog asking user to confirm segment all"""
|
|
6043
|
+
msg = QMessageBox()
|
|
6044
|
+
msg.setIcon(QMessageBox.Icon.Question)
|
|
6045
|
+
msg.setText("Alert")
|
|
6046
|
+
msg.setInformativeText("Close Window?")
|
|
6047
|
+
msg.setWindowTitle("Confirm")
|
|
6048
|
+
msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
6049
|
+
return msg.exec() == QMessageBox.StandardButton.Yes
|
|
5992
6050
|
|
|
5993
6051
|
def update_display(self):
|
|
5994
6052
|
if not hasattr(self, '_last_update'):
|
|
@@ -6030,6 +6088,8 @@ class MachineWindow(QMainWindow):
|
|
|
6030
6088
|
|
|
6031
6089
|
if not self.trained:
|
|
6032
6090
|
return
|
|
6091
|
+
elif not self.confirm_seg_dialog():
|
|
6092
|
+
return
|
|
6033
6093
|
else:
|
|
6034
6094
|
self.kill_segmentation()
|
|
6035
6095
|
time.sleep(0.1)
|
|
@@ -6066,13 +6126,19 @@ class MachineWindow(QMainWindow):
|
|
|
6066
6126
|
print("Finished segmentation moved to Overlay 2. Use File -> Save(As) for disk saving.")
|
|
6067
6127
|
|
|
6068
6128
|
def closeEvent(self, event):
|
|
6069
|
-
if self.
|
|
6070
|
-
self.
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6129
|
+
if self.parent().isVisible():
|
|
6130
|
+
if self.confirm_close_dialog():
|
|
6131
|
+
|
|
6132
|
+
if self.brush_button.isChecked():
|
|
6133
|
+
self.silence_button()
|
|
6134
|
+
self.toggle_brush_mode()
|
|
6135
|
+
self.parent().pen_button.setEnabled(True)
|
|
6136
|
+
self.parent().brush_mode = False
|
|
6137
|
+
self.parent().machine_window = None
|
|
6138
|
+
self.kill_segmentation()
|
|
6139
|
+
time.sleep(0.1)
|
|
6140
|
+
else:
|
|
6141
|
+
event.ignore()
|
|
6076
6142
|
|
|
6077
6143
|
|
|
6078
6144
|
|
|
@@ -6946,6 +7012,65 @@ class WatershedDialog(QDialog):
|
|
|
6946
7012
|
f"Error running watershed: {str(e)}"
|
|
6947
7013
|
)
|
|
6948
7014
|
|
|
7015
|
+
class InvertDialog(QDialog):
|
|
7016
|
+
|
|
7017
|
+
def __init__(self, parent=None):
|
|
7018
|
+
super().__init__(parent)
|
|
7019
|
+
self.setWindowTitle("Invert Active Channel?")
|
|
7020
|
+
self.setModal(True)
|
|
7021
|
+
|
|
7022
|
+
layout = QFormLayout(self)
|
|
7023
|
+
|
|
7024
|
+
# Add Run button
|
|
7025
|
+
run_button = QPushButton("Run Invert")
|
|
7026
|
+
run_button.clicked.connect(self.run_invert)
|
|
7027
|
+
layout.addRow(run_button)
|
|
7028
|
+
|
|
7029
|
+
def run_invert(self):
|
|
7030
|
+
|
|
7031
|
+
try:
|
|
7032
|
+
|
|
7033
|
+
# Get the active channel data from parent
|
|
7034
|
+
active_data = self.parent().channel_data[self.parent().active_channel]
|
|
7035
|
+
if active_data is None:
|
|
7036
|
+
raise ValueError("No active image selected")
|
|
7037
|
+
|
|
7038
|
+
try:
|
|
7039
|
+
# Call binarize method with parameters
|
|
7040
|
+
if active_data.dtype == 'uint8' or 'int8':
|
|
7041
|
+
num = 255
|
|
7042
|
+
elif active_data.dtype == 'uint16' or 'int16':
|
|
7043
|
+
num = 65,535
|
|
7044
|
+
elif active_data.dtype == 'uint32' or 'int32':
|
|
7045
|
+
num = 2,147,483,647
|
|
7046
|
+
|
|
7047
|
+
result = (num - active_data
|
|
7048
|
+
)
|
|
7049
|
+
|
|
7050
|
+
# Update both the display data and the network object
|
|
7051
|
+
self.parent().channel_data[self.parent().active_channel] = result
|
|
7052
|
+
|
|
7053
|
+
|
|
7054
|
+
# Update the corresponding property in my_network
|
|
7055
|
+
setattr(my_network, network_properties[self.parent().active_channel], result)
|
|
7056
|
+
|
|
7057
|
+
self.parent().update_display()
|
|
7058
|
+
self.accept()
|
|
7059
|
+
|
|
7060
|
+
except Exception as e:
|
|
7061
|
+
QMessageBox.critical(
|
|
7062
|
+
self,
|
|
7063
|
+
"Error",
|
|
7064
|
+
f"Error running invert: {str(e)}"
|
|
7065
|
+
)
|
|
7066
|
+
|
|
7067
|
+
except Exception as e:
|
|
7068
|
+
QMessageBox.critical(
|
|
7069
|
+
self,
|
|
7070
|
+
"Error",
|
|
7071
|
+
f"Error running invert: {str(e)}"
|
|
7072
|
+
)
|
|
7073
|
+
|
|
6949
7074
|
class ZDialog(QDialog):
|
|
6950
7075
|
|
|
6951
7076
|
def __init__(self, parent=None):
|
|
@@ -7224,6 +7349,13 @@ class BranchDialog(QDialog):
|
|
|
7224
7349
|
run_button.clicked.connect(self.branch_label)
|
|
7225
7350
|
layout.addRow(run_button)
|
|
7226
7351
|
|
|
7352
|
+
if self.parent().channel_data[0] is not None:
|
|
7353
|
+
QMessageBox.critical(
|
|
7354
|
+
self,
|
|
7355
|
+
"Alert",
|
|
7356
|
+
"The nodes channel will be intermittently overwritten when running this method"
|
|
7357
|
+
)
|
|
7358
|
+
|
|
7227
7359
|
def branch_label(self):
|
|
7228
7360
|
|
|
7229
7361
|
try:
|
|
@@ -11,6 +11,7 @@ import concurrent.futures
|
|
|
11
11
|
from concurrent.futures import ThreadPoolExecutor
|
|
12
12
|
import threading
|
|
13
13
|
from scipy import ndimage
|
|
14
|
+
import multiprocessing
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class InteractiveSegmenter:
|
|
@@ -18,11 +19,7 @@ class InteractiveSegmenter:
|
|
|
18
19
|
self.image_3d = image_3d
|
|
19
20
|
self.patterns = []
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
self.use_gpu = use_gpu and cp.cuda.is_available()
|
|
23
|
-
except:
|
|
24
|
-
self.use_gpu = False
|
|
25
|
-
|
|
22
|
+
self.use_gpu = use_gpu and cp.cuda.is_available()
|
|
26
23
|
if self.use_gpu:
|
|
27
24
|
print(f"Using GPU: {torch.cuda.get_device_name()}")
|
|
28
25
|
self.image_gpu = cp.asarray(image_3d)
|
|
@@ -350,23 +347,6 @@ class InteractiveSegmenter:
|
|
|
350
347
|
|
|
351
348
|
return result
|
|
352
349
|
|
|
353
|
-
|
|
354
|
-
def train(self):
|
|
355
|
-
"""Train random forest on accumulated patterns"""
|
|
356
|
-
if len(self.patterns) < 2:
|
|
357
|
-
return
|
|
358
|
-
|
|
359
|
-
X = []
|
|
360
|
-
y = []
|
|
361
|
-
for pattern in self.patterns:
|
|
362
|
-
X.extend(pattern['features'])
|
|
363
|
-
y.extend([pattern['is_foreground']] * len(pattern['features']))
|
|
364
|
-
|
|
365
|
-
X = np.array(X)
|
|
366
|
-
y = np.array(y)
|
|
367
|
-
self.model.fit(X, y)
|
|
368
|
-
self.patterns = []
|
|
369
|
-
|
|
370
350
|
def process_chunk_GPU(self, chunk_coords):
|
|
371
351
|
"""Process a chunk of coordinates using GPU acceleration"""
|
|
372
352
|
coords = np.array(chunk_coords)
|
|
@@ -409,20 +389,40 @@ class InteractiveSegmenter:
|
|
|
409
389
|
|
|
410
390
|
return foreground, background
|
|
411
391
|
|
|
412
|
-
def segment_volume(self, chunk_size=
|
|
392
|
+
def segment_volume(self, chunk_size=64, gpu=False):
|
|
413
393
|
"""Segment volume using parallel processing of chunks with vectorized chunk creation"""
|
|
394
|
+
#Change the above chunk size to None to have it auto-compute largest chunks (not sure which is faster, 64 seems reasonable in test cases)
|
|
414
395
|
|
|
415
396
|
try:
|
|
416
397
|
from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
|
|
417
398
|
except:
|
|
418
|
-
print("Cannot find
|
|
399
|
+
print("Cannot find cuML, using CPU to segment instead...")
|
|
419
400
|
gpu = False
|
|
420
|
-
|
|
421
|
-
|
|
401
|
+
|
|
422
402
|
if self.feature_cache is None:
|
|
423
403
|
with self.lock:
|
|
424
404
|
if self.feature_cache is None:
|
|
425
405
|
self.feature_cache = self.compute_feature_maps()
|
|
406
|
+
|
|
407
|
+
print("Chunking data...")
|
|
408
|
+
|
|
409
|
+
# Determine optimal chunk size based on number of cores if not specified
|
|
410
|
+
if chunk_size is None:
|
|
411
|
+
total_cores = multiprocessing.cpu_count()
|
|
412
|
+
|
|
413
|
+
# Calculate total volume and target volume per core
|
|
414
|
+
total_volume = np.prod(self.image_3d.shape)
|
|
415
|
+
target_volume_per_chunk = total_volume / total_cores
|
|
416
|
+
|
|
417
|
+
# Calculate chunk size that would give us roughly one chunk per core
|
|
418
|
+
# Using cube root since we want roughly equal sizes in all dimensions
|
|
419
|
+
chunk_size = int(np.cbrt(target_volume_per_chunk))
|
|
420
|
+
|
|
421
|
+
# Ensure chunk size is at least 32 (minimum reasonable size) and not larger than smallest dimension
|
|
422
|
+
chunk_size = max(32, min(chunk_size, min(self.image_3d.shape)))
|
|
423
|
+
|
|
424
|
+
# Round to nearest multiple of 32 for better memory alignment
|
|
425
|
+
chunk_size = ((chunk_size + 15) // 32) * 32
|
|
426
426
|
|
|
427
427
|
# Calculate number of chunks in each dimension
|
|
428
428
|
z_chunks = (self.image_3d.shape[0] + chunk_size - 1) // chunk_size
|
|
@@ -455,6 +455,9 @@ class InteractiveSegmenter:
|
|
|
455
455
|
|
|
456
456
|
foreground_coords = set()
|
|
457
457
|
background_coords = set()
|
|
458
|
+
|
|
459
|
+
print("Segmenting chunks...")
|
|
460
|
+
|
|
458
461
|
|
|
459
462
|
with ThreadPoolExecutor() as executor:
|
|
460
463
|
if gpu:
|
|
@@ -482,7 +485,7 @@ class InteractiveSegmenter:
|
|
|
482
485
|
self.current_y = y
|
|
483
486
|
|
|
484
487
|
|
|
485
|
-
def get_realtime_chunks(self, chunk_size =
|
|
488
|
+
def get_realtime_chunks(self, chunk_size = 64):
|
|
486
489
|
print("Computing some overhead...")
|
|
487
490
|
|
|
488
491
|
|
|
@@ -553,7 +556,7 @@ class InteractiveSegmenter:
|
|
|
553
556
|
try:
|
|
554
557
|
from cuml.ensemble import RandomForestClassifier as cuRandomForestClassifier
|
|
555
558
|
except:
|
|
556
|
-
print("Cannot find
|
|
559
|
+
print("Cannot find cuML, using CPU to segment instead...")
|
|
557
560
|
gpu = False
|
|
558
561
|
|
|
559
562
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: nettracer3d
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
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
|
|
@@ -12,20 +12,20 @@ Requires-Python: >=3.8
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy==1.26.4
|
|
15
|
-
Requires-Dist: scipy
|
|
16
|
-
Requires-Dist: scikit-image
|
|
17
|
-
Requires-Dist: Pillow
|
|
18
|
-
Requires-Dist: matplotlib
|
|
19
|
-
Requires-Dist: networkx
|
|
20
|
-
Requires-Dist: opencv-python-headless
|
|
21
|
-
Requires-Dist: openpyxl
|
|
22
|
-
Requires-Dist: pandas
|
|
23
|
-
Requires-Dist: napari
|
|
24
|
-
Requires-Dist: python-louvain
|
|
25
|
-
Requires-Dist: tifffile
|
|
26
|
-
Requires-Dist: qtrangeslider
|
|
27
|
-
Requires-Dist: PyQt6
|
|
28
|
-
Requires-Dist: scikit-learn
|
|
15
|
+
Requires-Dist: scipy==1.14.1
|
|
16
|
+
Requires-Dist: scikit-image==0.25.0
|
|
17
|
+
Requires-Dist: Pillow==11.1.0
|
|
18
|
+
Requires-Dist: matplotlib==3.9.2
|
|
19
|
+
Requires-Dist: networkx==3.2.1
|
|
20
|
+
Requires-Dist: opencv-python-headless==4.10.0.84
|
|
21
|
+
Requires-Dist: openpyxl==3.1.2
|
|
22
|
+
Requires-Dist: pandas==2.2.0
|
|
23
|
+
Requires-Dist: napari==0.5.5
|
|
24
|
+
Requires-Dist: python-louvain==0.16
|
|
25
|
+
Requires-Dist: tifffile==2023.7.18
|
|
26
|
+
Requires-Dist: qtrangeslider==0.1.5
|
|
27
|
+
Requires-Dist: PyQt6==6.8.0
|
|
28
|
+
Requires-Dist: scikit-learn==1.6.1
|
|
29
29
|
Provides-Extra: cuda11
|
|
30
30
|
Requires-Dist: cupy-cuda11x; extra == "cuda11"
|
|
31
31
|
Provides-Extra: cuda12
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
numpy==1.26.4
|
|
2
|
+
scipy==1.14.1
|
|
3
|
+
scikit-image==0.25.0
|
|
4
|
+
Pillow==11.1.0
|
|
5
|
+
matplotlib==3.9.2
|
|
6
|
+
networkx==3.2.1
|
|
7
|
+
opencv-python-headless==4.10.0.84
|
|
8
|
+
openpyxl==3.1.2
|
|
9
|
+
pandas==2.2.0
|
|
10
|
+
napari==0.5.5
|
|
11
|
+
python-louvain==0.16
|
|
12
|
+
tifffile==2023.7.18
|
|
13
|
+
qtrangeslider==0.1.5
|
|
14
|
+
PyQt6==6.8.0
|
|
15
|
+
scikit-learn==1.6.1
|
|
16
|
+
|
|
17
|
+
[CUDA11]
|
|
18
|
+
cupy-cuda11x
|
|
19
|
+
|
|
20
|
+
[CUDA12]
|
|
21
|
+
cupy-cuda12x
|
|
22
|
+
|
|
23
|
+
[cupy]
|
|
24
|
+
cupy
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
numpy==1.26.4
|
|
2
|
-
scipy
|
|
3
|
-
scikit-image
|
|
4
|
-
Pillow
|
|
5
|
-
matplotlib
|
|
6
|
-
networkx
|
|
7
|
-
opencv-python-headless
|
|
8
|
-
openpyxl
|
|
9
|
-
pandas
|
|
10
|
-
napari
|
|
11
|
-
python-louvain
|
|
12
|
-
tifffile
|
|
13
|
-
qtrangeslider
|
|
14
|
-
PyQt6
|
|
15
|
-
scikit-learn
|
|
16
|
-
|
|
17
|
-
[CUDA11]
|
|
18
|
-
cupy-cuda11x
|
|
19
|
-
|
|
20
|
-
[CUDA12]
|
|
21
|
-
cupy-cuda12x
|
|
22
|
-
|
|
23
|
-
[cupy]
|
|
24
|
-
cupy
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|