PyNutil 0.2.25__tar.gz → 0.3.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.
- {pynutil-0.2.25 → pynutil-0.3.0}/PKG-INFO +51 -19
- pynutil-0.3.0/PyNutil/config.py +112 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/io/atlas_loader.py +8 -18
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/io/file_operations.py +119 -90
- pynutil-0.3.0/PyNutil/io/nifti_writer.py +47 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/io/read_and_write.py +179 -19
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/io/reconstruct_dzi.py +3 -2
- pynutil-0.3.0/PyNutil/io/section_visualisation.py +291 -0
- pynutil-0.3.0/PyNutil/io/volume_nifti.py +95 -0
- pynutil-0.3.0/PyNutil/main.py +603 -0
- pynutil-0.3.0/PyNutil/processing/aggregator.py +60 -0
- pynutil-0.3.0/PyNutil/processing/coordinate_extraction.py +1097 -0
- pynutil-0.3.0/PyNutil/processing/counting_and_load.py +564 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/processing/data_analysis.py +113 -43
- pynutil-0.3.0/PyNutil/processing/image_loaders.py +23 -0
- pynutil-0.3.0/PyNutil/processing/section_volume.py +373 -0
- pynutil-0.3.0/PyNutil/processing/transform.py +53 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/processing/transformations.py +19 -12
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/processing/utils.py +108 -16
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/processing/visualign_deformations.py +7 -41
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil.egg-info/PKG-INFO +51 -19
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil.egg-info/SOURCES.txt +19 -1
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil.egg-info/requires.txt +3 -3
- {pynutil-0.2.25 → pynutil-0.3.0}/README.md +47 -15
- {pynutil-0.2.25 → pynutil-0.3.0}/setup.py +3 -3
- pynutil-0.3.0/tests/test_build_volume_from_sections.py +105 -0
- pynutil-0.3.0/tests/test_cellpose_quantification.py +74 -0
- pynutil-0.3.0/tests/test_coordinate_scaling.py +204 -0
- pynutil-0.3.0/tests/test_damage_volume_interpolation.py +115 -0
- pynutil-0.3.0/tests/test_helpers.py +23 -0
- pynutil-0.3.0/tests/test_intensity_quantification.py +131 -0
- pynutil-0.3.0/tests/test_interpolate_volume_value_modes.py +167 -0
- pynutil-0.3.0/tests/test_quantification.py +166 -0
- pynutil-0.3.0/tests/test_transformations.py +57 -0
- pynutil-0.3.0/tests/test_validation.py +48 -0
- pynutil-0.3.0/tests/test_visualisations.py +94 -0
- pynutil-0.2.25/PyNutil/main.py +0 -303
- pynutil-0.2.25/PyNutil/processing/coordinate_extraction.py +0 -641
- pynutil-0.2.25/PyNutil/processing/counting_and_load.py +0 -728
- pynutil-0.2.25/tests/test_quantification.py +0 -57
- {pynutil-0.2.25 → pynutil-0.3.0}/LICENSE +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/__init__.py +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/io/__init__.py +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/io/propagation.py +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/processing/__init__.py +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil/processing/generate_target_slice.py +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil.egg-info/dependency_links.txt +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/PyNutil.egg-info/top_level.txt +0 -0
- {pynutil-0.2.25 → pynutil-0.3.0}/setup.cfg +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNutil
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: a package to quantify atlas registered brain data
|
|
5
5
|
Home-page: https://github.com/Neural-Systems-at-UIO/PyNutil
|
|
6
6
|
License: MIT
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
|
-
Requires-Dist: numpy
|
|
9
|
+
Requires-Dist: numpy==2.4
|
|
10
10
|
Requires-Dist: brainglobe-atlasapi
|
|
11
11
|
Requires-Dist: pandas
|
|
12
|
-
Requires-Dist: requests
|
|
13
12
|
Requires-Dist: pynrrd
|
|
14
13
|
Requires-Dist: xmltodict
|
|
15
14
|
Requires-Dist: opencv-python
|
|
16
|
-
Requires-Dist:
|
|
15
|
+
Requires-Dist: scipy
|
|
16
|
+
Requires-Dist: nibabel
|
|
17
17
|
Dynamic: description
|
|
18
18
|
Dynamic: description-content-type
|
|
19
19
|
Dynamic: home-page
|
|
@@ -25,7 +25,12 @@ Dynamic: summary
|
|
|
25
25
|
# PyNutil
|
|
26
26
|
PyNutil is under development.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
**Warning:** There are known errors in the PyNutil reports (incorrect results). Please use Nutil until these are fixed.
|
|
29
|
+
|
|
30
|
+
PyNutil is a Python library for brain-wide quantification and spatial analysis of features in serial section images from the brain. It aims to replicate the Quantifier feature of the Nutil software (RRID: SCR_017183).
|
|
31
|
+
|
|
32
|
+
PyNutil is able to integrate outputs from atlas registration software and image segmentation software in order to produce atlas based quantifications, 3D point clouds, and 3D heatmaps of brain derived data.
|
|
33
|
+

|
|
29
34
|
|
|
30
35
|
For more information about the QUINT workflow:
|
|
31
36
|
https://quint-workflow.readthedocs.io/en/latest/
|
|
@@ -34,17 +39,26 @@ https://quint-workflow.readthedocs.io/en/latest/
|
|
|
34
39
|
|
|
35
40
|
PyNutil can be run using a custom atlas in .nrrd format (e.g. tests/test_data/Allen_mouse_2017_atlas)
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
PyNutil can also be used with the atlases available in the [BrainGlobe_Atlas API](https://GitHub.com/brainglobe/brainglobe-atlasapi).
|
|
38
43
|
|
|
39
44
|
# Installation
|
|
40
45
|
## Python package
|
|
41
46
|
```
|
|
42
|
-
pip install PyNutil
|
|
47
|
+
pip install PyNutil
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Running demos
|
|
51
|
+
|
|
52
|
+
The scripts in `demos/` assume PyNutil is importable as an installed package.
|
|
53
|
+
For development, install in editable mode from the repository root:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install -e .
|
|
43
57
|
```
|
|
44
58
|
## GUI
|
|
45
|
-
download the executable for
|
|
59
|
+
download the executable for Windows and macOS via the [GitHub releases tab](https://GitHub.com/Neural-Systems-at-UIO/PyNutil/releases)
|
|
46
60
|
|
|
47
|
-

|
|
48
62
|
|
|
49
63
|
# Usage
|
|
50
64
|
|
|
@@ -74,8 +88,13 @@ pnt = PyNutil(
|
|
|
74
88
|
alignment_json='../tests/test_data/non_linear_allen_mouse/alignment.json',
|
|
75
89
|
colour=[0, 0, 0],
|
|
76
90
|
atlas_name='allen_mouse_25um'
|
|
91
|
+
#If you would like to use cellpose segmentations add:
|
|
92
|
+
#cellpose = True
|
|
77
93
|
)
|
|
78
94
|
|
|
95
|
+
#optionally, if you want to generate a 3D heatmap
|
|
96
|
+
pnt.interpolate_volume()
|
|
97
|
+
|
|
79
98
|
pnt.get_coordinates(object_cutoff=0)
|
|
80
99
|
|
|
81
100
|
pnt.quantify_coordinates()
|
|
@@ -85,23 +104,32 @@ pnt.save_analysis("PyNutil/test_result/myResults")
|
|
|
85
104
|
PyNutil generates a series of reports in the folder which you specify.
|
|
86
105
|
|
|
87
106
|
## Per-Hemisphere Quantification
|
|
88
|
-
If you use an atlas which has a hemisphere map (All brainglobe atlases have this, it is a volume in the shape of the atlas with 1 in the
|
|
107
|
+
If you use an atlas which has a hemisphere map (All brainglobe atlases have this, it is a volume in the shape of the atlas with 1 in the left hemisphere and 2 in the right) PyNutil will generate per-hemisphere quantifications in addition to total numbers. In addition, PyNutil will also genearte additional per-hemisphere point cloud files for viewing in meshview.
|
|
89
108
|
## Damage Quantification
|
|
90
|
-
[The QCAlign tool](https://www.nitrc.org/projects/qcalign) allows you to mark damaged areas on your section. This means that these damaged areas are excluded from your point clouds. In addition, PyNutil will
|
|
109
|
+
[The QCAlign tool](https://www.nitrc.org/projects/qcalign) allows you to mark damaged areas on your section. This means that these damaged areas are excluded from your point clouds. In addition, PyNutil will separately quantify damaged and undamaged areas. Note the undamaged and damaged column names.
|
|
91
110
|
# Meshview json files
|
|
92
|
-
PyNutil will produce meshview json files.
|
|
111
|
+
PyNutil will produce meshview json files. These can be opened in [MeshView for the Allen Mouse](https://meshview.apps.ebrains.eu/?atlas=ABA_Mouse_CCFv3_2017_25um) or for [the Waxholm Rat](https://meshview.apps.ebrains.eu/)
|
|
93
112
|
|
|
94
113
|
https://github.com/user-attachments/assets/d3a43ca9-133e-40d1-a1b9-9a359deabf2d
|
|
95
114
|
|
|
115
|
+
|
|
116
|
+
# Siibra explorer compatible NifTI files
|
|
117
|
+
If you have interpolated your volume you will find an interpolated NifTI volume in your output directory. This can be dragged and dropped directly into [siibra explorer](https://atlases.ebrains.eu/viewer/#/). If you share your data you can also include a shareable link to your data in the Siibra viewer. These files are also viewable in [ITK-SNAP](https://github.com/pyushkevich/itksnap).
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
https://github.com/user-attachments/assets/30ca7f7f-92f5-4d83-a92a-b29603181b8f
|
|
122
|
+
|
|
123
|
+
|
|
96
124
|
# Interpreting the Results
|
|
97
125
|
Each column name has the following definition
|
|
98
126
|
| Column | Definition |
|
|
99
|
-
|
|
127
|
+
|---------------|-------------------------------------------------------------------------------------|
|
|
100
128
|
| idx | The atlas ID of the region. |
|
|
101
129
|
| name | The name of atlas region. |
|
|
102
|
-
| r | The amount of red in the RGB value for
|
|
103
|
-
| g | The amount of green in the RGB value for
|
|
104
|
-
| b | The amount of blue in the RGB value for
|
|
130
|
+
| r | The amount of red in the RGB value for the region colour. |
|
|
131
|
+
| g | The amount of green in the RGB value for the region colour. |
|
|
132
|
+
| b | The amount of blue in the RGB value for the region colour. |
|
|
105
133
|
| Region area | Area representing the region on the segmentation in pixel values. |
|
|
106
134
|
| Object count | Number of objects located in the region. An object is a disconnected group of pixels|
|
|
107
135
|
| Object pixels | Number of pixels representing objects in this region. |
|
|
@@ -111,9 +139,13 @@ Each column name has the following definition
|
|
|
111
139
|
| Right hemi | For each of the other columns, what is that value for the right hemisphere alone |
|
|
112
140
|
| Damaged | For each of the other columns, what is that value for the areas marked damaged alone|
|
|
113
141
|
| Undamaged | For each of the other columns, what is that value for the areas marked undamaged alone|
|
|
114
|
-
|
|
142
|
+
If you choose to measure the intensity of images rather than segmentations you will not get object counts. Instead you will get
|
|
143
|
+
| Column | Definition |
|
|
144
|
+
|---------------|-------------------------------------------------------------------------------------|
|
|
145
|
+
| Sum intensity | The sum of all image pixels in a region. |
|
|
146
|
+
| Mean inensity | The mean of all image pixels in a region. |
|
|
115
147
|
# Feature Requests
|
|
116
|
-
We are open to feature requests 😊 Simply open an issue in the
|
|
148
|
+
We are open to feature requests 😊 Simply open an issue in the GitHub describing the feature you would like to see.
|
|
117
149
|
|
|
118
150
|
# Acknowledgements
|
|
119
151
|
PyNutil is developed at the Neural Systems Laboratory at the Institute of Basic Medical Sciences, University of Oslo, Norway with support from the EBRAINS infrastructure, and funding support from the European Union’s Horizon 2020 Framework Programme for Research and Innovation under the Framework Partnership Agreement No. 650003 (HBP FPA).
|
|
@@ -136,4 +168,4 @@ Carey H, Pegios M, Martin L, Saleeba C, Turner A, Everett N, Puchades M, Bjaalie
|
|
|
136
168
|
Berg S., Kutra D., Kroeger T., Straehle C.N., Kausler B.X., Haubold C., et al. (2019) ilastik:interactive machine learning for (bio) image analysis. Nat Methods. 16, 1226–1232. https://doi.org/10.1038/s41592-019-0582-9
|
|
137
169
|
|
|
138
170
|
# Contact us
|
|
139
|
-
Report issues here on
|
|
171
|
+
Report issues here on GitHub or email: support@ebrains.eu
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _unwrap_singleton_list(value: Any) -> Any:
|
|
9
|
+
"""Unwrap legacy settings fields like [null] -> None, ["x"] -> "x"."""
|
|
10
|
+
if isinstance(value, list) and len(value) == 1:
|
|
11
|
+
return value[0]
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PyNutilConfig:
|
|
17
|
+
segmentation_folder: Optional[str] = None
|
|
18
|
+
image_folder: Optional[str] = None
|
|
19
|
+
alignment_json: Optional[str] = None
|
|
20
|
+
colour: Optional[list] = None
|
|
21
|
+
intensity_channel: Optional[str] = None
|
|
22
|
+
atlas_name: Optional[str] = None
|
|
23
|
+
atlas_path: Optional[str] = None
|
|
24
|
+
label_path: Optional[str] = None
|
|
25
|
+
hemi_path: Optional[str] = None
|
|
26
|
+
custom_region_path: Optional[str] = None
|
|
27
|
+
voxel_size_um: Optional[float] = None
|
|
28
|
+
min_intensity: Optional[int] = None
|
|
29
|
+
max_intensity: Optional[int] = None
|
|
30
|
+
cellpose: bool = False
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def from_settings_file(cls, settings_file: str) -> "PyNutilConfig":
|
|
34
|
+
with open(settings_file, "r") as f:
|
|
35
|
+
settings = json.load(f)
|
|
36
|
+
return cls.from_settings_dict(settings)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_settings_dict(cls, settings: Dict[str, Any]) -> "PyNutilConfig":
|
|
40
|
+
# Be tolerant of legacy settings saved as singleton lists.
|
|
41
|
+
def g(key: str, default: Any = None) -> Any:
|
|
42
|
+
return _unwrap_singleton_list(settings.get(key, default))
|
|
43
|
+
|
|
44
|
+
# alignment_json is required in settings files.
|
|
45
|
+
if "alignment_json" not in settings:
|
|
46
|
+
raise KeyError(
|
|
47
|
+
"Settings file must contain alignment_json, and either atlas_path and label_path or atlas_name. It should also contain either segmentation_folder or image_folder."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
cfg = cls(
|
|
51
|
+
segmentation_folder=g("segmentation_folder"),
|
|
52
|
+
image_folder=g("image_folder"),
|
|
53
|
+
alignment_json=settings["alignment_json"],
|
|
54
|
+
colour=g("colour"),
|
|
55
|
+
intensity_channel=g("intensity_channel"),
|
|
56
|
+
atlas_name=g("atlas_name"),
|
|
57
|
+
atlas_path=g("atlas_path"),
|
|
58
|
+
label_path=g("label_path"),
|
|
59
|
+
hemi_path=g("hemi_path"),
|
|
60
|
+
custom_region_path=g("custom_region_path"),
|
|
61
|
+
voxel_size_um=g("voxel_size_um"),
|
|
62
|
+
min_intensity=g("min_intensity"),
|
|
63
|
+
max_intensity=g("max_intensity"),
|
|
64
|
+
cellpose=bool(g("cellpose", False)),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# If atlas_path/label_path are present but empty/null, treat as not provided.
|
|
68
|
+
if not cfg.atlas_path or not cfg.label_path:
|
|
69
|
+
cfg.atlas_path = None
|
|
70
|
+
cfg.label_path = None
|
|
71
|
+
cfg.hemi_path = None
|
|
72
|
+
|
|
73
|
+
return cfg
|
|
74
|
+
|
|
75
|
+
def normalize(self, *, logger=None) -> "PyNutilConfig":
|
|
76
|
+
# If atlas_name is provided, voxel size is inferred from atlas name and
|
|
77
|
+
# any manually provided voxel_size_um is ignored (existing behavior).
|
|
78
|
+
if self.atlas_name is not None and self.voxel_size_um is not None:
|
|
79
|
+
if logger is not None:
|
|
80
|
+
logger.warning(
|
|
81
|
+
f"Voxel size ({self.voxel_size_um} um) was specified but will be ignored because atlas_name ({self.atlas_name}) is provided. Voxel size will be inferred from the atlas name."
|
|
82
|
+
)
|
|
83
|
+
self.voxel_size_um = None
|
|
84
|
+
return self
|
|
85
|
+
|
|
86
|
+
def validate(self) -> None:
|
|
87
|
+
if self.segmentation_folder and self.image_folder:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
"Please specify either segmentation_folder or image_folder, not both."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if self.segmentation_folder and (
|
|
93
|
+
self.min_intensity is not None or self.max_intensity is not None
|
|
94
|
+
):
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"min_intensity and max_intensity are only supported when using image_folder, not segmentation_folder."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if self.image_folder and (self.colour is not None):
|
|
100
|
+
raise ValueError(
|
|
101
|
+
"You can't specify both colour and image_folder since there are no segmentations"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (self.atlas_path or self.label_path) and self.atlas_name:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"Please specify either atlas_path and label_path or atlas_name. Atlas and label paths are only used for loading custom atlases."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if not (self.atlas_path and self.label_path) and not self.atlas_name:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"When atlas_path and label_path are not specified, atlas_name must be specified."
|
|
112
|
+
)
|
|
@@ -2,6 +2,8 @@ import brainglobe_atlasapi
|
|
|
2
2
|
import pandas as pd
|
|
3
3
|
import numpy as np
|
|
4
4
|
import nrrd
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def load_atlas_labels(atlas=None, atlas_name=None):
|
|
7
9
|
if atlas_name:
|
|
@@ -23,6 +25,8 @@ def load_atlas_labels(atlas=None, atlas_name=None):
|
|
|
23
25
|
atlas_labels = pd.DataFrame(atlas_structures)
|
|
24
26
|
return atlas_labels
|
|
25
27
|
|
|
28
|
+
|
|
29
|
+
@lru_cache(maxsize=8)
|
|
26
30
|
def load_atlas_data(atlas_name):
|
|
27
31
|
"""
|
|
28
32
|
Loads atlas data using the brainglobe_atlasapi.
|
|
@@ -66,32 +70,18 @@ def process_atlas_volume(vol):
|
|
|
66
70
|
return np.transpose(vol, [2, 0, 1])[::-1, ::-1, ::-1]
|
|
67
71
|
|
|
68
72
|
|
|
73
|
+
@lru_cache(maxsize=8)
|
|
69
74
|
def load_custom_atlas(atlas_path, hemi_path, label_path):
|
|
70
75
|
"""
|
|
71
76
|
Loads a custom atlas from provided file paths.
|
|
72
|
-
|
|
73
|
-
Parameters
|
|
74
|
-
----------
|
|
75
|
-
atlas_path : str
|
|
76
|
-
Path to the custom atlas volume file.
|
|
77
|
-
hemi_path : str or None
|
|
78
|
-
Path to the hemisphere file, if any.
|
|
79
|
-
label_path : str
|
|
80
|
-
Path to the label CSV file for region info.
|
|
81
|
-
|
|
82
|
-
Returns
|
|
83
|
-
-------
|
|
84
|
-
numpy.ndarray
|
|
85
|
-
The loaded atlas volume.
|
|
86
|
-
numpy.ndarray or None
|
|
87
|
-
The hemisphere array, or None if hemi_path is not provided.
|
|
88
|
-
pandas.DataFrame
|
|
89
|
-
A dataframe containing atlas labels.
|
|
90
77
|
"""
|
|
91
78
|
atlas_volume, _ = nrrd.read(atlas_path)
|
|
79
|
+
|
|
92
80
|
if hemi_path:
|
|
93
81
|
hemi_volume, _ = nrrd.read(hemi_path)
|
|
94
82
|
else:
|
|
95
83
|
hemi_volume = None
|
|
84
|
+
|
|
96
85
|
atlas_labels = pd.read_csv(label_path)
|
|
86
|
+
|
|
97
87
|
return atlas_volume, hemi_volume, atlas_labels
|
|
@@ -3,6 +3,17 @@ import json
|
|
|
3
3
|
from .read_and_write import write_hemi_points_to_meshview
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
def _ensure_analysis_output_dirs(output_folder: str) -> None:
|
|
7
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
8
|
+
for subdir in (
|
|
9
|
+
"whole_series_report",
|
|
10
|
+
"per_section_meshview",
|
|
11
|
+
"per_section_reports",
|
|
12
|
+
"whole_series_meshview",
|
|
13
|
+
):
|
|
14
|
+
os.makedirs(f"{output_folder}/{subdir}", exist_ok=True)
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
def save_analysis_output(
|
|
7
18
|
pixel_points,
|
|
8
19
|
centroids,
|
|
@@ -18,14 +29,18 @@ def save_analysis_output(
|
|
|
18
29
|
atlas_labels,
|
|
19
30
|
output_folder,
|
|
20
31
|
segmentation_folder=None,
|
|
32
|
+
image_folder=None,
|
|
21
33
|
alignment_json=None,
|
|
22
34
|
colour=None,
|
|
35
|
+
intensity_channel=None,
|
|
23
36
|
atlas_name=None,
|
|
24
37
|
custom_region_path=None,
|
|
25
38
|
atlas_path=None,
|
|
26
39
|
label_path=None,
|
|
27
40
|
settings_file=None,
|
|
28
41
|
prepend=None,
|
|
42
|
+
point_intensities=None,
|
|
43
|
+
**kwargs,
|
|
29
44
|
):
|
|
30
45
|
"""
|
|
31
46
|
Save the analysis output to the specified folder.
|
|
@@ -37,10 +52,14 @@ def save_analysis_output(
|
|
|
37
52
|
The folder where the output will be saved.
|
|
38
53
|
segmentation_folder : str, optional
|
|
39
54
|
The folder containing the segmentation files (default is None).
|
|
55
|
+
image_folder : str, optional
|
|
56
|
+
The folder containing the original images (default is None).
|
|
40
57
|
alignment_json : str, optional
|
|
41
58
|
The path to the alignment JSON file (default is None).
|
|
42
59
|
colour : list, optional
|
|
43
60
|
The RGB colour of the object to be quantified in the segmentation (default is None).
|
|
61
|
+
intensity_channel : str, optional
|
|
62
|
+
The channel used for intensity quantification (default is None).
|
|
44
63
|
atlas_name : str, optional
|
|
45
64
|
The name of the atlas in the brainglobe api to be used for quantification (default is None).
|
|
46
65
|
atlas_path : str, optional
|
|
@@ -51,83 +70,67 @@ def save_analysis_output(
|
|
|
51
70
|
The path to the settings file that was used (default is None).
|
|
52
71
|
"""
|
|
53
72
|
# Create the output folder if it doesn't exist
|
|
54
|
-
|
|
55
|
-
os.makedirs(f"{output_folder}/whole_series_report", exist_ok=True)
|
|
56
|
-
os.makedirs(f"{output_folder}/per_section_meshview", exist_ok=True)
|
|
57
|
-
os.makedirs(f"{output_folder}/per_section_reports", exist_ok=True)
|
|
58
|
-
os.makedirs(f"{output_folder}/whole_series_meshview", exist_ok=True)
|
|
73
|
+
_ensure_analysis_output_dirs(output_folder)
|
|
59
74
|
# Filter out rows where 'region_area' is 0 in label_df
|
|
60
75
|
# if label_df is not None and "region_area" in label_df.columns:
|
|
61
76
|
# label_df = label_df[label_df["region_area"] != 0]
|
|
62
77
|
if label_df is not None:
|
|
78
|
+
report_name = "intensity.csv" if image_folder else "counts.csv"
|
|
63
79
|
label_df.to_csv(
|
|
64
|
-
f"{output_folder}/whole_series_report/{prepend}
|
|
80
|
+
f"{output_folder}/whole_series_report/{prepend}{report_name}",
|
|
65
81
|
sep=";",
|
|
66
82
|
na_rep="",
|
|
67
83
|
index=False,
|
|
68
84
|
)
|
|
69
|
-
|
|
85
|
+
elif not prepend:
|
|
70
86
|
print("No quantification found, so only coordinates will be saved.")
|
|
71
87
|
print(
|
|
72
88
|
"If you want to save the quantification, please run quantify_coordinates."
|
|
73
89
|
)
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
if per_section_df is not None and segmentation_filenames is not None:
|
|
92
|
+
_save_per_section_reports(
|
|
93
|
+
per_section_df,
|
|
94
|
+
segmentation_filenames,
|
|
95
|
+
points_len,
|
|
96
|
+
centroids_len,
|
|
97
|
+
pixel_points,
|
|
98
|
+
centroids,
|
|
99
|
+
labeled_points,
|
|
100
|
+
labeled_points_centroids,
|
|
101
|
+
points_hemi_labels,
|
|
102
|
+
centroids_hemi_labels,
|
|
103
|
+
atlas_labels,
|
|
104
|
+
output_folder,
|
|
105
|
+
prepend,
|
|
106
|
+
point_intensities,
|
|
107
|
+
colormap=kwargs.get("colormap", "gray"),
|
|
108
|
+
)
|
|
109
|
+
if pixel_points is not None:
|
|
110
|
+
_save_whole_series_meshview(
|
|
111
|
+
pixel_points,
|
|
112
|
+
labeled_points,
|
|
113
|
+
centroids,
|
|
114
|
+
labeled_points_centroids,
|
|
115
|
+
points_hemi_labels,
|
|
116
|
+
centroids_hemi_labels,
|
|
117
|
+
atlas_labels,
|
|
118
|
+
output_folder,
|
|
119
|
+
prepend,
|
|
120
|
+
point_intensities,
|
|
121
|
+
colormap=kwargs.get("colormap", "gray"),
|
|
122
|
+
)
|
|
101
123
|
|
|
102
124
|
# Save settings to JSON file for reference
|
|
103
125
|
settings_dict = {
|
|
104
126
|
"segmentation_folder": segmentation_folder,
|
|
127
|
+
"image_folder": image_folder,
|
|
105
128
|
"alignment_json": alignment_json,
|
|
106
129
|
"colour": colour,
|
|
130
|
+
"intensity_channel": intensity_channel,
|
|
107
131
|
"custom_region_path": custom_region_path,
|
|
108
132
|
}
|
|
109
|
-
|
|
110
|
-
centroids,
|
|
111
|
-
label_df,
|
|
112
|
-
per_section_df,
|
|
113
|
-
labeled_points,
|
|
114
|
-
labeled_points_centroids,
|
|
115
|
-
points_hemi_labels,
|
|
116
|
-
centroids_hemi_labels,
|
|
117
|
-
points_len,
|
|
118
|
-
centroids_len,
|
|
119
|
-
segmentation_filenames,
|
|
120
|
-
atlas_labels,
|
|
121
|
-
output_folder,
|
|
122
|
-
segmentation_folder = (None,)
|
|
123
|
-
alignment_json = (None,)
|
|
124
|
-
colour = (None,)
|
|
125
|
-
atlas_name = (None,)
|
|
126
|
-
custom_region_path = (None,)
|
|
127
|
-
atlas_path = (None,)
|
|
128
|
-
label_path = (None,)
|
|
129
|
-
settings_file = (None,)
|
|
130
|
-
prepend = (None,)
|
|
133
|
+
|
|
131
134
|
# Add atlas information to settings
|
|
132
135
|
if atlas_name:
|
|
133
136
|
settings_dict["atlas_name"] = atlas_name
|
|
@@ -137,8 +140,7 @@ def save_analysis_output(
|
|
|
137
140
|
settings_dict["label_path"] = label_path
|
|
138
141
|
if settings_file:
|
|
139
142
|
settings_dict["settings_file"] = settings_file
|
|
140
|
-
|
|
141
|
-
settings_dict["custom_region_path"] = custom_region_path
|
|
143
|
+
|
|
142
144
|
# Write settings to file
|
|
143
145
|
settings_file_path = os.path.join(output_folder, "pynutil_settings.json")
|
|
144
146
|
with open(settings_file_path, "w") as f:
|
|
@@ -159,10 +161,18 @@ def _save_per_section_reports(
|
|
|
159
161
|
atlas_labels,
|
|
160
162
|
output_folder,
|
|
161
163
|
prepend,
|
|
164
|
+
point_intensities=None,
|
|
165
|
+
colormap="gray",
|
|
162
166
|
):
|
|
163
167
|
prev_pl = 0
|
|
164
168
|
prev_cl = 0
|
|
165
169
|
|
|
170
|
+
# Handle None for points_len and centroids_len (e.g. in intensity mode)
|
|
171
|
+
if points_len is None:
|
|
172
|
+
points_len = [0] * len(segmentation_filenames)
|
|
173
|
+
if centroids_len is None:
|
|
174
|
+
centroids_len = [0] * len(segmentation_filenames)
|
|
175
|
+
|
|
166
176
|
for pl, cl, fn, df in zip(
|
|
167
177
|
points_len,
|
|
168
178
|
centroids_len,
|
|
@@ -176,22 +186,27 @@ def _save_per_section_reports(
|
|
|
176
186
|
na_rep="",
|
|
177
187
|
index=False,
|
|
178
188
|
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
if pixel_points is not None or centroids is not None:
|
|
190
|
+
_save_per_section_meshview(
|
|
191
|
+
split_fn,
|
|
192
|
+
pl,
|
|
193
|
+
cl,
|
|
194
|
+
prev_pl,
|
|
195
|
+
prev_cl,
|
|
196
|
+
pixel_points,
|
|
197
|
+
centroids,
|
|
198
|
+
labeled_points,
|
|
199
|
+
labeled_points_centroids,
|
|
200
|
+
points_hemi_labels,
|
|
201
|
+
centroids_hemi_labels,
|
|
202
|
+
atlas_labels,
|
|
203
|
+
output_folder,
|
|
204
|
+
prepend,
|
|
205
|
+
point_intensities[prev_pl : pl + prev_pl]
|
|
206
|
+
if point_intensities is not None
|
|
207
|
+
else None,
|
|
208
|
+
colormap=colormap,
|
|
209
|
+
)
|
|
195
210
|
prev_cl += cl
|
|
196
211
|
prev_pl += pl
|
|
197
212
|
|
|
@@ -211,21 +226,29 @@ def _save_per_section_meshview(
|
|
|
211
226
|
atlas_labels,
|
|
212
227
|
output_folder,
|
|
213
228
|
prepend,
|
|
229
|
+
point_intensities=None,
|
|
230
|
+
colormap="gray",
|
|
214
231
|
):
|
|
215
232
|
write_hemi_points_to_meshview(
|
|
216
|
-
pixel_points[prev_pl : pl + prev_pl],
|
|
217
|
-
labeled_points[prev_pl : pl + prev_pl],
|
|
218
|
-
points_hemi_labels[prev_pl : pl + prev_pl]
|
|
233
|
+
pixel_points[prev_pl : pl + prev_pl] if pixel_points is not None else None,
|
|
234
|
+
labeled_points[prev_pl : pl + prev_pl] if labeled_points is not None else None,
|
|
235
|
+
points_hemi_labels[prev_pl : pl + prev_pl]
|
|
236
|
+
if points_hemi_labels is not None
|
|
237
|
+
else None,
|
|
219
238
|
f"{output_folder}/per_section_meshview/{prepend}{split_fn}_pixels.json",
|
|
220
239
|
atlas_labels,
|
|
240
|
+
point_intensities,
|
|
241
|
+
colormap=colormap,
|
|
221
242
|
)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
243
|
+
if centroids is not None:
|
|
244
|
+
write_hemi_points_to_meshview(
|
|
245
|
+
centroids[prev_cl : cl + prev_cl],
|
|
246
|
+
labeled_points_centroids[prev_cl : cl + prev_cl],
|
|
247
|
+
centroids_hemi_labels[prev_cl : cl + prev_cl],
|
|
248
|
+
f"{output_folder}/per_section_meshview/{prepend}{split_fn}_centroids.json",
|
|
249
|
+
atlas_labels,
|
|
250
|
+
colormap=colormap,
|
|
251
|
+
)
|
|
229
252
|
|
|
230
253
|
|
|
231
254
|
def _save_whole_series_meshview(
|
|
@@ -238,6 +261,8 @@ def _save_whole_series_meshview(
|
|
|
238
261
|
atlas_labels,
|
|
239
262
|
output_folder,
|
|
240
263
|
prepend,
|
|
264
|
+
point_intensities=None,
|
|
265
|
+
colormap="gray",
|
|
241
266
|
):
|
|
242
267
|
write_hemi_points_to_meshview(
|
|
243
268
|
pixel_points,
|
|
@@ -245,11 +270,15 @@ def _save_whole_series_meshview(
|
|
|
245
270
|
points_hemi_labels,
|
|
246
271
|
f"{output_folder}/whole_series_meshview/{prepend}pixels_meshview.json",
|
|
247
272
|
atlas_labels,
|
|
273
|
+
point_intensities,
|
|
274
|
+
colormap=colormap,
|
|
248
275
|
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
276
|
+
if centroids is not None:
|
|
277
|
+
write_hemi_points_to_meshview(
|
|
278
|
+
centroids,
|
|
279
|
+
labeled_points_centroids,
|
|
280
|
+
centroids_hemi_labels,
|
|
281
|
+
f"{output_folder}/whole_series_meshview/{prepend}objects_meshview.json",
|
|
282
|
+
atlas_labels,
|
|
283
|
+
colormap=colormap,
|
|
284
|
+
)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def write_nifti(
|
|
10
|
+
volume: np.ndarray,
|
|
11
|
+
voxel_size_um: float,
|
|
12
|
+
output_path: str,
|
|
13
|
+
origin_offsets_um: Optional[np.ndarray] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Write a NIfTI volume with a microns-based affine.
|
|
16
|
+
|
|
17
|
+
The header is written with both qform and sform set and units set to microns.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
volume: 3D volume array. Saved as uint8.
|
|
21
|
+
voxel_size_um: Isotropic voxel size in microns.
|
|
22
|
+
output_path: Output path without extension; ".nii.gz" is appended.
|
|
23
|
+
origin_offsets_um: Optional XYZ translation offsets in microns.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import nibabel as nib # type: ignore
|
|
28
|
+
except Exception as exc: # pragma: no cover
|
|
29
|
+
raise ImportError("nibabel is required for write_nifti") from exc
|
|
30
|
+
|
|
31
|
+
if origin_offsets_um is None:
|
|
32
|
+
origin_offsets_um = np.array([0, 0, 0], dtype=np.float32)
|
|
33
|
+
|
|
34
|
+
dims = np.array(volume.shape, dtype=np.float32)
|
|
35
|
+
affine = np.eye(4, dtype=np.float32)
|
|
36
|
+
affine[:3, :3] *= float(voxel_size_um)
|
|
37
|
+
affine[:3, 3] = -0.5 * dims * float(voxel_size_um) + np.asarray(
|
|
38
|
+
origin_offsets_um, dtype=np.float32
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
img = nib.Nifti1Image(np.asarray(volume, dtype=np.uint8), affine)
|
|
42
|
+
img.set_qform(affine, code=1)
|
|
43
|
+
img.set_sform(affine, code=1)
|
|
44
|
+
img.header["xyzt_units"] = 3
|
|
45
|
+
|
|
46
|
+
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
|
47
|
+
nib.save(img, output_path + ".nii.gz")
|