Skip to content

Commit

Permalink
Merge PR #341 | New export widget
Browse files Browse the repository at this point in the history
  • Loading branch information
qin-yu authored Oct 1, 2024
2 parents 8eca094 + dbf3ccc commit 29d71eb
Show file tree
Hide file tree
Showing 16 changed files with 299 additions and 178 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ __pycache__/
# Conda build
plantseg.egg-info/
dist/
build/

# Dev
settings.json
Expand Down
4 changes: 2 additions & 2 deletions docs/snippets/napari/io/widget_export_stacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from napari_widgets_render import render_widget

from plantseg.viewer_napari.widgets import widget_export_stacks
from plantseg.viewer_napari.widgets import widget_export_image

html = render_widget(widget_export_stacks)
html = render_widget(widget_export_image)
print(html)
70 changes: 46 additions & 24 deletions plantseg/core/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class ImageProperties(BaseModel):
voxel_size: VoxelSize
image_layout: ImageLayout
original_voxel_size: VoxelSize
original_name: str | None = None

@property
def dimensionality(self) -> ImageDimensionality:
Expand Down Expand Up @@ -274,6 +275,8 @@ def from_napari_layer(cls, layer: Image | Labels) -> "PlantSegImage":
raise ValueError("Voxel size not found in metadata")
new_voxel_size = VoxelSize(**metadata["voxel_size"])

original_name = metadata.get("original_name", None)

# Loading from napari layer, the id needs to be present in the metadata
# If not present, the layer is corrupted
if "id" in metadata:
Expand All @@ -287,6 +290,7 @@ def from_napari_layer(cls, layer: Image | Labels) -> "PlantSegImage":
voxel_size=new_voxel_size,
image_layout=image_layout,
original_voxel_size=original_voxel_size,
original_name=original_name,
)

if image_type != properties.image_type:
Expand Down Expand Up @@ -469,6 +473,10 @@ def voxel_size(self) -> VoxelSize:
def original_voxel_size(self) -> VoxelSize:
return self._properties.original_voxel_size

@property
def original_name(self) -> str | None:
return self._properties.original_name

@property
def name(self) -> str:
return self._properties.name
Expand Down Expand Up @@ -575,6 +583,7 @@ def import_image(
voxel_size=voxel_size,
image_layout=image_layout,
original_voxel_size=voxel_size,
original_name=image_name,
)

return PlantSegImage(data=data, properties=image_properties)
Expand Down Expand Up @@ -616,46 +625,59 @@ def _image_postprocessing(

def save_image(
image: PlantSegImage,
directory: Path,
file_name: str,
custom_key: str,
scale_to_origin: bool,
file_format: str = "tiff",
dtype: str = "uint16",
export_directory: Path,
name_pattern: str,
key: str | None = None,
scale_to_origin: bool = True,
export_format: str = "tiff",
data_type: str = "uint16",
) -> None:
"""
Write a PlantSegImage object to disk.
Args:
image (PlantSegImage): Image to save
directory (Path): Directory to save the image
file_name (str): Name of the file
custom_key (str): Custom key to save the image (it will be used as suffix in tiff files or as key in h5 or zarr files)
scale_to_origin (bool): Scale the image to the original voxel size (if different from the current voxel size)
file_format (str): File format to save the image, should be tiff, h5 or zarr
dtype (str): Data type to save the image, should be uint8, uint16, float32 or float64
image (PlantSegImage): input image to be saved to disk
export_directory (Path): output directory path where the image will be saved
name_pattern (str): output file name pattern, can contain the {image_name} or {original_name} tokens
to be replaced in the final file name.
key (str | None): key for the image (used only for h5 and zarr formats).
scale_to_origin (bool): scale the voxel size to the original one
export_format (str): file format (tiff, h5, zarr)
data_type (str): data type to save the image.
"""
data, voxel_size = _image_postprocessing(image, scale_to_origin, dtype)

directory = Path(directory)
data, voxel_size = _image_postprocessing(image, scale_to_origin=scale_to_origin, export_dtype=data_type)

directory = Path(export_directory)
directory.mkdir(parents=True, exist_ok=True)

if file_format == "tiff":
file_path_name = directory / f"{file_name}_{custom_key}.tiff"
name_pattern = name_pattern.replace("{image_name}", image.name)

if image.original_name is not None:
name_pattern = name_pattern.replace("{original_name}", image.original_name)

if export_format == "tiff":
file_path_name = directory / f"{name_pattern}.tiff"
create_tiff(path=file_path_name, stack=data, voxel_size=voxel_size, layout=image.image_layout.value)

elif file_format == "zarr":
file_path_name = directory / f"{file_name}.zarr"
elif export_format == "zarr":
if key is None or key == "":
raise ValueError("Key is required for zarr format")

file_path_name = directory / f"{name_pattern}.zarr"
create_zarr(
path=file_path_name,
stack=data,
voxel_size=voxel_size,
key=custom_key,
key=key,
)

elif file_format == "h5":
file_path_name = directory / f"{file_name}.h5"
create_h5(path=file_path_name, stack=data, voxel_size=voxel_size, key=custom_key)
elif export_format == "h5":
if key is None or key == "":
raise ValueError("Key is required for h5 format")

file_path_name = directory / f"{name_pattern}.h5"
create_h5(path=file_path_name, stack=data, voxel_size=voxel_size, key=key)

else:
raise ValueError(f"File format {file_format} not recognized, should be tiff, h5 or zarr")
raise ValueError(f"Export format {export_format} not recognized, should be tiff, h5 or zarr")
2 changes: 1 addition & 1 deletion plantseg/headless/headless.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def parse_input_config(inputs_config: dict):
list_input_keys[key] = parse_input_path(value)
has_input_path = True

elif key.find("output_dir") != -1:
elif key.find("export_directory") != -1:
single_input_keys[key] = output_directory(value)
has_output_dir = True

Expand Down
54 changes: 20 additions & 34 deletions plantseg/tasks/io_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,60 +59,46 @@ def import_image_task(
@task_tracker(
is_leaf=True,
list_inputs={
"output_directory": TaskUserInput(
"export_directory": TaskUserInput(
allowed_types=['str'],
description="Output directory path where the image will be saved",
headless_default=None,
user_input_required=True,
),
"output_file_name": TaskUserInput(
allowed_types=['str', 'None'],
description="Output file name (if None, the image name will be used)",
headless_default=None,
user_input_required=False,
"name_pattern": TaskUserInput(
allowed_types=['str'], description="Output file name", headless_default=None, user_input_required=False
),
},
)
def export_image_task(
image: PlantSegImage,
output_directory: Path,
output_file_name: str | None = None,
custom_key_suffix: str | None = None,
export_directory: Path,
name_pattern: str = "{original_name}_export",
key: str | None = None,
scale_to_origin: bool = True,
file_format: str = "tiff",
dtype: str = "uint16",
export_format: str = "tiff",
data_type: str = "uint16",
) -> None:
"""
Task wrapper for saving an PlantSegImage object to disk.
Args:
image (PlantSegImage): input image to be saved to disk
output_directory (Path): output directory path where the image will be saved
output_file_name (str | None): output file name (if None, the image name will be used)
custom_key_suffix (str | None): custom key for the image. If format is .h5 or .zarr this key will be used
to create the dataset. If None, the semantic type will be used (raw, segmentation, predictio).
If the image is tiff, the custom key will be added to the file name as a suffix.
If None, the custom key will not be added.
scale_to_origin (bool): scale to origin
file_format (str): file format
dtype (str): data type
export_directory (Path): output directory path where the image will be saved
name_pattern (str): output file name pattern, can contain the {image_name} or {original_name} tokens
to be replaced in the final file name.
key (str | None): key for the image (used only for h5 and zarr formats).
scale_to_origin (bool): scale the voxel size to the original one
export_format (str): file format (tiff, h5, zarr)
data_type (str): data type to save the image.
"""
if output_file_name is None:
output_file_name = image.name

if custom_key_suffix is None:
if file_format == "tiff":
custom_key_suffix = ""
else:
custom_key_suffix = image.semantic_type.value

save_image(
image=image,
directory=output_directory,
file_name=output_file_name,
custom_key=custom_key_suffix,
export_directory=export_directory,
name_pattern=name_pattern,
key=key,
scale_to_origin=scale_to_origin,
file_format=file_format,
dtype=dtype,
export_format=export_format,
data_type=data_type,
)
return None
6 changes: 4 additions & 2 deletions plantseg/viewer_napari/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
widget_cropping,
widget_docs,
widget_dt_ws,
widget_export_stacks,
widget_export_headless_workflow,
widget_export_image,
widget_filter_segmentation,
widget_fix_over_under_segmentation_from_nuclei,
widget_gaussian_smoothing,
Expand All @@ -34,7 +35,8 @@ def get_data_io_tab():
widgets=[
widget_docs,
widget_open_file,
widget_export_stacks,
widget_export_image,
widget_export_headless_workflow,
widget_show_info,
widget_infos,
],
Expand Down
11 changes: 9 additions & 2 deletions plantseg/viewer_napari/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
widget_set_biggest_instance_to_zero,
)
from plantseg.viewer_napari.widgets.docs import widget_docs
from plantseg.viewer_napari.widgets.io import widget_export_stacks, widget_infos, widget_open_file, widget_show_info
from plantseg.viewer_napari.widgets.io import (
widget_export_headless_workflow,
widget_export_image,
widget_infos,
widget_open_file,
widget_show_info,
)
from plantseg.viewer_napari.widgets.prediction import widget_add_custom_model, widget_unet_prediction
from plantseg.viewer_napari.widgets.proofreading import (
widget_add_label_to_corrected,
Expand All @@ -31,7 +37,8 @@
"widget_cropping",
# IO
"widget_open_file",
"widget_export_stacks",
"widget_export_image",
"widget_export_headless_workflow",
"widget_show_info",
"widget_infos",
# Main - Prediction
Expand Down
20 changes: 8 additions & 12 deletions plantseg/viewer_napari/widgets/dataprocessing.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from concurrent.futures import Future
from enum import Enum

from magicgui import magicgui
from napari.layers import Image, Labels, Layer, Shapes
from napari.types import LayerDataTuple

from plantseg.core.image import PlantSegImage
from plantseg.core.voxelsize import VoxelSize
Expand Down Expand Up @@ -49,9 +47,7 @@
"tooltip": "To allow toggle the update of other widgets in unit tests; invisible to users.",
},
)
def widget_gaussian_smoothing(
image: Image, sigma: float = 1.0, update_other_widgets: bool = True
) -> Future[LayerDataTuple]:
def widget_gaussian_smoothing(image: Image, sigma: float = 1.0, update_other_widgets: bool = True) -> None:
"""Apply Gaussian smoothing to an image layer."""

ps_image = PlantSegImage.from_napari_layer(image)
Expand Down Expand Up @@ -103,7 +99,7 @@ def widget_cropping(
crop_roi: Shapes | None = None,
crop_z: tuple[int, int] = (0, 100),
update_other_widgets: bool = True,
) -> Future[LayerDataTuple]:
) -> None:
if crop_roi is not None:
assert len(crop_roi.shape_type) == 1, "Only one rectangle should be used for cropping"
assert crop_roi.shape_type[0] == "rectangle", "Only a rectangle shape should be used for cropping"
Expand Down Expand Up @@ -242,7 +238,7 @@ def widget_rescaling(
reference_shape: tuple[int, int, int] = (1, 1, 1),
order: int = 0,
update_other_widgets: bool = True,
) -> Future[LayerDataTuple]:
) -> None:
"""Rescale an image or label layer."""

if isinstance(image, Image) or isinstance(image, Labels):
Expand Down Expand Up @@ -437,7 +433,7 @@ def _on_rescale_order_changed(order):
)
def widget_remove_false_positives_by_foreground(
segmentation: Labels, foreground: Image, threshold: float = 0.5
) -> Future[LayerDataTuple]:
) -> None:
"""Remove false positives from a segmentation layer using a foreground probability layer."""

ps_segmentation = PlantSegImage.from_napari_layer(segmentation)
Expand Down Expand Up @@ -487,7 +483,7 @@ def widget_fix_over_under_segmentation_from_nuclei(
boundary_pmaps: Image | None = None,
threshold=(33, 66),
quantile=(0.3, 99.9),
) -> Future[LayerDataTuple]:
) -> None:
"""
Widget interface for correcting over- and under-segmentation of cells based on nuclei segmentation.
Expand Down Expand Up @@ -537,7 +533,7 @@ def widget_fix_over_under_segmentation_from_nuclei(


@magicgui(
call_button=f"Relabel Instances",
call_button="Relabel Instances",
segmentation={
"label": "Segmentation",
"tooltip": "Segmentation can be any label layer.",
Expand All @@ -552,7 +548,7 @@ def widget_fix_over_under_segmentation_from_nuclei(
def widget_relabel(
segmentation: Labels,
background: int | None = None,
) -> Future[LayerDataTuple]:
) -> None:
"""Relabel an image layer."""

ps_image = PlantSegImage.from_napari_layer(segmentation)
Expand Down Expand Up @@ -605,7 +601,7 @@ def _on_relabel_segmentation_changed(segmentation: Labels):
def widget_set_biggest_instance_to_zero(
segmentation: Labels,
instance_could_be_zero: bool = False,
) -> Future[LayerDataTuple]:
) -> None:
"""Set the biggest instance to zero in a label layer."""

ps_image = PlantSegImage.from_napari_layer(segmentation)
Expand Down
Loading

0 comments on commit 29d71eb

Please sign in to comment.