PyNutil 0.5.1__tar.gz → 0.5.4__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.5.1 → pynutil-0.5.4}/PKG-INFO +25 -29
- pynutil-0.5.4/PyNutil/__init__.py +30 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/config.py +3 -3
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/context.py +37 -0
- pynutil-0.5.4/PyNutil/io/__init__.py +13 -0
- pynutil-0.5.4/PyNutil/io/atlas_loader.py +131 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/colormap.py +0 -23
- pynutil-0.5.4/PyNutil/io/file_operations.py +171 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/loaders.py +49 -143
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/meshview_writer.py +134 -123
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/reconstruct_dzi.py +3 -5
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/section_visualisation.py +48 -62
- pynutil-0.5.4/PyNutil/processing/__init__.py +17 -0
- pynutil-0.5.4/PyNutil/processing/adapters/__init__.py +26 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/anchoring.py +28 -21
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/base.py +26 -1
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/damage.py +26 -57
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/deformation.py +34 -10
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/registry.py +4 -5
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/segmentation.py +56 -17
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/adapters/visualign_deformations.py +67 -60
- pynutil-0.5.4/PyNutil/processing/analysis/data_analysis.py +264 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/analysis/region_counting.py +75 -73
- pynutil-0.5.4/PyNutil/processing/atlas_map.py +494 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/pipeline/__init__.py +4 -4
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/pipeline/batch_processor.py +212 -250
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/pipeline/connected_components.py +34 -42
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/pipeline/section_processor.py +185 -206
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/section_volume.py +166 -123
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/utils.py +17 -73
- pynutil-0.5.4/PyNutil/results/__init__.py +13 -0
- pynutil-0.5.4/PyNutil/results/atlas.py +17 -0
- pynutil-0.5.4/PyNutil/results/extraction.py +78 -0
- pynutil-0.5.4/PyNutil/results/section.py +61 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil.egg-info/PKG-INFO +25 -29
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil.egg-info/SOURCES.txt +4 -4
- {pynutil-0.5.1 → pynutil-0.5.4}/README.md +24 -28
- pynutil-0.5.4/tests/test_helpers.py +79 -0
- pynutil-0.5.1/PyNutil/__init__.py +0 -1
- pynutil-0.5.1/PyNutil/io/__init__.py +0 -63
- pynutil-0.5.1/PyNutil/io/atlas_loader.py +0 -87
- pynutil-0.5.1/PyNutil/io/file_operations.py +0 -221
- pynutil-0.5.1/PyNutil/io/propagation.py +0 -133
- pynutil-0.5.1/PyNutil/main.py +0 -720
- pynutil-0.5.1/PyNutil/processing/__init__.py +0 -94
- pynutil-0.5.1/PyNutil/processing/adapters/__init__.py +0 -98
- pynutil-0.5.1/PyNutil/processing/analysis/data_analysis.py +0 -354
- pynutil-0.5.1/PyNutil/processing/atlas_map.py +0 -440
- pynutil-0.5.1/PyNutil/processing/transforms.py +0 -221
- pynutil-0.5.1/PyNutil/results.py +0 -127
- pynutil-0.5.1/tests/test_helpers.py +0 -23
- {pynutil-0.5.1 → pynutil-0.5.4}/LICENSE +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/nifti_writer.py +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/io/volume_nifti.py +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/logging_utils.py +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/analysis/__init__.py +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil/processing/analysis/aggregator.py +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil.egg-info/dependency_links.txt +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil.egg-info/requires.txt +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/PyNutil.egg-info/top_level.txt +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/setup.cfg +0 -0
- {pynutil-0.5.1 → pynutil-0.5.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyNutil
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
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
|
|
@@ -26,7 +26,9 @@ Dynamic: summary
|
|
|
26
26
|
|
|
27
27
|
# PyNutil
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
> [!WARNING]
|
|
30
|
+
> PyNutil is still under development and the API is subject to change.
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
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).
|
|
32
34
|
|
|
@@ -71,37 +73,31 @@ As input, PyNutil requires:
|
|
|
71
73
|
3. A segmentation file for each brain section with the features to be quantified displayed with a unique RGB colour code (it currently accepts many image formats: png, jpg, jpeg, etc).
|
|
72
74
|
|
|
73
75
|
```python
|
|
74
|
-
from
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
segmentation_folder='../tests/test_data/non_linear_allen_mouse/segmentations/',
|
|
89
|
-
alignment_json='../tests/test_data/non_linear_allen_mouse/alignment.json',
|
|
90
|
-
colour=[0, 0, 0],
|
|
91
|
-
atlas_name='allen_mouse_25um'
|
|
92
|
-
#If you would like to use cellpose segmentations add:
|
|
93
|
-
#segmentation_format = 'cellpose'
|
|
76
|
+
from brainglobe_atlasapi import BrainGlobeAtlas
|
|
77
|
+
import PyNutil as pnt
|
|
78
|
+
|
|
79
|
+
# Load an atlas (BrainGlobe) and alignment
|
|
80
|
+
atlas = BrainGlobeAtlas("allen_mouse_25um")
|
|
81
|
+
alignment = pnt.read_alignment("path/to/alignment.json")
|
|
82
|
+
|
|
83
|
+
# Extract coordinates from segmentations
|
|
84
|
+
coords = pnt.seg_to_coords(
|
|
85
|
+
"path/to/segmentations/",
|
|
86
|
+
alignment,
|
|
87
|
+
atlas,
|
|
88
|
+
pixel_id=[0, 0, 0],
|
|
89
|
+
# For cellpose segmentations: segmentation_format="cellpose"
|
|
94
90
|
)
|
|
95
91
|
|
|
96
|
-
#
|
|
97
|
-
pnt.
|
|
98
|
-
|
|
99
|
-
pnt.get_coordinates(object_cutoff=0)
|
|
92
|
+
# Quantify by atlas region
|
|
93
|
+
label_df = pnt.quantify_coords(coords, atlas)
|
|
100
94
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
pnt.save_analysis("PyNutil/test_result/myResults")
|
|
95
|
+
# Save results
|
|
96
|
+
pnt.save_analysis("path/to/output", coords, atlas, label_df=label_df)
|
|
104
97
|
```
|
|
98
|
+
|
|
99
|
+
For custom atlases (not from BrainGlobe), use `pnt.load_custom_atlas()` instead.
|
|
100
|
+
See `demos/basic_example.py` and `demos/basic_example_custom_atlas.py` for complete examples.
|
|
105
101
|
PyNutil generates a series of reports in the folder which you specify.
|
|
106
102
|
|
|
107
103
|
## Per-Hemisphere Quantification
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .results import AtlasData, ExtractionResult, PointSetResult
|
|
2
|
+
from .processing.adapters.base import RegistrationData
|
|
3
|
+
from .processing.adapters import read_alignment
|
|
4
|
+
from .io.atlas_loader import load_atlas_data, load_custom_atlas
|
|
5
|
+
from .processing.pipeline.batch_processor import (
|
|
6
|
+
seg_to_coords,
|
|
7
|
+
image_to_coords,
|
|
8
|
+
xy_to_coords,
|
|
9
|
+
)
|
|
10
|
+
from .processing.analysis.data_analysis import quantify_coords
|
|
11
|
+
from .io.file_operations import save_analysis
|
|
12
|
+
from .processing.section_volume import interpolate_volume
|
|
13
|
+
from .io.volume_nifti import save_volume_niftis
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AtlasData",
|
|
17
|
+
"ExtractionResult",
|
|
18
|
+
"PointSetResult",
|
|
19
|
+
"RegistrationData",
|
|
20
|
+
"read_alignment",
|
|
21
|
+
"load_atlas_data",
|
|
22
|
+
"load_custom_atlas",
|
|
23
|
+
"seg_to_coords",
|
|
24
|
+
"image_to_coords",
|
|
25
|
+
"xy_to_coords",
|
|
26
|
+
"quantify_coords",
|
|
27
|
+
"save_analysis",
|
|
28
|
+
"interpolate_volume",
|
|
29
|
+
"save_volume_niftis",
|
|
30
|
+
]
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from dataclasses import dataclass
|
|
5
4
|
from typing import Any, Dict, Optional
|
|
6
5
|
|
|
6
|
+
from .io.loaders import load_json_file
|
|
7
|
+
|
|
7
8
|
|
|
8
9
|
@dataclass
|
|
9
10
|
class PyNutilConfig:
|
|
@@ -25,8 +26,7 @@ class PyNutilConfig:
|
|
|
25
26
|
|
|
26
27
|
@classmethod
|
|
27
28
|
def from_settings_file(cls, settings_file: str) -> "PyNutilConfig":
|
|
28
|
-
|
|
29
|
-
settings = json.load(f)
|
|
29
|
+
settings = load_json_file(settings_file)
|
|
30
30
|
return cls.from_settings_dict(settings)
|
|
31
31
|
|
|
32
32
|
@classmethod
|
|
@@ -62,6 +62,43 @@ class PipelineContext:
|
|
|
62
62
|
min_intensity: Optional[int] = None
|
|
63
63
|
max_intensity: Optional[int] = None
|
|
64
64
|
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_format(
|
|
67
|
+
cls,
|
|
68
|
+
*,
|
|
69
|
+
segmentation_format: str,
|
|
70
|
+
atlas_labels,
|
|
71
|
+
atlas_volume,
|
|
72
|
+
hemi_map,
|
|
73
|
+
non_linear: bool,
|
|
74
|
+
object_cutoff: int,
|
|
75
|
+
use_flat: bool,
|
|
76
|
+
pixel_id,
|
|
77
|
+
apply_damage_mask: bool,
|
|
78
|
+
flat_label_path=None,
|
|
79
|
+
intensity_channel=None,
|
|
80
|
+
min_intensity=None,
|
|
81
|
+
max_intensity=None,
|
|
82
|
+
) -> "PipelineContext":
|
|
83
|
+
"""Construct a PipelineContext, resolving *segmentation_format* to an adapter."""
|
|
84
|
+
from .processing.adapters.segmentation import SegmentationAdapterRegistry
|
|
85
|
+
|
|
86
|
+
return cls(
|
|
87
|
+
atlas_labels=atlas_labels,
|
|
88
|
+
atlas_volume=atlas_volume,
|
|
89
|
+
hemi_map=hemi_map,
|
|
90
|
+
segmentation_adapter=SegmentationAdapterRegistry.get(segmentation_format),
|
|
91
|
+
non_linear=non_linear,
|
|
92
|
+
object_cutoff=object_cutoff,
|
|
93
|
+
use_flat=use_flat,
|
|
94
|
+
pixel_id=pixel_id,
|
|
95
|
+
apply_damage_mask=apply_damage_mask,
|
|
96
|
+
flat_label_path=flat_label_path,
|
|
97
|
+
intensity_channel=intensity_channel,
|
|
98
|
+
min_intensity=min_intensity,
|
|
99
|
+
max_intensity=max_intensity,
|
|
100
|
+
)
|
|
101
|
+
|
|
65
102
|
|
|
66
103
|
@dataclass(frozen=True)
|
|
67
104
|
class SectionContext:
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
"""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import brainglobe_atlasapi
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import nrrd
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
from ..results import AtlasData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_atlas_labels(atlas=None, atlas_name=None):
|
|
11
|
+
if atlas_name:
|
|
12
|
+
atlas = brainglobe_atlasapi.BrainGlobeAtlas(atlas_name=atlas_name)
|
|
13
|
+
if not atlas_name and not atlas:
|
|
14
|
+
raise Exception("Either atlas or atlas name must be specified")
|
|
15
|
+
atlas_structures = {
|
|
16
|
+
"idx": [],
|
|
17
|
+
"name": [],
|
|
18
|
+
"r": [],
|
|
19
|
+
"g": [],
|
|
20
|
+
"b": [],
|
|
21
|
+
}
|
|
22
|
+
for structure in atlas.structures_list:
|
|
23
|
+
atlas_structures["idx"].append(structure["id"])
|
|
24
|
+
atlas_structures["name"].append(structure["name"])
|
|
25
|
+
rgb = structure["rgb_triplet"]
|
|
26
|
+
atlas_structures["r"].append(rgb[0])
|
|
27
|
+
atlas_structures["g"].append(rgb[1])
|
|
28
|
+
atlas_structures["b"].append(rgb[2])
|
|
29
|
+
atlas_structures["idx"].insert(0, 0)
|
|
30
|
+
atlas_structures["name"].insert(0, "Clear Label")
|
|
31
|
+
atlas_structures["r"].insert(0, 0)
|
|
32
|
+
atlas_structures["g"].insert(0, 0)
|
|
33
|
+
atlas_structures["b"].insert(0, 0)
|
|
34
|
+
atlas_labels = pd.DataFrame(atlas_structures)
|
|
35
|
+
return atlas_labels
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_atlas(atlas):
|
|
39
|
+
"""Convert an atlas argument to AtlasData.
|
|
40
|
+
|
|
41
|
+
Accepts an ``AtlasData`` instance (returned as-is) or a
|
|
42
|
+
``BrainGlobeAtlas``-like object (converted via volume processing and
|
|
43
|
+
label loading).
|
|
44
|
+
"""
|
|
45
|
+
if isinstance(atlas, AtlasData):
|
|
46
|
+
return atlas
|
|
47
|
+
# Assume BrainGlobeAtlas-like object
|
|
48
|
+
volume = process_atlas_volume(atlas.annotation)
|
|
49
|
+
hemi_map = process_atlas_volume(atlas.hemispheres)
|
|
50
|
+
labels = load_atlas_labels(atlas)
|
|
51
|
+
return AtlasData(volume=volume, hemi_map=hemi_map, labels=labels)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_atlas_labels(atlas_labels):
|
|
55
|
+
"""Resolve atlas labels input into a DataFrame.
|
|
56
|
+
|
|
57
|
+
Accepts a raw labels DataFrame, AtlasData-like objects exposing ``labels``,
|
|
58
|
+
or BrainGlobeAtlas-like objects exposing ``structures_list``.
|
|
59
|
+
"""
|
|
60
|
+
if isinstance(atlas_labels, pd.DataFrame):
|
|
61
|
+
return atlas_labels
|
|
62
|
+
if hasattr(atlas_labels, "labels"):
|
|
63
|
+
return atlas_labels.labels
|
|
64
|
+
if hasattr(atlas_labels, "structures_list"):
|
|
65
|
+
return load_atlas_labels(atlas_labels)
|
|
66
|
+
raise TypeError(
|
|
67
|
+
"atlas_labels must be a pandas DataFrame, AtlasData-like (.labels), "
|
|
68
|
+
"or BrainGlobeAtlas-like (.structures_list)."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@lru_cache(maxsize=8)
|
|
73
|
+
def load_atlas_data(atlas_name):
|
|
74
|
+
"""
|
|
75
|
+
Loads atlas data using the brainglobe_atlasapi.
|
|
76
|
+
|
|
77
|
+
Parameters
|
|
78
|
+
----------
|
|
79
|
+
atlas_name : str
|
|
80
|
+
Name of the atlas to load.
|
|
81
|
+
|
|
82
|
+
Returns
|
|
83
|
+
-------
|
|
84
|
+
AtlasData
|
|
85
|
+
Bundle containing atlas volume, hemisphere map, and labels.
|
|
86
|
+
"""
|
|
87
|
+
atlas = brainglobe_atlasapi.BrainGlobeAtlas(atlas_name=atlas_name)
|
|
88
|
+
atlas_labels = load_atlas_labels(atlas)
|
|
89
|
+
atlas_volume = process_atlas_volume(atlas.annotation)
|
|
90
|
+
hemi_map = process_atlas_volume(atlas.hemispheres)
|
|
91
|
+
print("atlas labels loaded ✅")
|
|
92
|
+
return AtlasData(volume=atlas_volume, hemi_map=hemi_map, labels=atlas_labels)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def process_atlas_volume(vol):
|
|
96
|
+
"""
|
|
97
|
+
Processes the atlas volume by transposing and reversing axes.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
vol : numpy.ndarray
|
|
102
|
+
The atlas volume to process.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
numpy.ndarray
|
|
107
|
+
The processed atlas volume.
|
|
108
|
+
"""
|
|
109
|
+
return np.transpose(vol, [2, 0, 1])[::-1, ::-1, ::-1]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@lru_cache(maxsize=8)
|
|
113
|
+
def load_custom_atlas(atlas_path, hemi_path, label_path):
|
|
114
|
+
"""
|
|
115
|
+
Loads a custom atlas from provided file paths.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
AtlasData
|
|
120
|
+
Bundle containing atlas volume, hemisphere map, and labels.
|
|
121
|
+
"""
|
|
122
|
+
atlas_volume, _ = nrrd.read(atlas_path)
|
|
123
|
+
|
|
124
|
+
if hemi_path:
|
|
125
|
+
hemi_volume, _ = nrrd.read(hemi_path)
|
|
126
|
+
else:
|
|
127
|
+
hemi_volume = None
|
|
128
|
+
|
|
129
|
+
atlas_labels = pd.read_csv(label_path)
|
|
130
|
+
|
|
131
|
+
return AtlasData(volume=atlas_volume, hemi_map=hemi_volume, labels=atlas_labels)
|
|
@@ -6,8 +6,6 @@ intensity values to RGB colors, avoiding matplotlib dependency.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from typing import Tuple
|
|
10
|
-
|
|
11
9
|
import numpy as np
|
|
12
10
|
|
|
13
11
|
|
|
@@ -71,24 +69,3 @@ def get_colormap_colors(values: np.ndarray, name: str = "gray") -> np.ndarray:
|
|
|
71
69
|
lut = _build_lut(name)
|
|
72
70
|
idx = np.clip(values, 0, 255).astype(np.intp)
|
|
73
71
|
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,171 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from .meshview_writer import write_hemi_points_to_meshview
|
|
10
|
+
from .atlas_loader import resolve_atlas_labels
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SaveContext:
|
|
15
|
+
"""Groups the parameters needed by :func:`save_analysis_output`."""
|
|
16
|
+
|
|
17
|
+
# Core data
|
|
18
|
+
points: Optional[np.ndarray] = None
|
|
19
|
+
objects: Optional[np.ndarray] = None
|
|
20
|
+
label_df: Optional[pd.DataFrame] = None
|
|
21
|
+
point_labels: Optional[np.ndarray] = None
|
|
22
|
+
object_labels: Optional[np.ndarray] = None
|
|
23
|
+
points_hemi_labels: Optional[np.ndarray] = None
|
|
24
|
+
objects_hemi_labels: Optional[np.ndarray] = None
|
|
25
|
+
atlas_labels: Optional[pd.DataFrame] = None
|
|
26
|
+
point_values: Optional[np.ndarray] = None
|
|
27
|
+
|
|
28
|
+
# Whether this is intensity mode (affects report filename)
|
|
29
|
+
is_intensity: bool = False
|
|
30
|
+
|
|
31
|
+
# Settings dict written to pynutil_settings.json (optional)
|
|
32
|
+
settings_dict: Optional[Dict[str, Any]] = None
|
|
33
|
+
|
|
34
|
+
# Output control
|
|
35
|
+
prepend: str = ""
|
|
36
|
+
colormap: str = "gray"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _ensure_analysis_output_dirs(output_folder: str) -> None:
|
|
40
|
+
os.makedirs(output_folder, exist_ok=True)
|
|
41
|
+
for subdir in (
|
|
42
|
+
"whole_series_report",
|
|
43
|
+
"whole_series_meshview",
|
|
44
|
+
):
|
|
45
|
+
os.makedirs(f"{output_folder}/{subdir}", exist_ok=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_analysis_output(ctx: SaveContext, output_folder: str):
|
|
49
|
+
"""
|
|
50
|
+
Save the analysis output to the specified folder.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
ctx : SaveContext
|
|
55
|
+
All data and configuration needed for saving.
|
|
56
|
+
output_folder : str
|
|
57
|
+
The folder where the output will be saved.
|
|
58
|
+
"""
|
|
59
|
+
# Create the output folder if it doesn't exist
|
|
60
|
+
_ensure_analysis_output_dirs(output_folder)
|
|
61
|
+
|
|
62
|
+
if ctx.label_df is not None:
|
|
63
|
+
report_name = "intensity.csv" if ctx.is_intensity else "counts.csv"
|
|
64
|
+
ctx.label_df.to_csv(
|
|
65
|
+
f"{output_folder}/whole_series_report/{ctx.prepend}{report_name}",
|
|
66
|
+
sep=";",
|
|
67
|
+
na_rep="",
|
|
68
|
+
index=False,
|
|
69
|
+
)
|
|
70
|
+
elif not ctx.prepend:
|
|
71
|
+
print("No quantification found, so only coordinates will be saved.")
|
|
72
|
+
print(
|
|
73
|
+
"If you want to save the quantification, please run quantify_coordinates."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if ctx.points is not None:
|
|
77
|
+
_save_whole_series_meshview(ctx, output_folder)
|
|
78
|
+
|
|
79
|
+
_save_settings_json(ctx, output_folder)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _save_settings_json(ctx: SaveContext, output_folder: str) -> None:
|
|
83
|
+
"""Write a reference settings JSON to *output_folder*."""
|
|
84
|
+
if ctx.settings_dict is None:
|
|
85
|
+
return
|
|
86
|
+
settings_file_path = os.path.join(output_folder, "pynutil_settings.json")
|
|
87
|
+
with open(settings_file_path, "w") as f:
|
|
88
|
+
json.dump(ctx.settings_dict, f, indent=4)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _save_whole_series_meshview(ctx: SaveContext, output_folder: str):
|
|
92
|
+
"""Write whole-series MeshView JSONs for pixels and centroids."""
|
|
93
|
+
write_hemi_points_to_meshview(
|
|
94
|
+
ctx.points,
|
|
95
|
+
ctx.point_labels,
|
|
96
|
+
ctx.points_hemi_labels,
|
|
97
|
+
f"{output_folder}/whole_series_meshview/{ctx.prepend}pixels_meshview.json",
|
|
98
|
+
ctx.atlas_labels,
|
|
99
|
+
ctx.point_values,
|
|
100
|
+
colormap=ctx.colormap,
|
|
101
|
+
)
|
|
102
|
+
if ctx.objects is not None:
|
|
103
|
+
write_hemi_points_to_meshview(
|
|
104
|
+
ctx.objects,
|
|
105
|
+
ctx.object_labels,
|
|
106
|
+
ctx.objects_hemi_labels,
|
|
107
|
+
f"{output_folder}/whole_series_meshview/{ctx.prepend}objects_meshview.json",
|
|
108
|
+
ctx.atlas_labels,
|
|
109
|
+
colormap=ctx.colormap,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def save_analysis(
|
|
114
|
+
output_folder,
|
|
115
|
+
result,
|
|
116
|
+
atlas_labels,
|
|
117
|
+
label_df=None,
|
|
118
|
+
*,
|
|
119
|
+
colormap="gray",
|
|
120
|
+
settings_dict=None,
|
|
121
|
+
):
|
|
122
|
+
"""Save analysis output to the specified directory.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
output_folder: Directory to write output files.
|
|
126
|
+
result: ExtractionResult from coordinate extraction.
|
|
127
|
+
atlas_labels: Atlas labels DataFrame (or AtlasData — ``.labels`` used).
|
|
128
|
+
label_df: Whole-series quantification DataFrame.
|
|
129
|
+
colormap: Colormap for MeshView intensity output.
|
|
130
|
+
settings_dict: Optional dict written to pynutil_settings.json.
|
|
131
|
+
"""
|
|
132
|
+
atlas_labels = resolve_atlas_labels(atlas_labels)
|
|
133
|
+
|
|
134
|
+
ctx = SaveContext(
|
|
135
|
+
points=result.points.filtered_points() if result else None,
|
|
136
|
+
objects=(
|
|
137
|
+
result.objects.filtered_points()
|
|
138
|
+
if (result and result.objects is not None)
|
|
139
|
+
else None
|
|
140
|
+
),
|
|
141
|
+
label_df=label_df,
|
|
142
|
+
point_labels=result.points.filtered_labels() if result else None,
|
|
143
|
+
object_labels=(
|
|
144
|
+
result.objects.filtered_labels()
|
|
145
|
+
if (result and result.objects is not None)
|
|
146
|
+
else None
|
|
147
|
+
),
|
|
148
|
+
points_hemi_labels=result.points.filtered_hemi_labels() if result else None,
|
|
149
|
+
objects_hemi_labels=(
|
|
150
|
+
result.objects.filtered_hemi_labels()
|
|
151
|
+
if (result and result.objects is not None)
|
|
152
|
+
else None
|
|
153
|
+
),
|
|
154
|
+
atlas_labels=atlas_labels,
|
|
155
|
+
point_values=result.points.filtered_point_values() if result else None,
|
|
156
|
+
is_intensity=result.region_intensities is not None if result else False,
|
|
157
|
+
settings_dict=settings_dict,
|
|
158
|
+
colormap=colormap,
|
|
159
|
+
)
|
|
160
|
+
save_analysis_output(ctx, output_folder)
|
|
161
|
+
|
|
162
|
+
# Remap compressed IDs if present
|
|
163
|
+
if label_df is not None and "original_idx" in label_df.columns:
|
|
164
|
+
remapped = label_df.copy()
|
|
165
|
+
remapped["idx"] = remapped["original_idx"]
|
|
166
|
+
remapped = remapped.drop(columns=["original_idx"])
|
|
167
|
+
remapped.to_csv(
|
|
168
|
+
f"{output_folder}/whole_series_report/counts.csv",
|
|
169
|
+
sep=";",
|
|
170
|
+
index=False,
|
|
171
|
+
)
|