lls-core 1.0.0__py3-none-any.whl
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/__init__.py +55 -0
- lls_core/cmds/__init__.py +8 -0
- lls_core/cmds/__main__.py +234 -0
- lls_core/config.py +6 -0
- lls_core/cropping.py +75 -0
- lls_core/deconvolution.py +266 -0
- lls_core/llsz_core.py +537 -0
- lls_core/models/__init__.py +5 -0
- lls_core/models/crop.py +68 -0
- lls_core/models/deconvolution.py +50 -0
- lls_core/models/deskew.py +251 -0
- lls_core/models/lattice_data.py +506 -0
- lls_core/models/output.py +85 -0
- lls_core/models/results.py +229 -0
- lls_core/models/utils.py +97 -0
- lls_core/sample/__init__.py +3 -0
- lls_core/types.py +38 -0
- lls_core/utils.py +336 -0
- lls_core/workflow.py +178 -0
- lls_core/writers.py +131 -0
- lls_core-1.0.0.dist-info/METADATA +68 -0
- lls_core-1.0.0.dist-info/RECORD +25 -0
- lls_core-1.0.0.dist-info/WHEEL +5 -0
- lls_core-1.0.0.dist-info/entry_points.txt +2 -0
- lls_core-1.0.0.dist-info/top_level.txt +1 -0
lls_core/__init__.py
ADDED
|
@@ -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()
|
lls_core/config.py
ADDED
|
@@ -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
|
lls_core/cropping.py
ADDED
|
@@ -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
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from strenum import StrEnum
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import importlib.util
|
|
9
|
+
from typing import Collection, Iterable,Union,Literal, Optional, TYPE_CHECKING
|
|
10
|
+
from aicsimageio.aics_image import AICSImage
|
|
11
|
+
from skimage.io import imread
|
|
12
|
+
from aicspylibczi import CziFile
|
|
13
|
+
from numpy.typing import NDArray
|
|
14
|
+
import os
|
|
15
|
+
import numpy as np
|
|
16
|
+
from dask.array.core import Array as DaskArray
|
|
17
|
+
|
|
18
|
+
from lls_core.utils import array_to_dask, pad_image_nearest_multiple
|
|
19
|
+
from lls_core.types import ArrayLike, is_arraylike
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from lls_core.models.lattice_data import LatticeData
|
|
23
|
+
|
|
24
|
+
class DeconvolutionChoice(StrEnum):
|
|
25
|
+
"""
|
|
26
|
+
Deconvolution algorithm
|
|
27
|
+
"""
|
|
28
|
+
cuda_gpu = "cuda_gpu"
|
|
29
|
+
opencl_gpu = "opencl_gpu"
|
|
30
|
+
cpu = "cpu"
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
def read_psf(psf_paths: Collection[Path],
|
|
35
|
+
decon_option: DeconvolutionChoice,
|
|
36
|
+
lattice_class: LatticeData) -> Iterable[NDArray]:
|
|
37
|
+
"""Read PSF files and return a list of PSF arrays appended to lattice_class.psf
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
decon_option (enum): Enum option from DeconvolutionChoice
|
|
41
|
+
lattice_class: lattice class object, either LLSZWidget.LlszMenu.lattice or lattice class from batch processing
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# remove empty paths; pathlib returns current directory as "." if None or empty str specified
|
|
45
|
+
# When running batch processing,empty directory will be an empty string
|
|
46
|
+
|
|
47
|
+
logging.debug(f"PSF paths are {psf_paths}")
|
|
48
|
+
# total no of psf images
|
|
49
|
+
psf_channels = len(psf_paths)
|
|
50
|
+
assert psf_channels > 0, f"No images detected for PSF. Check the psf paths -> {psf_paths}"
|
|
51
|
+
|
|
52
|
+
# Use CUDA for deconvolution
|
|
53
|
+
if decon_option == DeconvolutionChoice.cuda_gpu:
|
|
54
|
+
pycudadecon_import = importlib.util.find_spec("pycudadecon")
|
|
55
|
+
assert pycudadecon_import, f"Pycudadecon not detected. Please install using: conda install -c conda-forge pycudadecon"
|
|
56
|
+
otf_names = ["ch1", "ch2", "ch3", "ch4"]
|
|
57
|
+
channels = [488, 561, 640, 123]
|
|
58
|
+
# get temp directory to save generated otf
|
|
59
|
+
import tempfile
|
|
60
|
+
temp_dir = tempfile.gettempdir()+os.sep
|
|
61
|
+
|
|
62
|
+
for psf in psf_paths:
|
|
63
|
+
if psf.exists() and psf.is_file():
|
|
64
|
+
if psf.suffix == ".czi":
|
|
65
|
+
psf_czi = CziFile(psf.__str__())
|
|
66
|
+
psf_aics = psf_czi.read_image()
|
|
67
|
+
# make sure shape is 3D
|
|
68
|
+
psf_aics = psf_aics[0][0] # np.expand_dims(psf_aics[0],axis=0)
|
|
69
|
+
# if len(psf_aics[0])>=1:
|
|
70
|
+
#psf_channels = len(psf_aics[0])
|
|
71
|
+
assert len(
|
|
72
|
+
psf_aics.shape) == 3, f"PSF should be a 3D image (shape of 3), but got {psf_aics.shape}"
|
|
73
|
+
# pad psf to multiple of 16 for decon
|
|
74
|
+
yield pad_image_nearest_multiple(img=psf_aics, nearest_multiple=16)
|
|
75
|
+
else:
|
|
76
|
+
#Use skimage to read tiff
|
|
77
|
+
if psf.suffix in [".tif", ".tiff"]:
|
|
78
|
+
psf_aics_data = imread(str(psf))
|
|
79
|
+
if len(psf_aics_data.shape) != 3:
|
|
80
|
+
raise ValueError(f"PSF should be a 3D image (shape of 3), but got {psf_aics.shape}")
|
|
81
|
+
else:
|
|
82
|
+
#Use AICSImageIO
|
|
83
|
+
psf_aics = AICSImage(str(psf))
|
|
84
|
+
psf_aics_data = psf_aics.data[0][0]
|
|
85
|
+
psf_aics_data = pad_image_nearest_multiple(
|
|
86
|
+
img=psf_aics_data, nearest_multiple=16)
|
|
87
|
+
if psf_aics.dims.C != lattice_class.channels:
|
|
88
|
+
logger.warn(
|
|
89
|
+
f"PSF image has {psf_channels} channel/s, whereas image has {lattice_class.channels}")
|
|
90
|
+
|
|
91
|
+
yield psf_aics_data
|
|
92
|
+
|
|
93
|
+
# Ideally we want to use OpenCL, but in the case of deconvolution most CUDA based
|
|
94
|
+
# libraries are better designed.. Atleast until RL deconvolution is available in pyclesperant
|
|
95
|
+
# Talley Lamberts pycudadecon is a great library and highly optimised.
|
|
96
|
+
def pycuda_decon(
|
|
97
|
+
image: ArrayLike,
|
|
98
|
+
otf_path: Optional[str]=None,
|
|
99
|
+
dzdata: float=0.3,
|
|
100
|
+
dxdata: float=0.1449922,
|
|
101
|
+
dzpsf: float=0.3,
|
|
102
|
+
dxpsf: float=0.1449922,
|
|
103
|
+
psf: Optional[ArrayLike]=None,
|
|
104
|
+
num_iter: int = 10,
|
|
105
|
+
cropping: bool = False,
|
|
106
|
+
background: Union[float,Literal["auto","second_last"]] = 0
|
|
107
|
+
):
|
|
108
|
+
"""Perform deconvolution using pycudadecon
|
|
109
|
+
pycudadecon can return cropped images, so we pad the images with dimensions that are a multiple of 64
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
image : _description_
|
|
113
|
+
otf_path : (path to the generated otf file, if available. Otherwise psf needs to be provided)
|
|
114
|
+
dzdata : (pixel size in z in microns)
|
|
115
|
+
dxdata : (pixel size in xy in microns)
|
|
116
|
+
dzpsf : (pixel size of original psf file in z microns)
|
|
117
|
+
dxpsf : (pixel size of original psf file in xy microns)
|
|
118
|
+
psf (tiff): option to provide psf instead of the otfpath, this can be used when calling decon function
|
|
119
|
+
num_iter (int): number of iterations
|
|
120
|
+
cropping (bool): option to specify if cropping option is enabled
|
|
121
|
+
background (float,"auto","second_last"): background value to use for deconvolution. Can specify a value. If using auto, defaults to pycudadecon settings which is median of last slice
|
|
122
|
+
'second_last' means median of second last slice. This option is because last slice can sometimes be incomplete
|
|
123
|
+
Returns:
|
|
124
|
+
np.array: _description_
|
|
125
|
+
"""
|
|
126
|
+
image = np.squeeze(image)
|
|
127
|
+
assert image.ndim == 3, f"Image needs to be 3D. Got {image.ndim}"
|
|
128
|
+
|
|
129
|
+
#Option for median of second last slices to be used
|
|
130
|
+
#Similar to pycudadecon
|
|
131
|
+
if isinstance(background, str) and background == "second_last":
|
|
132
|
+
background = np.median(image[-2])
|
|
133
|
+
|
|
134
|
+
# if dask array, convert to numpy array
|
|
135
|
+
if isinstance(image, DaskArray):
|
|
136
|
+
image = np.array(image)
|
|
137
|
+
|
|
138
|
+
orig_img_shape = image.shape
|
|
139
|
+
|
|
140
|
+
# if cropping, add extra padding to avoid edge artefact
|
|
141
|
+
# not doing this with with large images as images can be quite large with all the padding leading to memory issues
|
|
142
|
+
if cropping:
|
|
143
|
+
# pad image y dimensionswith half of psf shape
|
|
144
|
+
z_psf_pad, y_psf_pad, x_psf_pad = np.array(psf.shape) // 2
|
|
145
|
+
image = np.pad(
|
|
146
|
+
image,
|
|
147
|
+
(
|
|
148
|
+
(z_psf_pad, z_psf_pad),
|
|
149
|
+
(y_psf_pad, y_psf_pad),
|
|
150
|
+
(x_psf_pad, x_psf_pad),
|
|
151
|
+
),
|
|
152
|
+
mode="reflect",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# pad image to a multiple of 64
|
|
156
|
+
image = pad_image_nearest_multiple(img=image, nearest_multiple=64)
|
|
157
|
+
|
|
158
|
+
if is_arraylike(psf):
|
|
159
|
+
from pycudadecon import RLContext, TemporaryOTF, rl_decon
|
|
160
|
+
|
|
161
|
+
psf = np.squeeze(psf) # remove unit dimensions
|
|
162
|
+
assert psf.ndim == 3, f"PSF needs to be 3D. Got {psf.ndim}"
|
|
163
|
+
# Temporary OTF generation; RLContext ensures memory cleanup (runs rl_init and rl_cleanup)
|
|
164
|
+
with TemporaryOTF(psf) as otf:
|
|
165
|
+
with RLContext(
|
|
166
|
+
rawdata_shape=image.shape,
|
|
167
|
+
otfpath=otf.path,
|
|
168
|
+
dzdata=dzdata,
|
|
169
|
+
dxdata=dxdata,
|
|
170
|
+
dzpsf=dzpsf,
|
|
171
|
+
dxpsf=dxpsf,
|
|
172
|
+
) as ctx:
|
|
173
|
+
decon_res = rl_decon(
|
|
174
|
+
im=image, output_shape=ctx.out_shape, n_iters=num_iter,background=background
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
else:
|
|
178
|
+
from pycudadecon import rl_decon, rl_init, rl_cleanup
|
|
179
|
+
|
|
180
|
+
rl_init(
|
|
181
|
+
rawdata_shape=image.shape,
|
|
182
|
+
otfpath=otf_path,
|
|
183
|
+
dzdata=dzdata,
|
|
184
|
+
dxdata=dxdata,
|
|
185
|
+
dzpsf=dzpsf,
|
|
186
|
+
dxpsf=dxpsf,
|
|
187
|
+
)
|
|
188
|
+
decon_res = rl_decon(image)
|
|
189
|
+
rl_cleanup()
|
|
190
|
+
|
|
191
|
+
if cropping:
|
|
192
|
+
# remove psf padding if cropping was chosen
|
|
193
|
+
decon_res = decon_res[
|
|
194
|
+
z_psf_pad:-z_psf_pad, y_psf_pad:-y_psf_pad, x_psf_pad:-x_psf_pad
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
# remove padding; get shape difference and use this shape difference to remove padding
|
|
198
|
+
shape_diff = np.array(decon_res.shape) - np.array(orig_img_shape)
|
|
199
|
+
# if above is negative,
|
|
200
|
+
if shape_diff[0] == 0:
|
|
201
|
+
shape_diff[0] = -orig_img_shape[0]
|
|
202
|
+
if shape_diff[1] == 0:
|
|
203
|
+
shape_diff[1] = -orig_img_shape[1]
|
|
204
|
+
if shape_diff[2] == 0:
|
|
205
|
+
shape_diff[2] = -orig_img_shape[2]
|
|
206
|
+
|
|
207
|
+
decon_res = decon_res[: -shape_diff[0], : -shape_diff[1], : -shape_diff[2]]
|
|
208
|
+
# print(orig_img_shape)
|
|
209
|
+
# make sure the decon image and the original image shapes are the same
|
|
210
|
+
assert (
|
|
211
|
+
decon_res.shape == orig_img_shape
|
|
212
|
+
), f"Deconvolved {decon_res.shape} and original image shape {orig_img_shape} do not match."
|
|
213
|
+
return decon_res
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def skimage_decon(
|
|
217
|
+
vol_zyx, psf, num_iter: int, clip: bool, filter_epsilon, boundary: str
|
|
218
|
+
):
|
|
219
|
+
"""Deconvolution using scikit image
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
vol_zyx (_type_): _description_
|
|
223
|
+
psf (_type_): _description_
|
|
224
|
+
num_iter (_type_): _description_
|
|
225
|
+
clip (_type_): _description_
|
|
226
|
+
filter_epsilon (_type_): _description_
|
|
227
|
+
boundary (_type_): _description_
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
_type_: _description_
|
|
231
|
+
"""
|
|
232
|
+
from skimage.restoration import richardson_lucy as rl_decon_skimage
|
|
233
|
+
|
|
234
|
+
depth = tuple(np.array(psf.shape) // 2)
|
|
235
|
+
vol_zyx = array_to_dask(vol_zyx)
|
|
236
|
+
decon_data = vol_zyx.map_overlap(
|
|
237
|
+
rl_decon_skimage,
|
|
238
|
+
psf=psf,
|
|
239
|
+
num_iter=num_iter,
|
|
240
|
+
clip=clip,
|
|
241
|
+
filter_epsilon=filter_epsilon,
|
|
242
|
+
boundary=boundary,
|
|
243
|
+
depth=depth,
|
|
244
|
+
trim=True,
|
|
245
|
+
)
|
|
246
|
+
return decon_data
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def pyopencl_decon(vol_zyx, psf, num_iter: int, clip: bool, filter_epsilon):
|
|
250
|
+
"""Deconvolution using RedFishLion library for pyopencl based deconvolution
|
|
251
|
+
This is slower than pycudadecon
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
vol_zyx (_type_): _description_
|
|
255
|
+
psf (_type_): _description_
|
|
256
|
+
num_iter (_type_): _description_
|
|
257
|
+
clip (_type_): _description_
|
|
258
|
+
filter_epsilon (_type_): _description_
|
|
259
|
+
boundary (_type_): _description_
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
_type_: _description_
|
|
263
|
+
"""
|
|
264
|
+
# CROP PSF; Crop psfs when reading and initialising psfs
|
|
265
|
+
|
|
266
|
+
return
|