Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bioformats2raw export: unify Zarr layout and add OMERO metadata #76

Merged
merged 5 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ repos:
rev: 5.9.3
hooks:
- id: isort
args: ["--profile", "black"]

- repo: https://github.com/psf/black
rev: 21.7b0
Expand Down
58 changes: 43 additions & 15 deletions src/omero_zarr/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
from omero.cli import CLI, BaseControl, Parser, ProxyStringType
from omero.gateway import BlitzGateway, BlitzObjectWrapper
from omero.model import ImageI, PlateI
from zarr.hierarchy import open_group
from zarr.storage import FSStore

from .masks import MASK_DTYPE_SIZE, image_shapes_to_zarr, plate_shapes_to_zarr
from .raw_pixels import image_to_zarr, plate_to_zarr
from .raw_pixels import (
add_omero_metadata,
add_toplevel_metadata,
image_to_zarr,
plate_to_zarr,
)

HELP = """Export data in zarr format.

Expand Down Expand Up @@ -237,19 +244,8 @@ def polygons(self, args: argparse.Namespace) -> None:
def export(self, args: argparse.Namespace) -> None:
if isinstance(args.object, ImageI):
image = self._lookup(self.gateway, "Image", args.object.id)
inplace = image.getInplaceImport()

if args.bf:
if self.client is None:
raise Exception("This cannot happen") # mypy is confused
prx, desc = self.client.getManagedRepository(description=True)
repo_path = Path(desc._path._val) / Path(desc._name._val)
if inplace:
for p in image.getImportedImageFilePaths()["client_paths"]:
self._bf_export(Path("/") / Path(p), args)
else:
for p in image.getImportedImageFilePaths()["server_paths"]:
self._bf_export(repo_path / p, args)
self._bf_export(image, args)
else:
image_to_zarr(image, args)
elif isinstance(args.object, PlateI):
Expand All @@ -266,8 +262,23 @@ def _lookup(
self.ctx.die(110, f"No such {otype}: {oid}")
return obj

def _bf_export(self, abs_path: Path, args: argparse.Namespace) -> None:
def _bf_export(self, image: BlitzObjectWrapper, args: argparse.Namespace) -> None:
if image.getInplaceImport():
p = image.getImportedImageFilePaths()["client_paths"][0]
abs_path = Path("/") / Path(p)
else:
if self.client is None:
raise Exception("This cannot happen") # mypy is confused
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens if the field is ever assigned with a None like in

self.client = None # type: ignore

I don't know if changing that to del self.client keeps mypy and everyone else happy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error is still confusing to me. I don't understand why mypy does not throw the same error with self.gateway which has the same behavior as self.client

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's certainly an element of magic involved, at least partially related to finallys and other if checks.

prx, desc = self.client.getManagedRepository(description=True)
p = image.getImportedImageFilePaths()["server_paths"][0]
abs_path = Path(desc._path._val) / Path(desc._name._val) / Path(p)
target = (Path(args.output) or Path.cwd()) / Path(abs_path).name
image_target = (Path(args.output) or Path.cwd()) / f"{image.id}.zarr"

if target.exists():
self.ctx.die(111, f"{target.resolve()} already exists")
if image_target.exists():
self.ctx.die(111, f"{image_target.resolve()} already exists")

cmd: List[str] = [
"bioformats2raw",
Expand All @@ -283,6 +294,9 @@ def _bf_export(self, abs_path: Path, args: argparse.Namespace) -> None:
cmd.append(f"--resolutions={args.resolutions}")
if args.max_workers:
cmd.append(f"--max_workers={args.max_workers}")
cmd.append(f"--series={image.series}")
cmd.append("--no-root-group")
cmd.append("--no-ome-meta-export")

self.ctx.dbg(" ".join(cmd))
process = subprocess.Popen(
Expand All @@ -294,7 +308,21 @@ def _bf_export(self, abs_path: Path, args: argparse.Namespace) -> None:
if stderr:
self.ctx.err(stderr.decode("utf-8"))
if process.returncode == 0:
self.ctx.out(f"Image exported to {target.resolve()}")
image_source = target / "0"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always 0 despite the series index? Interesting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes --series x,y is removing the series from the root metadata object and hence reindexing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunate, it means that you can't then make use of OME-XML if enabled.

image_source.rename(image_target)
target.rmdir()
self.ctx.out(f"Image exported to {image_target.resolve()}")

# Add OMERO metadata
store = FSStore(
str(image_target.resolve()),
auto_mkdir=False,
normalize_keys=False,
mode="w",
)
root = open_group(store)
add_omero_metadata(root, image)
add_toplevel_metadata(root)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this just sets _creator. Is it almost not more appropriately bf2raw, or a mix?

Copy link
Member Author

@sbesson sbesson Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a mix. There is some Bio-Formats metadata under multiscales/metadata but more could be captured.
Happy to look into capturing that but I am a bit worried you'll ask me to solve #48 ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to, ... 😉



try:
Expand Down
38 changes: 21 additions & 17 deletions src/omero_zarr/raw_pixels.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ def image_to_zarr(image: omero.gateway.ImageWrapper, args: argparse.Namespace) -
store = _open_store(name)
root = open_group(store)
n_levels, axes = add_image(image, root, cache_dir=cache_dir)
add_group_metadata(root, image, axes, n_levels)
add_multiscales_metadata(root, axes, n_levels)
add_omero_metadata(root, image)
add_toplevel_metadata(root)
print("Finished.")

Expand Down Expand Up @@ -268,7 +269,8 @@ def plate_to_zarr(plate: omero.gateway._PlateWrapper, args: argparse.Namespace)
col_group = row_group.require_group(col)
field_group = col_group.require_group(field_name)
n_levels, axes = add_image(img, field_group, cache_dir=cache_dir)
add_group_metadata(field_group, img, axes, n_levels)
add_multiscales_metadata(field_group, axes, n_levels)
add_omero_metadata(field_group, img)
# Update Well metadata after each image
col_group.attrs["well"] = {"images": fields, "version": VERSION}
max_fields = max(max_fields, field + 1)
Expand All @@ -283,26 +285,12 @@ def plate_to_zarr(plate: omero.gateway._PlateWrapper, args: argparse.Namespace)
print("Finished.")


def add_group_metadata(
def add_multiscales_metadata(
zarr_root: Group,
image: Optional[omero.gateway.ImageWrapper],
axes: List[str],
resolutions: int = 1,
) -> None:

if image:
image_data = {
"id": 1,
"channels": [channelMarshal(c) for c in image.getChannels()],
"rdefs": {
"model": (image.isGreyscaleRenderingModel() and "greyscale" or "color"),
"defaultZ": image._re.getDefaultZ(),
"defaultT": image._re.getDefaultT(),
},
"version": VERSION,
}
zarr_root.attrs["omero"] = image_data
image._closeRE()
multiscales = [
{
"version": "0.3",
Expand All @@ -313,6 +301,22 @@ def add_group_metadata(
zarr_root.attrs["multiscales"] = multiscales


def add_omero_metadata(zarr_root: Group, image: omero.gateway.ImageWrapper) -> None:

image_data = {
"id": 1,
"channels": [channelMarshal(c) for c in image.getChannels()],
"rdefs": {
"model": (image.isGreyscaleRenderingModel() and "greyscale" or "color"),
"defaultZ": image._re.getDefaultZ(),
"defaultT": image._re.getDefaultT(),
},
"version": VERSION,
}
zarr_root.attrs["omero"] = image_data
image._closeRE()


def add_toplevel_metadata(zarr_root: Group) -> None:

zarr_root.attrs["_creator"] = {"name": "omero-zarr", "version": __version__}
Expand Down