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.
Files changed (37) hide show
  1. lls_core-1.0.0/PKG-INFO +67 -0
  2. lls_core-1.0.0/lls_core/__init__.py +55 -0
  3. lls_core-1.0.0/lls_core/cmds/__init__.py +8 -0
  4. lls_core-1.0.0/lls_core/cmds/__main__.py +234 -0
  5. lls_core-1.0.0/lls_core/config.py +6 -0
  6. lls_core-1.0.0/lls_core/cropping.py +75 -0
  7. lls_core-1.0.0/lls_core/deconvolution.py +266 -0
  8. lls_core-1.0.0/lls_core/llsz_core.py +537 -0
  9. lls_core-1.0.0/lls_core/models/__init__.py +5 -0
  10. lls_core-1.0.0/lls_core/models/crop.py +68 -0
  11. lls_core-1.0.0/lls_core/models/deconvolution.py +50 -0
  12. lls_core-1.0.0/lls_core/models/deskew.py +251 -0
  13. lls_core-1.0.0/lls_core/models/lattice_data.py +506 -0
  14. lls_core-1.0.0/lls_core/models/output.py +85 -0
  15. lls_core-1.0.0/lls_core/models/results.py +229 -0
  16. lls_core-1.0.0/lls_core/models/utils.py +97 -0
  17. lls_core-1.0.0/lls_core/sample/__init__.py +3 -0
  18. lls_core-1.0.0/lls_core/types.py +38 -0
  19. lls_core-1.0.0/lls_core/utils.py +336 -0
  20. lls_core-1.0.0/lls_core/workflow.py +178 -0
  21. lls_core-1.0.0/lls_core/writers.py +131 -0
  22. lls_core-1.0.0/lls_core.egg-info/PKG-INFO +67 -0
  23. lls_core-1.0.0/lls_core.egg-info/SOURCES.txt +35 -0
  24. lls_core-1.0.0/lls_core.egg-info/dependency_links.txt +1 -0
  25. lls_core-1.0.0/lls_core.egg-info/entry_points.txt +2 -0
  26. lls_core-1.0.0/lls_core.egg-info/requires.txt +47 -0
  27. lls_core-1.0.0/lls_core.egg-info/top_level.txt +1 -0
  28. lls_core-1.0.0/pyproject.toml +156 -0
  29. lls_core-1.0.0/setup.cfg +4 -0
  30. lls_core-1.0.0/tests/test_arg_parser.py +18 -0
  31. lls_core-1.0.0/tests/test_cli.py +103 -0
  32. lls_core-1.0.0/tests/test_crop_deskew.py +35 -0
  33. lls_core-1.0.0/tests/test_deconvolution.py +46 -0
  34. lls_core-1.0.0/tests/test_deskew.py +28 -0
  35. lls_core-1.0.0/tests/test_process.py +233 -0
  36. lls_core-1.0.0/tests/test_validation.py +54 -0
  37. lls_core-1.0.0/tests/test_workflows.py +132 -0
@@ -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