import sys
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any
import numpy as np
import polars as pl
from argclz import argument, list_type, str_tuple_type, validator
from brainrender.actors import Points
from neuralib.atlas.brainrender.core import BrainRenderCLI
from neuralib.atlas.brainrender.util import get_color
from neuralib.atlas.util import allen_to_brainrender_coord, as_coords_array, iter_source_coordinates
__all__ = ['RoiRenderCLI']
[docs]
class RoiRenderCLI(BrainRenderCLI):
"""ROIs reconstruction with brainrender"""
DESCRIPTION = 'ROIs reconstruction with brainrender'
DEFAULT_ROI_COLORS = ('orange', 'magenta', 'dimgray')
# ========== #
# GROUP_ROIS #
# ========== #
GROUP_ROIS = 'ROI View Option'
roi_region: str | tuple[str, ...] = argument(
'--roi-region',
metavar='NAME,...',
type=str_tuple_type,
default=(),
group=GROUP_ROIS,
help='only show rois in region(s)'
)
radius: float = argument(
'--roi-radius',
default=30,
group=GROUP_ROIS,
help='each roi radius'
)
roi_alpha: float = argument(
'--roi-alpha',
validator.float.in_range_closed(0, 1), # pyright: ignore[reportArgumentType]
default=1,
group=GROUP_ROIS,
help='region alpha value'
)
roi_colors: str | tuple[str, ...] = argument(
'--roi-colors',
metavar='COLOR,...',
type=str_tuple_type,
default=DEFAULT_ROI_COLORS,
group=GROUP_ROIS,
help='colors of rois per region'
)
region_col: str | None = argument(
'--region-col',
metavar='TREE..',
default=None,
group=GROUP_ROIS,
help='if None, auto infer, and check the lowest merge level contain all the regions specified'
)
inverse_lut: bool = argument(
'--inverse-lut',
group=GROUP_ROIS,
help='inverse right/left maps to ipsi/contra hemisphere look up table'
)
source_order: tuple[str, ...] | None = argument(
'--source-order',
metavar='SOURCE,...',
type=str_tuple_type,
default=None,
group=GROUP_ROIS,
help='source order to follow the roi_colors'
)
# ============== #
# GROUP_ROI_LOAD #
# ============== #
GROUP_ROIS_LOAD = 'ROI load Option'
classifier_file: Path | None = argument(
'--classifier-file',
validator.path.is_suffix('.csv').is_exists().optional(), # pyright: ignore[reportArgumentType]
metavar='FILE',
type=Path,
default=None,
group=GROUP_ROIS_LOAD,
help='csv output file from allenccf'
)
only_source: tuple[str, ...] | None = argument(
'--only-source',
metavar='SOURCE,...',
type=str_tuple_type,
help='only show the rois from the given source'
)
file: list[Path] | None = argument(
'--file',
validator.list().on_item(validator.path.is_suffix(['.csv', '.npy'])) | validator.optional(), # pyright: ignore[reportArgumentType]
metavar='FILE',
type=list_type(Path), # pyright: ignore[reportArgumentType]
default=None,
action='extend',
group=GROUP_ROIS_LOAD,
help="points file as 'npy' or 'csv'"
)
_need_close_file: list[Any] = []
_point_file_list: list[str] = []
[docs]
def run(self):
self.post_parsing()
if not self._stop_render:
self.render()
self.render_output()
if len(self._need_close_file) != 0:
for f in self._need_close_file:
f.close()
Path(f.name).unlink(missing_ok=True) # winOS
[docs]
def render(self):
super().render()
if self.classifier_file is not None:
self._add_points_classifier_csv()
if self.file is not None:
self._add_points_generic_file()
self._reconstruct_points_from_file()
@property
def _get_hemisphere_lut(self):
if self.inverse_lut:
return {'right': 'contra', 'left': 'ipsi', 'both': 'both'}
else:
return {'right': 'ipsi', 'left': 'contra', 'both': 'both'}
def _add_points_classifier_csv(self):
assert self.classifier_file is not None
hemisphere = self._get_hemisphere_lut[self.hemisphere]
iter_coords = iter_source_coordinates(
self.classifier_file,
area=list(self.roi_region) if isinstance(self.roi_region, tuple) else self.roi_region,
source=list(self.only_source) if self.only_source is not None else None,
region_col=self.region_col,
hemisphere=hemisphere, # pyright: ignore[reportArgumentType]
to_brainrender=True if self.coordinate_space == 'ccf' else False,
source_order=self.source_order
)
ret = [sc.coordinates for sc in iter_coords]
self._save_tempfile(ret)
def _save_tempfile(self, rois_list: list[list[np.ndarray]] | list[np.ndarray]):
for p in rois_list:
if isinstance(p, np.ndarray):
# os handle for NamedTemporaryFile, https://stackoverflow.com/a/23212515
delete = False if sys.platform == 'win32' else True
f = NamedTemporaryFile(prefix='.temp-run-3d-proj-', suffix='.npy', delete=delete)
np.save(f, p)
f.seek(0)
self._point_file_list.append(f.name)
self._need_close_file.append(f)
def _add_points_generic_file(self):
assert self.file is not None
for it in self.file:
self._point_file_list.append(str(it))
def _reconstruct_points_from_file(self, error_while_empty: bool = False):
for i, file in enumerate(self._point_file_list):
# type handle
if file.endswith('.npy'):
data = np.load(file)
elif file.endswith('.csv'):
data = pl.read_csv(file)
data = as_coords_array(data)
if self.coordinate_space == 'ccf':
data = allen_to_brainrender_coord(data)
else:
raise ValueError('Unsupported file type')
# check
if data.ndim != 2:
raise ValueError(f'wrong dimension: {data.shape}')
# add points
if data.size == 0:
if error_while_empty:
raise ValueError('no points found')
else:
self.logger.warn('no points found')
else:
if data.shape[1] == 3:
colors = get_color(i, self.roi_colors)
self.logger.info(f'Plot Rois File: {i}, {file}, {colors}')
points = Points(data, name='roi', colors=colors, alpha=self.roi_alpha, radius=self.radius, res=20) # pyright: ignore[reportArgumentType]
self.scene.add(points)
elif data.shape[1] == 4: # TODO not test yet
k = data[:, 3].astype(int)
for t in np.unique(k):
self.scene.add(Points(
data[k == t, 0:3],
name='rois',
colors=get_color(t, self.roi_colors),
alpha=self.roi_alpha, # pyright: ignore[reportArgumentType]
radius=self.radius # pyright: ignore[reportArgumentType]
))
else:
raise ValueError(f'wrong shape: {data.shape}: {file}')
if __name__ == '__main__':
RoiRenderCLI().main()