diff --git a/core/lls_core/cmds/__main__.py b/core/lls_core/cmds/__main__.py index c5560d3d..89dc766a 100644 --- a/core/lls_core/cmds/__main__.py +++ b/core/lls_core/cmds/__main__.py @@ -36,20 +36,17 @@ class CliDeskewDirection(StrEnum): "input_image": ["input_image"], "angle": ["angle"], "skew": ["skew"], - "pixel_sizes": ["physical_pixel_sizes"], - "rois": ["crop", "roi_list"], - "roi_indices": ["crop", "roi_subset"], - "z_start": ["crop", "z_range", 0], - "z_end": ["crop", "z_range", 1], + "physical_pixel_sizes": ["physical_pixel_sizes"], + "roi_list": ["crop", "roi_list"], + "roi_subset": ["crop", "roi_subset"], + "z_range": ["crop", "z_range"], "decon_processing": ["deconvolution", "decon_processing"], "psf": ["deconvolution", "psf"], - "psf_num_iter": ["deconvolution", "psf_num_iter"], + "decon_num_iter": ["deconvolution", "decon_num_iter"], "background": ["deconvolution", "background"], "workflow": ["workflow"], - "time_start": ["time_range", 0], - "time_end": ["time_range", 1], - "channel_start": ["channel_range", 0], - "channel_end": ["channel_range", 1], + "time_range": ["time_range"], + "channel_range": ["channel_range"], "save_dir": ["save_dir"], "save_name": ["save_name"], "save_type": ["save_type"], @@ -144,35 +141,30 @@ def process( 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), skew: CliDeskewDirection = field_from_model(DeskewParams, "skew"),# DeskewParams.make_typer_field("skew"), angle: float = field_from_model(DeskewParams, "angle") , - 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=( + 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=( DefinedPixelSizes.get_default("Z"), DefinedPixelSizes.get_default("Y"), DefinedPixelSizes.get_default("X") )), - rois: List[Path] = field_from_model(CropParams, "roi_list", description="A list of paths pointing to regions of interest to crop to, in ImageJ format."), #Option([], help="A list of paths pointing to regions of interest to crop to, in ImageJ format."), - roi_indices: List[int] = field_from_model(CropParams, "roi_subset"), - # Ideally this and other range values would be defined as Tuples, but these seem to be broken: https://github.com/tiangolo/typer/discussions/667 - z_start: Optional[int] = Option(0, help="The index of the first Z slice to use. All prior Z slices will be discarded.", show_default=False), - z_end: Optional[int] = Option(None, help="The index of the last Z slice to use. The selected index and all subsequent Z slices will be discarded. Defaults to the last z index of the image.", show_default=False), - + roi_list: List[Path] = field_from_model(CropParams, "roi_list"), + roi_subset: List[int] = field_from_model(CropParams, "roi_subset"), + z_range: Optional[Tuple[int,int]] = field_from_model(CropParams, "z_range", show_default=False), + enable_deconvolution: bool = Option(False, "--deconvolution/--disable-deconvolution", rich_help_panel="Deconvolution"), decon_processing: DeconvolutionChoice = field_from_model(DeconvolutionParams, "decon_processing", rich_help_panel="Deconvolution"), - psf: List[Path] = field_from_model(DeconvolutionParams, "psf", description="One or more paths pointing to point spread functions to use for deconvolution. Each file should in a standard image format (.czi, .tiff etc), containing a 3D image array. This option can be used multiple times to provide multiple PSF files.", rich_help_panel="Deconvolution"), - psf_num_iter: int = field_from_model(DeconvolutionParams, "psf_num_iter", rich_help_panel="Deconvolution"), + psf: List[Path] = field_from_model(DeconvolutionParams, "psf", rich_help_panel="Deconvolution"), + decon_num_iter: int = field_from_model(DeconvolutionParams, "decon_num_iter", rich_help_panel="Deconvolution"), background: str = field_from_model(DeconvolutionParams, "background", rich_help_panel="Deconvolution"), - time_start: Optional[int] = Option(0, help="Index of the first time slice to use (inclusive). Defaults to the first time index of the image.", rich_help_panel="Output"), - time_end: Optional[int] = Option(None, help="Index of the first time slice to use (exclusive). Defaults to the last time index of the image.", show_default=False, rich_help_panel="Output"), - - channel_start: Optional[int] = Option(0, help="Index of the first channel slice to use (inclusive). Defaults to the first channel index of the image.", rich_help_panel="Output"), - channel_end: Optional[int] = Option(None, help="Index of the first channel slice to use (exclusive). Defaults to the last channel index of the image.", show_default=False, rich_help_panel="Output"), - + time_range: Optional[Tuple[int,int]] = field_from_model(OutputParams, "time_range", rich_help_panel="Output"), + channel_range: Optional[Tuple[int,int]] = field_from_model(OutputParams,"channel_range", rich_help_panel="Output"), + save_dir: Path = field_from_model(OutputParams, "save_dir", rich_help_panel="Output"), save_name: Optional[str] = field_from_model(OutputParams, "save_name", rich_help_panel="Output"), save_type: SaveFileType = field_from_model(OutputParams, "save_type", rich_help_panel="Output"), - workflow: Optional[Path] = Option(None, help="Path to a Napari Workflow file, in YAML format. If provided, the configured desekewing processing will be added to the chosen workflow.", show_default=False), + workflow: Optional[Path] = field_from_model(LatticeData, "workflow", show_default=False), json_config: Optional[Path] = Option(None, show_default=False, help="Path to a JSON file from which parameters will be read."), yaml_config: Optional[Path] = Option(None, show_default=False, help="Path to a YAML file from which parameters will be read."), diff --git a/core/lls_core/cropping.py b/core/lls_core/cropping.py index 2f0c35a9..f57f2894 100644 --- a/core/lls_core/cropping.py +++ b/core/lls_core/cropping.py @@ -43,7 +43,7 @@ def read_imagej_roi(roi_path: PathLike) -> List[Roi]: if roi_path.suffix == ".zip": ij_roi = read_roi_zip(roi_path) elif roi_path.suffix == ".roi": - ij_roi = read_roi_file(roi_path) + ij_roi = read_roi_file(str(roi_path)) else: raise Exception("ImageJ ROI file needs to be a zip/roi file") diff --git a/core/lls_core/models/deconvolution.py b/core/lls_core/models/deconvolution.py index c628afe8..6bfc32ab 100644 --- a/core/lls_core/models/deconvolution.py +++ b/core/lls_core/models/deconvolution.py @@ -22,7 +22,7 @@ class DeconvolutionParams(FieldAccessModel): default=[], description="List of Point Spread Functions to use for deconvolution. Each of which should be a 3D array. Each PSF can also be provided as a `str` path, in which case they will be loaded from disk as images." ) - psf_num_iter: NonNegativeInt = Field( + decon_num_iter: NonNegativeInt = Field( default=10, description="Number of iterations to perform in deconvolution" ) diff --git a/core/lls_core/models/deskew.py b/core/lls_core/models/deskew.py index d432443f..de741b47 100644 --- a/core/lls_core/models/deskew.py +++ b/core/lls_core/models/deskew.py @@ -191,7 +191,7 @@ def read_image(cls, values: dict): # If the image was convertible to AICSImage, we should use the metadata from there if aics: - values["input_image"] = aics.xarray_dask_data + values["input_image"] = aics.xarray_dask_data # Take pixel sizes from the image metadata, but only if they're defined # and only if we don't already have them if all(size is not None for size in aics.physical_pixel_sizes) and values.get("physical_pixel_sizes") is None: diff --git a/core/lls_core/models/lattice_data.py b/core/lls_core/models/lattice_data.py index f1322d84..d7e2c98b 100644 --- a/core/lls_core/models/lattice_data.py +++ b/core/lls_core/models/lattice_data.py @@ -61,6 +61,7 @@ def read_image(cls, values: dict): from lls_core.types import is_pathlike from pathlib import Path input_image = values.get("input_image") + logger.info(f"Processing File {input_image}") # this is handy for debugging if is_pathlike(input_image): if values.get("save_name") is None: values["save_name"] = Path(values["input_image"]).stem @@ -76,6 +77,21 @@ def read_image(cls, values: dict): # Use the Deskew version of this validator, to do the actual image loading return super().read_image(values) + @validator("input_image", pre=True, always=True) + def incomplete_final_frame(cls, v: DataArray) -> Any: + """ + Check final frame, if acquisition is stopped halfway through it causes failures + This validator will remove a bad final frame + """ + final_frame = v.isel(T=-1,C=-1, drop=True) + try: + final_frame.compute() + except ValueError: + logger.warning("Final frame is borked. Acquisition probably stopped prematurely. Removing final frame.") + v = v.drop_isel(T=-1) + return v + + @validator("workflow", pre=True) def parse_workflow(cls, v: Any): # Load the workflow from disk if it was provided as a path @@ -336,24 +352,6 @@ def generate_workflows( # The user can use any of these arguments as inputs to their tasks yield lattice_slice.copy_with_data(user_workflow) - def check_incomplete_acquisition(self, volume: ArrayLike, time_point: int, channel: int): - """ - Checks for a slice with incomplete data, caused by incomplete acquisition - """ - import numpy as np - if not isinstance(volume, DaskArray): - return volume - orig_shape = volume.shape - raw_vol = volume.compute() - if raw_vol.shape != orig_shape: - logger.warn(f"Time {time_point}, channel {channel} is incomplete. Actual shape {orig_shape}, got {raw_vol.shape}") - z_diff, y_diff, x_diff = np.subtract(orig_shape, raw_vol.shape) - logger.info(f"Padding with{z_diff,y_diff,x_diff}") - raw_vol = np.pad(raw_vol, ((0, z_diff), (0, y_diff), (0, x_diff))) - if raw_vol.shape != orig_shape: - raise Exception(f"Shape of last timepoint still doesn't match. Got {raw_vol.shape}") - return raw_vol - @property def deskewed_volume(self) -> DaskArray: from dask.array import zeros @@ -372,7 +370,7 @@ def _process_crop(self) -> Iterable[ImageSlice]: deconv_args: dict[Any, Any] = {} if self.deconvolution is not None: deconv_args = dict( - num_iter = self.deconvolution.psf_num_iter, + num_iter = self.deconvolution.decon_num_iter, psf = self.deconvolution.psf[slice.channel].to_numpy(), decon_processing=self.deconvolution.decon_processing ) @@ -417,13 +415,13 @@ def _process_non_crop(self) -> Iterable[ImageSlice]: dxdata=self.dx, dzpsf=self.dz, dxpsf=self.dx, - num_iter=self.deconvolution.psf_num_iter + num_iter=self.deconvolution.decon_num_iter ) else: data = skimage_decon( vol_zyx=data, psf=self.deconvolution.psf[slice.channel].to_numpy(), - num_iter=self.deconvolution.psf_num_iter, + num_iter=self.deconvolution.decon_num_iter, clip=False, filter_epsilon=0, boundary='nearest' diff --git a/core/tests/test_arg_parser.py b/core/tests/test_arg_parser.py index 26bb1e20..1505be06 100644 --- a/core/tests/test_arg_parser.py +++ b/core/tests/test_arg_parser.py @@ -10,9 +10,9 @@ def test_voxel_parsing(): parser = command.make_parser(ctx) args, _, _ = parser.parse_args(args=[ "process", - "input", + "input-image", "--save-name", "output", "--save-type", "tiff", - "--pixel-sizes", "1", "1", "1" + "--physical-pixel-sizes", "1", "1", "1" ]) - assert args["pixel_sizes"] == ("1", "1", "1") + assert args["physical_pixel_sizes"] == ("1", "1", "1") diff --git a/core/tests/test_cli.py b/core/tests/test_cli.py index 7d96b33f..38867aba 100644 --- a/core/tests/test_cli.py +++ b/core/tests/test_cli.py @@ -62,7 +62,7 @@ def assert_h5(output_dir: Path): [ [["--save-type", "h5"], assert_h5], [["--save-type", "tiff"], assert_tiff], - [["--save-type", "tiff", "--time-start", "0", "--time-end", "1"], assert_tiff], + [["--save-type", "tiff", "--time-range", "0", "1"], assert_tiff], ] ) def test_batch_deskew(flags: List[str], check_fn: Callable[[Path], None]): diff --git a/plugin/napari_lattice/fields.py b/plugin/napari_lattice/fields.py index 2360c69f..c46e1b59 100644 --- a/plugin/napari_lattice/fields.py +++ b/plugin/napari_lattice/fields.py @@ -375,7 +375,7 @@ class DeconvolutionFields(NapariFieldGroup): tooltip="PSFs must be in the same order as the image channels", layout="vertical" ) - psf_num_iter = field(int, label = "Number of Iterations") + decon_num_iter = field(int, label = "Number of Iterations") background = field(ComboBox).with_choices( [it.value for it in BackgroundSource] ).with_options(label="Background") @@ -397,7 +397,7 @@ def _enable_custom_background(self, background: str) -> bool: fields = [ decon_processing, psf, - psf_num_iter, + decon_num_iter, background ] ) @@ -418,7 +418,7 @@ def _make_model(self) -> Optional[DeconvolutionParams]: background=background, # Filter out unset PSFs psf=[psf for psf in self.psf.value if psf.is_file()], - psf_num_iter=self.psf_num_iter.value + decon_num_iter=self.decon_num_iter.value ) @magicclass