From 8f47d055ec390389979506f5708edaf739fe3191 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 20 Jun 2022 12:07:15 +0100 Subject: [PATCH 01/13] Re-enable plate labels --- ome_zarr/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 2bba93b8..1a3a369a 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -57,7 +57,7 @@ def __init__( self.specs.append(PlateLabels(self)) elif Plate.matches(zarr): self.specs.append(Plate(self)) - # self.add(zarr, plate_labels=True) + self.add(zarr, plate_labels=True) if Well.matches(zarr): self.specs.append(Well(self)) From 32804fa9ef45e9ceafea3d999f424413dc49498e Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 20 Jun 2022 12:08:40 +0100 Subject: [PATCH 02/13] hard-code etc to get plate labels to show in napari --- ome_zarr/reader.py | 60 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 1a3a369a..6c5acce8 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -559,15 +559,69 @@ def get_tile_path(self, level: int, row: int, col: int) -> str: # pragma: no co """251.zarr/A/1/0/labels/0/3/""" path = ( f"{self.row_names[row]}/{self.col_names[col]}/" - f"{self.first_field}/labels/0/{level}" + f"{self.first_field}/labels/segmentation/{level}" ) return path - def get_pyramid_lazy(self, node: Node) -> None: # pragma: no cover - super().get_pyramid_lazy(node) + def get_pyramid_lazy(self, node: Node) -> None: + """ + Return a pyramid of dask data, where the highest resolution is the + stitched full-resolution images. + """ + self.plate_data = self.lookup("plate", {}) + LOGGER.info("plate_data: %s", self.plate_data) + self.rows = self.plate_data.get("rows") + self.columns = self.plate_data.get("columns") + self.first_field = "0" + self.row_names = [row["name"] for row in self.rows] + self.col_names = [col["name"] for col in self.columns] + + self.well_paths = [well["path"] for well in self.plate_data.get("wells")] + self.well_paths.sort() + + self.row_count = len(self.rows) + self.column_count = len(self.columns) + + # Get the first well... + well_zarr = self.zarr.create(self.well_paths[0]) + well_node = Node(well_zarr, node) + well_spec: Optional[Well] = well_node.first(Well) + if well_spec is None: + raise Exception("could not find first well") + self.numpy_type = well_spec.numpy_type + + LOGGER.debug(f"img_pyramid_shapes: {well_spec.img_pyramid_shapes}") + + # TDDO - get axes from label, not WELL image + self.axes = well_spec.img_metadata["axes"][1:] + # ch_index = [a['type'] for a in self.axes].index('channel') + # print("CHANNEL INDEX", ch_index) + ch_index = 0 + + # Create a dask pyramid for the plate + pyramid = [] + for level, tile_shape in enumerate(well_spec.img_pyramid_shapes): + # remove channel dimension from labels shape + # TODO: better to load label axes directly? + shape_copy = list(tile_shape[:]) + del shape_copy[ch_index] + print(" SHAPE", level, shape_copy) + lazy_plate = self.get_stitched_grid(level, tuple(shape_copy)) + pyramid.append(lazy_plate) + + # Set the node.data to be pyramid view of the plate + node.data = pyramid + # Use the first image's metadata for viewing the whole Plate + node.metadata = well_spec.img_metadata + + # "metadata" dict gets added to each 'plate' layer in napari + node.metadata.update({"metadata": {"plate": self.plate_data}}) + + print("PlateLabels", node, self, node.metadata) # pyramid data may be multi-channel, but we only have 1 labels channel # TODO: when PlateLabels are re-enabled, update the logic to handle # 0.4 axes (list of dictionaries) + print("PlateLabels self.axes", self.axes) if "c" in self.axes: c_index = self.axes.index("c") idx = [slice(None)] * len(self.axes) From 5b421305c693dd93e6d4c1bd49f260a0c5327354 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 20 Jun 2022 13:28:10 +0100 Subject: [PATCH 03/13] Fix get_tile_path to look-up labels/.zattrs --- ome_zarr/reader.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 6c5acce8..2ebd60bc 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -555,13 +555,27 @@ def get_tile(tile_name: str) -> np.ndarray: class PlateLabels(Plate): + def __init__(self, node: Node) -> None: + super().__init__(node) + # cache well/image/labels/.zattrs for first field of each well. Key is e.g. A/1 + self.well_labels_zattrs: Dict[str, Dict] = {} + def get_tile_path(self, level: int, row: int, col: int) -> str: # pragma: no cover - """251.zarr/A/1/0/labels/0/3/""" - path = ( - f"{self.row_names[row]}/{self.col_names[col]}/" - f"{self.first_field}/labels/segmentation/{level}" - ) - return path + """Returns path to .zarray for Well labels, e.g. /A/1/0/labels/my_cells/3/""" + well_key = f"{self.row_names[row]}/{self.col_names[col]}" + labels_attrs = self.well_labels_zattrs.get(well_key) + if labels_attrs is None: + # if not cached, load... + path = f"{well_key}/{self.first_field}/labels/" + LOGGER.info("loading labels/.zattrs: %s.zattrs", path) + first_field_labels = self.zarr.create(path) + # loads labels/.zattrs when new ZarrLocation is created + labels_attrs = first_field_labels.root_attrs + self.well_labels_zattrs[well_key] = labels_attrs + label_paths = labels_attrs.get("labels", []) + if len(label_paths) > 0: + return f"{well_key}/{self.first_field}/labels/{label_paths[0]}/{level}/" + return "" def get_pyramid_lazy(self, node: Node) -> None: """ @@ -647,8 +661,13 @@ def get_pyramid_lazy(self, node: Node) -> None: node.metadata["properties"] = properties def get_numpy_type(self, image_node: Node) -> np.dtype: # pragma: no cover - # FIXME - don't assume Well A1 is valid - path = self.get_tile_path(0, 0, 0) + row_col = self.plate_data.get("wells")[0].get("path").split("/") + row_names = [row["name"] for row in self.plate_data.get("rows")] + col_names = [col["name"] for col in self.plate_data.get("columns")] + row_index = row_names.index(row_col[0]) + col_index = col_names.index(row_col[1]) + path = self.get_tile_path(row_index, col_index, 0) + # it's *possible* path is empty string if first Well has no labels label_zarr = self.zarr.load(path) return label_zarr.dtype From 3492f0d0aa7182a8b58fd0ee137cc9d2a70d0d23 Mon Sep 17 00:00:00 2001 From: William Moore Date: Mon, 20 Jun 2022 16:27:11 +0100 Subject: [PATCH 04/13] Fix PlateLabels and update Plate superclass --- ome_zarr/reader.py | 178 +++++++++++++-------------------------------- 1 file changed, 52 insertions(+), 126 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 2ebd60bc..feaad230 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -465,18 +465,17 @@ def matches(zarr: ZarrLocation) -> bool: def __init__(self, node: Node) -> None: super().__init__(node) LOGGER.debug(f"Plate created with ZarrLocation fmt:{ self.zarr.fmt}") - self.get_pyramid_lazy(node) - def get_pyramid_lazy(self, node: Node) -> None: - """ - Return a pyramid of dask data, where the highest resolution is the - stitched full-resolution images. - """ + self.first_field = "0" self.plate_data = self.lookup("plate", {}) + first_well_path = self.plate_data["wells"][0]["path"] + image_zarr = self.zarr.create(self.get_image_path(first_well_path)) + self.first_well_image = Node(image_zarr, node) + print("first_well_image", self.first_well_image) + LOGGER.info("plate_data: %s", self.plate_data) self.rows = self.plate_data.get("rows") self.columns = self.plate_data.get("columns") - self.first_field = "0" self.row_names = [row["name"] for row in self.rows] self.col_names = [col["name"] for col in self.columns] @@ -486,40 +485,44 @@ def get_pyramid_lazy(self, node: Node) -> None: self.row_count = len(self.rows) self.column_count = len(self.columns) - # Get the first well... - well_zarr = self.zarr.create(self.well_paths[0]) - well_node = Node(well_zarr, node) - well_spec: Optional[Well] = well_node.first(Well) - if well_spec is None: - raise Exception("could not find first well") - self.numpy_type = well_spec.numpy_type + self.get_pyramid_lazy(node) + + def get_pyramid_lazy(self, node: Node) -> None: + """ + Return a pyramid of dask data, where the highest resolution is the + stitched full-resolution images. + """ + + # Use the first well for dtype and shapes + img_data = self.first_well_image.data + img_pyramid_shapes = [d.shape for d in img_data] + level = 0 + self.numpy_type = img_data[level].dtype - LOGGER.debug(f"img_pyramid_shapes: {well_spec.img_pyramid_shapes}") + LOGGER.debug(f"img_pyramid_shapes: {img_pyramid_shapes}") - self.axes = well_spec.img_metadata["axes"] + self.axes = self.first_well_image.metadata["axes"] # Create a dask pyramid for the plate pyramid = [] - for level, tile_shape in enumerate(well_spec.img_pyramid_shapes): + for level, tile_shape in enumerate(img_pyramid_shapes): lazy_plate = self.get_stitched_grid(level, tile_shape) pyramid.append(lazy_plate) # Set the node.data to be pyramid view of the plate node.data = pyramid # Use the first image's metadata for viewing the whole Plate - node.metadata = well_spec.img_metadata + node.metadata = self.first_well_image.metadata # "metadata" dict gets added to each 'plate' layer in napari node.metadata.update({"metadata": {"plate": self.plate_data}}) - def get_numpy_type(self, image_node: Node) -> np.dtype: - return image_node.data[0].dtype + def get_image_path(self, well_path: str) -> str: + return f"{well_path}/{self.first_field}/" def get_tile_path(self, level: int, row: int, col: int) -> str: - return ( - f"{self.row_names[row]}/" - f"{self.col_names[col]}/{self.first_field}/{level}" - ) + well_path = f"{self.row_names[row]}/{self.col_names[col]}" + return f"{self.get_image_path(well_path)}{level}/" def get_stitched_grid(self, level: int, tile_shape: tuple) -> da.core.Array: LOGGER.debug(f"get_stitched_grid() level: {level}, tile_shape: {tile_shape}") @@ -556,121 +559,44 @@ def get_tile(tile_name: str) -> np.ndarray: class PlateLabels(Plate): def __init__(self, node: Node) -> None: - super().__init__(node) # cache well/image/labels/.zattrs for first field of each well. Key is e.g. A/1 self.well_labels_zattrs: Dict[str, Dict] = {} + super().__init__(node) + + # remove image metadata + # node.metadata = {} - def get_tile_path(self, level: int, row: int, col: int) -> str: # pragma: no cover - """Returns path to .zarray for Well labels, e.g. /A/1/0/labels/my_cells/3/""" - well_key = f"{self.row_names[row]}/{self.col_names[col]}" - labels_attrs = self.well_labels_zattrs.get(well_key) + # combine 'properties' from each image + # from https://github.com/ome/ome-zarr-py/pull/61/ + properties: Dict[int, Dict[str, Any]] = {} + for well_path in self.well_paths: + path = self.get_image_path(well_path) + ".zattrs" + labels_json = self.zarr.get_json(path).get("image-label", {}) + # NB: assume that 'label_val' is unique across all images + props_list = labels_json.get("properties", []) + if props_list: + for props in props_list: + label_val = props["label-value"] + properties[label_val] = dict(props) + del properties[label_val]["label-value"] + node.metadata["properties"] = properties + + def get_image_path(self, well_path: str) -> str: + """Returns path to .zattr for Well labels, e.g. /A/1/0/labels/my_cells/""" + labels_attrs = self.well_labels_zattrs.get(well_path) if labels_attrs is None: # if not cached, load... - path = f"{well_key}/{self.first_field}/labels/" + path = f"{well_path}/{self.first_field}/labels/" LOGGER.info("loading labels/.zattrs: %s.zattrs", path) first_field_labels = self.zarr.create(path) # loads labels/.zattrs when new ZarrLocation is created labels_attrs = first_field_labels.root_attrs - self.well_labels_zattrs[well_key] = labels_attrs + self.well_labels_zattrs[well_path] = labels_attrs label_paths = labels_attrs.get("labels", []) if len(label_paths) > 0: - return f"{well_key}/{self.first_field}/labels/{label_paths[0]}/{level}/" + return f"{well_path}/{self.first_field}/labels/{label_paths[0]}/" return "" - def get_pyramid_lazy(self, node: Node) -> None: - """ - Return a pyramid of dask data, where the highest resolution is the - stitched full-resolution images. - """ - self.plate_data = self.lookup("plate", {}) - LOGGER.info("plate_data: %s", self.plate_data) - self.rows = self.plate_data.get("rows") - self.columns = self.plate_data.get("columns") - self.first_field = "0" - self.row_names = [row["name"] for row in self.rows] - self.col_names = [col["name"] for col in self.columns] - - self.well_paths = [well["path"] for well in self.plate_data.get("wells")] - self.well_paths.sort() - - self.row_count = len(self.rows) - self.column_count = len(self.columns) - - # Get the first well... - well_zarr = self.zarr.create(self.well_paths[0]) - well_node = Node(well_zarr, node) - well_spec: Optional[Well] = well_node.first(Well) - if well_spec is None: - raise Exception("could not find first well") - self.numpy_type = well_spec.numpy_type - - LOGGER.debug(f"img_pyramid_shapes: {well_spec.img_pyramid_shapes}") - - # TDDO - get axes from label, not WELL image - self.axes = well_spec.img_metadata["axes"][1:] - # ch_index = [a['type'] for a in self.axes].index('channel') - # print("CHANNEL INDEX", ch_index) - ch_index = 0 - - # Create a dask pyramid for the plate - pyramid = [] - for level, tile_shape in enumerate(well_spec.img_pyramid_shapes): - # remove channel dimension from labels shape - # TODO: better to load label axes directly? - shape_copy = list(tile_shape[:]) - del shape_copy[ch_index] - print(" SHAPE", level, shape_copy) - lazy_plate = self.get_stitched_grid(level, tuple(shape_copy)) - pyramid.append(lazy_plate) - - # Set the node.data to be pyramid view of the plate - node.data = pyramid - # Use the first image's metadata for viewing the whole Plate - node.metadata = well_spec.img_metadata - - # "metadata" dict gets added to each 'plate' layer in napari - node.metadata.update({"metadata": {"plate": self.plate_data}}) - - print("PlateLabels", node, self, node.metadata) - # pyramid data may be multi-channel, but we only have 1 labels channel - # TODO: when PlateLabels are re-enabled, update the logic to handle - # 0.4 axes (list of dictionaries) - print("PlateLabels self.axes", self.axes) - if "c" in self.axes: - c_index = self.axes.index("c") - idx = [slice(None)] * len(self.axes) - idx[c_index] = slice(0, 1) - node.data[0] = node.data[0][tuple(idx)] - # remove image metadata - node.metadata = {} - - # combine 'properties' from each image - # from https://github.com/ome/ome-zarr-py/pull/61/ - properties: Dict[int, Dict[str, Any]] = {} - for row in self.row_names: - for col in self.col_names: - path = f"{row}/{col}/{self.first_field}/labels/0/.zattrs" - labels_json = self.zarr.get_json(path).get("image-label", {}) - # NB: assume that 'label_val' is unique across all images - props_list = labels_json.get("properties", []) - if props_list: - for props in props_list: - label_val = props["label-value"] - properties[label_val] = dict(props) - del properties[label_val]["label-value"] - node.metadata["properties"] = properties - - def get_numpy_type(self, image_node: Node) -> np.dtype: # pragma: no cover - row_col = self.plate_data.get("wells")[0].get("path").split("/") - row_names = [row["name"] for row in self.plate_data.get("rows")] - col_names = [col["name"] for col in self.plate_data.get("columns")] - row_index = row_names.index(row_col[0]) - col_index = col_names.index(row_col[1]) - path = self.get_tile_path(row_index, col_index, 0) - # it's *possible* path is empty string if first Well has no labels - label_zarr = self.zarr.load(path) - return label_zarr.dtype - class Reader: """Parses the given Zarr instance into a collection of Nodes properly ordered From d731b87e269527f9746480da5b661154b724f07d Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 21 Jun 2022 12:25:03 +0100 Subject: [PATCH 05/13] Don't pass plate node to first_well_image to avoid recursion --- ome_zarr/reader.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index feaad230..a5e40a7a 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -470,8 +470,8 @@ def __init__(self, node: Node) -> None: self.plate_data = self.lookup("plate", {}) first_well_path = self.plate_data["wells"][0]["path"] image_zarr = self.zarr.create(self.get_image_path(first_well_path)) - self.first_well_image = Node(image_zarr, node) - print("first_well_image", self.first_well_image) + # Create a Node for image, with no 'root' + self.first_well_image = Node(image_zarr, []) LOGGER.info("plate_data: %s", self.plate_data) self.rows = self.plate_data.get("rows") @@ -501,8 +501,6 @@ def get_pyramid_lazy(self, node: Node) -> None: LOGGER.debug(f"img_pyramid_shapes: {img_pyramid_shapes}") - self.axes = self.first_well_image.metadata["axes"] - # Create a dask pyramid for the plate pyramid = [] for level, tile_shape in enumerate(img_pyramid_shapes): @@ -553,8 +551,8 @@ def get_tile(tile_name: str) -> np.ndarray: lazy_reader(tile_name), shape=tile_shape, dtype=self.numpy_type ) lazy_row.append(lazy_tile) - lazy_rows.append(da.concatenate(lazy_row, axis=len(self.axes) - 1)) - return da.concatenate(lazy_rows, axis=len(self.axes) - 2) + lazy_rows.append(da.concatenate(lazy_row, axis=len(tile_shape) - 1)) + return da.concatenate(lazy_rows, axis=len(tile_shape) - 2) class PlateLabels(Plate): From 180e9d44d4fcd1e072a1c871857f4b986f307698 Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 23 Jun 2022 16:02:37 +0100 Subject: [PATCH 06/13] Avoid recursion: don't create Node with empty img_path creating a zarr, with an empty img_path, then creating a Node from that, resulted in a Plate node - recursively --- ome_zarr/reader.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index a5e40a7a..a6fbfbe1 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -468,10 +468,6 @@ def __init__(self, node: Node) -> None: self.first_field = "0" self.plate_data = self.lookup("plate", {}) - first_well_path = self.plate_data["wells"][0]["path"] - image_zarr = self.zarr.create(self.get_image_path(first_well_path)) - # Create a Node for image, with no 'root' - self.first_well_image = Node(image_zarr, []) LOGGER.info("plate_data: %s", self.plate_data) self.rows = self.plate_data.get("rows") @@ -485,6 +481,14 @@ def __init__(self, node: Node) -> None: self.row_count = len(self.rows) self.column_count = len(self.columns) + img_path = self.get_image_path(self.well_paths[0]) + if not img_path: + # E.g. PlateLabels subclass has no Labels + return + image_zarr = self.zarr.create(img_path) + # Create a Node for image, with no 'root' + self.first_well_image = Node(image_zarr, []) + self.get_pyramid_lazy(node) def get_pyramid_lazy(self, node: Node) -> None: @@ -515,7 +519,7 @@ def get_pyramid_lazy(self, node: Node) -> None: # "metadata" dict gets added to each 'plate' layer in napari node.metadata.update({"metadata": {"plate": self.plate_data}}) - def get_image_path(self, well_path: str) -> str: + def get_image_path(self, well_path: str) -> Optional[str]: return f"{well_path}/{self.first_field}/" def get_tile_path(self, level: int, row: int, col: int) -> str: @@ -568,8 +572,10 @@ def __init__(self, node: Node) -> None: # from https://github.com/ome/ome-zarr-py/pull/61/ properties: Dict[int, Dict[str, Any]] = {} for well_path in self.well_paths: - path = self.get_image_path(well_path) + ".zattrs" - labels_json = self.zarr.get_json(path).get("image-label", {}) + path = self.get_image_path(well_path) + if not path: + continue + labels_json = self.zarr.get_json(path + ".zattrs").get("image-label", {}) # NB: assume that 'label_val' is unique across all images props_list = labels_json.get("properties", []) if props_list: @@ -579,7 +585,7 @@ def __init__(self, node: Node) -> None: del properties[label_val]["label-value"] node.metadata["properties"] = properties - def get_image_path(self, well_path: str) -> str: + def get_image_path(self, well_path: str) -> Optional[str]: """Returns path to .zattr for Well labels, e.g. /A/1/0/labels/my_cells/""" labels_attrs = self.well_labels_zattrs.get(well_path) if labels_attrs is None: @@ -593,7 +599,7 @@ def get_image_path(self, well_path: str) -> str: label_paths = labels_attrs.get("labels", []) if len(label_paths) > 0: return f"{well_path}/{self.first_field}/labels/{label_paths[0]}/" - return "" + return None class Reader: From 5b8156f1e13d08eabba540d302e705a7173b5b78 Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 23 Jun 2022 16:30:00 +0100 Subject: [PATCH 07/13] Update tests - remove PlateLabels omission --- tests/test_reader.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index f5d4a3fd..fd23f232 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -50,8 +50,7 @@ def test_minimal_plate(self): reader = Reader(parse_url(str(self.path))) nodes = list(reader()) - # currently reading plate labels disabled. Only 1 node - assert len(nodes) == 1 + assert len(nodes) == 2 assert len(nodes[0].specs) == 1 assert isinstance(nodes[0].specs[0], Plate) # assert len(nodes[1].specs) == 1 @@ -73,8 +72,7 @@ def test_multiwells_plate(self): reader = Reader(parse_url(str(self.path))) nodes = list(reader()) - # currently reading plate labels disabled. Only 1 node - assert len(nodes) == 1 + assert len(nodes) == 2 assert len(nodes[0].specs) == 1 assert isinstance(nodes[0].specs[0], Plate) # assert len(nodes[1].specs) == 1 From 0bdbdbe5b51244f00e4bc8aca9063de6ac258f4c Mon Sep 17 00:00:00 2001 From: William Moore Date: Thu, 23 Jun 2022 23:19:07 +0100 Subject: [PATCH 08/13] Test reading plate labels --- tests/test_reader.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/test_reader.py b/tests/test_reader.py index fd23f232..128de31e 100644 --- a/tests/test_reader.py +++ b/tests/test_reader.py @@ -4,8 +4,13 @@ from ome_zarr.data import create_zarr from ome_zarr.io import parse_url -from ome_zarr.reader import Node, Plate, Reader -from ome_zarr.writer import write_image, write_plate_metadata, write_well_metadata +from ome_zarr.reader import Node, Plate, PlateLabels, Reader +from ome_zarr.writer import ( + write_image, + write_labels, + write_plate_metadata, + write_well_metadata, +) class TestReader: @@ -68,12 +73,19 @@ def test_multiwells_plate(self): write_well_metadata(well, ["0", "1", "2"]) for field in range(3): image = well.require_group(str(field)) - write_image(zeros((1, 1, 1, 256, 256)), image) + write_image(zeros((256, 256)), image) + + write_labels(zeros((256, 256)), image, name="test_labels") reader = Reader(parse_url(str(self.path))) nodes = list(reader()) assert len(nodes) == 2 assert len(nodes[0].specs) == 1 assert isinstance(nodes[0].specs[0], Plate) - # assert len(nodes[1].specs) == 1 - # assert isinstance(nodes[1].specs[0], PlateLabels) + assert len(nodes[1].specs) == 1 + assert isinstance(nodes[1].specs[0], PlateLabels) + # plate shape is the single image * grid dimensions + plate_shape = (256 * len(row_names), 256 * len(col_names)) + # check largest data for image and labels + assert nodes[0].data[0].shape == plate_shape + assert nodes[1].data[0].shape == plate_shape From 7bb2cb4cff5f846e7179da8c79ef3a4caf2a5595 Mon Sep 17 00:00:00 2001 From: William Moore Date: Tue, 28 Jun 2022 13:10:10 +0100 Subject: [PATCH 09/13] Handle len(first_well_image.data) == 0 for missing Plate labels --- ome_zarr/reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index a6fbfbe1..b0763037 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -501,13 +501,13 @@ def get_pyramid_lazy(self, node: Node) -> None: img_data = self.first_well_image.data img_pyramid_shapes = [d.shape for d in img_data] level = 0 - self.numpy_type = img_data[level].dtype LOGGER.debug(f"img_pyramid_shapes: {img_pyramid_shapes}") # Create a dask pyramid for the plate pyramid = [] for level, tile_shape in enumerate(img_pyramid_shapes): + self.numpy_type = img_data[level].dtype lazy_plate = self.get_stitched_grid(level, tile_shape) pyramid.append(lazy_plate) From dbbd940548ad8a381cd05280cfc2cb29838069b0 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 29 Jun 2022 10:31:27 +0100 Subject: [PATCH 10/13] WIP - trying to support plate.zarr/labels node for PlateLabels --- ome_zarr/reader.py | 52 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index b0763037..3ec37a6e 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -26,7 +26,7 @@ def __init__( zarr: ZarrLocation, root: Union["Node", "Reader", List[ZarrLocation]], visibility: bool = True, - plate_labels: bool = False, + # plate_labels: bool = False, ): self.zarr = zarr self.root = root @@ -53,11 +53,11 @@ def __init__( self.specs.append(Multiscales(self)) if OMERO.matches(zarr): self.specs.append(OMERO(self)) - if plate_labels: + # if plate_labels: + if PlateLabels.matches(zarr): self.specs.append(PlateLabels(self)) elif Plate.matches(zarr): self.specs.append(Plate(self)) - self.add(zarr, plate_labels=True) if Well.matches(zarr): self.specs.append(Well(self)) @@ -136,7 +136,7 @@ def add( visibility = self.visible self.seen.append(zarr) - node = Node(zarr, self, visibility=visibility, plate_labels=plate_labels) + node = Node(zarr, self, visibility=visibility) if prepend: self.pre_nodes.append(node) else: @@ -467,9 +467,10 @@ def __init__(self, node: Node) -> None: LOGGER.debug(f"Plate created with ZarrLocation fmt:{ self.zarr.fmt}") self.first_field = "0" - self.plate_data = self.lookup("plate", {}) + self.plate_data = self.get_plate_zarr().root_attrs.get("plate", {}) LOGGER.info("plate_data: %s", self.plate_data) + print("Plate --- self.plate_data --> ", self.plate_data) self.rows = self.plate_data.get("rows") self.columns = self.plate_data.get("columns") self.row_names = [row["name"] for row in self.rows] @@ -482,15 +483,24 @@ def __init__(self, node: Node) -> None: self.column_count = len(self.columns) img_path = self.get_image_path(self.well_paths[0]) + print("Plate.__init__ - img_path ------> ", img_path) if not img_path: # E.g. PlateLabels subclass has no Labels return - image_zarr = self.zarr.create(img_path) + image_zarr = self.get_plate_zarr().create(img_path) # Create a Node for image, with no 'root' self.first_well_image = Node(image_zarr, []) self.get_pyramid_lazy(node) + # Load possible node data + child_zarr = self.zarr.create("labels") + # This is a 'virtual' path to plate.zarr/labels + node.add(child_zarr, visibility=False) + + def get_plate_zarr(self) -> ZarrLocation: + return self.zarr + def get_pyramid_lazy(self, node: Node) -> None: """ Return a pyramid of dask data, where the highest resolution is the @@ -560,6 +570,20 @@ def get_tile(tile_name: str) -> np.ndarray: class PlateLabels(Plate): + @staticmethod + def matches(zarr: ZarrLocation) -> bool: + print("PlateLabels matches", zarr.path) + # If the path ends in plate/labels... + if not zarr.path.endswith("labels"): + return False + + # and the parent is a plate + path = zarr.path + parent_path = path[: path.rfind("/")] + print("parent_path", parent_path) + parent = zarr.create(parent_path) + return "plate" in parent.root_attrs + def __init__(self, node: Node) -> None: # cache well/image/labels/.zattrs for first field of each well. Key is e.g. A/1 self.well_labels_zattrs: Dict[str, Dict] = {} @@ -585,16 +609,30 @@ def __init__(self, node: Node) -> None: del properties[label_val]["label-value"] node.metadata["properties"] = properties + def get_plate_zarr(self) -> ZarrLocation: + # lookup parent plate + path = self.zarr.path + # remove the /labels + parent_path = path[: path.rfind("/")] + print("get_plate_zarr parent_path", parent_path) + return self.zarr.create(parent_path) + def get_image_path(self, well_path: str) -> Optional[str]: """Returns path to .zattr for Well labels, e.g. /A/1/0/labels/my_cells/""" labels_attrs = self.well_labels_zattrs.get(well_path) + print("PlateLabels get_image_path well_path", well_path) if labels_attrs is None: # if not cached, load... path = f"{well_path}/{self.first_field}/labels/" LOGGER.info("loading labels/.zattrs: %s.zattrs", path) - first_field_labels = self.zarr.create(path) + # print("labels_path", path) + plate_zarr = self.get_plate_zarr() + # print("check plate_zarr", plate_zarr.root_attrs.get("plate", {})) + first_field_labels = plate_zarr.create(path) + # print("first_field_labels zarr", first_field_labels) # loads labels/.zattrs when new ZarrLocation is created labels_attrs = first_field_labels.root_attrs + # print("labels_attrs", labels_attrs) self.well_labels_zattrs[well_path] = labels_attrs label_paths = labels_attrs.get("labels", []) if len(label_paths) > 0: From 8ab3ad6e76443c8414159ed02f8ed6ee03aaa40f Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 29 Jun 2022 11:30:20 +0100 Subject: [PATCH 11/13] Fix loading of PlateLabels --- ome_zarr/reader.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 3ec37a6e..9e75c81f 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -467,10 +467,12 @@ def __init__(self, node: Node) -> None: LOGGER.debug(f"Plate created with ZarrLocation fmt:{ self.zarr.fmt}") self.first_field = "0" - self.plate_data = self.get_plate_zarr().root_attrs.get("plate", {}) + # For Plate, plate_zarr is same as self.zarr, but for PlateLabels + # (node at /plate.zarr/labels) this is the parent at /plate.zarr node. + self.plate_zarr = self.get_plate_zarr() + self.plate_data = self.plate_zarr.root_attrs.get("plate", {}) LOGGER.info("plate_data: %s", self.plate_data) - print("Plate --- self.plate_data --> ", self.plate_data) self.rows = self.plate_data.get("rows") self.columns = self.plate_data.get("columns") self.row_names = [row["name"] for row in self.rows] @@ -483,11 +485,10 @@ def __init__(self, node: Node) -> None: self.column_count = len(self.columns) img_path = self.get_image_path(self.well_paths[0]) - print("Plate.__init__ - img_path ------> ", img_path) if not img_path: # E.g. PlateLabels subclass has no Labels return - image_zarr = self.get_plate_zarr().create(img_path) + image_zarr = self.plate_zarr.create(img_path) # Create a Node for image, with no 'root' self.first_well_image = Node(image_zarr, []) @@ -496,7 +497,7 @@ def __init__(self, node: Node) -> None: # Load possible node data child_zarr = self.zarr.create("labels") # This is a 'virtual' path to plate.zarr/labels - node.add(child_zarr, visibility=False) + node.add(child_zarr) def get_plate_zarr(self) -> ZarrLocation: return self.zarr @@ -546,7 +547,7 @@ def get_tile(tile_name: str) -> np.ndarray: LOGGER.debug(f"LOADING tile... {path} with shape: {tile_shape}") try: - data = self.zarr.load(path) + data = self.plate_zarr.load(path) except ValueError as e: LOGGER.error(f"Failed to load {path}") LOGGER.debug(f"{e}") @@ -572,7 +573,6 @@ def get_tile(tile_name: str) -> np.ndarray: class PlateLabels(Plate): @staticmethod def matches(zarr: ZarrLocation) -> bool: - print("PlateLabels matches", zarr.path) # If the path ends in plate/labels... if not zarr.path.endswith("labels"): return False @@ -580,7 +580,6 @@ def matches(zarr: ZarrLocation) -> bool: # and the parent is a plate path = zarr.path parent_path = path[: path.rfind("/")] - print("parent_path", parent_path) parent = zarr.create(parent_path) return "plate" in parent.root_attrs @@ -614,27 +613,22 @@ def get_plate_zarr(self) -> ZarrLocation: path = self.zarr.path # remove the /labels parent_path = path[: path.rfind("/")] - print("get_plate_zarr parent_path", parent_path) return self.zarr.create(parent_path) def get_image_path(self, well_path: str) -> Optional[str]: """Returns path to .zattr for Well labels, e.g. /A/1/0/labels/my_cells/""" labels_attrs = self.well_labels_zattrs.get(well_path) - print("PlateLabels get_image_path well_path", well_path) if labels_attrs is None: # if not cached, load... path = f"{well_path}/{self.first_field}/labels/" LOGGER.info("loading labels/.zattrs: %s.zattrs", path) - # print("labels_path", path) plate_zarr = self.get_plate_zarr() - # print("check plate_zarr", plate_zarr.root_attrs.get("plate", {})) first_field_labels = plate_zarr.create(path) - # print("first_field_labels zarr", first_field_labels) # loads labels/.zattrs when new ZarrLocation is created labels_attrs = first_field_labels.root_attrs - # print("labels_attrs", labels_attrs) self.well_labels_zattrs[well_path] = labels_attrs label_paths = labels_attrs.get("labels", []) + LOGGER.debug("label_paths: %s", label_paths) if len(label_paths) > 0: return f"{well_path}/{self.first_field}/labels/{label_paths[0]}/" return None From 1f1067accec717c5caf0c1dc05cd5f61da44cea6 Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 29 Jun 2022 11:50:22 +0100 Subject: [PATCH 12/13] Prevent creation of node at plate.zarr/labels/labels --- ome_zarr/reader.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 9e75c81f..83bcd3df 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -494,10 +494,11 @@ def __init__(self, node: Node) -> None: self.get_pyramid_lazy(node) - # Load possible node data - child_zarr = self.zarr.create("labels") - # This is a 'virtual' path to plate.zarr/labels - node.add(child_zarr) + # Load possible node data IF this is a Plate + if Plate.matches(self.zarr): + child_zarr = self.zarr.create("labels") + # This is a 'virtual' path to plate.zarr/labels + node.add(child_zarr) def get_plate_zarr(self) -> ZarrLocation: return self.zarr From 2b71cd7e56089821f324df371a84e31186cbf43c Mon Sep 17 00:00:00 2001 From: William Moore Date: Wed, 29 Jun 2022 12:10:02 +0100 Subject: [PATCH 13/13] Use os.path.dirname to fix Windows test failures --- ome_zarr/reader.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ome_zarr/reader.py b/ome_zarr/reader.py index 83bcd3df..33369370 100644 --- a/ome_zarr/reader.py +++ b/ome_zarr/reader.py @@ -2,6 +2,7 @@ import logging import math +import os from abc import ABC from typing import Any, Dict, Iterator, List, Optional, Type, Union, cast, overload @@ -579,8 +580,7 @@ def matches(zarr: ZarrLocation) -> bool: return False # and the parent is a plate - path = zarr.path - parent_path = path[: path.rfind("/")] + parent_path = os.path.dirname(zarr.path) parent = zarr.create(parent_path) return "plate" in parent.root_attrs @@ -610,10 +610,8 @@ def __init__(self, node: Node) -> None: node.metadata["properties"] = properties def get_plate_zarr(self) -> ZarrLocation: - # lookup parent plate - path = self.zarr.path - # remove the /labels - parent_path = path[: path.rfind("/")] + # lookup parent plate, remove the /labels + parent_path = os.path.dirname(self.zarr.path) return self.zarr.create(parent_path) def get_image_path(self, well_path: str) -> Optional[str]: