ASHLAR image-stitching helper class

NOTE: This feature has not been merged into the production branch yet.

I have incorporated a class called Stitcher, which acts as a wrapper for the ASHLAR image-stitching software running in its own thread. An instance of Stitcher assumes it is receiving tiles which are each the sole data for their specific X-Y coordinate, so one should instantiate a Stitcher for each Z-level and each configuration taken at this Z-level. If using this class from within a multipoint_custom_script_entry, the dictionary mulitPointWorker.multiPointController.tile_stitchers is provided as a suggested place to store these. Below is the documentation for initializing a Stitcher instance.

class Stitcher():
    """
    :brief: Spawns a process for combining tiles as they are acquired
        into an OME TIFF file, followed by running ashlar to stitch them
        together.
    """
    def __init__(self, tiled_file_path="", stitched_file_path="", ashlar_args = {},
                 auto_run_ashlar = False, image_reader = default_image_reader):
        """
        :brief: Initializes class, creates control variables.
        :param tiled_file_path: output file to write the tiled OME-TIFF to, to
                        pass to ashlar as an input
        :param stitched_file_path: output file to write ashlar's stitching to
        :param ashlar_args: dict of additional arguments to ashlar besides I/O file,
                        will be unpacked and passed to ashlar wrapper. can also put
                        stdin/stdout/stderr for the ashlar process here
        :param auto_run_ashlar: Whether to automatically run ashlar after all tiles
                        are acquired
        :param image_reader: function to pass to queued_ometiff_writer as an image reader
        """

As an example, to either initialize or do operations on the relevant Stitcher for a given Z-level/configuration combination, first from control.stitcher import Stitcher at the start of your code, and then:

for k in range(multiPointWorker.NZ):
...
    for config in multiPointWorker.selected_configurations:
        stitcher_dict = multiPointWorker.multiPointController.tile_stitchers
        stitcher_key = str(config.name)+"_Z_"+str(k)
        stitcher_to_use = None
        image = multiPointWorker.camera.read_frame()
        #... (additional processing) ...
        saving_path = #(your method for generating individual tile filepaths here)
        cv2.imwrite(saving_path, image)
        try:
            stitcher_to_use = stitcher_dict[stitcher_key]
        except: # create stitcher if necessary
            stitcher_tiled_file_path = #(your method for generating a filename from Z-level + config name)
            stitcher_stitched_file_path #(same method, but with "ashlar_" prepended)
            #arguments to pass to the ashlar terminal command to run when stitching.
            #argument names should have dashes replaced with underscores and have leading
            #dashes removed before being written as kwargs
            stitcher_default_options = {'align_channel':0, 'maximum_shift':int(min(multiPointWorker.crop_width,multPointWorker.crop_height)*0.05), 'filter_sigma':1}
            stitcher_to_use = Stitcher(stitcher_tiled_file_path, stitcher_stitched_file_path, stitcher_default_options, auto_run_ashlar=True, image_reader = multiPointWorker.multiPointController.stitcher_image_reader)
            multiPointWorker.multiPointController.tile_stitchers[stitcher_key] = stitcher_to_use
            stitcher_to_use.start_ometiff_writer()
        tile_metadata = { 'Plane': { # necessary positioning data that ashlar needs to know
            'PositionX':int((j if multiPointWorker.x_scan_direction==1 else multiPointWorker.NX-1-j)*multiPointWorker.crop_width)
            'PositionY':int(i*multiPointWorker.crop_height)
        }
        stitcher_to_use.add_tile(saving_path, tile_metadata)

In this example, each Z-level/config combo will get an ASHLAR-stitched OME-TIFF image of the tiles captured with that configuration saved to its respective stitcher_stitched_file_path once all FOVs at that level have been captured.

For reference on how to pass the ashlar_args, here is the Python ASHLAR wrapper’s docstring:

def ashlar(inputpath, outputpath, stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, **kwargs):
    """
    :brief Uses subprocess Popen to call ashlar with arguments
    :param inputpath: String, path to input file, assumed multipage TIFF
        with metadata Plane {PositionX/PositionY} defined
    :param outputpath: String, path to output file
    :param kwargs: For other arguments to Ashlar, see labsyspharm/ashlar
        on Github. All arguments must be given in their "word" form, omitting
        the double dash at the start and replacing internal dashes with underscores.
        Only use 0 and 1 for False and True values
    :param stdin: stdin to pass to subprocess handler, defaults to dev null
    :param stdout: stdout to pass to subprocess handler, defaults to dev null
    :param stderr: stderr to pass to subprocess handler, defaults to dev null
    :return: A subprocess.Popen object representing the ongoing ashlar process.
        Can be polled or waited for.
    """

UPDATE: ASHLAR integration has been scrapped due to performance issues. We are now adding a script using Fiji for grid-based stitching instead.

For posterity, here’s the ashlar stitcher class: ASHLAR helper class for stitching large number of tiles together · GitHub