PyNutil 0.3.3__tar.gz → 0.4.2__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.3.3 → pynutil-0.4.2}/PKG-INFO +5 -4
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/config.py +19 -25
- pynutil-0.4.2/PyNutil/context.py +84 -0
- pynutil-0.4.2/PyNutil/io/__init__.py +63 -0
- pynutil-0.4.2/PyNutil/io/colormap.py +94 -0
- pynutil-0.4.2/PyNutil/io/file_operations.py +221 -0
- pynutil-0.4.2/PyNutil/io/loaders.py +323 -0
- pynutil-0.4.2/PyNutil/io/meshview_writer.py +316 -0
- pynutil-0.4.2/PyNutil/io/propagation.py +133 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/io/section_visualisation.py +95 -67
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/io/volume_nifti.py +0 -4
- pynutil-0.4.2/PyNutil/logging_utils.py +82 -0
- pynutil-0.4.2/PyNutil/main.py +616 -0
- pynutil-0.4.2/PyNutil/processing/__init__.py +96 -0
- pynutil-0.4.2/PyNutil/processing/adapters/__init__.py +105 -0
- pynutil-0.4.2/PyNutil/processing/adapters/anchoring.py +63 -0
- pynutil-0.4.2/PyNutil/processing/adapters/base.py +161 -0
- pynutil-0.4.2/PyNutil/processing/adapters/damage.py +146 -0
- pynutil-0.4.2/PyNutil/processing/adapters/deformation.py +184 -0
- pynutil-0.4.2/PyNutil/processing/adapters/registry.py +127 -0
- pynutil-0.4.2/PyNutil/processing/adapters/segmentation.py +223 -0
- {pynutil-0.3.3/PyNutil/processing → pynutil-0.4.2/PyNutil/processing/adapters}/visualign_deformations.py +67 -144
- pynutil-0.4.2/PyNutil/processing/analysis/__init__.py +21 -0
- {pynutil-0.3.3/PyNutil/processing → pynutil-0.4.2/PyNutil/processing/analysis}/aggregator.py +3 -1
- pynutil-0.4.2/PyNutil/processing/analysis/data_analysis.py +337 -0
- pynutil-0.4.2/PyNutil/processing/analysis/region_counting.py +242 -0
- pynutil-0.4.2/PyNutil/processing/atlas_map.py +376 -0
- pynutil-0.4.2/PyNutil/processing/pipeline/__init__.py +17 -0
- pynutil-0.4.2/PyNutil/processing/pipeline/batch_processor.py +420 -0
- pynutil-0.4.2/PyNutil/processing/pipeline/connected_components.py +224 -0
- pynutil-0.4.2/PyNutil/processing/pipeline/section_processor.py +508 -0
- pynutil-0.4.2/PyNutil/processing/section_volume.py +462 -0
- pynutil-0.4.2/PyNutil/processing/transforms.py +221 -0
- pynutil-0.4.2/PyNutil/processing/utils.py +235 -0
- pynutil-0.4.2/PyNutil/results.py +127 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil.egg-info/PKG-INFO +5 -4
- pynutil-0.4.2/PyNutil.egg-info/SOURCES.txt +58 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil.egg-info/requires.txt +1 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/README.md +3 -3
- {pynutil-0.3.3 → pynutil-0.4.2}/setup.py +1 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_coordinate_scaling.py +49 -52
- pynutil-0.4.2/tests/test_meshview_regression.py +161 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_transformations.py +1 -1
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_visualisations.py +1 -1
- pynutil-0.3.3/PyNutil/io/__init__.py +0 -1
- pynutil-0.3.3/PyNutil/io/file_operations.py +0 -284
- pynutil-0.3.3/PyNutil/io/propagation.py +0 -123
- pynutil-0.3.3/PyNutil/io/read_and_write.py +0 -498
- pynutil-0.3.3/PyNutil/main.py +0 -603
- pynutil-0.3.3/PyNutil/processing/__init__.py +0 -1
- pynutil-0.3.3/PyNutil/processing/coordinate_extraction.py +0 -1097
- pynutil-0.3.3/PyNutil/processing/counting_and_load.py +0 -564
- pynutil-0.3.3/PyNutil/processing/data_analysis.py +0 -426
- pynutil-0.3.3/PyNutil/processing/generate_target_slice.py +0 -53
- pynutil-0.3.3/PyNutil/processing/image_loaders.py +0 -23
- pynutil-0.3.3/PyNutil/processing/section_volume.py +0 -373
- pynutil-0.3.3/PyNutil/processing/transform.py +0 -53
- pynutil-0.3.3/PyNutil/processing/transformations.py +0 -146
- pynutil-0.3.3/PyNutil/processing/utils.py +0 -356
- pynutil-0.3.3/PyNutil.egg-info/SOURCES.txt +0 -43
- {pynutil-0.3.3 → pynutil-0.4.2}/LICENSE +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/__init__.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/io/atlas_loader.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/io/nifti_writer.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil/io/reconstruct_dzi.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil.egg-info/dependency_links.txt +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/PyNutil.egg-info/top_level.txt +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/setup.cfg +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_build_volume_from_sections.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_cellpose_quantification.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_damage_volume_interpolation.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_helpers.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_intensity_quantification.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_interpolate_volume_value_modes.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_quantification.py +0 -0
- {pynutil-0.3.3 → pynutil-0.4.2}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNutil
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
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
|
|
@@ -14,6 +14,7 @@ Requires-Dist: xmltodict
|
|
|
14
14
|
Requires-Dist: opencv-python
|
|
15
15
|
Requires-Dist: scipy
|
|
16
16
|
Requires-Dist: nibabel
|
|
17
|
+
Requires-Dist: orjson
|
|
17
18
|
Dynamic: description
|
|
18
19
|
Dynamic: description-content-type
|
|
19
20
|
Dynamic: home-page
|
|
@@ -23,9 +24,8 @@ Dynamic: requires-dist
|
|
|
23
24
|
Dynamic: summary
|
|
24
25
|
|
|
25
26
|
# PyNutil
|
|
26
|
-
PyNutil is under development.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
## PyNutil is still under development and the API is subject to change
|
|
29
29
|
|
|
30
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
31
|
|
|
@@ -89,7 +89,7 @@ pnt = PyNutil(
|
|
|
89
89
|
colour=[0, 0, 0],
|
|
90
90
|
atlas_name='allen_mouse_25um'
|
|
91
91
|
#If you would like to use cellpose segmentations add:
|
|
92
|
-
#
|
|
92
|
+
#segmentation_format = 'cellpose'
|
|
93
93
|
)
|
|
94
94
|
|
|
95
95
|
#optionally, if you want to generate a 3D heatmap
|
|
@@ -139,6 +139,7 @@ Each column name has the following definition
|
|
|
139
139
|
| Right hemi | For each of the other columns, what is that value for the right hemisphere alone |
|
|
140
140
|
| Damaged | For each of the other columns, what is that value for the areas marked damaged alone|
|
|
141
141
|
| Undamaged | For each of the other columns, what is that value for the areas marked undamaged alone|
|
|
142
|
+
|
|
142
143
|
If you choose to measure the intensity of images rather than segmentations you will not get object counts. Instead you will get
|
|
143
144
|
| Column | Definition |
|
|
144
145
|
|---------------|-------------------------------------------------------------------------------------|
|
|
@@ -5,13 +5,6 @@ from dataclasses import dataclass
|
|
|
5
5
|
from typing import Any, Dict, Optional
|
|
6
6
|
|
|
7
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
8
|
@dataclass
|
|
16
9
|
class PyNutilConfig:
|
|
17
10
|
segmentation_folder: Optional[str] = None
|
|
@@ -27,7 +20,7 @@ class PyNutilConfig:
|
|
|
27
20
|
voxel_size_um: Optional[float] = None
|
|
28
21
|
min_intensity: Optional[int] = None
|
|
29
22
|
max_intensity: Optional[int] = None
|
|
30
|
-
|
|
23
|
+
segmentation_format: str = "binary" # "binary" or "cellpose"
|
|
31
24
|
|
|
32
25
|
@classmethod
|
|
33
26
|
def from_settings_file(cls, settings_file: str) -> "PyNutilConfig":
|
|
@@ -37,10 +30,6 @@ class PyNutilConfig:
|
|
|
37
30
|
|
|
38
31
|
@classmethod
|
|
39
32
|
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
33
|
# alignment_json is required in settings files.
|
|
45
34
|
if "alignment_json" not in settings:
|
|
46
35
|
raise KeyError(
|
|
@@ -48,20 +37,20 @@ class PyNutilConfig:
|
|
|
48
37
|
)
|
|
49
38
|
|
|
50
39
|
cfg = cls(
|
|
51
|
-
segmentation_folder=
|
|
52
|
-
image_folder=
|
|
40
|
+
segmentation_folder=settings.get("segmentation_folder"),
|
|
41
|
+
image_folder=settings.get("image_folder"),
|
|
53
42
|
alignment_json=settings["alignment_json"],
|
|
54
|
-
colour=
|
|
55
|
-
intensity_channel=
|
|
56
|
-
atlas_name=
|
|
57
|
-
atlas_path=
|
|
58
|
-
label_path=
|
|
59
|
-
hemi_path=
|
|
60
|
-
custom_region_path=
|
|
61
|
-
voxel_size_um=
|
|
62
|
-
min_intensity=
|
|
63
|
-
max_intensity=
|
|
64
|
-
|
|
43
|
+
colour=settings.get("colour"),
|
|
44
|
+
intensity_channel=settings.get("intensity_channel"),
|
|
45
|
+
atlas_name=settings.get("atlas_name"),
|
|
46
|
+
atlas_path=settings.get("atlas_path"),
|
|
47
|
+
label_path=settings.get("label_path"),
|
|
48
|
+
hemi_path=settings.get("hemi_path"),
|
|
49
|
+
custom_region_path=settings.get("custom_region_path"),
|
|
50
|
+
voxel_size_um=settings.get("voxel_size_um"),
|
|
51
|
+
min_intensity=settings.get("min_intensity"),
|
|
52
|
+
max_intensity=settings.get("max_intensity"),
|
|
53
|
+
segmentation_format=settings.get("segmentation_format", "binary"),
|
|
65
54
|
)
|
|
66
55
|
|
|
67
56
|
# If atlas_path/label_path are present but empty/null, treat as not provided.
|
|
@@ -84,6 +73,10 @@ class PyNutilConfig:
|
|
|
84
73
|
return self
|
|
85
74
|
|
|
86
75
|
def validate(self) -> None:
|
|
76
|
+
self._validate_folders()
|
|
77
|
+
self._validate_atlas()
|
|
78
|
+
|
|
79
|
+
def _validate_folders(self) -> None:
|
|
87
80
|
if self.segmentation_folder and self.image_folder:
|
|
88
81
|
raise ValueError(
|
|
89
82
|
"Please specify either segmentation_folder or image_folder, not both."
|
|
@@ -101,6 +94,7 @@ class PyNutilConfig:
|
|
|
101
94
|
"You can't specify both colour and image_folder since there are no segmentations"
|
|
102
95
|
)
|
|
103
96
|
|
|
97
|
+
def _validate_atlas(self) -> None:
|
|
104
98
|
if (self.atlas_path or self.label_path) and self.atlas_name:
|
|
105
99
|
raise ValueError(
|
|
106
100
|
"Please specify either atlas_path and label_path or atlas_name. Atlas and label paths are only used for loading custom atlases."
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Immutable context objects for pipeline orchestration.
|
|
2
|
+
|
|
3
|
+
These frozen dataclasses carry state through the batch and section
|
|
4
|
+
processing layers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List, Optional, Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
from .processing.adapters.base import SliceInfo
|
|
16
|
+
from .processing.adapters.segmentation import SegmentationAdapter
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class PipelineContext:
|
|
21
|
+
"""Immutable state shared across all sections in a batch run.
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
atlas_labels : pd.DataFrame
|
|
26
|
+
Region lookup table (idx, name, r, g, b …).
|
|
27
|
+
atlas_volume : np.ndarray or None
|
|
28
|
+
3-D annotation volume for atlas-based slicing.
|
|
29
|
+
hemi_map : np.ndarray or None
|
|
30
|
+
3-D hemisphere mask (same shape as *atlas_volume*).
|
|
31
|
+
segmentation_adapter : SegmentationAdapter
|
|
32
|
+
Pre-resolved adapter for the current segmentation format.
|
|
33
|
+
non_linear : bool
|
|
34
|
+
Whether to apply non-linear deformation.
|
|
35
|
+
object_cutoff : int
|
|
36
|
+
Minimum connected-component area (binary pipeline).
|
|
37
|
+
use_flat : bool
|
|
38
|
+
If True, load flat-file atlas maps instead of slicing the volume.
|
|
39
|
+
pixel_id : object
|
|
40
|
+
Pixel colour to match, or ``"auto"`` for auto-detection.
|
|
41
|
+
apply_damage_mask : bool
|
|
42
|
+
Apply the damage / exclusion mask when available.
|
|
43
|
+
intensity_channel : str or None
|
|
44
|
+
Channel name for the intensity pipeline (``None`` in binary mode).
|
|
45
|
+
min_intensity : int or None
|
|
46
|
+
Lower intensity bound (intensity pipeline only).
|
|
47
|
+
max_intensity : int or None
|
|
48
|
+
Upper intensity bound (intensity pipeline only).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
atlas_labels: pd.DataFrame
|
|
52
|
+
atlas_volume: Optional[np.ndarray]
|
|
53
|
+
hemi_map: Optional[np.ndarray]
|
|
54
|
+
segmentation_adapter: SegmentationAdapter
|
|
55
|
+
non_linear: bool
|
|
56
|
+
object_cutoff: int
|
|
57
|
+
use_flat: bool
|
|
58
|
+
pixel_id: object
|
|
59
|
+
apply_damage_mask: bool
|
|
60
|
+
intensity_channel: Optional[str] = None
|
|
61
|
+
min_intensity: Optional[int] = None
|
|
62
|
+
max_intensity: Optional[int] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class SectionContext:
|
|
67
|
+
"""Immutable per-section state built inside the batch loop.
|
|
68
|
+
|
|
69
|
+
Attributes
|
|
70
|
+
----------
|
|
71
|
+
section_number : int
|
|
72
|
+
Numeric section identifier matching the alignment JSON.
|
|
73
|
+
slice_info : SliceInfo
|
|
74
|
+
Registration data for this section (anchoring, deformation, damage …).
|
|
75
|
+
segmentation_path : str
|
|
76
|
+
Path to the segmentation / image file on disk.
|
|
77
|
+
flat_file_path : str or None
|
|
78
|
+
Path to the corresponding flat-file atlas, if any.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
section_number: int
|
|
82
|
+
slice_info: SliceInfo
|
|
83
|
+
segmentation_path: str
|
|
84
|
+
flat_file_path: Optional[str] = None
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""PyNutil I/O subpackage.
|
|
2
|
+
|
|
3
|
+
This package contains modules for reading and writing data:
|
|
4
|
+
- atlas_loader: Loading BrainGlobe and custom atlases
|
|
5
|
+
- file_operations: Saving analysis outputs
|
|
6
|
+
- loaders: File loading (custom regions, flat files, segmentations, JSON)
|
|
7
|
+
- meshview_writer: MeshView JSON output
|
|
8
|
+
- colormap: Colormap utilities
|
|
9
|
+
- section_visualisation: Creating section PNG visualizations
|
|
10
|
+
- volume_nifti: NIfTI volume output
|
|
11
|
+
- propagation: Slice anchoring interpolation
|
|
12
|
+
- reconstruct_dzi: DZI image reconstruction
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .atlas_loader import (
|
|
16
|
+
load_atlas_data,
|
|
17
|
+
load_atlas_labels,
|
|
18
|
+
load_custom_atlas,
|
|
19
|
+
process_atlas_volume,
|
|
20
|
+
)
|
|
21
|
+
from .file_operations import save_analysis_output, SaveContext
|
|
22
|
+
from .loaders import (
|
|
23
|
+
load_quint_json,
|
|
24
|
+
load_segmentation,
|
|
25
|
+
number_sections,
|
|
26
|
+
get_flat_files,
|
|
27
|
+
get_current_flat_file,
|
|
28
|
+
open_custom_region_file,
|
|
29
|
+
read_flat_file,
|
|
30
|
+
read_seg_file,
|
|
31
|
+
)
|
|
32
|
+
from .meshview_writer import (
|
|
33
|
+
create_region_dict,
|
|
34
|
+
write_hemi_points_to_meshview,
|
|
35
|
+
write_points_to_meshview,
|
|
36
|
+
)
|
|
37
|
+
from .colormap import get_colormap_color
|
|
38
|
+
from .volume_nifti import save_volume_niftis
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
# atlas_loader
|
|
42
|
+
"load_atlas_data",
|
|
43
|
+
"load_atlas_labels",
|
|
44
|
+
"load_custom_atlas",
|
|
45
|
+
"process_atlas_volume",
|
|
46
|
+
# file_operations
|
|
47
|
+
"save_analysis_output",
|
|
48
|
+
"SaveContext",
|
|
49
|
+
# loaders
|
|
50
|
+
"load_quint_json",
|
|
51
|
+
"load_segmentation",
|
|
52
|
+
"open_custom_region_file",
|
|
53
|
+
"read_flat_file",
|
|
54
|
+
"read_seg_file",
|
|
55
|
+
# meshview_writer
|
|
56
|
+
"create_region_dict",
|
|
57
|
+
"write_hemi_points_to_meshview",
|
|
58
|
+
"write_points_to_meshview",
|
|
59
|
+
# colormap
|
|
60
|
+
"get_colormap_color",
|
|
61
|
+
# volume_nifti
|
|
62
|
+
"save_volume_niftis",
|
|
63
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Colormap utilities for PyNutil.
|
|
2
|
+
|
|
3
|
+
This module provides simple colormap implementations for mapping
|
|
4
|
+
intensity values to RGB colors, avoiding matplotlib dependency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Tuple
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _viridis(t):
|
|
15
|
+
return 1.0 - t, t, 0.5 + 0.5 * t
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _plasma(t):
|
|
19
|
+
return t, 1.0 - t, 1.0 - 0.5 * t
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _magma(t):
|
|
23
|
+
return t, t**2, 1.0 - t
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _hot(t):
|
|
27
|
+
return (
|
|
28
|
+
np.minimum(1.0, t * 3),
|
|
29
|
+
np.minimum(1.0, np.maximum(0.0, t * 3 - 1)),
|
|
30
|
+
np.minimum(1.0, np.maximum(0.0, t * 3 - 2)),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_COLORMAPS = {
|
|
35
|
+
"viridis": _viridis,
|
|
36
|
+
"plasma": _plasma,
|
|
37
|
+
"magma": _magma,
|
|
38
|
+
"hot": _hot,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Pre-built 256-entry LUT cache: {name: (N, 3) uint8 array}
|
|
42
|
+
_LUT_CACHE: dict[str, np.ndarray] = {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_lut(name: str) -> np.ndarray:
|
|
46
|
+
"""Build a (256, 3) uint8 lookup table for *name*."""
|
|
47
|
+
if name in _LUT_CACHE:
|
|
48
|
+
return _LUT_CACHE[name]
|
|
49
|
+
t = np.arange(256, dtype=np.float64) / 255.0
|
|
50
|
+
fn = _COLORMAPS.get(name)
|
|
51
|
+
if fn is None:
|
|
52
|
+
# gray
|
|
53
|
+
v = np.arange(256, dtype=np.uint8)
|
|
54
|
+
lut = np.column_stack([v, v, v])
|
|
55
|
+
else:
|
|
56
|
+
r, g, b = fn(t)
|
|
57
|
+
lut = np.column_stack([
|
|
58
|
+
np.clip(np.asarray(r) * 255, 0, 255).astype(np.uint8),
|
|
59
|
+
np.clip(np.asarray(g) * 255, 0, 255).astype(np.uint8),
|
|
60
|
+
np.clip(np.asarray(b) * 255, 0, 255).astype(np.uint8),
|
|
61
|
+
])
|
|
62
|
+
_LUT_CACHE[name] = lut
|
|
63
|
+
return lut
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_colormap_colors(values: np.ndarray, name: str = "gray") -> np.ndarray:
|
|
67
|
+
"""Vectorised colormap lookup for an array of intensity values (0-255).
|
|
68
|
+
|
|
69
|
+
Returns a (N, 3) uint8 array of RGB colours.
|
|
70
|
+
"""
|
|
71
|
+
lut = _build_lut(name)
|
|
72
|
+
idx = np.clip(values, 0, 255).astype(np.intp)
|
|
73
|
+
return lut[idx]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_colormap_color(value: int, name: str = "gray") -> Tuple[int, int, int]:
|
|
77
|
+
"""Map an intensity value (0-255) to RGB color based on colormap name.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
value : int
|
|
82
|
+
Intensity value (0-255).
|
|
83
|
+
name : str
|
|
84
|
+
Colormap name. Options: "gray", "viridis", "plasma", "magma", "hot".
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
tuple
|
|
89
|
+
(r, g, b) color values (0-255).
|
|
90
|
+
"""
|
|
91
|
+
lut = _build_lut(name)
|
|
92
|
+
idx = int(np.clip(value, 0, 255))
|
|
93
|
+
row = lut[idx]
|
|
94
|
+
return int(row[0]), int(row[1]), int(row[2])
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .meshview_writer import write_hemi_points_to_meshview
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SaveContext:
|
|
14
|
+
"""Groups the many parameters needed by :func:`save_analysis_output`.
|
|
15
|
+
|
|
16
|
+
Replaces 26+ positional parameters with a single, documented object.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Core data
|
|
20
|
+
pixel_points: Optional[np.ndarray] = None
|
|
21
|
+
centroids: Optional[np.ndarray] = None
|
|
22
|
+
label_df: Optional[pd.DataFrame] = None
|
|
23
|
+
per_section_df: Optional[list] = None
|
|
24
|
+
labeled_points: Optional[np.ndarray] = None
|
|
25
|
+
labeled_points_centroids: Optional[np.ndarray] = None
|
|
26
|
+
points_hemi_labels: Optional[np.ndarray] = None
|
|
27
|
+
centroids_hemi_labels: Optional[np.ndarray] = None
|
|
28
|
+
points_len: Optional[list] = None
|
|
29
|
+
centroids_len: Optional[list] = None
|
|
30
|
+
segmentation_filenames: Optional[list] = None
|
|
31
|
+
atlas_labels: Optional[pd.DataFrame] = None
|
|
32
|
+
point_intensities: Optional[np.ndarray] = None
|
|
33
|
+
|
|
34
|
+
# Configuration / metadata (saved to settings JSON)
|
|
35
|
+
segmentation_folder: Optional[str] = None
|
|
36
|
+
image_folder: Optional[str] = None
|
|
37
|
+
alignment_json: Optional[str] = None
|
|
38
|
+
colour: Optional[list] = None
|
|
39
|
+
intensity_channel: Optional[str] = None
|
|
40
|
+
atlas_name: Optional[str] = None
|
|
41
|
+
custom_region_path: Optional[str] = None
|
|
42
|
+
atlas_path: Optional[str] = None
|
|
43
|
+
label_path: Optional[str] = None
|
|
44
|
+
settings_file: Optional[str] = None
|
|
45
|
+
|
|
46
|
+
# Output control
|
|
47
|
+
prepend: str = ""
|
|
48
|
+
colormap: str = "gray"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _ensure_analysis_output_dirs(output_folder: str) -> None:
|
|
52
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
53
|
+
for subdir in (
|
|
54
|
+
"whole_series_report",
|
|
55
|
+
"per_section_meshview",
|
|
56
|
+
"per_section_reports",
|
|
57
|
+
"whole_series_meshview",
|
|
58
|
+
):
|
|
59
|
+
os.makedirs(f"{output_folder}/{subdir}", exist_ok=True)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_analysis_output(ctx: SaveContext, output_folder: str):
|
|
63
|
+
"""
|
|
64
|
+
Save the analysis output to the specified folder.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
ctx : SaveContext
|
|
69
|
+
All data and configuration needed for saving.
|
|
70
|
+
output_folder : str
|
|
71
|
+
The folder where the output will be saved.
|
|
72
|
+
"""
|
|
73
|
+
# Create the output folder if it doesn't exist
|
|
74
|
+
_ensure_analysis_output_dirs(output_folder)
|
|
75
|
+
|
|
76
|
+
if ctx.label_df is not None:
|
|
77
|
+
report_name = "intensity.csv" if ctx.image_folder else "counts.csv"
|
|
78
|
+
ctx.label_df.to_csv(
|
|
79
|
+
f"{output_folder}/whole_series_report/{ctx.prepend}{report_name}",
|
|
80
|
+
sep=";",
|
|
81
|
+
na_rep="",
|
|
82
|
+
index=False,
|
|
83
|
+
)
|
|
84
|
+
elif not ctx.prepend:
|
|
85
|
+
print("No quantification found, so only coordinates will be saved.")
|
|
86
|
+
print(
|
|
87
|
+
"If you want to save the quantification, please run quantify_coordinates."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if ctx.per_section_df is not None and ctx.segmentation_filenames is not None:
|
|
91
|
+
_save_per_section_reports(ctx, output_folder)
|
|
92
|
+
if ctx.pixel_points is not None:
|
|
93
|
+
_save_whole_series_meshview(ctx, output_folder)
|
|
94
|
+
|
|
95
|
+
_save_settings_json(ctx, output_folder)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _save_settings_json(ctx: SaveContext, output_folder: str) -> None:
|
|
99
|
+
"""Write a reference settings JSON to *output_folder*."""
|
|
100
|
+
settings_dict = {
|
|
101
|
+
"segmentation_folder": ctx.segmentation_folder,
|
|
102
|
+
"image_folder": ctx.image_folder,
|
|
103
|
+
"alignment_json": ctx.alignment_json,
|
|
104
|
+
"colour": ctx.colour,
|
|
105
|
+
"intensity_channel": ctx.intensity_channel,
|
|
106
|
+
"custom_region_path": ctx.custom_region_path,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for key, val in [
|
|
110
|
+
("atlas_name", ctx.atlas_name),
|
|
111
|
+
("atlas_path", ctx.atlas_path),
|
|
112
|
+
("label_path", ctx.label_path),
|
|
113
|
+
("settings_file", ctx.settings_file),
|
|
114
|
+
]:
|
|
115
|
+
if val:
|
|
116
|
+
settings_dict[key] = val
|
|
117
|
+
|
|
118
|
+
settings_file_path = os.path.join(output_folder, "pynutil_settings.json")
|
|
119
|
+
with open(settings_file_path, "w") as f:
|
|
120
|
+
json.dump(settings_dict, f, indent=4)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _save_per_section_reports(ctx: SaveContext, output_folder: str):
|
|
124
|
+
"""Write per-section CSVs and MeshView JSONs."""
|
|
125
|
+
prev_pl = 0
|
|
126
|
+
prev_cl = 0
|
|
127
|
+
|
|
128
|
+
# Handle None for points_len and centroids_len (e.g. in intensity mode)
|
|
129
|
+
points_len = ctx.points_len or [0] * len(ctx.segmentation_filenames)
|
|
130
|
+
centroids_len = ctx.centroids_len or [0] * len(ctx.segmentation_filenames)
|
|
131
|
+
|
|
132
|
+
for pl, cl, fn, df in zip(
|
|
133
|
+
points_len,
|
|
134
|
+
centroids_len,
|
|
135
|
+
ctx.segmentation_filenames,
|
|
136
|
+
ctx.per_section_df,
|
|
137
|
+
):
|
|
138
|
+
split_fn = fn.split(os.sep)[-1].split(".")[0]
|
|
139
|
+
df.to_csv(
|
|
140
|
+
f"{output_folder}/per_section_reports/{ctx.prepend}{split_fn}.csv",
|
|
141
|
+
sep=";",
|
|
142
|
+
na_rep="",
|
|
143
|
+
index=False,
|
|
144
|
+
)
|
|
145
|
+
if ctx.pixel_points is not None or ctx.centroids is not None:
|
|
146
|
+
section_intensities = (
|
|
147
|
+
ctx.point_intensities[prev_pl : pl + prev_pl]
|
|
148
|
+
if ctx.point_intensities is not None
|
|
149
|
+
else None
|
|
150
|
+
)
|
|
151
|
+
_save_per_section_meshview(
|
|
152
|
+
ctx,
|
|
153
|
+
output_folder,
|
|
154
|
+
split_fn,
|
|
155
|
+
pl,
|
|
156
|
+
cl,
|
|
157
|
+
prev_pl,
|
|
158
|
+
prev_cl,
|
|
159
|
+
section_intensities,
|
|
160
|
+
)
|
|
161
|
+
prev_cl += cl
|
|
162
|
+
prev_pl += pl
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _save_per_section_meshview(
|
|
166
|
+
ctx: SaveContext,
|
|
167
|
+
output_folder: str,
|
|
168
|
+
split_fn: str,
|
|
169
|
+
pl: int,
|
|
170
|
+
cl: int,
|
|
171
|
+
prev_pl: int,
|
|
172
|
+
prev_cl: int,
|
|
173
|
+
section_intensities=None,
|
|
174
|
+
):
|
|
175
|
+
"""Write per-section MeshView JSONs for pixels and centroids."""
|
|
176
|
+
write_hemi_points_to_meshview(
|
|
177
|
+
ctx.pixel_points[prev_pl : pl + prev_pl]
|
|
178
|
+
if ctx.pixel_points is not None
|
|
179
|
+
else None,
|
|
180
|
+
ctx.labeled_points[prev_pl : pl + prev_pl]
|
|
181
|
+
if ctx.labeled_points is not None
|
|
182
|
+
else None,
|
|
183
|
+
ctx.points_hemi_labels[prev_pl : pl + prev_pl]
|
|
184
|
+
if ctx.points_hemi_labels is not None
|
|
185
|
+
else None,
|
|
186
|
+
f"{output_folder}/per_section_meshview/{ctx.prepend}{split_fn}_pixels.json",
|
|
187
|
+
ctx.atlas_labels,
|
|
188
|
+
section_intensities,
|
|
189
|
+
colormap=ctx.colormap,
|
|
190
|
+
)
|
|
191
|
+
if ctx.centroids is not None:
|
|
192
|
+
write_hemi_points_to_meshview(
|
|
193
|
+
ctx.centroids[prev_cl : cl + prev_cl],
|
|
194
|
+
ctx.labeled_points_centroids[prev_cl : cl + prev_cl],
|
|
195
|
+
ctx.centroids_hemi_labels[prev_cl : cl + prev_cl],
|
|
196
|
+
f"{output_folder}/per_section_meshview/{ctx.prepend}{split_fn}_centroids.json",
|
|
197
|
+
ctx.atlas_labels,
|
|
198
|
+
colormap=ctx.colormap,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _save_whole_series_meshview(ctx: SaveContext, output_folder: str):
|
|
203
|
+
"""Write whole-series MeshView JSONs for pixels and centroids."""
|
|
204
|
+
write_hemi_points_to_meshview(
|
|
205
|
+
ctx.pixel_points,
|
|
206
|
+
ctx.labeled_points,
|
|
207
|
+
ctx.points_hemi_labels,
|
|
208
|
+
f"{output_folder}/whole_series_meshview/{ctx.prepend}pixels_meshview.json",
|
|
209
|
+
ctx.atlas_labels,
|
|
210
|
+
ctx.point_intensities,
|
|
211
|
+
colormap=ctx.colormap,
|
|
212
|
+
)
|
|
213
|
+
if ctx.centroids is not None:
|
|
214
|
+
write_hemi_points_to_meshview(
|
|
215
|
+
ctx.centroids,
|
|
216
|
+
ctx.labeled_points_centroids,
|
|
217
|
+
ctx.centroids_hemi_labels,
|
|
218
|
+
f"{output_folder}/whole_series_meshview/{ctx.prepend}objects_meshview.json",
|
|
219
|
+
ctx.atlas_labels,
|
|
220
|
+
colormap=ctx.colormap,
|
|
221
|
+
)
|