diff --git a/VERSION.txt b/VERSION.txt index 0f82685..6678432 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.3.7 +0.3.8 diff --git a/src/portal_visualization/builder_factory.py b/src/portal_visualization/builder_factory.py index 9e6c61b..787b055 100644 --- a/src/portal_visualization/builder_factory.py +++ b/src/portal_visualization/builder_factory.py @@ -8,7 +8,8 @@ SeqFISHViewConfBuilder, IMSViewConfBuilder, ImagePyramidViewConfBuilder, - SegImagePyramidViewConfBuilder, + EpicSegImagePyramidViewConfBuilder, + KaggleSegImagePyramidViewConfBuilder, NanoDESIViewConfBuilder, ) from .builders.anndata_builders import ( @@ -34,6 +35,7 @@ def process_hints(hints): is_json = "json_based" in hints is_spatial = "spatial" in hints is_support = "is_support" in hints + is_seg_mask = "segmentation_mask" in hints return ( is_image, @@ -45,6 +47,7 @@ def process_hints(hints): is_json, is_spatial, is_support, + is_seg_mask, ) @@ -66,14 +69,17 @@ def get_view_config_builder(entity, get_entity, parent=None, epic_uuid=None): is_anndata, is_json, is_spatial, - is_support + is_support, + is_seg_mask, ) = process_hints(hints) # vis-lifted image pyramids if parent is not None: # TODO: For now epic (base image's) support datasets doesn't have any hints - if epic_uuid is not None: - return SegImagePyramidViewConfBuilder + if is_seg_mask and epic_uuid: + return EpicSegImagePyramidViewConfBuilder + elif is_seg_mask: + return KaggleSegImagePyramidViewConfBuilder elif is_support and is_image: ancestor_assaytype = get_entity(parent).get("soft_assaytype") diff --git a/src/portal_visualization/builders/imaging_builders.py b/src/portal_visualization/builders/imaging_builders.py index d125fea..51decf8 100644 --- a/src/portal_visualization/builders/imaging_builders.py +++ b/src/portal_visualization/builders/imaging_builders.py @@ -5,18 +5,33 @@ VitessceConfig, MultiImageWrapper, OmeTiffWrapper, + CoordinationLevel as CL, + get_initial_coordination_scope_prefix, + ObsSegmentationsOmeTiffWrapper, ImageOmeTiffWrapper, Component as cm, ) -from ..utils import get_matches, group_by_file_name, get_conf_cells +from ..utils import get_matches, group_by_file_name, get_conf_cells, get_found_images from ..paths import (IMAGE_PYRAMID_DIR, OFFSETS_DIR, SEQFISH_HYB_CYCLE_REGEX, SEQFISH_FILE_REGEX, SEGMENTATION_SUPPORT_IMAGE_SUBDIR, SEGMENTATION_SUBDIR) from .base_builders import ViewConfBuilder +BASE_IMAGE_VIEW_TYPE = 'image' +SEG_IMAGE_VIEW_TYPE = 'seg' +KAGGLE_IMAGE_VIEW_TYPE = 'kaggle-seg' + class AbstractImagingViewConfBuilder(ViewConfBuilder): + def __init__(self, entity, groups_token, assets_endpoint, **kwargs): + self.image_pyramid_regex = None + self.seg_image_pyramid_regex = None + self.use_full_resolution = [] + self.use_physical_size_scaling = False + self.view_type = BASE_IMAGE_VIEW_TYPE + super().__init__(entity, groups_token, assets_endpoint, **kwargs) + def _get_img_and_offset_url(self, img_path, img_dir): """Create a url for the offsets and img. :param str img_path: The path of the image @@ -58,129 +73,155 @@ def _get_img_and_offset_url_seg(self, img_path, img_dir): """ img_url = self._build_assets_url(img_path) - offset_path = f'{SEGMENTATION_SUBDIR}/{OFFSETS_DIR}/{SEGMENTATION_SUPPORT_IMAGE_SUBDIR}' + offsets_path = re.sub(IMAGE_PYRAMID_DIR, OFFSETS_DIR, img_dir) return ( img_url, str( re.sub( r"ome\.tiff?", "offsets.json", - re.sub(img_dir, offset_path, img_url), + re.sub(img_dir, offsets_path, img_url), ) ), ) - def _setup_view_config_raster(self, vc, dataset, disable_3d=[], use_full_resolution=[]): - vc.add_view(cm.SPATIAL, dataset=dataset, x=3, y=0, w=9, h=12).set_props( - useFullResolutionImage=use_full_resolution - ) - vc.add_view(cm.DESCRIPTION, dataset=dataset, x=0, y=8, w=3, h=4) - vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset, x=0, y=0, w=3, h=8).set_props( - disable3d=disable_3d, disableChannelsIfRgbDetected=True - ) - return vc + def _add_segmentation_image(self, dataset): + file_paths_found = self._get_file_paths() + if self.seg_image_pyramid_regex is None: + raise ValueError("seg_image_pyramid_regex is not set. Cannot find segmentation images.") - def _setup_view_config_seg(self, vc, dataset, disable_3d=[], use_full_resolution=[]): - vc.add_view("spatialBeta", dataset=dataset, x=4, y=0, w=8, h=12).set_props( - useFullResolutionImage=use_full_resolution - ) - vc.add_view("layerControllerBeta", dataset=dataset, x=0, y=0, w=4, h=8).set_props( - disable3d=disable_3d, disableChannelsIfRgbDetected=True - ) - return vc + try: + found_images = get_found_images(self.seg_image_pyramid_regex, file_paths_found) + except Exception as e: + raise RuntimeError(f"Error while searching for segmentation images: {e}") + filtered_images = [img for img in found_images if SEGMENTATION_SUPPORT_IMAGE_SUBDIR not in img] -class ImagePyramidViewConfBuilder(AbstractImagingViewConfBuilder): - def __init__(self, entity, groups_token, assets_endpoint, **kwargs): - """Wrapper class for creating a standard view configuration for image pyramids, - i.e for high resolution viz-lifted imaging datasets like - https://portal.hubmapconsortium.org/browse/dataset/dc289471333309925e46ceb9bafafaf4 - """ - self.image_pyramid_regex = IMAGE_PYRAMID_DIR - self.use_full_resolution = [] - self.use_physical_size_scaling = False - super().__init__(entity, groups_token, assets_endpoint, **kwargs) + if not filtered_images: + raise FileNotFoundError(f"Segmentation assay with uuid {self._uuid} has no matching files") - def get_conf_cells(self, **kwargs): - file_paths_found = self._get_file_paths() - found_images = [ - path for path in get_matches( - file_paths_found, self.image_pyramid_regex + r".*\.ome\.tiff?$", + img_url, offsets_url = self._get_img_and_offset_url(filtered_images[0], self.seg_image_pyramid_regex) + if dataset is not None: + dataset.add_object( + ObsSegmentationsOmeTiffWrapper(img_url=img_url, offsets_url=offsets_url, + obs_types_from_channel_names=True, + # coordinate_transformations=[{"type": "scale", "scale": + # [0.377.,0.377,1,1,1]}] # need to read from a file + ) ) - if 'separate/' not in path # Exclude separate/* in MALDI-IMS - ] + + def _setup_view_config(self, vc, dataset, view_type, disable_3d=[], use_full_resolution=[]): + if view_type == BASE_IMAGE_VIEW_TYPE: + vc.add_view(cm.SPATIAL, dataset=dataset, x=3, y=0, w=9, h=12).set_props( + useFullResolutionImage=use_full_resolution + ) + vc.add_view(cm.DESCRIPTION, dataset=dataset, x=0, y=8, w=3, h=4) + vc.add_view(cm.LAYER_CONTROLLER, dataset=dataset, x=0, y=0, w=3, h=8).set_props( + disable3d=disable_3d, disableChannelsIfRgbDetected=True + ) + elif "seg" in view_type: + spatial_view = vc.add_view("spatialBeta", dataset=dataset, x=4, y=0, w=8, h=12).set_props( + useFullResolutionImage=use_full_resolution + ) + lc_view = vc.add_view("layerControllerBeta", dataset=dataset, x=0, y=0, w=4, h=8).set_props( + disable3d=disable_3d, disableChannelsIfRgbDetected=True + ) + # Adding the segmentation mask on top of the image + if view_type == KAGGLE_IMAGE_VIEW_TYPE: + # vc.link_views_by_dict([spatial_view, lc_view]) + # TODO: The image-channel view disappears after the following + vc.link_views_by_dict([spatial_view, lc_view], { + 'imageLayer': CL([{'photometricInterpretation': 'RGB', }]), + }, meta=True, scope_prefix=get_initial_coordination_scope_prefix("A", "image")) + + return vc + + def get_conf_cells_common(self, get_img_and_offset_url_func, **kwargs): + file_paths_found = self._get_file_paths() + found_images = get_found_images(self.image_pyramid_regex, file_paths_found) found_images = sorted(found_images) - if len(found_images) == 0: + if len(found_images) == 0: # pragma: no cover message = f"Image pyramid assay with uuid {self._uuid} has no matching files" raise FileNotFoundError(message) vc = VitessceConfig(name="HuBMAP Data Portal", schema_version=self._schema_version) dataset = vc.add_dataset(name="Visualization Files") - images = [] - for img_path in found_images: - img_url, offsets_url = self._get_img_and_offset_url( - img_path, self.image_pyramid_regex + + if 'seg' in self.view_type: + img_url, offsets_url = get_img_and_offset_url_func(found_images[0], self.image_pyramid_regex) + dataset = dataset.add_object( + ImageOmeTiffWrapper(img_url=img_url, offsets_url=offsets_url, name=Path(found_images[0]).name) ) - images.append( + if self.view_type == KAGGLE_IMAGE_VIEW_TYPE: + self._add_segmentation_image(dataset) + + else: + images = [ OmeTiffWrapper( img_url=img_url, offsets_url=offsets_url, name=Path(img_path).name ) + for img_path in found_images + for img_url, offsets_url in [get_img_and_offset_url_func(img_path, self.image_pyramid_regex)] + ] + dataset.add_object( + MultiImageWrapper(images, use_physical_size_scaling=self.use_physical_size_scaling) ) - dataset = dataset.add_object( - MultiImageWrapper( - images, - use_physical_size_scaling=self.use_physical_size_scaling - ) - ) - vc = self._setup_view_config_raster( - vc, dataset, use_full_resolution=self.use_full_resolution) - conf = vc.to_dict() - # Don't want to render all layers - del conf["datasets"][0]["files"][0]["options"]["renderLayers"] + conf = self._setup_view_config( + vc, + dataset, + self.view_type, + use_full_resolution=self.use_full_resolution).to_dict() + if self.view_type == BASE_IMAGE_VIEW_TYPE: + del conf["datasets"][0]["files"][0]["options"]["renderLayers"] return get_conf_cells(conf) -class SegImagePyramidViewConfBuilder(AbstractImagingViewConfBuilder): - def __init__(self, entity, groups_token, assets_endpoint, **kwargs): - """Wrapper class for creating a standard view configuration for image pyramids for segmenation mask, +class ImagePyramidViewConfBuilder(AbstractImagingViewConfBuilder): + """Wrapper class for creating a standard view configuration for image pyramids, i.e for high resolution viz-lifted imaging datasets like - https://portal.hubmapconsortium.org/browse/dataset/ - """ - self.image_pyramid_regex = f'{SEGMENTATION_SUBDIR}/{IMAGE_PYRAMID_DIR}/{SEGMENTATION_SUPPORT_IMAGE_SUBDIR}' - self.use_full_resolution = [] - self.use_physical_size_scaling = False + https://portal.hubmapconsortium.org/browse/dataset/dc289471333309925e46ceb9bafafaf4 + """ + + def __init__(self, entity, groups_token, assets_endpoint, **kwargs): super().__init__(entity, groups_token, assets_endpoint, **kwargs) + self.image_pyramid_regex = IMAGE_PYRAMID_DIR + self.view_type = BASE_IMAGE_VIEW_TYPE def get_conf_cells(self, **kwargs): - file_paths_found = self._get_file_paths() - found_images = [ - path for path in get_matches( - file_paths_found, self.image_pyramid_regex + r".*\.ome\.tiff?$", - ) - if 'separate/' not in path # Exclude separate/* in MALDI-IMS - ] - found_images = sorted(found_images) - if len(found_images) == 0: # pragma: no cover - message = f"Image pyramid assay with uuid {self._uuid} has no matching files" - raise FileNotFoundError(message) + return self.get_conf_cells_common(self._get_img_and_offset_url, **kwargs) - vc = VitessceConfig(name="HuBMAP Data Portal", schema_version=self._schema_version) - dataset = vc.add_dataset(name="Visualization Files") - # The base-image will always be 1 - if len(found_images) == 1: - img_url, offsets_url = self._get_img_and_offset_url_seg( - found_images[0], self.image_pyramid_regex - ) - image = ImageOmeTiffWrapper( - img_url=img_url, offsets_url=offsets_url, name=Path(found_images[0]).name - ) +class EpicSegImagePyramidViewConfBuilder(AbstractImagingViewConfBuilder): + """Wrapper class for creating a standard view configuration for image pyramids for EPIC segmentation mask, + i.e for high resolution viz-lifted imaging datasets like + https://portal.dev.hubmapconsortium.org/browse/dataset/df7cac7cb67a822f7007b57c4d8f5e7d + """ - dataset = dataset.add_object(image) - vc = self._setup_view_config_seg( - vc, dataset, use_full_resolution=self.use_full_resolution) - conf = vc.to_dict() - return get_conf_cells(conf) + def __init__(self, entity, groups_token, assets_endpoint, **kwargs): + super().__init__(entity, groups_token, assets_endpoint, **kwargs) + self.image_pyramid_regex = f"{SEGMENTATION_SUBDIR}/{IMAGE_PYRAMID_DIR}/{SEGMENTATION_SUPPORT_IMAGE_SUBDIR}" + self.view_type = SEG_IMAGE_VIEW_TYPE + + def get_conf_cells(self, **kwargs): + return self.get_conf_cells_common(self._get_img_and_offset_url_seg, **kwargs) + + +class KaggleSegImagePyramidViewConfBuilder(AbstractImagingViewConfBuilder): + """Wrapper class for creating a standard view configuration for image pyramids for kaggle-2 datasets, that show, + segmentation mask layered over a base image-pyramid, however, the file structure is different than + EPIC segmentation masks (EpicSegImagePyramidViewConfBuilder) + i.e for high resolution viz-lifted imaging datasets like + https://portal.dev.hubmapconsortium.org/browse/dataset/534a590d7336aa99c7fc7afd41e995fc + """ + + def __init__(self, entity, groups_token, assets_endpoint, **kwargs): + super().__init__(entity, groups_token, assets_endpoint, **kwargs) + self.image_pyramid_regex = f"{IMAGE_PYRAMID_DIR}/{SEGMENTATION_SUPPORT_IMAGE_SUBDIR}" + self.seg_image_pyramid_regex = IMAGE_PYRAMID_DIR + self.view_type = KAGGLE_IMAGE_VIEW_TYPE + + def get_conf_cells(self, **kwargs): + return self.get_conf_cells_common(self._get_img_and_offset_url_seg, **kwargs) class IMSViewConfBuilder(ImagePyramidViewConfBuilder): @@ -248,9 +289,10 @@ def get_conf_cells(self, **kwargs): ) ) dataset = dataset.add_object(MultiImageWrapper(image_wrappers)) - vc = self._setup_view_config_raster( + vc = self._setup_view_config( vc, dataset, + self.view_type, disable_3d=[self._get_hybcycle(img_path) for img_path in sorted_images] ) conf = vc.to_dict() diff --git a/src/portal_visualization/builders/sprm_builders.py b/src/portal_visualization/builders/sprm_builders.py index 06c3d59..f59467a 100644 --- a/src/portal_visualization/builders/sprm_builders.py +++ b/src/portal_visualization/builders/sprm_builders.py @@ -118,7 +118,7 @@ def get_conf_cells(self, **kwargs): if self._files[0]["rel_path"] not in file_paths_found: # This tile has no segmentations, # so only show Spatial component without cells sets, genes etc. - vc = self._setup_view_config_raster(vc, dataset, disable_3d=[self._image_name]) + vc = self._setup_view_config(vc, dataset, self.view_type, disable_3d=[self._image_name]) else: # This tile has segmentations so show the analysis results. for file in self._files: diff --git a/src/portal_visualization/client.py b/src/portal_visualization/client.py index 82ad008..3d20e65 100644 --- a/src/portal_visualization/client.py +++ b/src/portal_visualization/client.py @@ -247,7 +247,7 @@ def get_vitessce_conf_cells_and_lifted_uuid( else: # pragma: no cover # We have separate tests for the builder logic try: def get_entity(entity): - if (type(entity) is str): + if (isinstance(entity, str)): return self.get_entity(uuid=entity) return self.get_entity(uuid=entity.get('uuid')) Builder = get_view_config_builder(entity, get_entity, parent, epic_uuid) diff --git a/src/portal_visualization/utils.py b/src/portal_visualization/utils.py index 2b62eed..cc08a76 100644 --- a/src/portal_visualization/utils.py +++ b/src/portal_visualization/utils.py @@ -72,6 +72,16 @@ def _get_cells_from_obj(vc_obj): ] +def get_found_images(image_pyramid_regex, file_paths_found): + found_images = [ + path for path in get_matches( + file_paths_found, image_pyramid_regex + r".*\.ome\.tiff?$", + ) + if 'separate/' not in path + ] + return found_images + + def files_from_response(response_json): ''' >>> response_json = {'hits': {'hits': [ diff --git a/src/vis-preview.py b/src/vis-preview.py index 86ca999..00938f1 100755 --- a/src/vis-preview.py +++ b/src/vis-preview.py @@ -11,10 +11,10 @@ from portal_visualization.builder_factory import get_view_config_builder from portal_visualization.epic_factory import get_epic_builder +defaults = json.load((Path(__file__).parent / 'defaults.json').open()) def main(): # pragma: no cover - defaults = json.load((Path(__file__).parent / 'defaults.json').open()) assets_default_url = defaults['assets_url'] parser = argparse.ArgumentParser(description=''' @@ -53,7 +53,7 @@ def main(): # pragma: no cover parent_uuid = args.parent_uuid headers = get_headers(args.token) - entity = get_entity(args.url, args.json, headers) + entity = get_entity_from_args(args.url, args.json, headers) Builder = get_view_config_builder(entity, get_entity, parent_uuid, epic_uuid) builder = Builder(entity, args.token, args.assets_url) @@ -87,13 +87,29 @@ def main(): # pragma: no cover def get_headers(token): # pragma: no cover + global headers headers = {} if token: headers['Authorization'] = f'Bearer {token}' return headers -def get_entity(url_arg, json_arg, headers): # pragma: no cover +def get_entity(uuid): # pragma: no cover + try: + response = requests.get(f'{defaults["dataset_url"]}{uuid}.json', headers=headers) + if response.status_code != 200: + print(f"Error: Received status code {response.status_code}") + else: + try: + data = response.json() + return data + except Exception as e: + print(f"Error in parsing the response {str(e)}") + except Exception as e: + print(f"Error accessing {defaults['assaytypes_url']}{uuid}: {str(e)}") + + +def get_entity_from_args(url_arg, json_arg, headers): # pragma: no cover if url_arg: response = requests.get(url_arg, headers=headers) if response.status_code == 403: diff --git a/test/assaytype-fixtures/23a25976beb8c02ab589b13a05b28c55.json b/test/assaytype-fixtures/23a25976beb8c02ab589b13a05b28c55.json new file mode 100644 index 0000000..684fceb --- /dev/null +++ b/test/assaytype-fixtures/23a25976beb8c02ab589b13a05b28c55.json @@ -0,0 +1,4 @@ +{ + "soft_assaytype": "h-and-e", + "vitessce-hints": ["segmentation_mask", "pyramid", "is_image"] +} diff --git a/test/assaytype-fixtures/df7cac7cb67a822f7007b57c4d8f5e7d.json b/test/assaytype-fixtures/df7cac7cb67a822f7007b57c4d8f5e7d.json index b54b535..6f1fad4 100644 --- a/test/assaytype-fixtures/df7cac7cb67a822f7007b57c4d8f5e7d.json +++ b/test/assaytype-fixtures/df7cac7cb67a822f7007b57c4d8f5e7d.json @@ -1,3 +1,3 @@ { - "vitessce-hints": ["segmentation_mask", "is_image", "pyramid"] + "vitessce-hints": ["segmentation_mask", "is_image", "pyramid", "epic"] } diff --git a/test/good-fixtures/KaggleSegImagePyramidViewConfBuilder/fake-conf.json b/test/good-fixtures/KaggleSegImagePyramidViewConfBuilder/fake-conf.json new file mode 100644 index 0000000..abe151a --- /dev/null +++ b/test/good-fixtures/KaggleSegImagePyramidViewConfBuilder/fake-conf.json @@ -0,0 +1,97 @@ +{ + "version": "1.0.15", + "name": "HuBMAP Data Portal", + "description": "", + "datasets": [ + { + "uid": "A", + "name": "Visualization Files", + "files": [ + { + "fileType": "image.ome-tiff", + "url": "https://example.com/23a25976beb8c02ab589b13a05b28c55/ometiff-pyramids/lab_processed/images/B001_SB-reg005.ome.tif?token=groups_token", + "options": { + "offsetsUrl": "https://example.com/23a25976beb8c02ab589b13a05b28c55/output_offsets/lab_processed/images/B001_SB-reg005.offsets.json?token=groups_token" + } + }, + { + "fileType": "obsSegmentations.ome-tiff", + "url": "https://example.com/23a25976beb8c02ab589b13a05b28c55/ometiff-pyramids/B001_SB-reg005.segmentations.ome.tif?token=groups_token", + "options": { + "obsTypesFromChannelNames": true, + "offsetsUrl": "https://example.com/23a25976beb8c02ab589b13a05b28c55/output_offsets/B001_SB-reg005.segmentations.offsets.json?token=groups_token" + } + } + ] + } + ], + "coordinationSpace": { + "dataset": { + "A": "A" + }, + "imageLayer": { + "init_A_image_0": "__dummy__" + }, + "photometricInterpretation": { + "init_A_image_0": "RGB" + }, + "metaCoordinationScopes": { + "init_A_image_0": { + "imageLayer": [ + "init_A_image_0" + ] + } + }, + "metaCoordinationScopesBy": { + "init_A_image_0": { + "imageLayer": { + "photometricInterpretation": { + "init_A_image_0": "init_A_image_0" + } + } + } + } + }, + "layout": [ + { + "component": "spatialBeta", + "coordinationScopes": { + "dataset": "A", + "metaCoordinationScopes": [ + "init_A_image_0" + ], + "metaCoordinationScopesBy": [ + "init_A_image_0" + ] + }, + "x": 4, + "y": 0, + "w": 8, + "h": 12, + "props": { + "useFullResolutionImage": [] + } + }, + { + "component": "layerControllerBeta", + "coordinationScopes": { + "dataset": "A", + "metaCoordinationScopes": [ + "init_A_image_0" + ], + "metaCoordinationScopesBy": [ + "init_A_image_0" + ] + }, + "x": 0, + "y": 0, + "w": 4, + "h": 8, + "props": { + "disable3d": [], + "disableChannelsIfRgbDetected": true + } + } + ], + "initStrategy": "auto" +} \ No newline at end of file diff --git a/test/good-fixtures/KaggleSegImagePyramidViewConfBuilder/fake-entity.json b/test/good-fixtures/KaggleSegImagePyramidViewConfBuilder/fake-entity.json new file mode 100644 index 0000000..9d3da74 --- /dev/null +++ b/test/good-fixtures/KaggleSegImagePyramidViewConfBuilder/fake-entity.json @@ -0,0 +1,28 @@ +{ + "data_types": ["Histology"], + "status": "QA", + "immediate_ancestors": [ + { + "data_types": ["Histology"] + } + ], + "files": [ + { + "rel_path": "ometiff-pyramids/lab_processed/images/B001_SB-reg005.ome.tif" + }, + { + "rel_path": "output_offsets/lab_processed/images/B001_SB-reg005.offsets.json" + }, + { + "rel_path": "ometiff-pyramids/B001_SB-reg005.segmentations.ome.tif" + }, + { + "rel_path": "output_offsets/B001_SB-reg005.segmentations.offsets.json" + } + ], + "uuid": "23a25976beb8c02ab589b13a05b28c55", + "metadata": { "dag_provenance_list": [] }, + "parent": { "uuid": "8adc3c31ca84ec4b958ed20a7c4f4919" }, + "soft_assaytype": "h-and-e", + "vitessce-hints": ["segmentation_mask", "pyramid", "is_image"] +} diff --git a/test/good-fixtures/SegmentationMaskBuilder/fake-entity.json b/test/good-fixtures/SegmentationMaskBuilder/fake-entity.json index ec85d0c..6d63c7b 100644 --- a/test/good-fixtures/SegmentationMaskBuilder/fake-entity.json +++ b/test/good-fixtures/SegmentationMaskBuilder/fake-entity.json @@ -39,5 +39,5 @@ "uuid": "df7cac7cb67a822f7007b57c4d8f5e7d", "metadata": {"dag_provenance_list": []}, "parent": { "uuid": "22901da5f080b219a514e38381acbb0e" }, - "vitessce-hints": ["segmentation_mask", "is_image", "pyramid"] + "vitessce-hints": ["segmentation_mask", "is_image", "pyramid", "epic"] } \ No newline at end of file diff --git a/test/test_builders.py b/test/test_builders.py index 7f9d076..ba177db 100644 --- a/test/test_builders.py +++ b/test/test_builders.py @@ -5,16 +5,23 @@ from pathlib import Path from os import environ from dataclasses import dataclass +from unittest.mock import patch import pytest import zarr +from src.portal_visualization.utils import get_found_images from src.portal_visualization.epic_factory import get_epic_builder from src.portal_visualization.builders.base_builders import ConfCells +from src.portal_visualization.builders.imaging_builders import KaggleSegImagePyramidViewConfBuilder from src.portal_visualization.builder_factory import ( get_view_config_builder, has_visualization, ) +from src.portal_visualization.paths import IMAGE_PYRAMID_DIR + +groups_token = environ.get("GROUPS_TOKEN", "groups_token") +assets_url = environ.get("ASSETS_URL", "https://example.com") def str_presenter(dumper, data): @@ -90,7 +97,7 @@ def test_has_visualization(has_vis_entity): # TODO: Once other epic hints exist, this may need to be adjusted epic_uuid = ( entity.get("uuid") - if "segmentation_mask" in entity.get("vitessce-hints", {}) + if "epic" in entity.get("vitessce-hints", {}) else None ) assert has_vis == has_visualization(entity, get_entity, parent, epic_uuid) @@ -153,20 +160,18 @@ def test_entity_to_vitessce_conf(entity_path, mocker): entity = json.loads(entity_path.read_text()) parent = entity.get("parent") or None # Only used for image pyramids assay_type = get_entity(entity["uuid"]) - if "segmentation_mask" in assay_type["vitessce-hints"]: + if "epic" in assay_type["vitessce-hints"]: epic_uuid = entity.get("uuid") Builder = get_view_config_builder(entity, get_entity, parent, epic_uuid) # Envvars should not be set during normal test runs, # but to test the end-to-end integration, they are useful. - groups_token = environ.get("GROUPS_TOKEN", "groups_token") - assets_url = environ.get("ASSETS_URL", "https://example.com") # epic_uuid = environ.get("EPIC_UUID", "epic_uuid") builder = Builder(entity, groups_token, assets_url) conf, cells = builder.get_conf_cells(marker=marker) - if "segmentation_mask" not in assay_type["vitessce-hints"]: + if "epic" not in assay_type["vitessce-hints"]: assert Builder.__name__ == entity_path.parent.name compare_confs(entity_path, conf, cells) - if "segmentation_mask" in assay_type["vitessce-hints"]: + if "epic" in assay_type["vitessce-hints"]: epic_builder = get_epic_builder(epic_uuid) assert epic_builder is not None assert epic_builder.__name__ == entity_path.parent.name @@ -237,6 +242,75 @@ def compare_confs(entity_path, conf, cells): assert yaml.dump(clean_cells(cells)) == yaml.dump(expected_cells) +@pytest.fixture +def mock_seg_image_pyramid_builder(): + class MockBuilder(KaggleSegImagePyramidViewConfBuilder): + def _get_file_paths(self): + return [] + + entity = json.loads( + next( + (Path(__file__).parent / "good-fixtures") + .glob("KaggleSegImagePyramidViewConfBuilder/*-entity.json") + ).read_text() + ) + return MockBuilder(entity, groups_token, assets_url) + + +def test_filtered_images_not_found(mock_seg_image_pyramid_builder): + mock_seg_image_pyramid_builder.seg_image_pyramid_regex = IMAGE_PYRAMID_DIR + try: + mock_seg_image_pyramid_builder._add_segmentation_image(None) + except FileNotFoundError as e: + assert str(e) == f"Segmentation assay with uuid {mock_seg_image_pyramid_builder._uuid} has no matching files" + + +def test_filtered_images_no_regex(mock_seg_image_pyramid_builder): + mock_seg_image_pyramid_builder.seg_image_pyramid_regex = None + try: + mock_seg_image_pyramid_builder._add_segmentation_image(None) + except ValueError as e: + assert str(e) == "seg_image_pyramid_regex is not set. Cannot find segmentation images." + + +def mock_get_found_images(regex, file_paths): + raise ValueError("Simulated failure in get_found_images") + + +def test_runtime_error_in_add_segmentation_image(mock_seg_image_pyramid_builder): + with patch('src.portal_visualization.builders.imaging_builders.get_found_images', + side_effect=mock_get_found_images): + mock_seg_image_pyramid_builder.seg_image_pyramid_regex = "image_pyramid" + + with pytest.raises(RuntimeError) as err: + mock_seg_image_pyramid_builder._add_segmentation_image(None) + + assert "Error while searching for segmentation images" in str(err.value) + assert "Simulated failure in get_found_images" in str(err.value) + + +def test_find_segmentation_images_runtime_error(): + with pytest.raises(RuntimeError) as e: + try: + raise FileNotFoundError("No files found in the directory") + except Exception as err: + raise RuntimeError(f"Error while searching for segmentation images: {err}") + + assert "Error while searching for segmentation images:" in str(e.value) + assert "No files found in the directory" in str(e.value) + + +def test_get_found_images(): + file_paths = [ + "image_pyramid/sample.ome.tiff", + "image_pyramid/sample_separate/sample.ome.tiff", + ] + regex = "image_pyramid" + result = get_found_images(regex, file_paths) + assert len(result) == 1 + assert result[0] == "image_pyramid/sample.ome.tiff" + + if __name__ == "__main__": # pragma: no cover parser = argparse.ArgumentParser(description="Generate fixtures") parser.add_argument("--input", required=True, type=Path, help="Input JSON path")