lls-core 1.0.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.
- lls_core-1.0.0/PKG-INFO +67 -0
- lls_core-1.0.0/lls_core/__init__.py +55 -0
- lls_core-1.0.0/lls_core/cmds/__init__.py +8 -0
- lls_core-1.0.0/lls_core/cmds/__main__.py +234 -0
- lls_core-1.0.0/lls_core/config.py +6 -0
- lls_core-1.0.0/lls_core/cropping.py +75 -0
- lls_core-1.0.0/lls_core/deconvolution.py +266 -0
- lls_core-1.0.0/lls_core/llsz_core.py +537 -0
- lls_core-1.0.0/lls_core/models/__init__.py +5 -0
- lls_core-1.0.0/lls_core/models/crop.py +68 -0
- lls_core-1.0.0/lls_core/models/deconvolution.py +50 -0
- lls_core-1.0.0/lls_core/models/deskew.py +251 -0
- lls_core-1.0.0/lls_core/models/lattice_data.py +506 -0
- lls_core-1.0.0/lls_core/models/output.py +85 -0
- lls_core-1.0.0/lls_core/models/results.py +229 -0
- lls_core-1.0.0/lls_core/models/utils.py +97 -0
- lls_core-1.0.0/lls_core/sample/__init__.py +3 -0
- lls_core-1.0.0/lls_core/types.py +38 -0
- lls_core-1.0.0/lls_core/utils.py +336 -0
- lls_core-1.0.0/lls_core/workflow.py +178 -0
- lls_core-1.0.0/lls_core/writers.py +131 -0
- lls_core-1.0.0/lls_core.egg-info/PKG-INFO +67 -0
- lls_core-1.0.0/lls_core.egg-info/SOURCES.txt +35 -0
- lls_core-1.0.0/lls_core.egg-info/dependency_links.txt +1 -0
- lls_core-1.0.0/lls_core.egg-info/entry_points.txt +2 -0
- lls_core-1.0.0/lls_core.egg-info/requires.txt +47 -0
- lls_core-1.0.0/lls_core.egg-info/top_level.txt +1 -0
- lls_core-1.0.0/pyproject.toml +156 -0
- lls_core-1.0.0/setup.cfg +4 -0
- lls_core-1.0.0/tests/test_arg_parser.py +18 -0
- lls_core-1.0.0/tests/test_cli.py +103 -0
- lls_core-1.0.0/tests/test_crop_deskew.py +35 -0
- lls_core-1.0.0/tests/test_deconvolution.py +46 -0
- lls_core-1.0.0/tests/test_deskew.py +28 -0
- lls_core-1.0.0/tests/test_process.py +233 -0
- lls_core-1.0.0/tests/test_validation.py +54 -0
- lls_core-1.0.0/tests/test_workflows.py +132 -0
lls_core-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: lls_core
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Core functionality for analysing and visualizing lattice lightsheet and Oblique Plane Microscopy data.
|
|
5
|
+
Author: Pradeep Rajasekhar, Lachlan Whitehead, Robert Haase, Michael Milton
|
|
6
|
+
License: GPL-3.0-only
|
|
7
|
+
Project-URL: Repository, https://github.com/BioimageAnalysisCoreWEHI/napari_lattice
|
|
8
|
+
Project-URL: BugTracker, https://github.com/BioimageAnalysisCoreWEHI/napari_lattice/issues
|
|
9
|
+
Project-URL: Documentation, https://github.com/BioimageAnalysisCoreWEHI/napari_lattice/wiki
|
|
10
|
+
Project-URL: SourceCode, https://github.com/BioimageAnalysisCoreWEHI/napari_lattice
|
|
11
|
+
Project-URL: UserSupport, https://github.com/BioimageAnalysisCoreWEHI/napari_lattice/issues
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Framework :: napari
|
|
15
|
+
Classifier: Topic :: Software Development :: Testing
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Operating System :: OS Independent
|
|
22
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: aicsimageio>=4.6.3
|
|
26
|
+
Requires-Dist: aicspylibczi>=3.1.1
|
|
27
|
+
Requires-Dist: click
|
|
28
|
+
Requires-Dist: dask
|
|
29
|
+
Requires-Dist: dask[distributed]
|
|
30
|
+
Requires-Dist: fsspec>=2022.8.2
|
|
31
|
+
Requires-Dist: importlib_resources
|
|
32
|
+
Requires-Dist: napari-workflows>=0.2.8
|
|
33
|
+
Requires-Dist: npy2bdv
|
|
34
|
+
Requires-Dist: numpy
|
|
35
|
+
Requires-Dist: pandas
|
|
36
|
+
Requires-Dist: pyclesperanto_prototype>=0.20.0
|
|
37
|
+
Requires-Dist: pyopencl
|
|
38
|
+
Requires-Dist: pydantic<3,>=1.10.17
|
|
39
|
+
Requires-Dist: pyyaml
|
|
40
|
+
Requires-Dist: read-roi
|
|
41
|
+
Requires-Dist: rich
|
|
42
|
+
Requires-Dist: resource-backed-dask-array>=0.1.0
|
|
43
|
+
Requires-Dist: scikit-image
|
|
44
|
+
Requires-Dist: StrEnum
|
|
45
|
+
Requires-Dist: tifffile>=2023.3.15
|
|
46
|
+
Requires-Dist: toolz
|
|
47
|
+
Requires-Dist: tqdm
|
|
48
|
+
Requires-Dist: typer
|
|
49
|
+
Requires-Dist: typing_extensions>=4.7.0
|
|
50
|
+
Requires-Dist: xarray[parallel]
|
|
51
|
+
Provides-Extra: testing
|
|
52
|
+
Requires-Dist: tox; extra == "testing"
|
|
53
|
+
Requires-Dist: pytest; extra == "testing"
|
|
54
|
+
Requires-Dist: pytest-cov; extra == "testing"
|
|
55
|
+
Requires-Dist: napari; extra == "testing"
|
|
56
|
+
Provides-Extra: deconvolution
|
|
57
|
+
Requires-Dist: pycudadecon~=0.5; extra == "deconvolution"
|
|
58
|
+
Requires-Dist: numpy<2; extra == "deconvolution"
|
|
59
|
+
Provides-Extra: docs
|
|
60
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
61
|
+
Requires-Dist: mkdocs-click; extra == "docs"
|
|
62
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
63
|
+
Requires-Dist: fieldz>=0.1.0; extra == "docs"
|
|
64
|
+
Requires-Dist: griffe-fieldz; extra == "docs"
|
|
65
|
+
Provides-Extra: deploy
|
|
66
|
+
Requires-Dist: build; extra == "deploy"
|
|
67
|
+
Requires-Dist: twine; extra == "deploy"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
__version__ = "0.2.6"
|
|
3
|
+
|
|
4
|
+
from strenum import StrEnum
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from pyclesperanto_prototype._tier8._affine_transform_deskew_3d import DeskewDirection
|
|
7
|
+
from lls_core.models.lattice_data import LatticeData
|
|
8
|
+
from lls_core.models.deconvolution import DeconvolutionParams
|
|
9
|
+
from lls_core.models.lattice_data import CropParams
|
|
10
|
+
|
|
11
|
+
# Initialize configuration options
|
|
12
|
+
|
|
13
|
+
#CONFIGURE LOGGING using a dictionary (can also be done with yaml file)
|
|
14
|
+
import logging.config
|
|
15
|
+
LOGGING_CONFIG = {
|
|
16
|
+
'version': 1,
|
|
17
|
+
'disable_existing_loggers': False,
|
|
18
|
+
|
|
19
|
+
'formatters': {
|
|
20
|
+
'default_formatter': {
|
|
21
|
+
'format': '[%(levelname)s:%(asctime)s] %(message)s'
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
'handlers': {
|
|
26
|
+
'stream_handler': {
|
|
27
|
+
'class': 'logging.StreamHandler',
|
|
28
|
+
'formatter': 'default_formatter',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
'loggers': {
|
|
33
|
+
'': {
|
|
34
|
+
'handlers': ['stream_handler'],
|
|
35
|
+
'level': 'INFO',
|
|
36
|
+
'propagate': True
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#Configuring logging level with empty string "" under the key loggers means its for root
|
|
42
|
+
#This will override levels for all other python libraries as they haven't been imported yet
|
|
43
|
+
#Specifying levels in each modules will override the root level
|
|
44
|
+
|
|
45
|
+
# Specify during initialization:
|
|
46
|
+
logging.config.dictConfig(LOGGING_CONFIG)
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
logger.debug("Logging is configured.")
|
|
49
|
+
|
|
50
|
+
#specify an enum for log levels
|
|
51
|
+
class Log_Levels(Enum):
|
|
52
|
+
DEBUG = 10
|
|
53
|
+
INFO = 20
|
|
54
|
+
WARNING = 30
|
|
55
|
+
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# lattice_processing.py
|
|
2
|
+
|
|
3
|
+
# Run processing on command line instead of napari.
|
|
4
|
+
# Example for deskewing files in a folder
|
|
5
|
+
# python lattice_processing.py --input /home/pradeep/to_deskew --output /home/pradeep/output_save/ --processing deskew
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from enum import auto
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
|
11
|
+
from strenum import StrEnum
|
|
12
|
+
|
|
13
|
+
from lls_core.models.lattice_data import LatticeData
|
|
14
|
+
from lls_core.models.deskew import DeskewParams, DefinedPixelSizes
|
|
15
|
+
from lls_core.models.deconvolution import DeconvolutionParams
|
|
16
|
+
from lls_core.models.output import OutputParams
|
|
17
|
+
from lls_core.models.crop import CropParams
|
|
18
|
+
from lls_core.deconvolution import DeconvolutionChoice
|
|
19
|
+
from typer import Typer, Argument, Option, Context, Exit
|
|
20
|
+
from typer.main import get_command
|
|
21
|
+
|
|
22
|
+
from lls_core.models.output import SaveFileType
|
|
23
|
+
from pydantic.v1 import ValidationError
|
|
24
|
+
from toolz.dicttoolz import merge_with
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from lls_core.models.utils import FieldAccessModel
|
|
28
|
+
from typing import Type, Any, Iterable
|
|
29
|
+
from rich.table import Table
|
|
30
|
+
|
|
31
|
+
class CliDeskewDirection(StrEnum):
|
|
32
|
+
X = auto()
|
|
33
|
+
Y = auto()
|
|
34
|
+
|
|
35
|
+
CLI_PARAM_MAP = {
|
|
36
|
+
"input_image": ["input_image"],
|
|
37
|
+
"angle": ["angle"],
|
|
38
|
+
"skew": ["skew"],
|
|
39
|
+
"physical_pixel_sizes": ["physical_pixel_sizes"],
|
|
40
|
+
"roi_list": ["crop", "roi_list"],
|
|
41
|
+
"roi_subset": ["crop", "roi_subset"],
|
|
42
|
+
"z_range": ["crop", "z_range"],
|
|
43
|
+
"decon_processing": ["deconvolution", "decon_processing"],
|
|
44
|
+
"psf": ["deconvolution", "psf"],
|
|
45
|
+
"decon_num_iter": ["deconvolution", "decon_num_iter"],
|
|
46
|
+
"background": ["deconvolution", "background"],
|
|
47
|
+
"workflow": ["workflow"],
|
|
48
|
+
"time_range": ["time_range"],
|
|
49
|
+
"channel_range": ["channel_range"],
|
|
50
|
+
"save_dir": ["save_dir"],
|
|
51
|
+
"save_name": ["save_name"],
|
|
52
|
+
"save_type": ["save_type"],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
app = Typer(add_completion=False, rich_markup_mode="rich", no_args_is_help=True)
|
|
56
|
+
|
|
57
|
+
def field_from_model(model: Type[FieldAccessModel], field_name: str, extra_description: str = "", description: Optional[str] = None, default: Optional[Any] = None, **kwargs) -> Any:
|
|
58
|
+
"""
|
|
59
|
+
Generates a type Field from a Pydantic model field
|
|
60
|
+
"""
|
|
61
|
+
field = model.__fields__[field_name]
|
|
62
|
+
|
|
63
|
+
from enum import Enum
|
|
64
|
+
if default is None:
|
|
65
|
+
default = field.get_default()
|
|
66
|
+
if isinstance(default, Enum):
|
|
67
|
+
default = default.name
|
|
68
|
+
|
|
69
|
+
if description is None:
|
|
70
|
+
description = f"{field.field_info.description} {extra_description}"
|
|
71
|
+
|
|
72
|
+
return Option(
|
|
73
|
+
default = default,
|
|
74
|
+
help=description,
|
|
75
|
+
**kwargs
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def handle_merge(values: list):
|
|
79
|
+
if len(values) > 1:
|
|
80
|
+
if all(isinstance(param, dict) for param in values):
|
|
81
|
+
return merge_with(handle_merge, values)
|
|
82
|
+
else:
|
|
83
|
+
raise ValueError(f"A parameter has been passed multiple times! Got: {', '.join(values)}")
|
|
84
|
+
return values[0]
|
|
85
|
+
|
|
86
|
+
def rich_validation(e: ValidationError) -> Table:
|
|
87
|
+
"""
|
|
88
|
+
Converts
|
|
89
|
+
"""
|
|
90
|
+
from rich.table import Table
|
|
91
|
+
|
|
92
|
+
table = Table(title="Validation Errors")
|
|
93
|
+
table.add_column("Parameter")
|
|
94
|
+
# table.add_column("Command Line Argument")
|
|
95
|
+
table.add_column("Error")
|
|
96
|
+
|
|
97
|
+
for error in e.errors():
|
|
98
|
+
table.add_row(
|
|
99
|
+
str(error["loc"][0]),
|
|
100
|
+
str(error["msg"]),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return table
|
|
104
|
+
|
|
105
|
+
def pairwise(iterable: Iterable) -> Iterable:
|
|
106
|
+
"""
|
|
107
|
+
An implementation of the pairwise() function in Python 3.10+
|
|
108
|
+
See: https://docs.python.org/3.12/library/itertools.html#itertools.pairwise
|
|
109
|
+
"""
|
|
110
|
+
from itertools import tee
|
|
111
|
+
a, b = tee(iterable)
|
|
112
|
+
next(b, None)
|
|
113
|
+
return zip(a, b)
|
|
114
|
+
|
|
115
|
+
def update_nested_data(data: Union[dict, list], keys: list, new_value: Any):
|
|
116
|
+
current = data
|
|
117
|
+
|
|
118
|
+
for key, next_key in pairwise(keys):
|
|
119
|
+
next = {} if isinstance(next_key, str) else []
|
|
120
|
+
if isinstance(current, dict):
|
|
121
|
+
current = current.setdefault(key, next)
|
|
122
|
+
elif isinstance(current, list):
|
|
123
|
+
if key >= len(current):
|
|
124
|
+
current.insert(key, next)
|
|
125
|
+
current = current[key]
|
|
126
|
+
else:
|
|
127
|
+
raise ValueError(f"Unknown data type {type(current)}. Cannot traverse.")
|
|
128
|
+
|
|
129
|
+
last_key = keys[-1]
|
|
130
|
+
if isinstance(current, dict):
|
|
131
|
+
current[last_key] = new_value
|
|
132
|
+
elif isinstance(current, list):
|
|
133
|
+
current.insert(last_key, new_value)
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(f"Unknown data type {type(current)}. Cannot traverse.")
|
|
136
|
+
|
|
137
|
+
# Example usage:
|
|
138
|
+
@app.command()
|
|
139
|
+
def process(
|
|
140
|
+
ctx: Context,
|
|
141
|
+
input_image: Path = Argument(None, help="Path to the image file to read, in a format readable by AICSImageIO, for example .tiff or .czi", show_default=False),
|
|
142
|
+
skew: CliDeskewDirection = field_from_model(DeskewParams, "skew"),# DeskewParams.make_typer_field("skew"),
|
|
143
|
+
angle: float = field_from_model(DeskewParams, "angle") ,
|
|
144
|
+
physical_pixel_sizes: Tuple[float, float, float] = field_from_model(DeskewParams, "physical_pixel_sizes", extra_description="This takes three arguments, corresponding to the Z, Y and X pixel dimensions respectively", default=(
|
|
145
|
+
DefinedPixelSizes.get_default("Z"),
|
|
146
|
+
DefinedPixelSizes.get_default("Y"),
|
|
147
|
+
DefinedPixelSizes.get_default("X")
|
|
148
|
+
)),
|
|
149
|
+
|
|
150
|
+
roi_list: List[Path] = field_from_model(CropParams, "roi_list"),
|
|
151
|
+
roi_subset: List[int] = field_from_model(CropParams, "roi_subset"),
|
|
152
|
+
z_range: Optional[Tuple[int,int]] = field_from_model(CropParams, "z_range", show_default=False),
|
|
153
|
+
|
|
154
|
+
enable_deconvolution: bool = Option(False, "--deconvolution/--disable-deconvolution", rich_help_panel="Deconvolution"),
|
|
155
|
+
decon_processing: DeconvolutionChoice = field_from_model(DeconvolutionParams, "decon_processing", rich_help_panel="Deconvolution"),
|
|
156
|
+
psf: List[Path] = field_from_model(DeconvolutionParams, "psf", rich_help_panel="Deconvolution"),
|
|
157
|
+
decon_num_iter: int = field_from_model(DeconvolutionParams, "decon_num_iter", rich_help_panel="Deconvolution"),
|
|
158
|
+
background: str = field_from_model(DeconvolutionParams, "background", rich_help_panel="Deconvolution"),
|
|
159
|
+
|
|
160
|
+
time_range: Optional[Tuple[int,int]] = field_from_model(OutputParams, "time_range", rich_help_panel="Output"),
|
|
161
|
+
channel_range: Optional[Tuple[int,int]] = field_from_model(OutputParams,"channel_range", rich_help_panel="Output"),
|
|
162
|
+
|
|
163
|
+
save_dir: Path = field_from_model(OutputParams, "save_dir", rich_help_panel="Output"),
|
|
164
|
+
save_name: Optional[str] = field_from_model(OutputParams, "save_name", rich_help_panel="Output"),
|
|
165
|
+
save_type: SaveFileType = field_from_model(OutputParams, "save_type", rich_help_panel="Output"),
|
|
166
|
+
|
|
167
|
+
workflow: Optional[Path] = field_from_model(LatticeData, "workflow", show_default=False),
|
|
168
|
+
json_config: Optional[Path] = Option(None, show_default=False, help="Path to a JSON file from which parameters will be read."),
|
|
169
|
+
yaml_config: Optional[Path] = Option(None, show_default=False, help="Path to a YAML file from which parameters will be read."),
|
|
170
|
+
|
|
171
|
+
show_schema: bool = Option(default=False, help="If provided, image processing will not be performed, and instead a JSON document outlining the JSON/YAML options will be printed to stdout. This can be used to assist with writing a config file for use with the --json-config and --yaml-config options.")
|
|
172
|
+
) -> None:
|
|
173
|
+
from click.core import ParameterSource
|
|
174
|
+
from rich.console import Console
|
|
175
|
+
|
|
176
|
+
console = Console(stderr=True)
|
|
177
|
+
|
|
178
|
+
if show_schema:
|
|
179
|
+
import json
|
|
180
|
+
import sys
|
|
181
|
+
json.dump(
|
|
182
|
+
LatticeData.to_definition_dict(),
|
|
183
|
+
sys.stdout,
|
|
184
|
+
indent=4
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
# Just print help if the user didn't provide any arguments
|
|
189
|
+
if all(src != ParameterSource.COMMANDLINE for src in ctx._parameter_source.values()):
|
|
190
|
+
print(ctx.get_help())
|
|
191
|
+
raise Exit()
|
|
192
|
+
|
|
193
|
+
from toolz.dicttoolz import merge_with
|
|
194
|
+
cli_args = {}
|
|
195
|
+
for source, dest in CLI_PARAM_MAP.items():
|
|
196
|
+
from click.core import ParameterSource
|
|
197
|
+
if ctx.get_parameter_source(source) != ParameterSource.DEFAULT:
|
|
198
|
+
update_nested_data(cli_args, dest, ctx.params[source])
|
|
199
|
+
|
|
200
|
+
json_args = {}
|
|
201
|
+
if json_config is not None:
|
|
202
|
+
import json
|
|
203
|
+
with json_config.open() as fp:
|
|
204
|
+
json_args = json.load(fp)
|
|
205
|
+
|
|
206
|
+
yaml_args = {}
|
|
207
|
+
if yaml_config is not None:
|
|
208
|
+
with yaml_config.open() as fp:
|
|
209
|
+
from yaml import safe_load
|
|
210
|
+
yaml_args = safe_load(fp)
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
lattice = LatticeData.parse_obj(
|
|
214
|
+
# Merge all three sources of config: YAML, JSON and CLI
|
|
215
|
+
merge_with(
|
|
216
|
+
handle_merge,
|
|
217
|
+
[yaml_args, json_args, cli_args]
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
except ValidationError as e:
|
|
221
|
+
console.print(rich_validation(e))
|
|
222
|
+
raise Exit(code=1)
|
|
223
|
+
|
|
224
|
+
lattice.save()
|
|
225
|
+
console.print(f"Processing successful. Results can be found in {lattice.save_dir.resolve()}")
|
|
226
|
+
|
|
227
|
+
# Used by the docs
|
|
228
|
+
click_app = get_command(app)
|
|
229
|
+
|
|
230
|
+
def main():
|
|
231
|
+
app()
|
|
232
|
+
|
|
233
|
+
if __name__ == '__main__':
|
|
234
|
+
main()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#module for accessing variables across modules in workflow
|
|
2
|
+
#https://docs.python.org/3/faq/programming.html?highlight=global#how-do-i-share-global-variables-across-modules
|
|
3
|
+
channel = 0
|
|
4
|
+
time = 0
|
|
5
|
+
#configure default logging level for use in packages; inherit from dock_widget or __main__.py
|
|
6
|
+
log_level = 10
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, NamedTuple, Tuple, List
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from lls_core.types import PathLike
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
from numpy.typing import NDArray
|
|
8
|
+
|
|
9
|
+
RoiCoord = Tuple[float, float]
|
|
10
|
+
|
|
11
|
+
class Roi(NamedTuple):
|
|
12
|
+
top_left: RoiCoord
|
|
13
|
+
top_right: RoiCoord
|
|
14
|
+
bottom_left: RoiCoord
|
|
15
|
+
bottom_right: RoiCoord
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_array(cls, array: NDArray) -> Self:
|
|
19
|
+
import numpy as np
|
|
20
|
+
return Roi(*np.reshape(array, (-1, 2)).tolist())
|
|
21
|
+
|
|
22
|
+
def read_roi_array(roi: PathLike) -> NDArray:
|
|
23
|
+
from read_roi import read_roi_file
|
|
24
|
+
from numpy import array
|
|
25
|
+
return array(read_roi_file(str(roi)))
|
|
26
|
+
|
|
27
|
+
def read_imagej_roi(roi_path: PathLike) -> List[Roi]:
|
|
28
|
+
"""Read an ImageJ ROI zip file so it loaded into napari shapes layer
|
|
29
|
+
If non rectangular ROI, will convert into a rectangle based on extreme points
|
|
30
|
+
Args:
|
|
31
|
+
roi_zip_path (zip file): ImageJ ROI zip file
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
list: List of ROIs
|
|
35
|
+
"""
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from os import fspath
|
|
38
|
+
from read_roi import read_roi_file, read_roi_zip
|
|
39
|
+
|
|
40
|
+
roi_path = Path(fspath(roi_path))
|
|
41
|
+
|
|
42
|
+
# handle reading single roi or collection of rois in zip file
|
|
43
|
+
if roi_path.suffix == ".zip":
|
|
44
|
+
ij_roi = read_roi_zip(roi_path)
|
|
45
|
+
elif roi_path.suffix == ".roi":
|
|
46
|
+
ij_roi = read_roi_file(str(roi_path))
|
|
47
|
+
else:
|
|
48
|
+
raise Exception("ImageJ ROI file needs to be a zip/roi file")
|
|
49
|
+
|
|
50
|
+
if ij_roi is None:
|
|
51
|
+
raise Exception("Failed reading ROI file")
|
|
52
|
+
|
|
53
|
+
# initialise list of rois
|
|
54
|
+
roi_list = []
|
|
55
|
+
|
|
56
|
+
# Read through each roi and create a list so that it matches the organisation of the shapes from napari shapes layer
|
|
57
|
+
for value in ij_roi.values():
|
|
58
|
+
if value['type'] in ('oval', 'rectangle'):
|
|
59
|
+
width = int(value['width'])
|
|
60
|
+
height = int(value['height'])
|
|
61
|
+
left = int(value['left'])
|
|
62
|
+
top = int(value['top'])
|
|
63
|
+
roi = Roi((top, left), (top, left+width), (top+height, left+width), (top+height, left))
|
|
64
|
+
roi_list.append(roi)
|
|
65
|
+
elif value['type'] in ('polygon', 'freehand'):
|
|
66
|
+
left = min(int(it) for it in value['x'])
|
|
67
|
+
top = min(int(it) for it in value['y'])
|
|
68
|
+
right = max(int(it) for it in value['x'])
|
|
69
|
+
bottom = max(int(it) for it in value['y'])
|
|
70
|
+
roi = Roi((top, left), (top, right), (bottom, right), (bottom, left))
|
|
71
|
+
roi_list.append(roi)
|
|
72
|
+
else:
|
|
73
|
+
print(f"Cannot read ROI {value}. Recognised as type {value['type']}")
|
|
74
|
+
|
|
75
|
+
return roi_list
|