From cbc8192af73973db4c14298ef168071d621358c8 Mon Sep 17 00:00:00 2001 From: Tony Tung Date: Thu, 21 Mar 2019 11:35:01 -0700 Subject: [PATCH] Restructure the relationship between PipelineComponent and AlgorithmBase Rather than each specific PipelineComponent have a link to the AlgorithmBase that describes the contract that the algorithm must implement, each AlgorithmBase implementation points to the PipelineComponent that it serves. The current arrangement forces all the algorithms to be defined and evaluated in the interpreter before the pipeline component is defined and evaluated. If we want to create an algorithm that's only used in tests, this is not possible. The new arrangement ensures that any implementation of a specific AlgorithmBase is registered with its corresponding PipelineComponent. This adds explicit abstract classes to the AlgorithmBase class hierarchy to make it easier to detect what's an actual algorithm implementation. Depends on #1094 Test plan: `make -j fast` --- starfish/image/_filter/__init__.py | 43 +---------- starfish/image/_filter/_base.py | 41 ++++++++++ starfish/image/_registration/__init__.py | 44 +---------- starfish/image/_registration/_base.py | 43 ++++++++++- starfish/image/_segmentation/__init__.py | 51 +------------ starfish/image/_segmentation/_base.py | 49 ++++++++++++ starfish/pipeline/algorithmbase.py | 38 ++++++++-- starfish/pipeline/pipelinecomponent.py | 34 +-------- starfish/spots/_decoder/__init__.py | 47 +----------- starfish/spots/_decoder/_base.py | 46 ++++++++++++ starfish/spots/_detector/__init__.py | 75 +------------------ starfish/spots/_detector/_base.py | 73 +++++++++++++++++- starfish/spots/_pixel_decoder/__init__.py | 53 +------------ starfish/spots/_pixel_decoder/_base.py | 57 ++++++++++++-- starfish/spots/_target_assignment/__init__.py | 51 +------------ starfish/spots/_target_assignment/_base.py | 48 ++++++++++++ 16 files changed, 392 insertions(+), 401 deletions(-) diff --git a/starfish/image/_filter/__init__.py b/starfish/image/_filter/__init__.py index 7f8e259bd..1eccbe0ba 100644 --- a/starfish/image/_filter/__init__.py +++ b/starfish/image/_filter/__init__.py @@ -1,42 +1,3 @@ -from typing import Type - -from starfish.imagestack.imagestack import ImageStack -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.util import click -from . import _base +from starfish.pipeline import import_all_submodules +from ._base import Filter import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "filter" - - -class Filter(PipelineComponent): - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return _base.FilterAlgorithmBase - - @classmethod - def _cli_run(cls, ctx, instance): - output = ctx.obj["output"] - stack = ctx.obj["stack"] - filtered = instance.run(stack) - filtered.export(output) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("-i", "--input", type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.pass_context - def _cli(ctx, input, output): - """smooth, sharpen, denoise, etc""" - print("Filtering images...") - ctx.obj = dict( - component=Filter, - input=input, - output=output, - stack=ImageStack.from_path_or_url(input), - ) diff --git a/starfish/image/_filter/_base.py b/starfish/image/_filter/_base.py index dfa2ad585..c15969105 100644 --- a/starfish/image/_filter/_base.py +++ b/starfish/image/_filter/_base.py @@ -1,8 +1,49 @@ +from abc import abstractmethod +from typing import Type + from starfish.imagestack.imagestack import ImageStack from starfish.pipeline.algorithmbase import AlgorithmBase +from starfish.pipeline.pipelinecomponent import PipelineComponent +from starfish.util import click + + +COMPONENT_NAME = "filter" + + +class Filter(PipelineComponent): + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + output = ctx.obj["output"] + stack = ctx.obj["stack"] + filtered = instance.run(stack) + filtered.export(output) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("-i", "--input", type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.pass_context + def _cli(ctx, input, output): + """smooth, sharpen, denoise, etc""" + print("Filtering images...") + ctx.obj = dict( + component=Filter, + input=input, + output=output, + stack=ImageStack.from_path_or_url(input), + ) class FilterAlgorithmBase(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return Filter + + @abstractmethod def run(self, stack: ImageStack) -> ImageStack: """Performs filtering on an ImageStack.""" raise NotImplementedError() diff --git a/starfish/image/_registration/__init__.py b/starfish/image/_registration/__init__.py index aa26ec7aa..0322ec76d 100644 --- a/starfish/image/_registration/__init__.py +++ b/starfish/image/_registration/__init__.py @@ -1,43 +1,3 @@ -from typing import Type - -from starfish.imagestack.imagestack import ImageStack -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.util import click -from . import _base +from starfish.pipeline import import_all_submodules +from ._base import Registration import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "registration" - - -class Registration(PipelineComponent): - - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return _base.RegistrationAlgorithmBase - - @classmethod - def _cli_run(cls, ctx, instance): - output = ctx.obj["output"] - stack = ctx.obj["stack"] - instance.run(stack) - stack.export(output) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("-i", "--input", type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.pass_context - def _cli(ctx, input, output): - """translation correction of image stacks""" - print("Registering...") - ctx.obj = dict( - component=Registration, - input=input, - output=output, - stack=ImageStack.from_path_or_url(input), - ) diff --git a/starfish/image/_registration/_base.py b/starfish/image/_registration/_base.py index ad1bf9801..18bc33478 100644 --- a/starfish/image/_registration/_base.py +++ b/starfish/image/_registration/_base.py @@ -1,10 +1,51 @@ -from typing import Optional +from abc import abstractmethod +from typing import Optional, Type + +import click from starfish.imagestack.imagestack import ImageStack +from starfish.pipeline import PipelineComponent from starfish.pipeline.algorithmbase import AlgorithmBase +COMPONENT_NAME = "registration" + + +class Registration(PipelineComponent): + + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + output = ctx.obj["output"] + stack = ctx.obj["stack"] + instance.run(stack) + stack.export(output) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("-i", "--input", type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.pass_context + def _cli(ctx, input, output): + """translation correction of image stacks""" + print("Registering...") + ctx.obj = dict( + component=Registration, + input=input, + output=output, + stack=ImageStack.from_path_or_url(input), + ) + + class RegistrationAlgorithmBase(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return Registration + + @abstractmethod def run(self, stack) -> Optional[ImageStack]: """Performs registration on the stack provided.""" raise NotImplementedError() diff --git a/starfish/image/_segmentation/__init__.py b/starfish/image/_segmentation/__init__.py index abdbdab2d..eaff872f4 100644 --- a/starfish/image/_segmentation/__init__.py +++ b/starfish/image/_segmentation/__init__.py @@ -1,50 +1,3 @@ -from typing import Type - -from skimage.io import imsave - -from starfish.imagestack.imagestack import ImageStack -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.util import click -from . import _base +from starfish.pipeline import import_all_submodules +from ._base import Segmentation import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "segment" - - -class Segmentation(PipelineComponent): - - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return _base.SegmentationAlgorithmBase - - @classmethod - def _cli_run(cls, ctx, instance): - output = ctx.obj["output"] - pri_stack = ctx.obj["primary_images"] - nuc_stack = ctx.obj["nuclei"] - - label_image = instance.run(pri_stack, nuc_stack) - - print(f"Writing label image to {output}") - imsave(output, label_image) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("--primary-images", required=True, type=click.Path(exists=True)) - @click.option("--nuclei", required=True, type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.pass_context - def _cli(ctx, primary_images, nuclei, output): - """define polygons for cell boundaries and assign spots""" - print('Segmenting ...') - ctx.obj = dict( - component=Segmentation, - output=output, - primary_images=ImageStack.from_path_or_url(primary_images), - nuclei=ImageStack.from_path_or_url(nuclei), - ) diff --git a/starfish/image/_segmentation/_base.py b/starfish/image/_segmentation/_base.py index d6f438705..828e178eb 100644 --- a/starfish/image/_segmentation/_base.py +++ b/starfish/image/_segmentation/_base.py @@ -1,8 +1,57 @@ +from abc import abstractmethod +from typing import Type + +import click +from skimage.io import imsave + from starfish.imagestack.imagestack import ImageStack +from starfish.pipeline import PipelineComponent from starfish.pipeline.algorithmbase import AlgorithmBase +COMPONENT_NAME = "segment" + + +class Segmentation(PipelineComponent): + + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + output = ctx.obj["output"] + pri_stack = ctx.obj["primary_images"] + nuc_stack = ctx.obj["nuclei"] + + label_image = instance.run(pri_stack, nuc_stack) + + print(f"Writing label image to {output}") + imsave(output, label_image) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("--primary-images", required=True, type=click.Path(exists=True)) + @click.option("--nuclei", required=True, type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.pass_context + def _cli(ctx, primary_images, nuclei, output): + """define polygons for cell boundaries and assign spots""" + print('Segmenting ...') + ctx.obj = dict( + component=Segmentation, + output=output, + primary_images=ImageStack.from_path_or_url(primary_images), + nuclei=ImageStack.from_path_or_url(nuclei), + ) + + class SegmentationAlgorithmBase(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return Segmentation + + @abstractmethod def run(self, primary_image_stack: ImageStack, nuclei_stack: ImageStack): """Performs registration on the stack provided.""" raise NotImplementedError() diff --git a/starfish/pipeline/algorithmbase.py b/starfish/pipeline/algorithmbase.py index 0eabe9b0a..38f6108d2 100644 --- a/starfish/pipeline/algorithmbase.py +++ b/starfish/pipeline/algorithmbase.py @@ -1,29 +1,43 @@ +import inspect +from abc import ABCMeta, abstractmethod +from typing import Type + from starfish.imagestack.imagestack import ImageStack from starfish.intensity_table.intensity_table import IntensityTable from starfish.types import LOG from starfish.types._constants import STARFISH_EXTRAS_KEY from starfish.util.logging import LogEncoder +from .pipelinecomponent import PipelineComponent -class AlgorithmBaseType(type): - +class AlgorithmBaseType(ABCMeta): def __init__(cls, name, bases, namespace): super().__init__(name, bases, namespace) - if len(bases) != 0: - # this is _not_ AlgorithmBase. Instead, it's a subclass of AlgorithmBase. + if not inspect.isabstract(cls): + AlgorithmBaseType.register_with_pipeline_component(cls) cls.run = AlgorithmBaseType.run_with_logging(cls.run) + @staticmethod + def register_with_pipeline_component(algorithm_cls): + pipeline_component_cls = algorithm_cls.get_pipeline_component_class() + if pipeline_component_cls._algorithm_to_class_map_int is None: + pipeline_component_cls._algorithm_to_class_map_int = {} + pipeline_component_cls._algorithm_to_class_map_int[algorithm_cls.__name__] = algorithm_cls + setattr(pipeline_component_cls, algorithm_cls._get_algorithm_name(), algorithm_cls) + + pipeline_component_cls._cli.add_command(algorithm_cls._cli) + @staticmethod def run_with_logging(func): """ This method extends each pipeline component.run() method to also log itself and - runtime parameters to the IntensityTable and Imagestack objects. There are two + runtime parameters to the IntensityTable and ImageStack objects. There are two scenarios for this method: 1.) Filtering: - Imagestack -> Imagestack + ImageStack -> ImageStack 2.) Spot Detection: - Imagestack -> IntensityTable - Imagestack -> [IntenistyTable, ConnectedComponentDecodingResult] + ImageStack -> IntensityTable + ImageStack -> [IntensityTable, ConnectedComponentDecodingResult] TODO segmentation and decoding """ def helper(*args, **kwargs): @@ -91,3 +105,11 @@ def _get_algorithm_name(cls): https://docs.python.org/3/reference/lexical_analysis.html#identifiers """ return cls.__name__ + + @classmethod + @abstractmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + """ + Returns the class of PipelineComponent this algorithm implements. + """ + raise NotImplementedError() diff --git a/starfish/pipeline/pipelinecomponent.py b/starfish/pipeline/pipelinecomponent.py index 555842c56..549c747f4 100644 --- a/starfish/pipeline/pipelinecomponent.py +++ b/starfish/pipeline/pipelinecomponent.py @@ -1,10 +1,7 @@ -import collections import importlib from pathlib import Path from typing import Mapping, MutableMapping, Optional, Set, Type -from .algorithmbase import AlgorithmBase - class PipelineComponentType(type): """ @@ -15,8 +12,6 @@ def __init__(cls, name, bases, namespace): super().__init__(name, bases, namespace) if len(bases) != 0: # this is _not_ PipelineComponent. Instead, it's a subclass of PipelineComponent. - PipelineComponentType._ensure_algorithms_setup(cls) - PipelineComponentType._cli_register(cls) PipelineComponentType._register_pipeline_component_type_name(cls) _pipeline_component_type_name_to_class_map: MutableMapping[str, Type["PipelineComponent"]] = \ @@ -31,25 +26,6 @@ def _register_pipeline_component_type_name(mcs, cls: Type["PipelineComponent"]) def get_pipeline_component_type_by_name(name: str) -> Type["PipelineComponent"]: return PipelineComponentType._pipeline_component_type_name_to_class_map[name] - @classmethod - def _ensure_algorithms_setup(mcs, cls): - if cls._algorithm_to_class_map_int is None: - cls._algorithm_to_class_map_int = dict() - - queue = collections.deque(cls._get_algorithm_base_class().__subclasses__()) - while len(queue) > 0: - algorithm_cls = queue.popleft() - queue.extend(algorithm_cls.__subclasses__()) - - cls._algorithm_to_class_map_int[algorithm_cls.__name__] = algorithm_cls - - setattr(cls, algorithm_cls._get_algorithm_name(), algorithm_cls) - - @classmethod - def _cli_register(mcs, cls): - for algorithm_cls in cls._algorithm_to_class_map().values(): - cls._cli.add_command(algorithm_cls._cli) - class PipelineComponent(metaclass=PipelineComponentType): """ @@ -88,14 +64,6 @@ def pipeline_component_type_name(cls) -> str: """ raise NotImplementedError() - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - """ - Get the base class that algorithms which implement this pipeline stage must extend. - Pipeline components must provide this method. - """ - raise NotImplementedError() - @classmethod def _algorithm_to_class_map(cls) -> Mapping[str, Type]: """Returns a mapping from algorithm names to the classes that implement them.""" @@ -103,7 +71,7 @@ def _algorithm_to_class_map(cls) -> Mapping[str, Type]: return cls._algorithm_to_class_map_int @classmethod - def _cli_run(cls, ctx, instance, *args, **kwargs): + def _cli_run(cls, ctx, instance): raise NotImplementedError() diff --git a/starfish/spots/_decoder/__init__.py b/starfish/spots/_decoder/__init__.py index 3e90f539a..039bb5921 100644 --- a/starfish/spots/_decoder/__init__.py +++ b/starfish/spots/_decoder/__init__.py @@ -1,46 +1,3 @@ -from typing import Type - -from starfish.codebook.codebook import Codebook -from starfish.intensity_table.intensity_table import IntensityTable -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.util import click -from . import _base +from starfish.pipeline import import_all_submodules +from ._base import Decoder import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "decode" - - -class Decoder(PipelineComponent): - - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return _base.DecoderAlgorithmBase - - @classmethod - def _cli_run(cls, ctx, instance): - table = ctx.obj["intensities"] - codes = ctx.obj["codebook"] - output = ctx.obj["output"] - intensities = instance.run(table, codes) - intensities.save(output) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("-i", "--input", required=True, type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.option("--codebook", required=True, type=click.Path(exists=True)) - @click.pass_context - def _cli(ctx, input, output, codebook): - """assign genes to spots""" - ctx.obj = dict( - component=Decoder, - input=input, - output=output, - intensities=IntensityTable.load(input), - codebook=Codebook.from_json(codebook), - ) diff --git a/starfish/spots/_decoder/_base.py b/starfish/spots/_decoder/_base.py index 10b405647..24fa8c65f 100644 --- a/starfish/spots/_decoder/_base.py +++ b/starfish/spots/_decoder/_base.py @@ -1,7 +1,53 @@ +from abc import abstractmethod +from typing import Type + +import click + +from starfish.codebook.codebook import Codebook +from starfish.intensity_table.intensity_table import IntensityTable from starfish.pipeline.algorithmbase import AlgorithmBase +from starfish.pipeline.pipelinecomponent import PipelineComponent + + +COMPONENT_NAME = "decode" + + +class Decoder(PipelineComponent): + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + table = ctx.obj["intensities"] + codes = ctx.obj["codebook"] + output = ctx.obj["output"] + intensities = instance.run(table, codes) + intensities.save(output) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("-i", "--input", required=True, type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.option("--codebook", required=True, type=click.Path(exists=True)) + @click.pass_context + def _cli(ctx, input, output, codebook): + """assign genes to spots""" + ctx.obj = dict( + component=Decoder, + input=input, + output=output, + intensities=IntensityTable.load(input), + codebook=Codebook.from_json(codebook), + ) class DecoderAlgorithmBase(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return Decoder + + @abstractmethod def run(self, encoded, codebook): """Performs decoding on the spots found, using the codebook specified.""" raise NotImplementedError() diff --git a/starfish/spots/_detector/__init__.py b/starfish/spots/_detector/__init__.py index 0f9158a59..65dc5ed44 100644 --- a/starfish/spots/_detector/__init__.py +++ b/starfish/spots/_detector/__init__.py @@ -1,74 +1,3 @@ -from typing import Type - -from starfish.imagestack.imagestack import ImageStack -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.types import Axes -from starfish.util import click -from . import _base +from starfish.pipeline import import_all_submodules +from ._base import SpotFinder import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "detect_spots" - - -class SpotFinder(PipelineComponent): - - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return _base.SpotFinderAlgorithmBase - - @classmethod - def _cli_run(cls, ctx, instance): - output = ctx.obj["output"] - blobs_stack = ctx.obj["blobs_stack"] - image_stack = ctx.obj["image_stack"] - ref_image = ctx.obj["reference_image_from_max_projection"] - if blobs_stack is not None: - blobs_stack = ImageStack.from_path_or_url(blobs_stack) # type: ignore - mp = blobs_stack.max_proj(Axes.ROUND, Axes.CH) - mp_numpy = mp._squeezed_numpy(Axes.ROUND, Axes.CH) - intensities = instance.run( - image_stack, - blobs_image=mp_numpy, - reference_image_from_max_projection=ref_image, - ) - else: - intensities = instance.run(image_stack) - - # When run() returns a tuple, we only save the intensities for now - # TODO ambrosejcarr find a way to save arbitrary detector results - if isinstance(intensities, tuple): - intensities = intensities[0] - intensities.save(output) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("-i", "--input", required=True, type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.option( - '--blobs-stack', default=None, required=False, help=( - 'ImageStack that contains the blobs. Will be max-projected across imaging round ' - 'and channel to produce the blobs_image' - ) - ) - @click.option( - '--reference-image-from-max-projection', default=False, is_flag=True, help=( - 'Construct a reference image by max projecting imaging rounds and channels. Spots ' - 'are found in this image and then measured across all images in the input stack.' - ) - ) - @click.pass_context - def _cli(ctx, input, output, blobs_stack, reference_image_from_max_projection): - """detect spots""" - print('Detecting Spots ...') - ctx.obj = dict( - component=SpotFinder, - image_stack=ImageStack.from_path_or_url(input), - output=output, - blobs_stack=blobs_stack, - reference_image_from_max_projection=reference_image_from_max_projection, - ) diff --git a/starfish/spots/_detector/_base.py b/starfish/spots/_detector/_base.py index f8ad0ec44..2479b8e4b 100644 --- a/starfish/spots/_detector/_base.py +++ b/starfish/spots/_detector/_base.py @@ -1,15 +1,83 @@ -from typing import Any, Callable, Sequence, Tuple, Union +from abc import abstractmethod +from typing import Any, Callable, Sequence, Tuple, Type, Union +import click import numpy as np import xarray as xr from starfish.imagestack.imagestack import ImageStack from starfish.intensity_table.intensity_table import IntensityTable from starfish.pipeline.algorithmbase import AlgorithmBase -from starfish.types import Number, SpotAttributes +from starfish.pipeline.pipelinecomponent import PipelineComponent +from starfish.types import Axes, Number, SpotAttributes + +COMPONENT_NAME = "detect_spots" + + +class SpotFinder(PipelineComponent): + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + output = ctx.obj["output"] + blobs_stack = ctx.obj["blobs_stack"] + image_stack = ctx.obj["image_stack"] + ref_image = ctx.obj["reference_image_from_max_projection"] + if blobs_stack is not None: + blobs_stack = ImageStack.from_path_or_url(blobs_stack) # type: ignore + mp = blobs_stack.max_proj(Axes.ROUND, Axes.CH) + mp_numpy = mp._squeezed_numpy(Axes.ROUND, Axes.CH) + intensities = instance.run( + image_stack, + blobs_image=mp_numpy, + reference_image_from_max_projection=ref_image, + ) + else: + intensities = instance.run(image_stack) + + # When run() returns a tuple, we only save the intensities for now + # TODO ambrosejcarr find a way to save arbitrary detector results + if isinstance(intensities, tuple): + intensities = intensities[0] + intensities.save(output) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("-i", "--input", required=True, type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.option( + '--blobs-stack', default=None, required=False, help=( + 'ImageStack that contains the blobs. Will be max-projected across imaging round ' + 'and channel to produce the blobs_image' + ) + ) + @click.option( + '--reference-image-from-max-projection', default=False, is_flag=True, help=( + 'Construct a reference image by max projecting imaging rounds and channels. Spots ' + 'are found in this image and then measured across all images in the input stack.' + ) + ) + @click.pass_context + def _cli(ctx, input, output, blobs_stack, reference_image_from_max_projection): + """detect spots""" + print('Detecting Spots ...') + ctx.obj = dict( + component=SpotFinder, + image_stack=ImageStack.from_path_or_url(input), + output=output, + blobs_stack=blobs_stack, + reference_image_from_max_projection=reference_image_from_max_projection, + ) class SpotFinderAlgorithmBase(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return SpotFinder + + @abstractmethod def run( self, primary_image: ImageStack, @@ -17,6 +85,7 @@ def run( """Finds spots in an ImageStack""" raise NotImplementedError() + @abstractmethod def image_to_spots(self, data_image: Union[np.ndarray, xr.DataArray]) -> SpotAttributes: """Finds spots in a 3d volume""" raise NotImplementedError() diff --git a/starfish/spots/_pixel_decoder/__init__.py b/starfish/spots/_pixel_decoder/__init__.py index 11b54034a..cf227ccc9 100644 --- a/starfish/spots/_pixel_decoder/__init__.py +++ b/starfish/spots/_pixel_decoder/__init__.py @@ -1,52 +1,3 @@ -from typing import Type - -from starfish.codebook.codebook import Codebook -from starfish.imagestack.imagestack import ImageStack -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.util import click -from . import _base +from starfish.pipeline import import_all_submodules +from ._base import PixelSpotDecoder import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "detect_pixels" - - -class PixelSpotDecoder(PipelineComponent): - - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return _base.PixelDecoderAlgorithmBase - - @classmethod - def _cli_run(cls, ctx, instance): - output = ctx.obj["output"] - image_stack = ctx.obj["image_stack"] - # TODO ambrosejcarr serialize and save ConnectedComponentDecodingResult somehow - intensities, ccdr = instance.run(image_stack) - intensities.save(output) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("-i", "--input", required=True, type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.option( - '--codebook', default=None, required=True, help=( - 'A spaceTx spec-compliant json file that describes a three dimensional tensor ' - 'whose values are the expected intensity of a spot for each code in each imaging ' - 'round and each color channel.' - ) - ) - @click.pass_context - def _cli(ctx, input, output, codebook): - """pixel-wise spot detection and decoding""" - print('Detecting Spots ...') - ctx.obj = dict( - component=PixelSpotDecoder, - image_stack=ImageStack.from_path_or_url(input), - output=output, - codebook=Codebook.from_json(codebook), - ) diff --git a/starfish/spots/_pixel_decoder/_base.py b/starfish/spots/_pixel_decoder/_base.py index 535ee8c1f..e11d639a1 100644 --- a/starfish/spots/_pixel_decoder/_base.py +++ b/starfish/spots/_pixel_decoder/_base.py @@ -1,16 +1,63 @@ -from typing import Callable, Sequence, Tuple, Union +from abc import abstractmethod +from typing import Callable, Sequence, Tuple, Type +import click import numpy as np -import xarray as xr +from starfish.codebook.codebook import Codebook from starfish.imagestack.imagestack import ImageStack from starfish.intensity_table.intensity_table import IntensityTable from starfish.pipeline.algorithmbase import AlgorithmBase -from starfish.types import Number, SpotAttributes +from starfish.pipeline.pipelinecomponent import PipelineComponent +from starfish.types import Number from .combine_adjacent_features import ConnectedComponentDecodingResult +COMPONENT_NAME = "detect_pixels" + + +class PixelSpotDecoder(PipelineComponent): + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + output = ctx.obj["output"] + image_stack = ctx.obj["image_stack"] + # TODO ambrosejcarr serialize and save ConnectedComponentDecodingResult somehow + intensities, ccdr = instance.run(image_stack) + intensities.save(output) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("-i", "--input", required=True, type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.option( + '--codebook', default=None, required=True, help=( + 'A spaceTx spec-compliant json file that describes a three dimensional tensor ' + 'whose values are the expected intensity of a spot for each code in each imaging ' + 'round and each color channel.' + ) + ) + @click.pass_context + def _cli(ctx, input, output, codebook): + """pixel-wise spot detection and decoding""" + print('Detecting Spots ...') + ctx.obj = dict( + component=PixelSpotDecoder, + image_stack=ImageStack.from_path_or_url(input), + output=output, + codebook=Codebook.from_json(codebook), + ) + + class PixelDecoderAlgorithmBase(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return PixelSpotDecoder + + @abstractmethod def run( self, primary_image: ImageStack, @@ -18,10 +65,6 @@ def run( """Finds spots in an ImageStack""" raise NotImplementedError() - def image_to_spots(self, data_image: Union[np.ndarray, xr.DataArray]) -> SpotAttributes: - """Finds spots in a 3d volume""" - raise NotImplementedError() - @staticmethod def _get_measurement_function(measurement_type: str) -> Callable[[Sequence], Number]: try: diff --git a/starfish/spots/_target_assignment/__init__.py b/starfish/spots/_target_assignment/__init__.py index d818fed3b..469d2dba5 100644 --- a/starfish/spots/_target_assignment/__init__.py +++ b/starfish/spots/_target_assignment/__init__.py @@ -1,50 +1,3 @@ -import os -from typing import Type - -from skimage.io import imread - -from starfish.intensity_table.intensity_table import IntensityTable -from starfish.pipeline import AlgorithmBase, import_all_submodules, PipelineComponent -from starfish.util import click -from ._base import TargetAssignmentAlgorithm +from starfish.pipeline import import_all_submodules +from ._base import TargetAssignment import_all_submodules(__file__, __package__) - - -COMPONENT_NAME = "target_assignment" - - -class TargetAssignment(PipelineComponent): - - @classmethod - def pipeline_component_type_name(cls) -> str: - return COMPONENT_NAME - - @classmethod - def _get_algorithm_base_class(cls) -> Type[AlgorithmBase]: - return TargetAssignmentAlgorithm - - @classmethod - def _cli_run(cls, ctx, instance): - output = ctx.obj["output"] - intensity_table = ctx.obj["intensity_table"] - label_image = ctx.obj["label_image"] - assigned = instance.run(label_image, intensity_table) - print(f"Writing intensities, including cell ids to {output}") - assigned.save(os.path.join(output)) - - @staticmethod - @click.group(COMPONENT_NAME) - @click.option("--label-image", required=True, type=click.Path(exists=True)) - @click.option("--intensities", required=True, type=click.Path(exists=True)) - @click.option("-o", "--output", required=True) - @click.pass_context - def _cli(ctx, label_image, intensities, output): - """assign targets to cells""" - - print('Assigning targets to cells...') - ctx.obj = dict( - component=TargetAssignment, - output=output, - intensity_table=IntensityTable.load(intensities), - label_image=imread(label_image) - ) diff --git a/starfish/spots/_target_assignment/_base.py b/starfish/spots/_target_assignment/_base.py index 0ebb5ccba..9dd674789 100644 --- a/starfish/spots/_target_assignment/_base.py +++ b/starfish/spots/_target_assignment/_base.py @@ -1,10 +1,58 @@ +import os +from abc import abstractmethod +from typing import Type + +import click import regional +from skimage.io import imread from starfish.intensity_table.intensity_table import IntensityTable from starfish.pipeline.algorithmbase import AlgorithmBase +from starfish.pipeline.pipelinecomponent import PipelineComponent + + +COMPONENT_NAME = "target_assignment" + + +class TargetAssignment(PipelineComponent): + + @classmethod + def pipeline_component_type_name(cls) -> str: + return COMPONENT_NAME + + @classmethod + def _cli_run(cls, ctx, instance): + output = ctx.obj["output"] + intensity_table = ctx.obj["intensity_table"] + label_image = ctx.obj["label_image"] + assigned = instance.run(label_image, intensity_table) + print(f"Writing intensities, including cell ids to {output}") + assigned.save(os.path.join(output)) + + @staticmethod + @click.group(COMPONENT_NAME) + @click.option("--label-image", required=True, type=click.Path(exists=True)) + @click.option("--intensities", required=True, type=click.Path(exists=True)) + @click.option("-o", "--output", required=True) + @click.pass_context + def _cli(ctx, label_image, intensities, output): + """assign targets to cells""" + + print('Assigning targets to cells...') + ctx.obj = dict( + component=TargetAssignment, + output=output, + intensity_table=IntensityTable.load(intensities), + label_image=imread(label_image) + ) class TargetAssignmentAlgorithm(AlgorithmBase): + @classmethod + def get_pipeline_component_class(cls) -> Type[PipelineComponent]: + return TargetAssignment + + @abstractmethod def run(self, spots: IntensityTable, regions: regional.many) -> IntensityTable: """Performs target (e.g. gene) assignment given the spots and the regions.""" raise NotImplementedError()