Skip to content

Commit

Permalink
Merge pull request #82 from BioimageAnalysisCoreWEHI/final_frame_fix
Browse files Browse the repository at this point in the history
Final frame fix
  • Loading branch information
DrLachie authored Oct 15, 2024
2 parents e20ec1b + e3da612 commit 454d204
Show file tree
Hide file tree
Showing 8 changed files with 47 additions and 57 deletions.
44 changes: 18 additions & 26 deletions core/lls_core/cmds/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -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."),

Expand Down
2 changes: 1 addition & 1 deletion core/lls_core/cropping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
2 changes: 1 addition & 1 deletion core/lls_core/models/deconvolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
2 changes: 1 addition & 1 deletion core/lls_core/models/deskew.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 19 additions & 21 deletions core/lls_core/models/lattice_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
)
Expand Down Expand Up @@ -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'
Expand Down
6 changes: 3 additions & 3 deletions core/tests/test_arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
2 changes: 1 addition & 1 deletion core/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
6 changes: 3 additions & 3 deletions plugin/napari_lattice/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -397,7 +397,7 @@ def _enable_custom_background(self, background: str) -> bool:
fields = [
decon_processing,
psf,
psf_num_iter,
decon_num_iter,
background
]
)
Expand All @@ -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
Expand Down

0 comments on commit 454d204

Please sign in to comment.