diff --git a/doc/sphinx/source/catalog.rst b/doc/sphinx/source/catalog.rst new file mode 100644 index 0000000..9bd5199 --- /dev/null +++ b/doc/sphinx/source/catalog.rst @@ -0,0 +1,6 @@ + +The catalog module +------------------ + +.. automodule:: lipase.catalog + :members: diff --git a/doc/sphinx/source/circsymdetector.rst b/doc/sphinx/source/circsymdetector.rst new file mode 100644 index 0000000..c6c193a --- /dev/null +++ b/doc/sphinx/source/circsymdetector.rst @@ -0,0 +1,5 @@ +The circsymdetector module +-------------------------- + +.. automodule:: lipase.circsymdetector + :members: diff --git a/doc/sphinx/source/conf.py b/doc/sphinx/source/conf.py index ced0e84..6c5c22c 100644 --- a/doc/sphinx/source/conf.py +++ b/doc/sphinx/source/conf.py @@ -17,11 +17,29 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('../../../src')) +# use mock mechanism for modules that sphinx cannot find (eg ij, which is only available in jython) +#import mock # sudo apt install python3-mock + +#MOCK_MODULES = ['numpy', 'matplotlib', 'matplotlib.pyplot'] +MOCK_MODULES = ['ij', 'ij.process', 'ij.plugin', 'ncsa', 'jarray'] +autodoc_mock_imports = MOCK_MODULES +#for mod_name in MOCK_MODULES: +# sys.modules[mod_name] = mock.Mock() + + +def skip(app, what, name, obj, would_skip, options): + if name == "__init__": + return False + return would_skip + +def setup(app): + app.connect("autodoc-skip-member", skip) + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -32,6 +50,7 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.napoleon', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', @@ -40,6 +59,12 @@ extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.githubpages'] + +# enable automatic translation of google style comments to rest comments as google style comments are less wordy (see https://brendanhasz.github.io/2019/01/05/sphinx.html#napoleon-extension) +napoleon_google_docstring = True +napoleon_numpy_docstring = True + + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -90,7 +115,7 @@ todo_include_todos = True # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = 'classic' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/doc/sphinx/source/hdf5.rst b/doc/sphinx/source/hdf5.rst new file mode 100644 index 0000000..a2f9059 --- /dev/null +++ b/doc/sphinx/source/hdf5.rst @@ -0,0 +1,7 @@ + +The hdf5 module +----------------- + +.. automodule:: lipase.hdf5.hdf5_data + :members: + diff --git a/doc/sphinx/source/imageengine.rst b/doc/sphinx/source/imageengine.rst new file mode 100644 index 0000000..0ba8dae --- /dev/null +++ b/doc/sphinx/source/imageengine.rst @@ -0,0 +1,5 @@ +The imageengine module +---------------------- + +.. automodule:: lipase.imageengine + :members: diff --git a/doc/sphinx/source/index.rst b/doc/sphinx/source/index.rst index 6517d10..8350071 100644 --- a/doc/sphinx/source/index.rst +++ b/doc/sphinx/source/index.rst @@ -6,10 +6,20 @@ Welcome to lipase's documentation! ================================== + .. toctree:: :maxdepth: 2 :caption: Contents: + imageengine + catalog + lipase + circsymdetector + traps_detector + template_matcher + maxima_finder + hdf5 + telemos Indices and tables diff --git a/doc/sphinx/source/lipase.rst b/doc/sphinx/source/lipase.rst new file mode 100644 index 0000000..d32f4f1 --- /dev/null +++ b/doc/sphinx/source/lipase.rst @@ -0,0 +1,6 @@ + +The lipase module +----------------- + +.. automodule:: lipase.lipase + :members: diff --git a/doc/sphinx/source/maxima_finder.rst b/doc/sphinx/source/maxima_finder.rst new file mode 100644 index 0000000..dbfe499 --- /dev/null +++ b/doc/sphinx/source/maxima_finder.rst @@ -0,0 +1,5 @@ +The maxima_finder module +--------------------------- + +.. automodule:: lipase.maxima_finder + :members: diff --git a/doc/sphinx/source/telemos.rst b/doc/sphinx/source/telemos.rst new file mode 100644 index 0000000..b88eb67 --- /dev/null +++ b/doc/sphinx/source/telemos.rst @@ -0,0 +1,6 @@ + +The telemos module +------------------ + +.. automodule:: lipase.telemos + :members: diff --git a/doc/sphinx/source/template_matcher.rst b/doc/sphinx/source/template_matcher.rst new file mode 100644 index 0000000..1cf8559 --- /dev/null +++ b/doc/sphinx/source/template_matcher.rst @@ -0,0 +1,6 @@ + +The template_matcher module +--------------------------- + +.. automodule:: lipase.template_matcher + :members: diff --git a/doc/sphinx/source/traps_detector.rst b/doc/sphinx/source/traps_detector.rst new file mode 100644 index 0000000..429e307 --- /dev/null +++ b/doc/sphinx/source/traps_detector.rst @@ -0,0 +1,6 @@ + +The traps_detector module +------------------------- + +.. automodule:: lipase.traps_detector + :members: diff --git a/src/lipase/catalog.py b/src/lipase/catalog.py index 9289685..18b5286 100644 --- a/src/lipase/catalog.py +++ b/src/lipase/catalog.py @@ -1,17 +1,20 @@ import os import json -from imageengine import IHyperStack, IImageEngine, PixelType +from .imageengine import IHyperStack, IImageEngine, PixelType class DacMetadata(object): - """Represents a display_and_comments.txt metadata file.""" + """Represents a display_and_comments.txt metadata file. + + display_and_comments.txt is a metadata file stored by micro manager + """ def __init__(self, dac_id, dac_file_path): """Contructor. - :param str dac_id: eg "res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1" - :param str dac_file_path: eg "/Users/graffy/ownCloud/ipr/lipase/raw-images/res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/display_and_comments.txt" + :param str dac_id: eg ``res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1`` + :param str dac_file_path: eg ``/Users/graffy/ownCloud/ipr/lipase/raw-images/res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/display_and_comments.txt`` """ self.dac_id = dac_id self.dac_file_path = dac_file_path @@ -19,16 +22,20 @@ class DacMetadata(object): self.dac = json.load(dac_file, encoding='latin-1') # note : the micromanager metadata files are encoded in latin-1, not utf8 (see accents in comments) def get_channel_name(self, channel_index): + """Returns the name of the given channel index + """ channels = self.dac['Channels'] return channels[channel_index]["Name"] class Sequence(object): + """A sequence of images stored in micro manager format + """ def __init__(self, catalog, sequence_id, micro_manager_metadata_file_path): """ - :param Catalog catalog: - :param str sequence_id: eg 'res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0' - :param str micro_manager_metadata_file_path: eg '/Users/graffy/ownCloud/ipr/lipase/raw-images/res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0/metadata.txt' + :param ImageCatalog catalog: + :param str sequence_id: eg ``res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0`` + :param str micro_manager_metadata_file_path: eg ``/Users/graffy/ownCloud/ipr/lipase/raw-images/res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0/metadata.txt`` """ self.catalog = catalog self.sequence_id = sequence_id @@ -52,26 +59,36 @@ class Sequence(object): @property def num_frames(self): + """Gets the number of frames in the sequence + """ summary = self.mmm['Summary'] return int(summary['Frames']) @property def width(self): + """Gets the width of images in the sequence + """ summary = self.mmm['Summary'] return int(summary['Width']) @property def height(self): + """Gets the height of images in the sequence + """ summary = self.mmm['Summary'] return int(summary['Height']) @property def num_channels(self): + """Gets the number of channels in the sequence + """ summary = self.mmm['Summary'] return int(summary['Channels']) @property def num_slices(self): + """Gets the number of slices in the sequence + """ summary = self.mmm['Summary'] return int(summary['Slices']) @@ -81,11 +98,13 @@ class Sequence(object): return int(summary['BitDepth']) def get_root_path(self): + """Returns the root location of th is sequence + """ (dir_name, file_name) = os.path.split(self.micro_manager_metadata_file_path) # pylint: disable=unused-variable return dir_name def get_image_file_path(self, channel_index, frame_index, slice_index=0): - ''' + '''Returnsthe file location of the given image in the sequence :param int channel_index: :param int frame_index: :param int slice_index: @@ -98,7 +117,7 @@ class Sequence(object): def get_channel_index(self, channel_id): ''' - :param str channel_id: + :param str channel_id: the identifier of the channel (eg ``'vis'``) ''' summary = self.mmm['Summary'] channel_index = summary['ChNames'].index(channel_id) @@ -115,7 +134,7 @@ class Sequence(object): def get_black(self): ''' returns the black sequence related to the the sequence self - :return Sequence: + :rtype: Sequence ''' seqid_to_black = { 'res_soleil2018/GGH/GGH_2018_cin2_phiG_I_327_vis_-40_1/Pos0': 'res_soleil2018/DARK/DARK_40X_60min_1 im pae min_1/Pos0', @@ -127,7 +146,7 @@ class Sequence(object): def get_white(self): ''' returns the white sequence related to the the sequence self - :return Sequence: + :rtype: Sequence ''' # assert fixme : res_soleil2018/white/white_24112018_2/Pos0 is visible seqid_to_white = { @@ -143,7 +162,8 @@ class Sequence(object): :param list(str) selected_channel_ids: :param list(int) selected_frames: :param list(int) selected_slices: - :return IHyperStack: the resulting hyperstack + :return: the resulting hyperstack + :rtype: IHyperStack """ if selected_frames is None: selected_frames = range(self.num_frames) @@ -170,6 +190,8 @@ def find_dirs_containing_file(file_name, root_dir): """ :param str file_name: the name of the searched files (eg 'metadata.txt') :param str root_dir: the root directory where we search + :rtype: list(str) + :return: the list of directories that contain the given file """ result = [] for root, dirs, files in os.walk(root_dir): # pylint: disable=unused-variable @@ -178,9 +200,20 @@ def find_dirs_containing_file(file_name, root_dir): return result class ImageCatalog(object): + """A catalog of micromanager sequences + """ def __init__(self, raw_images_root): + """Creates a new image catalog and populates it by finding the image sequences in the given root directory + + Args: + raw_images_root (str): the root directory that is expected to contain the micromanager formatted sequences. + + """ self.raw_images_root = raw_images_root self.sequences = {} + """ + the dictionary which stores the catalog sequences (dict(str, :py:class:`Sequence`)). The sequences are indexed with the sequence identifiers. + """ sequence_paths = find_dirs_containing_file('metadata.txt', self.raw_images_root) sequence_ids = [ os.path.relpath(sequence_path, raw_images_root) for sequence_path in sequence_paths ] diff --git a/src/lipase/circsymdetector.py b/src/lipase/circsymdetector.py index ab35809..b2970d7 100644 --- a/src/lipase/circsymdetector.py +++ b/src/lipase/circsymdetector.py @@ -2,7 +2,7 @@ an image processing technique to produce a 1D signal for each pixel : this 1D signal is obtained by projecting the neighborhood of this pixel on a set of bases (each base is used as a convilution kernel) https://subversion.ipr.univ-rennes1.fr/repos/main/projects/antipode/src/python/antipode/texori.py """ -from imageengine import IImageEngine, PixelType, StackImageFeeder +from .imageengine import IImageEngine, PixelType, StackImageFeeder import math class IProjectorBase(object): @@ -22,8 +22,11 @@ class IProjectorBase(object): pass def create_circle_image(image_size, circle_radius, circle_pos, circle_thickness, background_value=0.0, circle_value=1.0): - """ - :param dict image_size: + """Creates an image containing the a circle + + :param dict image_size: The width and height of the image to create + :rtype: IImage + """ ie = IImageEngine.get_instance() width = image_size['width'] @@ -152,6 +155,8 @@ class CircularSymmetryProjectorBase(object): class CircularSymmetryDetector: + """Method to detect objects exhibiting a circular symmetry + """ def __init__(self, max_radius, num_angular_sectors, num_radial_sectors=None): """ :param float max_radius: in pixels @@ -168,15 +173,16 @@ class CircularSymmetryDetector: """ Computes for each pixel the radial profile (with this pixel as center) :param IImage src_image: - :rtype IHyperstack, Image : :returns: - - radial_profiles (:py:class:`IHyperstack`) - each radial profile is stored in the channel coordinate of the hyperstack + - radial_profiles (:py:class:`~lipase.imageengine.IHyperStack`) - each pixel stores the radial profile for this pixel. The radial profile is a 1D signal stored along the channel axis of the hyperstack, each channel being related to a radius range. + - angular_variance_profiles (:py:class:`IHyperStack`) - each pixel stores the angular variance profile for this pixel. The angular variance profile is a 1D signal stored along the channel axis of the hyperstack, each channel being related to a radius range. - angular_variance_avg (:py:class:`IImage`) - each pixel stores the average variance of signal along the circles of different diameters in the neighborhood """ # https://stackoverflow.com/questions/39759503/how-to-document-multiple-return-values-using-restructuredtext-in-python-2 ie = IImageEngine.get_instance() ie.debugger.on_image(src_image, 'src_image') projector_base = CircularSymmetryProjectorBase(self.max_radius, num_angular_sectors=self.num_angular_sectors, num_radial_sectors=self.num_radial_sectors, oversampling_scale=1) + radial_profile_image = ie.create_hyperstack(width=src_image.get_width(), height=src_image.get_height(), num_channels=projector_base.num_radial_sectors, num_slices=1, num_frames=1, pixel_type=PixelType.F32) angular_variances_image = ie.create_hyperstack(width=src_image.get_width(), height=src_image.get_height(), num_channels=projector_base.num_radial_sectors, num_slices=1, num_frames=1, pixel_type=PixelType.F32) for radius_index in range(projector_base.num_radial_sectors): diff --git a/src/lipase/hdf5/__init__.py b/src/lipase/hdf5/__init__.py index d157e6f..9bb69b8 100644 --- a/src/lipase/hdf5/__init__.py +++ b/src/lipase/hdf5/__init__.py @@ -1 +1 @@ -from hdf5_data import Group, DataSet, ElementType +from .hdf5_data import Group, DataSet, ElementType diff --git a/src/lipase/hdf5/hdf5_data.py b/src/lipase/hdf5/hdf5_data.py index 848c8c8..98a1628 100644 --- a/src/lipase/hdf5/hdf5_data.py +++ b/src/lipase/hdf5/hdf5_data.py @@ -6,12 +6,20 @@ note: these structures are intentionally made independent of h5py library as thi """ class ElementType(object): + """The types that can be used for elements of arrays + """ U1=0 + """1 bit boolean""" U8=1 + """8 bits unsigned integer""" U32=2 + """32 bits unsigned integer""" S32=3 + """32 bits signed integer""" F32=4 + """32 bits floating precision number""" F64=5 + """64 bits floating precision number""" class DataSet(object): """ an hdf5 dataset @@ -20,34 +28,64 @@ class DataSet(object): """ :param str name: the name of the dataset :param tuple(int) size: the size of each dimension of the dataset + :param ElementType element_type: the type of elements stored in this dataset """ self.name = name + """The name of the dataset (str) + """ self.size = size + """The size of the dataset (tuple of integers) + """ self._element_type = element_type + """The type of elements in this dataset (ElementType) + """ self._elements = [] for i in range(self.num_elements): # pylint: disable=unused-variable self._elements.append(0) @property def element_type(self): + """Gets the element type of this array + + :rtype: ElementType + """ return self._element_type @property def dimension(self): + """Gets the dimension of the array contained in this dataset + + Would return 3 for a 3D dataset. + + :rtype: int + """ return len(self.size) @property def elements(self): + """Gets the elements value in the form of a list + + :rtype: list + """ return self._elements @property def num_elements(self): + """Gets the number of elements in this dataset + + :rtype: int + """ num_elements = 1 for i in range(self.dimension): num_elements *= self.size[i] return num_elements def get_element(self, pos): + """Gets the value of the element indexed by pos + + :param tuple(int) pos: the index of the element to retreive. This index is expected to be a tuple of integers. The pos argument is expected to have the same length as the number of dimensions of this dataset. + + """ assert len(pos) == len(self.size) return self._elements[self._pos_to_index(pos)] @@ -56,10 +94,20 @@ class DataSet(object): return self._elements[self._pos_to_index(pos)] def set_element(self, pos, value): + """Sets the value of the element indexed by pos + + :param tuple(int) pos: the index of the element to set. This index is expected to be a tuple of integers. The pos argument is expected to have the same length as the number of dimensions of this dataset. + :param value: the type is expected to be compatible (convertible to) with this dataset's element type + """ assert len(pos) == len(self.size) self._elements[self._pos_to_index(pos)] = value def __setitem__(self, pos, value): + """Sets the value of the element indexed by pos + + :param tuple(int) pos: the index of the element to set. This index is expected to be a tuple of integers. The pos argument is expected to have the same length as the number of dimensions of this dataset. + :param value: the type is expected to be compatible (convertible to) with this dataset's element type + """ assert len(pos) == len(self.size) self._elements[self._pos_to_index(pos)] = value @@ -73,7 +121,10 @@ class DataSet(object): class Group(object): + """An hdf5 group + A group can contain datasets and other groups + """ def __init__(self, name): """ :param str name: the name of the group @@ -83,19 +134,35 @@ class Group(object): self._groups = {} def add_dataset(self, dataset): + """Adds the given dataset to the datasets stored in this group + + :param DataSet dataset: + """ assert isinstance(dataset, DataSet) self._datasets[dataset.name] = dataset def add_group(self, group): + """Adds the given group to the groups stored in this group + + :param Group group: + """ assert isinstance(group, Group) self._datasets[group.name] = group @property def datasets(self): + """ + + :rtype: dict(str, :py:class:`DataSet`) + """ return self._datasets @property def groups(self): + """ + + :rtype: dict(str, :py:class:`Group`) + """ return self._groups def __getitem__(self, dataset_name): diff --git a/src/lipase/imageengine.py b/src/lipase/imageengine.py index af62afa..3567b2f 100644 --- a/src/lipase/imageengine.py +++ b/src/lipase/imageengine.py @@ -41,7 +41,9 @@ class PixelType: return PixelType.PIXEL_TYPE_TO_NUM_BITS[ pixel_type ] class IImage(ABC): + """An abstact class representing an image (2D matrix of pixels) + """ @abc.abstractmethod def clone(self, clone_pixel_type=None): """ @@ -73,27 +75,29 @@ class IImage(ABC): def set_subimage(self, subimage, position): """ :param IImage subimage: the image to copy into self - :param (int, int) position: where to insert the subimage in self (upper left pixel) + :param (int,int) position: where to insert the subimage in self (upper left pixel) """ @abc.abstractmethod def get_pixel_type(self): """ - :rtype PixelType: + :rtype: PixelType """ pass @abc.abstractmethod def get_value_range(self): """ - :rtype (float, float): the min and the max value + :rtype: (float, float) + :returns: the min and the max value """ pass @abc.abstractmethod def get_mean_value(self): """ - :rtype float: the mean pixel value in the image + :rtype float: + :return: the mean pixel value in the image """ pass @@ -106,6 +110,11 @@ class IImage(ABC): """ class IHyperStack(ABC): + """An abstact class representing an hyperstack (5D matrix of pixels) + + The name hyperstachk comes from imagej's terminology. + + """ @abc.abstractmethod def get_width(self): @@ -158,17 +167,22 @@ class IImageFeeder(ABC): def next(self): """returns the next image in the collection for iterator + + :rtype: IImage """ pass @abc.abstractmethod def get_num_images(self): + """Gets the number of images in this collection + """ pass def create_hyperstack(self): """ creates an hyperstack from this image feeder - :rtype IHyperStack + + :rtype: IHyperStack """ it = iter(self) @@ -214,7 +228,7 @@ class StackImageFeeder(IImageFeeder): def __init__(self, hyperstack): """ - :param IHyperStack hyperstack + :param IHyperStack hyperstack: """ self.hyperstack = hyperstack @@ -270,19 +284,29 @@ class StackImageFeeder(IImageFeeder): class IImageProcessingDebugger(ABC): + """ + An abstract image processing debugger. + + Its role is to do a specific action on some events related to image processing + """ def __init__(self): pass @abc.abstractmethod def on_image(self, image, image_id): - ''' + '''This method is called by image processing methods when they have an intermediate image they want to send to the debugger + :param IImage image: :param str image_id: ''' class NullDebugger(IImageProcessingDebugger): + """ + This debugger is simply ignoring anything it receives. + Therefore this debugger is to be chosen when we want to disable the debugger. + """ def __init__(self): IImageProcessingDebugger.__init__(self) @@ -335,7 +359,7 @@ class IImageEngine(ABC): @staticmethod def get_instance(): """Static access method. - :rtype IImageEngine: + :rtype: IImageEngine """ assert IImageEngine.__instance is not None @@ -350,13 +374,13 @@ class IImageEngine(ABC): @abc.abstractmethod def create_hyperstack(self, width, height, num_channels, num_slices, num_frames, pixel_type): """ - :rtype IHyperStack: + :rtype: IHyperStack """ @abc.abstractmethod def create_image(self, width, height, pixel_type): """ - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -380,7 +404,8 @@ class IImageEngine(ABC): :param IImage image1: :param IImage image2: - :rtype IImage: image1-image2 + :rtype: IImage + :return: image1-image2 """ @abc.abstractmethod @@ -390,7 +415,8 @@ class IImageEngine(ABC): :param IImage image1: :param IImage image2: - :rtype IImage: image1*image2 + :rtype: IImage + :return: image1*image2 """ @abc.abstractmethod @@ -400,7 +426,8 @@ class IImageEngine(ABC): :param IImage numerator_image: :param IImage denominator_image: - :rtype IImage: numerator_image/denominator_image + :rtype: IImage + :return: numerator_image/denominator_image """ @abc.abstractmethod @@ -409,7 +436,8 @@ class IImageEngine(ABC): computes the absolute value of each pixel in the input image :param IImage image: - :rtype IImage: an image containing the absolute values of the input image + :rtype: IImage + :return: an image containing the absolute values of the input image """ @@ -419,7 +447,7 @@ class IImageEngine(ABC): computes the edge transform using a sobel filter :param IImage image: - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -427,7 +455,7 @@ class IImageEngine(ABC): """Compute for each pixel position the maximum at this position in all input images. :param IImageFeeder image_feeder: - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -435,7 +463,7 @@ class IImageEngine(ABC): """Compute for each pixel position the median value at this position in all input images. :param IImageFeeder image_feeder: - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -443,7 +471,7 @@ class IImageEngine(ABC): """Compute for each pixel position the mean value at this position in all input images. :param IImageFeeder image_feeder: - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -462,7 +490,7 @@ class IImageEngine(ABC): :param PixelType or None dst_type: pixel type of the destination image (if None, the pixel type is the same as the source image type) :param IImage kernel: convolution kernel (or rather a correlation kernel), a single-channel floating point matrix; if you want to apply different kernels to different channels, split the image into separate color planes using split() and process them individually. :param tuple(int, int) angor: anchor of the kernel that indicates the relative position of a filtered point within the kernel; the anchor should lie within the kernel; default value (-1,-1) means that the anchor is at the kernel center. - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -480,9 +508,11 @@ class IImageEngine(ABC): :param IImage image: :param int band_width: width of the outer band, in pixels - :rtype IImage: + :rtype: IImage - adaptation for the following code from matlab telemosToolbx's estimatedwhiteFluoImageTelemos function + adaptation for the following code from matlab telemosToolbx's:: + + estimatedwhiteFluoImageTelemos function outer = white_estimate; white_estimate = white_estimate(4:(end-3),4:(end-3)); outer(1:3,4:(end-3))=repmat(white_estimate(1,:),3,1); # top @@ -498,33 +528,37 @@ class IImageEngine(ABC): """ :param IImage src_image: :param IImage template_image: - :return IImage: similarity image + :rtype: IImage + :return: similarity image """ @abc.abstractmethod def find_maxima(self, src_image, threshold, tolerance): """ to understand the meaning of threshold and maxima, I had to look at the source code in https://github.com/imagej/imagej1/blob/master/ij/plugin/filter/MaximumFinder.java#L636 + 1. The algorithm first builds the list of maxima candidates. Only local maxima above this threshold are considered as valid maxima candidates 2. Then each maximum candidate is processed to find its neighborhood area, starting from the highest. For ecach candidate, a propagation is performed on neighbours as long as the neighbour value is within the tolerance of the maximum candidate. If another candidate is encountered as a neighbour, then this candidate is removed as it('s been absorbed by the current maximum candidate) :param IImage src_image: :param float threshold: local maxima below this value are ignored :param float tolerance: ignore local maxima if they are in the neighborhood of another maximum. The neighborhood of a maximum is defined by the area that propagates until the the difference between the pixel value and the value of the considered local maximum exceeds tolerance. The higher the tolerance, the more local maxima are removed.... - :return list(Match): maxima + :return: maxima + :rtype: list(Match) """ @abc.abstractmethod def threshold(self, src_image, threshold_value): """ :param float threshold_value: - :rtype IImage: + :rtype: IImage + :return: the resulting image """ @abc.abstractmethod def auto_threshold(self, src_image): """ - :rtype IImage: + :rtype: IImage """ @abc.abstractmethod @@ -534,7 +568,8 @@ class IImageEngine(ABC): :param IImage src_binary_image: :param (int, int) flood_start_pos: start position of the flood fill - :rtype IImage: binary image + :rtype: IImage + :return: binary image """ @abc.abstractmethod @@ -543,7 +578,8 @@ class IImageEngine(ABC): fills the holes in the given binary image :param IImage src_binary_image: - :rtype IImage: binary image + :rtype: IImage + :return: binary image """ @abc.abstractmethod @@ -552,6 +588,7 @@ class IImageEngine(ABC): inverts the given image pixel values (black becomes white and white becomes black) :param IImage src_binary_image: - :rtype IImage: binary image + :rtype: IImage + :return: binary image """ diff --git a/src/lipase/imagej/ijimageengine.py b/src/lipase/imagej/ijimageengine.py index 2ed9aa9..899f7cc 100644 --- a/src/lipase/imagej/ijimageengine.py +++ b/src/lipase/imagej/ijimageengine.py @@ -34,7 +34,7 @@ def clone_processor(src_processor, clone_pixel_type): """ :param Processor processor: :param PixelType clone_pixel_type: - :rtype Processor: + :rtype: Processor """ clone_processor = { PixelType.U8: src_processor.convertToByteProcessor(), @@ -315,7 +315,7 @@ class IJImageEngine(IImageEngine): """Computes for each pixel position the maximum at this position in all input images. :param IImageFeeder image_feeder: - :rtype IJImage: + :rtype: IJImage """ it = iter(image_feeder) @@ -336,7 +336,7 @@ class IJImageEngine(IImageEngine): """Computes for each pixel position the median value at this position in all input images. :param IImageFeeder image_feeder: - :rtype IJmage: + :rtype: IJmage """ hyperstack = image_feeder.create_hyperstack() # https://imagej.nih.gov/ij/developer/api/ij/plugin/ZProjector.html diff --git a/src/lipase/lipase.py b/src/lipase/lipase.py index b2737f2..2296129 100644 --- a/src/lipase/lipase.py +++ b/src/lipase/lipase.py @@ -11,12 +11,12 @@ from ij.process import ImageStatistics # pylint: disable=import-error from ij.plugin import ImageCalculator # pylint: disable=import-error from ij.plugin import ZProjector # pylint: disable=import-error from ij import WindowManager # pylint: disable=import-error -from catalog import Sequence, ImageCatalog -import telemos -from imageengine import IImageEngine, IImageProcessingDebugger, NullDebugger, StackImageFeeder, IHyperStack +from .catalog import Sequence, ImageCatalog +from . import telemos +from .imageengine import IImageEngine, IImageProcessingDebugger, NullDebugger, StackImageFeeder, IHyperStack # greeting = "Hello, " + name + "!" -from hdf5.hdf5_data import Group, DataSet, ElementType -from imagej.hdf5serializer import save_hdf5_file +from .hdf5.hdf5_data import Group, DataSet, ElementType +from .imagej.hdf5serializer import save_hdf5_file import abc @@ -144,7 +144,7 @@ class IBackgroundEstimator(ABC): def estimate_background(self, visible_traps_sequence): """ :param IHyperStack visible_traps_sequence: - :rtype IImage: the estimated background image + :rtype: IImage the estimated background image """ pass @@ -189,7 +189,7 @@ class GlobulesAreaEstimator(object): def detect_particles(self, visible_traps_sequence): """ :param IHyperStack visible_traps_sequence: - :rtype hdf5_data.Group: + :rtype: hdf5_data.Group """ background_image = self.background_estimator.estimate_background(visible_traps_sequence) ie = IImageEngine.get_instance() diff --git a/src/lipase/maxima_finder.py b/src/lipase/maxima_finder.py index abdef41..172b8ad 100644 --- a/src/lipase/maxima_finder.py +++ b/src/lipase/maxima_finder.py @@ -1,5 +1,5 @@ import abc -from imageengine import IImageEngine +from .imageengine import IImageEngine ABC = abc.ABCMeta('ABC', (object,), {}) @@ -14,15 +14,22 @@ class Match(object): return "(%d, %d) - %f" % (self.x, self.y, self.correlation) class IMaximaFinder(ABC): + """Abstract class for methods aiming at detecting the maxima of an image + """ @abc.abstractmethod def find_maxima(self, image): """ :param IImage image: - :rtype list(Match): + :rtype: list(Match) """ class MaximaFinder(IMaximaFinder): + """ + A maxima finder based on imagej's MaximumFinder + + Uses the technique implemented in https://github.com/imagej/imagej1/blob/master/ij/plugin/filter/MaximumFinder.java + """ def __init__(self, threshold, tolerance): """ diff --git a/src/lipase/telemos.py b/src/lipase/telemos.py index f8dc42a..5493330 100644 --- a/src/lipase/telemos.py +++ b/src/lipase/telemos.py @@ -1,33 +1,15 @@ """preprocessing of synchrotron images based on telemosToolbox.""" -from catalog import ImageCatalog, Sequence -from imageengine import IImageEngine, PixelType, FileImageFeeder +from .catalog import ImageCatalog, Sequence +from .imageengine import IImageEngine, PixelType, FileImageFeeder class WhiteEstimator(object): + """ + Method that estimates the white from a sequence of images - def __init__(self, open_size, close_size, average_size): - """ - :param int open_size: the diameter of the structuring element used to perform mathematical morphology's opening operator (in pixels) - :param int close_size: the diameter of the structuring element used to perform mathematical morphology's closing operator (in pixels) - """ - self.open_size = open_size - self.close_size = close_size - self.average_size = average_size + This method expects the sequence of images to represen sparse particles moving over a white background - def _remove_particles(self, white_estimate): - IImageEngine.get_instance().perform_gray_morphology(white_estimate, operator='open', structuring_element_shape='square', structuring_element_radius=(self.open_size + 1) / 2) - IImageEngine.get_instance().perform_gray_morphology(white_estimate, operator='close', structuring_element_shape='square', structuring_element_radius=(self.close_size + 1) / 2) - - IImageEngine.get_instance().mean_filter(white_estimate, radius=(self.average_size + 1) / 2) - - def estimate_white(self, sequences, channel_ids, dark=None): - """Estimation of the white fluorescence image shape of synchrotron light from experimental images of Telemos microscope. - - :param list(Sequence) sequences: the sequences to consider - :param list(str) channel_ids: the channels to consider, eg ['DM300_327-353_fluo'] - :param ImagePlus or None dark: - :rtype ImagePlus: - Code adapted from matlab telemosToolbx's estimatewhiteFluoImageTelemos function + Code adapted from matlab telemosToolbx's estimatewhiteFluoImageTelemos function:: % this function is specific of Telemos images (DISCO line SOLEIL) % acquired using micromanager software linked to imageJ @@ -114,6 +96,30 @@ class WhiteEstimator(object): % 27 mai 2019 : offset dark % 4 juin 2019 : replace exist by isfolder or isfile % 14 juin : close figure at the end + """ + + def __init__(self, open_size, close_size, average_size): + """ + :param int open_size: the diameter of the structuring element used to perform mathematical morphology's opening operator (in pixels) + :param int close_size: the diameter of the structuring element used to perform mathematical morphology's closing operator (in pixels) + """ + self.open_size = open_size + self.close_size = close_size + self.average_size = average_size + + def _remove_particles(self, white_estimate): + IImageEngine.get_instance().perform_gray_morphology(white_estimate, operator='open', structuring_element_shape='square', structuring_element_radius=(self.open_size + 1) / 2) + IImageEngine.get_instance().perform_gray_morphology(white_estimate, operator='close', structuring_element_shape='square', structuring_element_radius=(self.close_size + 1) / 2) + + IImageEngine.get_instance().mean_filter(white_estimate, radius=(self.average_size + 1) / 2) + + def estimate_white(self, sequences, channel_ids, dark=None): + """Estimation of the white fluorescence image shape of synchrotron light from experimental images of Telemos microscope. + + :param list(Sequence) sequences: the sequences to consider + :param list(str) channel_ids: the channels to consider, eg ['DM300_327-353_fluo'] + :param ImagePlus or None dark: + :rtype: ImagePlus """ image_set = FileImageFeeder() for sequence in sequences: @@ -170,10 +176,12 @@ class InteractiveWhiteEstimator(WhiteEstimator): def correct_non_uniform_lighting(non_uniform_sequence, channel_id, white_estimator=WhiteEstimator(open_size=75, close_size=75, average_size=75)): """ correction of non uniform lighting + :param Sequence non_uniform_sequence: the input sequence, which is expected to have a non uniform lighting :param str channel_id: :param WhiteEstimator white_estimator: the method used to estimate the light image - :return IHyperStack: the hyperstack of images after correction + :rtype: IHyperStack + :return: the hyperstack of images after correction """ ie = IImageEngine.get_instance() @@ -235,95 +243,98 @@ def find_white_reference_image(white_estimate, white_z_stack): :param IImage white_estimate: :param Sequence white_z_stack: :param IImageEngine image_engine: the image processor to use - % this function is specific of Telemos images (DISCO line SOLEIL) - % acquired using micromanager software linked to imageJ - %% input - % nothing : interactive function + Based on original matlab code:: - %% output - % zmax : focal plane + % this function is specific of Telemos images (DISCO line SOLEIL) + % acquired using micromanager software linked to imageJ - %% principe - % images were acquired for a given roi which position is found in - % metadata.txt file - % white and dark images were recorded for the full field of view of the DISCO - % TELEMOS camera - % - % correlation between a estimated white fluorescence image estalished from - % actual acquisitions of a given sample and all z plane acquired for the reference TELEMOS white - % image (Matthieu slide or any fluorescent homogeneous image) : + %% input + % nothing : interactive function - % The estimated white fluorescence image is generallly obtained bt using function whiteFluoImageTelemosestimation - % This is not compulsary as any homogenous sample image hat can roughly show the shape of illumination can be used to find - % the white reference image - % - % - % the z plane for which the maximum correlation is observed between estimated white and reference white images is retained. - % the white image is then offsetted (its corresponding Dark is subtracted) and copied in the subfolder of the sample - % rootfolder to show that it has been specifically selected for the considered experiment - %The matlab corrcoeff function is used - % - % correlation coefficients are saved in a file called - % 'corr.txt' in the subfolder 'WhiteReference' - % - % - % expected input folder hierarchy: - % - % >sampleFolder - % > PosFolders or RoiFolders - % > WhiteEstimate - % >darkFolder - % >darkFolder.smooth - % >whiteFolder - % > darkFolderforWhite - % > darkFolderforWhite.smooth - % >whiteFolder.smooth - % - % - % expected output folder hierarchy: - % - % >sampleFolder - % > PosFolders or RoiFolders - % > WhiteEstimate - % > WhiteReference - % > white after offset - % >darkFolder - % >darkFolder.smooth - % >whiteFolder - % > darkFolderforWhite - % > darkFolderforWhite.smooth - % >whiteFolder.smooth + %% output + % zmax : focal plane + + %% principe + % images were acquired for a given roi which position is found in + % metadata.txt file + % white and dark images were recorded for the full field of view of the DISCO + % TELEMOS camera + % + % correlation between a estimated white fluorescence image estalished from + % actual acquisitions of a given sample and all z plane acquired for the reference TELEMOS white + % image (Matthieu slide or any fluorescent homogeneous image) : + + % The estimated white fluorescence image is generallly obtained bt using function whiteFluoImageTelemosestimation + % This is not compulsary as any homogenous sample image hat can roughly show the shape of illumination can be used to find + % the white reference image + % + % + % the z plane for which the maximum correlation is observed between estimated white and reference white images is retained. + % the white image is then offsetted (its corresponding Dark is subtracted) and copied in the subfolder of the sample + % rootfolder to show that it has been specifically selected for the considered experiment + %The matlab corrcoeff function is used + % + % correlation coefficients are saved in a file called + % 'corr.txt' in the subfolder 'WhiteReference' + % + % + % expected input folder hierarchy: + % + % >sampleFolder + % > PosFolders or RoiFolders + % > WhiteEstimate + % >darkFolder + % >darkFolder.smooth + % >whiteFolder + % > darkFolderforWhite + % > darkFolderforWhite.smooth + % >whiteFolder.smooth + % + % + % expected output folder hierarchy: + % + % >sampleFolder + % > PosFolders or RoiFolders + % > WhiteEstimate + % > WhiteReference + % > white after offset + % >darkFolder + % >darkFolder.smooth + % >whiteFolder + % > darkFolderforWhite + % > darkFolderforWhite.smooth + % >whiteFolder.smooth - %% use - % [zmax]=findWhiteReferenceImageTelemos + %% use + % [zmax]=findWhiteReferenceImageTelemos - %% Comments - % written for proposal 20161050 - % adapted for proposal 20170043 + %% Comments + % written for proposal 20161050 + % adapted for proposal 20170043 - %% Author - % MF Devaux - % INRA BIA - % PVPP + %% Author + % MF Devaux + % INRA BIA + % PVPP - %% date - % 5 octobre 2017: - % 15 decembre 2017 : adapted to take into account the roi known from - % metadata - % 27 fevrier 2018 : comments details - % 4 septembre 2018: comments and check and naming of folders - % 12 mars 2019 : save white reference with the same size as white estimate - % 16 avril 2019 : include diagonals to check the relevance of white - % reference - % 20 mai 2019 : track folder - % 27 mai 2019 : offset dark - % 4 juin 2019 : replace exist by isfolder or isfile + %% date + % 5 octobre 2017: + % 15 decembre 2017 : adapted to take into account the roi known from + % metadata + % 27 fevrier 2018 : comments details + % 4 septembre 2018: comments and check and naming of folders + % 12 mars 2019 : save white reference with the same size as white estimate + % 16 avril 2019 : include diagonals to check the relevance of white + % reference + % 20 mai 2019 : track folder + % 27 mai 2019 : offset dark + % 4 juin 2019 : replace exist by isfolder or isfile """ raise NotImplementedError() diff --git a/src/lipase/template_matcher.py b/src/lipase/template_matcher.py index 459f121..aeef318 100644 --- a/src/lipase/template_matcher.py +++ b/src/lipase/template_matcher.py @@ -1,11 +1,16 @@ -from imageengine import IImageEngine -from maxima_finder import IMaximaFinder +from .imageengine import IImageEngine +from .maxima_finder import IMaximaFinder class TemplateMatcher(object): + """ + Method to detect a pattern in an image. + + This method finds the locations of the given pattern in the image, provided the pattern is replicated without other geometric distortions than translations. + """ def __init__(self, maxima_finder): """ - :param IMaximaFinder: maxima_finder + :param IMaximaFinder maxima_finder: """ self.maxima_finder = maxima_finder diff --git a/src/lipase/traps_detector.py b/src/lipase/traps_detector.py index fe202a2..2f6d886 100644 --- a/src/lipase/traps_detector.py +++ b/src/lipase/traps_detector.py @@ -1,11 +1,11 @@ -from catalog import ImageCatalog, Sequence +from .catalog import ImageCatalog, Sequence # from imageengine import Toto -from imageengine import IImageEngine, Aabb, StackImageFeeder -from imageengine import PixelType -from telemos import WhiteEstimator, correct_non_uniform_lighting -from maxima_finder import Match -from template_matcher import TemplateMatcher +from .imageengine import IImageEngine, Aabb, StackImageFeeder +from .imageengine import PixelType +from .telemos import WhiteEstimator, correct_non_uniform_lighting +from .maxima_finder import Match +from .template_matcher import TemplateMatcher class TrapBinarizer(object):