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 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,8 @@
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
@@ -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