class RunCellpose(ImageSegmentation):
    category = "Object Processing"

    module_name = "RunCellpose"

    variable_revision_number = 2

    def create_settings(self):
        super(RunCellpose, self).create_settings()

        self.expected_diameter = Integer(
            text="Expected object diameter",
            value=15,
            minval=0,
            doc="""\
The average diameter of the objects to be detected. Setting this to 0 will attempt to automatically detect object size.
Note that automatic diameter mode does not work when running on 3D images.

Cellpose models come with a pre-defined object diameter. Your image will be resized during detection to attempt to 
match the diameter expected by the model. The default models have an expected diameter of ~16 pixels, if trying to 
detect much smaller objects it may be more efficient to resize the image first using the Resize module.
""",
        )

        self.mode = Choice(
            text="Detection mode",
            choices=[MODE_NUCLEI, MODE_CELLS, MODE_CUSTOM],
            value=MODE_NUCLEI,
            doc="""\
CellPose comes with models for detecting nuclei or cells. Alternatively, you can supply a custom-trained model 
generated using the command line or Cellpose GUI. Custom models can be useful if working with unusual cell types.
""",
        )

        self.use_gpu = Binary(text="Use GPU",
                              value=False,
                              doc=f"""\
If enabled, Cellpose will attempt to run detection on your system's graphics card (GPU). 
Note that you will need a CUDA-compatible GPU and correctly configured PyTorch version, see this link for details: 
{CUDA_LINK}

If disabled or incorrectly configured, Cellpose will run on your CPU instead. This is much slower but more compatible 
with different hardware setups.

Note that, particularly when in 3D mode, lack of GPU memory can become a limitation. If a model crashes you may need to 
re-start CellProfiler to release GPU memory. Resizing large images prior to running them through the model can free up 
GPU memory.

""")

        self.use_averaging = Binary(text="Use averaging",
                                    value=True,
                                    doc="""\
If enabled, CellPose will run it's 4 inbuilt models and take a consensus to determine the results. If disabled, only a 
single model will be called to produce results. Disabling averaging is faster to run but less accurate."""
                                    )

        self.supply_nuclei = Binary(text="Supply nuclei image as well?",
                                    value=False,
                                    doc="""
When detecting whole cells, you can provide a second image featuring a nuclear stain to assist 
the model with segmentation. This can help to split touching cells.""")

        self.nuclei_image = ImageSubscriber(
            "Select the nuclei image",
            doc="Select the image you want to use as the nuclear stain.")

        self.save_probabilities = Binary(
            text="Save probability image?",
            value=False,
            doc="""
If enabled, the probability scores from the model will be recorded as a new image. 
Probability >0 is considered as being part of a cell. 
You may want to use a higher threshold to manually generate objects.""",
        )

        self.probabilities_name = ImageName(
            "Name the probability image",
            "Probabilities",
            doc=
            "Enter the name you want to call the probability image produced by this module.",
        )

        self.model_directory = Directory(
            "Location of the pre-trained model file",
            doc=f"""\
*(Used only when using a custom pre-trained model)*

Select the location of the pre-trained CellPose model file that will be used for detection."""
        )

        def get_directory_fn():
            """Get the directory for the rules file name"""
            return self.model_directory.get_absolute_path()

        def set_directory_fn(path):
            dir_choice, custom_path = self.model_directory.get_parts_from_path(
                path)

            self.model_directory.join_parts(dir_choice, custom_path)

        self.model_file_name = Filename("Pre-trained model file name",
                                        "cyto_0",
                                        get_directory_fn=get_directory_fn,
                                        set_directory_fn=set_directory_fn,
                                        doc=f"""\
*(Used only when using a custom pre-trained model)*

This file can be generated by training a custom model withing the CellPose GUI or command line applications."""
                                        )

        self.gpu_test = DoSomething(
            "",
            "Test GPU",
            self.do_check_gpu,
            doc=f"""\
Press this button to check whether a GPU is correctly configured.

If you have a dedicated GPU, a failed test usually means that either your GPU does not support deep learning or the 
required dependencies are not installed.

If you have multiple GPUs on your system, this button will only test the first one.
""",
        )

        self.flow_threshold = Float(
            text="Flow threshold",
            value=0.4,
            minval=0,
            doc=
            """Flow error threshold. All cells with errors below this threshold are kept. Recommended default is 0.4""",
        )

        self.dist_threshold = Float(
            text="Cell probability threshold",
            value=0.0,
            minval=0,
            doc=f"""\
Cell probability threshold (all pixels with probability above threshold kept for masks). Recommended default is 0.0. """,
        )

    def settings(self):
        return [
            self.x_name, self.expected_diameter, self.mode, self.y_name,
            self.use_gpu, self.use_averaging, self.supply_nuclei,
            self.nuclei_image, self.save_probabilities,
            self.probabilities_name, self.model_directory,
            self.model_file_name, self.flow_threshold, self.dist_threshold
        ]

    def visible_settings(self):
        vis_settings = [self.mode, self.x_name]

        if self.mode.value != MODE_NUCLEI:
            vis_settings += [self.supply_nuclei]
            if self.supply_nuclei.value:
                vis_settings += [self.nuclei_image]
        if self.mode.value == MODE_CUSTOM:
            vis_settings += [self.model_directory, self.model_file_name]

        vis_settings += [
            self.expected_diameter, self.flow_threshold, self.dist_threshold,
            self.y_name, self.save_probabilities
        ]

        if self.save_probabilities.value:
            vis_settings += [self.probabilities_name]

        vis_settings += [self.use_averaging, self.use_gpu]

        if self.use_gpu.value:
            vis_settings += [self.gpu_test]

        return vis_settings

    def run(self, workspace):
        if self.mode.value != MODE_CUSTOM:
            model = models.Cellpose(model_type='cyto' if self.mode.value
                                    == MODE_CELLS else 'nuclei',
                                    gpu=self.use_gpu.value)
        else:
            model_file = self.model_file_name.value
            model_directory = self.model_directory.get_absolute_path()
            model_path = os.path.join(model_directory, model_file)
            model = models.CellposeModel(pretrained_model=model_path,
                                         gpu=self.use_gpu.value)

        x_name = self.x_name.value
        y_name = self.y_name.value
        images = workspace.image_set
        x = images.get_image(x_name)
        dimensions = x.dimensions
        x_data = x.pixel_data

        if x.multichannel:
            raise ValueError(
                "Color images are not currently supported. Please provide greyscale images."
            )

        if self.mode.value != "Nuclei" and self.supply_nuclei.value:
            nuc_image = images.get_image(self.nuclei_image.value)
            # CellPose expects RGB, we'll have a blank red channel, cells in green and nuclei in blue.
            if x.volumetric:
                x_data = numpy.stack(
                    (numpy.zeros_like(x_data), x_data, nuc_image.pixel_data),
                    axis=1)
            else:
                x_data = numpy.stack(
                    (numpy.zeros_like(x_data), x_data, nuc_image.pixel_data),
                    axis=-1)
            channels = [2, 3]
        else:
            channels = [0, 0]

        diam = self.expected_diameter.value if self.expected_diameter.value > 0 else None

        try:
            y_data, flows, *_ = model.eval(
                x_data,
                channels=channels,
                diameter=diam,
                net_avg=self.use_averaging.value,
                do_3D=x.volumetric,
                flow_threshold=self.flow_threshold.value,
                cellprob_threshold=self.dist_threshold.value)
        finally:
            if self.use_gpu.value and model.torch:
                # Try to clear some GPU memory for other worker processes.
                try:
                    from torch import cuda
                    cuda.empty_cache()
                except Exception as e:
                    print(
                        f"Unable to clear GPU memory. You may need to restart CellProfiler to change models. {e}"
                    )

        y = Objects()
        y.segmented = y_data
        y.parent_image = x.parent_image
        objects = workspace.object_set
        objects.add_objects(y, y_name)

        if self.save_probabilities.value:
            # Flows come out sized relative to CellPose's inbuilt model size.
            # We need to slightly resize to match the original image.
            size_corrected = resize(flows[2], y_data.shape)
            prob_image = Image(
                size_corrected,
                parent_image=x.parent_image,
                convert=False,
                dimensions=len(size_corrected.shape),
            )

            workspace.image_set.add(self.probabilities_name.value, prob_image)

            if self.show_window:
                workspace.display_data.probabilities = size_corrected

        self.add_measurements(workspace)

        if self.show_window:
            if x.volumetric:
                # Can't show CellPose-accepted colour images in 3D
                workspace.display_data.x_data = x.pixel_data
            else:
                workspace.display_data.x_data = x_data
            workspace.display_data.y_data = y_data
            workspace.display_data.dimensions = dimensions

    def display(self, workspace, figure):
        if self.save_probabilities.value:
            layout = (2, 2)
        else:
            layout = (2, 1)

        figure.set_subplots(dimensions=workspace.display_data.dimensions,
                            subplots=layout)

        figure.subplot_imshow(
            colormap="gray",
            image=workspace.display_data.x_data,
            title="Input Image",
            x=0,
            y=0,
        )

        figure.subplot_imshow_labels(
            image=workspace.display_data.y_data,
            sharexy=figure.subplot(0, 0),
            title=self.y_name.value,
            x=1,
            y=0,
        )
        if self.save_probabilities.value:
            figure.subplot_imshow(
                colormap="gray",
                image=workspace.display_data.probabilities,
                sharexy=figure.subplot(0, 0),
                title=self.probabilities_name.value,
                x=0,
                y=1,
            )

    def do_check_gpu(self):
        import importlib.util
        torch_installed = importlib.util.find_spec('torch') is not None
        if models.use_gpu(istorch=torch_installed):
            message = "GPU appears to be working correctly!"
        else:
            message = "GPU test failed. There may be something wrong with your configuration."
        import wx
        wx.MessageBox(message, caption="GPU Test")

    def upgrade_settings(self, setting_values, variable_revision_number,
                         module_name):
        if variable_revision_number == 1:
            setting_values = setting_values + ["0.4", "0.0"]
            variable_revision_number = 2
        return setting_values, variable_revision_number
class ExportToCellH5(cpm.Module):
    #
    # TODO: model z and t. Currently, CellProfiler would analyze each
    #       stack plane independently. I think the easiest way to handle
    #       z and t would be to add them to the site path if they are
    #       used in the experiment (e.g. a time series would have a
    #       path of "/plate/well/site/time")
    #
    #       I can add two more optional metadata keys that would let
    #       users capture this.
    #
    #       The more-complicated choice would be to store the data in a
    #       stack which would mean reworking the indices in every segmentation
    #       after the first. There are some legacy measurements that are
    #       really object numbers, so these would require a lot of work
    #       to get right. Also, the resulting segmentations are a little
    #       artificial since they seem to say that every object is one
    #       pixel thick in the T or Z direction.
    #

    module_name = "ExportToCellH5"
    variable_revision_number = 1
    category = ["File Processing"]

    SUBFILE_KEY = "subfile"
    IGNORE_METADATA = "None"

    def create_settings(self):
        '''Create the settings for the ExportToCellH5 module'''
        self.directory = Directory("Output file location",
                                   doc="""
            This setting lets you choose the folder for the output files.
            %(IO_FOLDER_CHOICE_HELP_TEXT)s
            """ % globals())

        def get_directory_fn():
            '''Get the directory for the CellH5 file'''
            return self.directory.get_absolute_path()

        def set_directory_fn(path):
            dir_choice, custom_path = self.directory.get_parts_from_path(path)
            self.directory.join_parts(dir_choice, custom_path)

        self.file_name = cps.text.Filename("Output file name",
                                           "DefaultOut.ch5",
                                           get_directory_fn=get_directory_fn,
                                           set_directory_fn=set_directory_fn,
                                           metadata=True,
                                           browse_msg="Choose CellH5 file",
                                           mode=cps.text.Filename.MODE_APPEND,
                                           exts=[("CellH5 file (*.cellh5)",
                                                  "*.ch5"),
                                                 ("HDF5 file (*.h5)", "*.h5"),
                                                 ("All files (*.*", "*.*")],
                                           doc="""
            This setting lets you name your CellH5 file. If you choose an
            existing file, CellProfiler will add new data to the file
            or overwrite existing locations.
            <p>%(IO_WITH_METADATA_HELP_TEXT)s %(USING_METADATA_TAGS_REF)s.
            For instance, if you have a metadata tag named
            "Plate", you can create a per-plate folder by selecting one the subfolder options
            and then specifying the subfolder name as "\g&lt;Plate&gt;". The module will
            substitute the metadata values for the current image set for any metadata tags in the
            folder name.%(USING_METADATA_HELP_REF)s.</p>

            """ % globals())
        self.overwrite_ok = cps.Binary(
            "Overwrite existing data without warning?",
            False,
            doc="""
            Select <i>"Yes"</i> to automatically overwrite any existing data
            for a site. Select <i>"No"</i> to be prompted first.

            If you are running the pipeline on a computing cluster,
            select <i>"Yes"</i> unless you want execution to stop because you
            will not be prompted to intervene. Also note that two instances
            of CellProfiler cannot write to the same file at the same time,
            so you must ensure that separate names are used on a cluster.
            """ % globals())
        self.repack = cps.Binary("Repack after analysis",
                                 True,
                                 doc="""
            This setting determines whether CellProfiler in multiprocessing mode
            repacks the data at the end of analysis. If you select <i>"Yes"</i>,
            CellProfiler will combine all of the satellite files into a single
            file upon completion. This option requires some extra temporary disk
            space and takes some time at the end of analysis, but results in
            a single file which may occupy less disk space. If you select
            <i>"No"</i>, CellProfiler will create a master file using the
            name that you give and this file will have links to individual
            data files that contain the actual data. Using the data generated by
            this option requires that you keep the master file and the linked
            files together when copying them to a new folder.
            """ % globals())
        self.plate_metadata = Choice("Plate metadata", [],
                                     value="Plate",
                                     choices_fn=self.get_metadata_choices,
                                     doc="""
            This is the metadata tag that identifies the plate name of
            the images for the current cycle. Choose <i>None</i> if
            your assay does not have metadata for plate name. If your
            assay is slide-based, you can use a metadata item that identifies
            the slide as the choice for this setting and set the well
            and site metadata items to <i>None</i>.""")
        self.well_metadata = Choice(
            "Well metadata", [],
            value="Well",
            choices_fn=self.get_metadata_choices,
            doc="""This is the metadata tag that identifies the well name
            for the images in the current cycle. Choose <i>None</i> if
            your assay does not have metadata for the well.""")
        self.site_metadata = Choice(
            "Site metadata", [],
            value="Site",
            choices_fn=self.get_metadata_choices,
            doc="""This is the metadata tag that identifies the site name
                for the images in the current cycle. Choose <i>None</i> if
                your assay doesn't divide wells up into sites or if this
                tag is not required for other reasons.""")
        self.divider = cps.Divider()
        self.wants_to_choose_measurements = cps.Binary("Choose measurements?",
                                                       False,
                                                       doc="""
            This setting lets you choose between exporting all measurements or
            just the ones that you choose. Select <i>"Yes"</i> to pick the
            measurements to be exported. Select <i>"No"</i> to automatically
            export all measurements available at this stage of the pipeline.
            """ % globals())
        self.measurements = MeasurementMultiChoice("Measurements to export",
                                                   doc="""
            <i>(Used only if choosing measurements.)</i>
            <br>
            This setting lets you choose individual measurements to be exported.
            Check the measurements you want to export.
            """)
        self.objects_to_export = []
        self.add_objects_button = DoSomething("Add objects to export",
                                              "Add objects", self.add_objects)
        self.images_to_export = []
        self.add_image_button = DoSomething("Add an image to export",
                                            "Add image", self.add_image)
        self.objects_count = cps.HiddenCount(self.objects_to_export)
        self.images_count = cps.HiddenCount(self.images_to_export)

    def add_objects(self, can_delete=True):
        group = cps.SettingsGroup()
        self.objects_to_export.append(group)
        group.append(
            "objects_name",
            LabelSubscriber("Objects name",
                            value="Nuclei",
                            doc="""
                This setting lets you choose the objects you want to export.
                <b>ExportToCellH5</b> will write the segmentation of the objects
                to your CellH5 file so that they can be saved and used by other
                applications that support the format.
                """))
        group.append(
            "Remover",
            cps.do_something.RemoveSettingButton("Remove the objects above",
                                                 "Remove",
                                                 self.objects_to_export,
                                                 group))

    def add_image(self, can_delete=True):
        group = cps.SettingsGroup()
        self.images_to_export.append(group)
        group.append(
            "image_name",
            ImageSubscriber("Image name",
                            value="DNA",
                            doc="""
            This setting lets you choose the images you want to export.
            <b>ExportToCellH5</b> will write the image
            to your CellH5 file so that it can be used by other
            applications that support the format.
            """))
        group.append(
            "remover",
            cps.do_something.RemoveSettingButton("Remove the image above",
                                                 "Remove",
                                                 self.objects_to_export,
                                                 group))

    def get_metadata_choices(self, pipeline):
        columns = pipeline.get_measurement_columns(self)
        choices = [self.IGNORE_METADATA]
        for column in columns:
            object_name, feature_name, column_type = column[:3]
            if object_name == "Image" and \
                    column_type.startswith(COLTYPE_VARCHAR) and \
                    feature_name.startswith(C_METADATA + "_"):
                choices.append(feature_name.split("_", 1)[1])
        return choices

    def settings(self):
        result = [
            self.objects_count, self.images_count, self.directory,
            self.file_name, self.overwrite_ok, self.repack,
            self.plate_metadata, self.well_metadata, self.site_metadata,
            self.wants_to_choose_measurements, self.measurements
        ]
        for objects_group in self.objects_to_export:
            result += objects_group.pipeline_settings()
        for images_group in self.images_to_export:
            result += images_group.pipeline_settings()
        return result

    def visible_settings(self):
        result = [
            self.directory, self.file_name, self.overwrite_ok, self.repack,
            self.plate_metadata, self.well_metadata, self.site_metadata,
            self.divider, self.wants_to_choose_measurements
        ]
        if self.wants_to_choose_measurements:
            result.append(self.measurements)

        for group in self.objects_to_export:
            result += group.visible_settings()
        result.append(self.add_objects_button)
        for group in self.images_to_export:
            result += group.visible_settings()
        result.append(self.add_image_button)
        return result

    def get_path_to_master_file(self, measurements):
        return os.path.join(self.directory.get_absolute_path(measurements),
                            self.file_name.value)

    def get_site_path(self, workspace, image_number):
        '''Get the plate / well / site tuple that identifies a field of view

        workspace - workspace for the analysis containing the metadata
                    measurements to be mined.

        image_number - the image number for the field of view

        returns a tuple which can be used for the hierarchical path
        to the group for a particular field of view
        '''
        m = workspace.measurements
        path = []
        for setting in self.plate_metadata, self.well_metadata, self.site_metadata:
            if setting.value == self.IGNORE_METADATA:
                path.append("NA")
            else:
                feature = "_".join((C_METADATA, setting.value))
                path.append(m["Image", feature, image_number])
        return tuple(path)

    def get_subfile_name(self, workspace):
        '''Contact the UI to find the cellh5 file to use to store results

        Internally, this tells the UI to create a link from the master file
        to the plate / well / site group that will be used to store results.
        Then, the worker writes into that file.
        '''
        master_file_name = self.get_path_to_master_file(workspace.measurements)
        path = self.get_site_path(workspace,
                                  workspace.measurements.image_set_number)
        return workspace.interaction_request(self,
                                             master_file_name,
                                             os.getpid(),
                                             path,
                                             headless_ok=True)

    def handle_interaction(self, master_file, pid, path):
        '''Handle an analysis worker / UI interaction

        This function is used to coordinate linking a group in the master file
        with a group in a subfile that is reserved for a particular
        analysis worker. Upon entry, the worker should be sure to have
        flushed and closed its subfile.

        master_file - the master cellh5 file which has links to groups
                      for each field of view
        pid - the process ID or other unique identifier of the worker
              talking to the master
        path - The combination of (Plate, Well, Site) that should be used
               as the folder path to the data.

        returns the name of the subfile to be used. After return, the
        subfile has been closed by the UI and a link has been established
        to the group named by the path.
        '''
        master_dict = self.get_dictionary().setdefault(master_file, {})
        if pid not in master_dict:
            md_head, md_tail = os.path.splitext(master_file)
            subfile = "%s_%s%s" % (md_head, str(pid), md_tail)
            master_dict[pid] = subfile
        else:
            subfile = master_dict[pid]

        ch5_master = cellh5.cellh5write.CH5MasterFile(master_file, "a")
        try:
            ch5_master.add_link_to_coord(self._to_ch5_coord(*path), subfile)
        finally:
            ch5_master.close()

        return subfile

    def _to_ch5_coord(self, plate, well, site):
        return cellh5.CH5PositionCoordinate(plate, well, site)

    def run(self, workspace):
        m = workspace.measurements
        object_set = workspace.object_set
        #
        # get plate / well / site as tuple
        #
        path = self.get_site_path(workspace, m.image_set_number)
        subfile_name = self.get_subfile_name(workspace)

        ### create CellH5 file
        with cellh5.cellh5write.CH5FileWriter(subfile_name,
                                              mode="a") as c5_file:
            ### add Postion (==plate, well, site) triple
            c5_pos = c5_file.add_position(self._to_ch5_coord(*path))

            for ch_idx, object_group in enumerate(self.objects_to_export):
                objects_name = object_group.objects_name.value
                objects = object_set.get_objects(objects_name)
                labels = objects.segmented
                if ch_idx == 0:
                    ### get shape of 5D cube
                    shape5D = (len(self.objects_to_export), 1, 1,
                               labels.shape[0], labels.shape[1])
                    dtype5D = np.uint16

                    ### create lablel writer for incremental writing
                    c5_label_writer = c5_pos.add_label_image(shape=shape5D,
                                                             dtype=dtype5D)
                    c5_label_def = cellh5.cellh5write.CH5ImageRegionDefinition(
                    )

                c5_label_writer.write(labels, c=ch_idx, t=0, z=0)
                c5_label_def.add_row(region_name=objects_name,
                                     channel_idx=ch_idx)

            if len(self.objects_to_export) > 0:
                ### finalize the writer
                c5_label_writer.write_definition(c5_label_def)
                c5_label_writer.finalize()

            n_channels = 0
            max_scale = 1
            max_i = 1
            max_j = 1
            for image_group in self.images_to_export:
                image = m.get_image(image_group.image_name.value)
                pixel_data = image.pixel_data
                if pixel_data.ndim == 3:
                    n_channels += min(pixel_data.shape[2], 3)
                else:
                    n_channels += 1
                max_scale = max(image.scale, max_scale)
                max_i = max(pixel_data.shape[0], max_i)
                max_j = max(pixel_data.shape[1], max_j)

            ### get shape of 5D cube
            shape5D = (n_channels, 1, 1, max_i, max_j)
            for dtype in (np.uint8, np.uint16, np.uint32, np.uint64):
                if max_scale <= np.iinfo(dtype).max:
                    dtype5D = dtype
                    break

            ### create image writer for incremental writing
            c5_image_writer = c5_pos.add_image(shape=shape5D, dtype=dtype5D)
            c5_image_def = cellh5.cellh5write.CH5ImageChannelDefinition()

            ch_idx = 0
            for image_group in self.images_to_export:
                image_name = image_group.image_name.value
                image = m.get_image(image_name).pixel_data
                scale = m.get_image(image_name).scale
                if not np.issubdtype(image.dtype, np.dtype(bool).type):
                    if scale == 1:
                        scale = max_scale
                    image = image * scale
                if image.ndim == 3:
                    for c in range(min(image.shape[2], 3)):
                        color_name, html_color = COLORS[c]
                        c5_image_writer.write(image[:, :, c].astype(dtype5D),
                                              c=ch_idx,
                                              t=0,
                                              z=0)
                        c5_image_def.add_row(channel_name="_".join(
                            (image_name, color_name)),
                                             description="%s %s intensity" %
                                             (image_name, color_name),
                                             is_physical=True,
                                             voxel_size=(1, 1, 1),
                                             color=html_color)
                        ch_idx += 1
                else:
                    c5_image_writer.write(image.astype(dtype5D),
                                          c=ch_idx,
                                          t=0,
                                          z=0)
                    c5_image_def.add_row(channel_name=image_name,
                                         description=image_name,
                                         is_physical=True,
                                         voxel_size=(1, 1, 1),
                                         color="0xFFFFFF")
                    ch_idx += 1
            c5_image_writer.write_definition(c5_image_def)
            c5_image_writer.finalize()

            columns = workspace.pipeline.get_measurement_columns(self)
            if self.wants_to_choose_measurements:
                to_keep = set([(self.measurements.get_measurement_object(s),
                                self.measurements.get_measurement_feature(s))
                               for s in self.measurements.selections])

                def keep(column):
                    return (column[0], column[1]) in to_keep

                columns = filter(keep, columns)
            #
            # I'm breaking the data up into the most granular form so that
            # it's clearer how it's organized. I'm expecting that you would
            # organize it differently when actually storing.
            #

            ### 0) extract object information (i.e. object_label_no)
            ### 1) extract all single cell features and write it as feature matrix (for e.g. classification)
            ### 2) extract Center
            ### 3) create artifical Bounding box... usefull for displaying it in fiji lateron
            ### 4) Don't see the point of features extracted on "Image" the only real and useful feature there is "Count" which can be deduced from single cell information

            ### 0) and 1) filter columns for cellular features
            feature_cols = filter(
                lambda xxx:
                (xxx[0] not in
                 (EXPERIMENT, "Image")) and m.has_feature(xxx[0], xxx[1]),
                columns)

            ### iterate over objects to export
            for ch_idx, object_group in enumerate(self.objects_to_export):
                objects_name = object_group.objects_name.value
                objects = object_set.get_objects(objects_name)

                ### find features for that object
                feature_cols_per_object = filter(
                    lambda xxx: xxx[0] == objects_name, feature_cols)

                c5_object_writer = c5_pos.add_region_object(objects_name)
                object_labels = objects.indices

                c5_object_writer.write(t=0,
                                       object_labels=np.array(object_labels))
                c5_object_writer.write_definition()
                c5_object_writer.finalize()

                ### iterate over all cellular feature to get feature matrix

                n_features = len(feature_cols_per_object)
                if n_features > 0:
                    feature_names = []
                    feature_matrix = []
                    for column in feature_cols_per_object:
                        object_name, feature_name = column[:2]
                        values = m[object_name, feature_name]

                        feature_names.append(feature_name)
                        feature_matrix.append(values[:, np.newaxis])

                    feature_matrix = np.concatenate(feature_matrix, axis=1)

                    c5_feature_writer = c5_pos.add_object_feature_matrix(
                        object_name=object_name,
                        feature_name="object_features",
                        n_features=n_features,
                        dtype=np.float32)
                    c5_feature_writer.write(feature_matrix)
                    c5_feature_writer.write_definition(feature_names)
                    c5_feature_writer.finalize()

                ### iterate over Location  to create bounding_box and center
                c5_bbox = c5_pos.add_object_bounding_box(
                    object_name=objects_name)

                if objects.count > 0:
                    ijv = objects.ijv
                    min_x = scipy.ndimage.minimum(ijv[:, 1], ijv[:, 2],
                                                  objects.indices)
                    max_x = scipy.ndimage.maximum(ijv[:, 1], ijv[:, 2],
                                                  objects.indices)
                    min_y = scipy.ndimage.minimum(ijv[:, 0], ijv[:, 2],
                                                  objects.indices)
                    max_y = scipy.ndimage.maximum(ijv[:, 0], ijv[:, 2],
                                                  objects.indices)
                    location_x = scipy.ndimage.mean(ijv[:, 1], ijv[:, 2],
                                                    objects.indices)
                    location_y = scipy.ndimage.mean(ijv[:, 0], ijv[:, 2],
                                                    objects.indices)
                    bb = np.c_[min_x, max_x, min_y, max_y]
                else:
                    bb = np.zeros((0, 4))
                    location_x = np.zeros(0)
                    location_y = np.zeros(0)

                c5_bbox.write(bb.astype(np.int32))
                c5_bbox.write_definition()
                c5_bbox.finalize()

                c5_center = c5_pos.add_object_center(object_name=objects_name)
                locations = {'x': location_x, 'y': location_y}
                cent = np.column_stack(
                    [locations[axis] for axis in c5_center.dtype.names])

                c5_center.write(cent.astype(np.int32))
                c5_center.write_definition()
                c5_center.finalize()
            #
            # The last part deals with relationships between segmentations.
            # The most typical relationship is "Parent" which is explained below,
            # but you can also have things like first nearest and second nearest
            # neighbor or in tracking, the relationship between the segmentation
            # of the previous and next frames.
            #
            for key in m.get_relationship_groups():
                relationships = m.get_relationships(key.module_number,
                                                    key.relationship,
                                                    key.object_name1,
                                                    key.object_name2,
                                                    [m.image_set_number])
                for image_number1, image_number2, \
                    object_number1, object_number2 in relationships:
                    if image_number1 == image_number2 and \
                                    key.relationship == R_PARENT:
                        #
                        # Object 1 is the parent to object 2 - this is the
                        # most common relationship, so if you can only record
                        # one, this is it. "Parent" usually means that
                        # the child's segmentation was seeded by the parent
                        # segmentation (e.g. Parent = nucleus, child = cell),
                        # but can also be something like Parent = cell,
                        # child = all organelles within the cell
                        #
                        # object_name1 is the name of the parent segmentation
                        # object_name2 is the name of the child segmentation
                        # object_number1 is the index used to label the
                        #                parent in the parent segmentation
                        # object_number2 is the index used to label the
                        #                child in the child segmentation
                        continue
                    if image_number1 != m.image_set_number:
                        path1 = self.get_site_path(workspace, image_number1)
                    else:
                        path1 = path
                    if image_number2 != m.image_set_number:
                        path2 = self.get_site_path(workspace, image_number2)
                    else:
                        path2 = path
                    #
                    # TODO: this is sort of extra credit, but the relationships
                    #       relate an object in one segmentation to another.
                    #       For tracking, these can be in different image
                    #       sets, (e.g. the cell at time T and at time T+1).
                    #       So, given object 1 and object 2, path1 and path2
                    #       tell you how the objects are related between planes.
                    pass

    def post_run(self, workspace):
        if self.repack:
            ### to be implemented with
            ### ch5_master.repack()
            return
            measurements = workspace.measurements
            fd, temp_name = tempfile.mkstemp(
                suffix=".ch5", dir=self.directory.get_absolute_path())

            master_name = self.get_path_to_master_file(workspace.measurements)
            src = h5py.File(master_name, "r")
            dest = h5py.File(temp_name)
            os.close(fd)
            for key in src:
                dest.copy(src[key], dest, expand_external=True)
            src.close()
            dest.close()
            os.unlink(master_name)
            os.rename(temp_name, master_name)

    def prepare_settings(self, setting_values):
        objects_count, images_count = [int(x) for x in setting_values[:2]]
        del self.objects_to_export[:]
        while len(self.objects_to_export) < objects_count:
            self.add_objects()
        del self.images_to_export[:]
        while len(self.images_to_export) < images_count:
            self.add_image()
Exemplo n.º 3
0
class RunImageJScript(Module):
    """
    Module to run ImageJ scripts via pyimagej
    """
    module_name = "RunImageJScript"
    variable_revision_number = 2
    category = "Advanced"

    def __init__(self):
        super().__init__()
        self.parsed_params = False  # Used for validation
        self.initialization_failed = False  # Used for validation

    def create_settings(self):
        module_explanation = [
            "The" + self.module_name + "module allows you to run any supported ImageJ script as part of your workflow.",
            "First, select your desired initialization method and specify the app directory or endpoint(s) if needed.",
            "Then select a script file to be executed by this module.",
            "Click the \"Get parameters from script\" button to detect required inputs for your script:",
            "each input will have its own setting created, allowing you to pass data from CellProfiler to ImageJ.",
            "After filling in any required inputs you can run the module normally.",
            "Note: ImageJ will only be initialized once per CellProfiler session.",
            "Note: only numeric, text and image parameters are currently supported.",
            "See also ImageJ Scripting: https://imagej.net/Scripting."

        ]
        self.set_notes([" ".join(module_explanation)])

        self.init_choice = Choice(
            "Initialization type", [INIT_LOCAL, INIT_ENDPOINT, INIT_LATEST],
            tooltips={INIT_LOCAL: "Use a local ImageJ/Fiji installation", INIT_ENDPOINT: "Specify a particular endpoint",
                      INIT_LATEST: "Use the latest Fiji, downloading if needed."},
            doc="""\
Note that initialization will only occur once per CellProfiler session! After initialization, these options will be
locked for the remainder of the session.

Select the mechanism for initializing ImageJ:
 * {init_local}: Use a local Fiji or ImageJ installation
 * {init_endpoint}: Precisely specify the version of one or more components
 * {init_latest}: Use the latest Fiji version

Note that any option besides {init_local} may result in a download of the requested components.
            """.format(
                init_local=INIT_LOCAL,
                init_endpoint=INIT_ENDPOINT,
                init_latest=INIT_LATEST,
            ),
        )

        self.endpoint_string = Text(
            "Initialization endpoint", "sc.fiji:fiji:2.1.0", doc="""\
Specify an initialization string as described in https://github.com/imagej/pyimagej/blob/master/doc/Initialization.md
            """,
        )

        self.initialized_method = Text("Initialization type", value="Do not use", doc="""\
Indicates the method that was used to initialized ImageJ in this CellProfiler session. 
            """,
        )

        self.convert_types = Binary("Adjust image type?", True, doc="""\
If enabled, ensures images are always converted to unsigned integer types when sent to ImageJ, and back to signed float types when returned to CellProfiler.
This can help common display issues by providing each application a best guess at its "expected" data type.
If you choose to disable this function, your ImageJ script will need to account for images coming in as signed float types.
            """,
        )

        global init_display_string
        if init_display_string:
            # ImageJ thread is already running
            self.initialized_method.set_value(init_display_string)

        self.app_directory = Directory(
            "ImageJ directory", allow_metadata=False, doc="""\
Select the folder containing the desired ImageJ/Fiji application.

{fcht}
""".format(
                fcht=IO_FOLDER_CHOICE_HELP_TEXT
            ),
        )
        if platform != 'darwin':
            self.app_directory.join_parts(ABSOLUTE_FOLDER_NAME, "Fiji.app")

        def set_directory_fn_app(path):
            dir_choice, custom_path = self.app_directory.get_parts_from_path(path)
            self.app_directory.join_parts(dir_choice, custom_path)

        self.app_file = Filename(
            "Local App", "Fiji.app", doc="Select the desired app, such as Fiji.app",
            get_directory_fn=self.app_directory.get_absolute_path,
            set_directory_fn=set_directory_fn_app,
            browse_msg="Choose local application"
        )

        self.script_directory = Directory(
            "Script directory",
            allow_metadata=False,
            doc="""\
Select the folder containing the script.

{fcht}
""".format(
                fcht=IO_FOLDER_CHOICE_HELP_TEXT
            ),
        )

        def set_directory_fn_script(script_path):
            dir_choice, custom_path = self.script_directory.get_parts_from_path(script_path)
            self.script_directory.join_parts(dir_choice, custom_path)
            self.clear_script_parameters()

        self.script_file = Filename(
            "ImageJ Script", "script.py", doc="Select a script file written in any ImageJ-supported scripting language.",
            get_directory_fn=self.script_directory.get_absolute_path,
            set_directory_fn=set_directory_fn_script,
            browse_msg="Choose ImageJ script file",
        )
        self.get_parameters_button = DoSomething("", 'Get parameters from script',
                                                 self.get_parameters_helper,
                                                 doc="""\
Parse parameters from the currently selected script and add the appropriate settings to this CellProfiler module.

Note: this must be done each time you change the script, before running the CellProfiler pipeline!
"""
                                                 )
        self.script_parameter_list = []
        self.script_input_settings = {}  # Map of input parameter names to CellProfiler settings objects
        self.script_output_settings = {}  # Map of output parameter names to CellProfiler settings objects
        self.script_parameter_count = HiddenCount(self.script_parameter_list)

    def get_init_string(self):
        """
        Determine if a particular initialization method has been specified. This could be a path to a local installation
        or a version string.
        """
        choice = self.init_choice.get_value()
        if choice == INIT_LATEST:
            return None

        if choice == INIT_LOCAL:
            init_string = self.app_directory.get_absolute_path()
            if platform == 'darwin':
                init_string = path.join(init_string, self.app_file.value)
        elif choice == INIT_ENDPOINT:
            init_string = self.endpoint_string.get_value()

        return init_string

    def close_pyimagej(self):
        """
        Close the pyimagej daemon thread
        """
        global imagej_process, to_imagej
        if imagej_process is not None:
            to_imagej.put({PYIMAGEJ_KEY_COMMAND: PYIMAGEJ_CMD_EXIT})

    def init_pyimagej(self):
        """
        Start the pyimagej daemon thread if it isn't already running.
        """
        self.initialization_failed = False
        init_string = self.get_init_string()

        global imagej_process, to_imagej, from_imagej, init_display_string
        if imagej_process is None:
            to_imagej = mp.Queue()
            from_imagej = mp.Queue()
            # TODO if needed we could set daemon=True
            imagej_process = Process(target=start_imagej_process, name="PyImageJ Daemon",
                                          args=(to_imagej, from_imagej, init_string,))
            imagej_process.start()
            result = from_imagej.get()
            if result == PYIMAGEJ_STATUS_STARTUP_FAILED:
                imagej_process = None
                self.initialization_failed = True
            else:
                atexit.register(self.close_pyimagej)  # TODO is there a more CP-ish way to do this?
                init_display_string = self.init_choice.get_value()
                if init_display_string != INIT_LATEST:
                    init_display_string += ": " + init_string
                self.initialized_method.set_value(init_display_string)

    def clear_script_parameters(self):
        """
        Remove any existing settings added by scripts
        """
        self.script_parameter_list.clear()
        self.script_input_settings.clear()
        self.script_output_settings.clear()
        self.parsed_params = False
        self.initialization_failed = False

    def get_parameters_helper(self):
        """
        Helper method to launch get_parameters_from_script on a thread so that it isn't run on the GUI thread, since
        it may be slow (when initializing pyimagej).
        """
        # Reset previously parsed parameters
        self.clear_script_parameters()

        global stop_progress_thread
        stop_progress_thread = False

        progress_gauge = Gauge(Window.FindFocus(), -1, size=(100, -1))
        progress_gauge.Show(True)

        parse_param_thread = Thread(target=self.get_parameters_from_script, name="Parse Parameters Thread", daemon=True)
        parse_param_thread.start()

        while True:
            # Wait for get_parameters_from_script to finish
            progress_gauge.Pulse()
            time.sleep(0.025)
            if stop_progress_thread:
                progress_gauge.Show(False)
                break

        if not self.initialization_failed:
            self.parsed_params = True

    def get_parameters_from_script(self):
        """
        Use PyImageJ to read header text from an ImageJ script and extract inputs/outputs, which are then converted to
        CellProfiler settings for this module
        """
        global stop_progress_thread, imagej_process, to_imagej, from_imagej
        script_filepath = path.join(self.script_directory.get_absolute_path(), self.script_file.value)

        if not self.script_file.value or not path.exists(script_filepath):
            # nothing to do
            stop_progress_thread = True
            return

        # Start pyimagej if needed
        self.init_pyimagej()
        if not imagej_process:
            stop_progress_thread = True
            return

        # Tell pyimagej to parse the script parameters
        to_imagej.put({PYIMAGEJ_KEY_COMMAND: PYIMAGEJ_CMD_SCRIPT_PARSE, PYIMAGEJ_KEY_INPUT: script_filepath})

        ij_return = from_imagej.get()

        # Process pyimagej's output, converting script parameters to settings
        if ij_return != PYIMAGEJ_STATUS_CMD_UNKNOWN:
            input_params = ij_return[PYIMAGEJ_SCRIPT_PARSE_INPUTS]
            output_params = ij_return[PYIMAGEJ_SCRIPT_PARSE_OUTPUTS]

            for param_dict, settings_dict, io_class in ((input_params, self.script_input_settings, INPUT_CLASS),
                                                        (output_params, self.script_output_settings, OUTPUT_CLASS)):
                for param_name in param_dict:
                    param_type = param_dict[param_name]
                    next_setting = convert_java_type_to_setting(param_name, param_type, io_class)
                    if next_setting is not None:
                        settings_dict[param_name] = next_setting
                        group = SettingsGroup()
                        group.append("setting", next_setting)
                        group.append("remover", RemoveSettingButton("", "Remove this variable",
                                                                    self.script_parameter_list, group))
                        add_param_info_settings(group, param_name, param_type, io_class)
                        # Each setting gets a group containing:
                        # 0 - the setting
                        # 1 - its remover
                        # 2 - (hidden) parameter name
                        # 3 - (hidden) parameter type
                        # 4 - (hidden) parameter i/o class
                        self.script_parameter_list.append(group)

            stop_progress_thread = True

    def settings(self):
        result = [self.script_parameter_count, self.init_choice, self.app_directory, self.app_file, self.endpoint_string, self.script_directory, self.script_file, self.get_parameters_button, self.convert_types]
        if len(self.script_parameter_list) > 0:
            result += [Divider(line=True)]
        for script_parameter_group in self.script_parameter_list:
            result += [script_parameter_group.setting]
            result += [script_parameter_group.remover]
            result += [script_parameter_group.name]
            result += [script_parameter_group.type]
            result += [script_parameter_group.io_class]

        return result

    def visible_settings(self):
        visible_settings = []
        global imagej_process

        # Update the visible settings based on the selected initialization method
        # If ImageJ is already initialized we just want to report how it was initialized
        # Otherwise we show: a string entry for "endpoint", a directory chooser for "local" (and file chooser if on mac),
        # and nothing if "latest"
        if not imagej_process:
            visible_settings += [self.init_choice]
            input_type = self.init_choice.get_value()
            # ImageJ is not initialized yet
            if input_type == INIT_ENDPOINT:
                visible_settings += [self.endpoint_string]
            elif input_type == INIT_LOCAL:
                visible_settings += [self.app_directory]
                if platform == 'darwin':
                    visible_settings += [self.app_file]
        else:
            # ImageJ is initialized
            visible_settings += [self.initialized_method]
        visible_settings += [Divider(line=True)]
        visible_settings += [self.script_directory, self.script_file, self.get_parameters_button, self.convert_types]
        if len(self.script_parameter_list) > 0:
            visible_settings += [Divider(line=True)]
        for script_parameter in self.script_parameter_list:
            visible_settings += [script_parameter.setting]
            visible_settings += [script_parameter.remover]

        return visible_settings

    def prepare_settings(self, setting_values):
        settings_count = int(setting_values[0])

        if settings_count == 0:
            # No params were saved
            return

        # Params were parsed previously and saved
        self.parsed_params = True

        # Looking at the last 5N elements will give the us (value, remover, name, type, io_class) for the N settings
        # We care about the name and type information, since this goes in one of our settings
        settings_info = setting_values[-settings_count * 5:]
        for i in range(0, len(settings_info), 5):
            group = SettingsGroup()
            param_name = settings_info[i + 2]
            param_type = settings_info[i + 3]
            io_class = settings_info[i + 4]
            setting = convert_java_type_to_setting(param_name, param_type, io_class)
            group.append("setting", setting)
            group.append("remover", RemoveSettingButton("", "Remove this variable", self.script_parameter_list, group))
            add_param_info_settings(group, param_name, param_type, io_class)
            self.script_parameter_list.append(group)
            if INPUT_CLASS == io_class:
                self.script_input_settings[param_name] = setting
            elif OUTPUT_CLASS == io_class:
                self.script_output_settings[param_name] = setting

    def validate_module(self, pipeline):
        if self.initialization_failed:
            raise ValidationError(
                "Error starting ImageJ. Please check your initialization settings and try again.",
                self.init_choice
            )

        no_script_msg = "Please select a valid ImageJ script and use the \"Get parameters from script\" button."

        if not self.parsed_params or not self.script_directory or not self.script_file.value:
            raise ValidationError(
                no_script_msg,
                self.script_file
            )

        script_filepath = path.join(self.script_directory.get_absolute_path(), self.script_file.value)
        if not path.exists(script_filepath):
            raise ValidationError(
                "The script you have selected is not a valid path. " + no_script_msg,
                self.script_file
            )

        if self.init_choice.get_value() == INIT_LOCAL:
            app_path = self.get_init_string()
            if not path.exists(app_path):
                raise ValidationError(
                    "The local application you have selected is not a valid path.",
                    self.app_directory
                )

    def validate_module_warnings(self, pipeline):
        global imagej_process
        """Warn user if the specified FIJI executable directory is not found, and warn that a copy of FIJI will be downloaded"""
        warn_msg = "Please note: any initialization method except \"Local\", a new Fiji may be downloaded"
        " to your machine if cached dependencies not found."
        init_type = self.init_choice.get_value()
        if init_type != INIT_LOCAL:
            # The component we attach the error to depends on if initialization has happened or not
            if not imagej_process:
                raise ValidationError(warn_msg, self.init_choice)
            else:
                raise ValidationError(warn_msg + " If re-initialization is required, please restart CellProfiler.",
                                      self.initialized_method)


    def run(self, workspace):
        self.init_pyimagej()

        if self.show_window:
            workspace.display_data.script_input_pixels = {}
            workspace.display_data.script_input_dimensions = {}
            workspace.display_data.script_output_pixels = {}
            workspace.display_data.script_output_dimensions = {}

        script_filepath = path.join(self.script_directory.get_absolute_path(), self.script_file.value)
        # convert the CP settings to script parameters for pyimagej
        script_inputs = {}
        for name in self.script_input_settings:
            setting = self.script_input_settings[name]
            if isinstance(setting, ImageSubscriber):
                # Images need to be pulled from the workspace
                script_inputs[name] = workspace.image_set.get_image(setting.get_value())
                if self.show_window:
                    workspace.display_data.script_input_pixels[name] = script_inputs[name].pixel_data
                    workspace.display_data.script_input_dimensions[name] = script_inputs[name].dimensions
            else:
                # Other settings can be read directly
                script_inputs[name] = setting.get_value()

        # Start the script
        to_imagej.put({PYIMAGEJ_KEY_COMMAND: PYIMAGEJ_CMD_SCRIPT_RUN, PYIMAGEJ_KEY_INPUT:
            {PYIMAGEJ_SCRIPT_RUN_FILE_KEY: script_filepath,
             PYIMAGEJ_SCRIPT_RUN_INPUT_KEY: script_inputs,
             PYIMAGEJ_SCRIPT_RUN_CONVERT_IMAGES: self.convert_types.value}
                            })

        # Retrieve script output
        ij_return = from_imagej.get()
        if ij_return != PYIMAGEJ_STATUS_CMD_UNKNOWN:
            script_outputs = ij_return[PYIMAGEJ_KEY_OUTPUT]
            for name in self.script_output_settings:
                output_key = self.script_output_settings[name].get_value()
                output_value = script_outputs[name]
                # convert back to floats for CellProfiler
                if self.convert_types.value:
                    output_value = skimage.img_as_float(output_value)
                output_image = Image(image=output_value, convert=False)
                workspace.image_set.add(output_key, output_image)
                if self.show_window:
                    workspace.display_data.script_output_pixels[name] = output_image.pixel_data
                    workspace.display_data.dimensions = output_image.dimensions

    def display(self, workspace, figure):
        # TODO how do we handle differences in dimensionality between input/output images?
        figure.set_subplots((2, max(len(workspace.display_data.script_input_pixels),
                                    len(workspace.display_data.script_output_pixels))), dimensions=2)

        i = 0
        for name in workspace.display_data.script_input_pixels:
            figure.subplot_imshow_grayscale(
                0,
                i,
                workspace.display_data.script_input_pixels[name],
                title="Input image: {}".format(name),
            )
            i += 1

        i = 0
        for name in workspace.display_data.script_output_pixels:
            figure.subplot_imshow_grayscale(
                1,
                i,
                workspace.display_data.script_output_pixels[name],
                title="Output image: {}".format(name),
            )
            i += 1


    def upgrade_settings(self, setting_values, variable_revision_number, module_name):
        if variable_revision_number == 1:
            # Added convert_types Binary setting
            setting_values = setting_values[:8]+[True]+setting_values[8:]
            variable_revision_number = 2

        return setting_values, variable_revision_number
Exemplo n.º 4
0
class FilterObjects(ObjectProcessing):
    module_name = "FilterObjects"

    variable_revision_number = 8

    def create_settings(self):
        super(FilterObjects, self).create_settings()

        self.x_name.text = """Select the objects to filter"""

        self.x_name.doc = """\
Select the set of objects that you want to filter. This setting also
controls which measurement choices appear for filtering: you can only
filter based on measurements made on the object you select. Be sure
the **FilterObjects** module is downstream of the necessary **Measure**
modules. If you
intend to use a measurement calculated by the **CalculateMath** module
to to filter objects, select the first operand’s object here, because
**CalculateMath** measurements are stored with the first operand’s
object."""

        self.y_name.text = """Name the output objects"""

        self.y_name.doc = "Enter a name for the collection of objects that are retained after applying the filter(s)."

        self.spacer_1 = Divider(line=False)

        self.mode = Choice(
            "Select the filtering mode",
            [MODE_MEASUREMENTS, MODE_RULES, MODE_BORDER, MODE_CLASSIFIERS],
            doc="""\
You can choose from the following options:

-  *{MODE_MEASUREMENTS}*: Specify a per-object measurement made by an
   upstream module in the pipeline.
-  *{MODE_BORDER}*: Remove objects touching the border of the image
   and/or the edges of an image mask.
-  *{MODE_RULES}*: Use a file containing rules generated by
   CellProfiler Analyst. You will need to ensure that the measurements
   specified by the rules file are produced by upstream modules in the
   pipeline. This setting is not compatible with data processed as 3D.
-  *{MODE_CLASSIFIERS}*: Use a file containing a trained classifier from
   CellProfiler Analyst. You will need to ensure that the measurements
   specified by the file are produced by upstream modules in the
   pipeline. This setting is not compatible with data processed as 3D.""".format(
                **{
                    "MODE_MEASUREMENTS": MODE_MEASUREMENTS,
                    "MODE_RULES": MODE_RULES,
                    "MODE_BORDER": MODE_BORDER,
                    "MODE_CLASSIFIERS": MODE_CLASSIFIERS,
                }
            ),
        )

        self.spacer_2 = Divider(line=False)

        self.measurements = []

        self.measurement_count = HiddenCount(self.measurements, "Measurement count")

        self.add_measurement(False)

        self.add_measurement_button = DoSomething(
            "", "Add another measurement", self.add_measurement
        )

        self.filter_choice = Choice(
            "Select the filtering method",
            FI_ALL,
            FI_LIMITS,
            doc="""\
*(Used only if filtering using measurements)*

There are five different ways to filter objects:

-  *{FI_LIMITS}:* Keep an object if its measurement value falls within
   a range you specify.
-  *{FI_MAXIMAL}:* Keep the object with the maximum value for the
   measurement of interest. If multiple objects share a maximal value,
   retain one object selected arbitrarily per image.
-  *{FI_MINIMAL}:* Keep the object with the minimum value for the
   measurement of interest. If multiple objects share a minimal value,
   retain one object selected arbitrarily per image.
-  *{FI_MAXIMAL_PER_OBJECT}:* This option requires you to choose a
   parent object. The parent object might contain several child objects
   of choice (for instance, mitotic spindles within a cell or FISH probe
   spots within a nucleus). Only the child object whose measurements
   equal the maximum child-measurement value among that set of child
   objects will be kept (for example, the longest spindle in each cell).
   You do not have to explicitly relate objects before using this
   module.
-  *{FI_MINIMAL_PER_OBJECT}:* Same as *Maximal per object*, except
   filtering is based on the minimum value.""".format(
                **{
                    "FI_LIMITS": FI_LIMITS,
                    "FI_MAXIMAL": FI_MAXIMAL,
                    "FI_MINIMAL": FI_MINIMAL,
                    "FI_MAXIMAL_PER_OBJECT": FI_MAXIMAL_PER_OBJECT,
                    "FI_MINIMAL_PER_OBJECT": FI_MINIMAL_PER_OBJECT,
                }
            ),
        )

        self.per_object_assignment = Choice(
            "Assign overlapping child to",
            PO_ALL,
            doc="""\
*(Used only if filtering per object)*

A child object can overlap two parent objects and can have the
maximal/minimal measurement of all child objects in both parents. This
option controls how an overlapping maximal/minimal child affects
filtering of other children of its parents and to which parent the
maximal child is assigned. The choices are:

-  *{PO_BOTH}*: The child will be assigned to both parents and all
   other children of both parents will be filtered. Only the maximal
   child per parent will be left, but if **RelateObjects** is used to
   relate the maximal child to its parent, one or the other of the
   overlapping parents will not have a child even though the excluded
   parent may have other child objects. The maximal child can still be
   assigned to both parents using a database join via the relationships
   table if you are using **ExportToDatabase** and separate object
   tables.
-  *{PO_PARENT_WITH_MOST_OVERLAP}*: The child will be assigned to
   the parent with the most overlap and a child with a less
   maximal/minimal measurement, if available, will be assigned to other
   parents. Use this option to ensure that parents with an alternate
   non-overlapping child object are assigned some child object by a
   subsequent **RelateObjects** module.""".format(
                **{
                    "PO_BOTH": PO_BOTH,
                    "PO_PARENT_WITH_MOST_OVERLAP": PO_PARENT_WITH_MOST_OVERLAP,
                }
            ),
        )

        self.enclosing_object_name = LabelSubscriber(
            "Select the objects that contain the filtered objects",
            "None",
            doc="""\
*(Used only if a per-object filtering method is selected)*

This setting selects the container (i.e., parent) objects for the
*{FI_MAXIMAL_PER_OBJECT}* and *{FI_MINIMAL_PER_OBJECT}* filtering
choices.""".format(
                **{
                    "FI_MAXIMAL_PER_OBJECT": FI_MAXIMAL_PER_OBJECT,
                    "FI_MINIMAL_PER_OBJECT": FI_MINIMAL_PER_OBJECT,
                }
            ),
        )

        self.rules_directory = Directory(
            "Select the location of the rules or classifier file",
            doc="""\
*(Used only when filtering using {MODE_RULES} or {MODE_CLASSIFIERS})*

Select the location of the rules or classifier file that will be used for
filtering.

{IO_FOLDER_CHOICE_HELP_TEXT}
""".format(
                **{
                    "MODE_CLASSIFIERS": MODE_CLASSIFIERS,
                    "MODE_RULES": MODE_RULES,
                    "IO_FOLDER_CHOICE_HELP_TEXT": _help.IO_FOLDER_CHOICE_HELP_TEXT,
                }
            ),
        )

        self.rules_class = Choice(
            "Class number",
            choices=["1", "2"],
            choices_fn=self.get_class_choices,
            doc="""\
*(Used only when filtering using {MODE_RULES} or {MODE_CLASSIFIERS})*

Select which of the classes to keep when filtering. The CellProfiler
Analyst classifier user interface lists the names of the classes in
left-to-right order. **FilterObjects** uses the first class from
CellProfiler Analyst if you choose “1”, etc.

Please note the following:

-  The object is retained if the object falls into the selected class.
-  You can make multiple class selections. If you do so, the module will
   retain the object if the object falls into any of the selected
   classes.""".format(
                **{"MODE_CLASSIFIERS": MODE_CLASSIFIERS, "MODE_RULES": MODE_RULES}
            ),
        )

        def get_directory_fn():
            """Get the directory for the rules file name"""
            return self.rules_directory.get_absolute_path()

        def set_directory_fn(path):
            dir_choice, custom_path = self.rules_directory.get_parts_from_path(path)

            self.rules_directory.join_parts(dir_choice, custom_path)

        self.rules_file_name = Filename(
            "Rules or classifier file name",
            "rules.txt",
            get_directory_fn=get_directory_fn,
            set_directory_fn=set_directory_fn,
            doc="""\
*(Used only when filtering using {MODE_RULES} or {MODE_CLASSIFIERS})*

The name of the rules or classifier file.

A rules file is a plain text file containing the complete set of rules.

Each line of the rules file should be a rule naming a measurement to be made
on the object you selected, for instance:

    IF (Nuclei_AreaShape_Area < 351.3, [0.79, -0.79], [-0.94, 0.94])

The above rule will score +0.79 for the positive category and -0.94
for the negative category for nuclei whose area is less than 351.3
pixels and will score the opposite for nuclei whose area is larger.
The filter adds positive and negative and keeps only objects whose
positive score is higher than the negative score.

A classifier file is a trained classifier exported from CellProfiler Analyst.
You will need to ensure that the measurements specified by the file are
produced by upstream modules in the pipeline. This setting is not compatible
with data processed as 3D.
""".format(
                **{"MODE_CLASSIFIERS": MODE_CLASSIFIERS, "MODE_RULES": MODE_RULES}
            ),
        )

        self.additional_objects = []

        self.additional_object_count = HiddenCount(
            self.additional_objects, "Additional object count"
        )

        self.spacer_3 = Divider(line=False)

        self.additional_object_button = DoSomething(
            "Relabel additional objects to match the filtered object?",
            "Add an additional object",
            self.add_additional_object,
            doc="""\
Click this button to add an object to receive the same post-filtering labels as
the filtered object. This is useful in making sure that labeling is maintained
between related objects (e.g., primary and secondary objects) after filtering.""",
        )

    def get_class_choices(self, pipeline):
        if self.mode == MODE_CLASSIFIERS:
            return self.get_bin_labels()
        elif self.mode == MODE_RULES:
            rules = self.get_rules()
            nclasses = len(rules.rules[0].weights[0])
            return [str(i) for i in range(1, nclasses + 1)]

    def get_rules_class_choices(self, pipeline):
        try:
            rules = self.get_rules()
            nclasses = len(rules.rules[0].weights[0])
            return [str(i) for i in range(1, nclasses + 1)]
        except:
            return [str(i) for i in range(1, 3)]

    def add_measurement(self, can_delete=True):
        """Add another measurement to the filter list"""
        group = SettingsGroup()

        group.append(
            "measurement",
            Measurement(
                "Select the measurement to filter by",
                self.x_name.get_value,
                "AreaShape_Area",
                doc="""\
*(Used only if filtering using {MODE_MEASUREMENTS})*

See the **Measurements** modules help pages for more information on the
features measured.""".format(
                    **{"MODE_MEASUREMENTS": MODE_MEASUREMENTS}
                ),
            ),
        )

        group.append(
            "wants_minimum",
            Binary(
                "Filter using a minimum measurement value?",
                True,
                doc="""\
*(Used only if {FI_LIMITS} is selected for filtering method)*

Select "*{YES}*" to filter the objects based on a minimum acceptable
object measurement value. Objects which are greater than or equal to
this value will be retained.""".format(
                    **{"FI_LIMITS": FI_LIMITS, "YES": "Yes"}
                ),
            ),
        )

        group.append("min_limit", Float("Minimum value", 0))

        group.append(
            "wants_maximum",
            Binary(
                "Filter using a maximum measurement value?",
                True,
                doc="""\
*(Used only if {FI_LIMITS} is selected for filtering method)*

Select "*{YES}*" to filter the objects based on a maximum acceptable
object measurement value. Objects which are less than or equal to this
value will be retained.""".format(
                    **{"FI_LIMITS": FI_LIMITS, "YES": "Yes"}
                ),
            ),
        )

        group.append("max_limit", Float("Maximum value", 1))

        group.append("divider", Divider())

        self.measurements.append(group)

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton(
                    "", "Remove this measurement", self.measurements, group
                ),
            )

    def add_additional_object(self):
        group = SettingsGroup()

        group.append(
            "object_name",
            LabelSubscriber("Select additional object to relabel", "None"),
        )

        group.append(
            "target_name", LabelName("Name the relabeled objects", "FilteredGreen"),
        )

        group.append(
            "remover",
            RemoveSettingButton(
                "", "Remove this additional object", self.additional_objects, group
            ),
        )

        group.append("divider", Divider(line=False))

        self.additional_objects.append(group)

    def prepare_settings(self, setting_values):
        """Make sure the # of slots for additional objects matches
           the anticipated number of additional objects"""
        additional_object_count = int(setting_values[ADDITIONAL_OBJECT_SETTING_INDEX])
        while len(self.additional_objects) > additional_object_count:
            self.remove_additional_object(self.additional_objects[-1].key)
        while len(self.additional_objects) < additional_object_count:
            self.add_additional_object()

        measurement_count = int(setting_values[MEASUREMENT_COUNT_SETTING_INDEX])
        while len(self.measurements) > measurement_count:
            del self.measurements[-1]
        while len(self.measurements) < measurement_count:
            self.add_measurement()

    def settings(self):
        settings = super(FilterObjects, self).settings()

        settings += [
            self.mode,
            self.filter_choice,
            self.enclosing_object_name,
            self.rules_directory,
            self.rules_file_name,
            self.rules_class,
            self.measurement_count,
            self.additional_object_count,
            self.per_object_assignment,
        ]

        for x in self.measurements:
            settings += x.pipeline_settings()

        for x in self.additional_objects:
            settings += [x.object_name, x.target_name]

        return settings

    def help_settings(self):
        return [
            self.x_name,
            self.y_name,
            self.mode,
            self.filter_choice,
            self.per_object_assignment,
            self.rules_directory,
            self.rules_file_name,
            self.rules_class,
            self.enclosing_object_name,
            self.additional_object_button,
        ]

    def visible_settings(self):
        visible_settings = super(FilterObjects, self).visible_settings()

        visible_settings += [self.spacer_2, self.mode]

        if self.mode == MODE_RULES or self.mode == MODE_CLASSIFIERS:
            visible_settings += [
                self.rules_file_name,
                self.rules_directory,
                self.rules_class,
            ]
            self.rules_class.text = (
                "Class number" if self.mode == MODE_RULES else "Class name"
            )
            try:
                self.rules_class.test_valid(None)
            except:
                pass

        elif self.mode == MODE_MEASUREMENTS:
            visible_settings += [self.spacer_1, self.filter_choice]
            if self.filter_choice in (FI_MINIMAL, FI_MAXIMAL):
                visible_settings += [
                    self.measurements[0].measurement,
                    self.measurements[0].divider,
                ]
            elif self.filter_choice in (FI_MINIMAL_PER_OBJECT, FI_MAXIMAL_PER_OBJECT):
                visible_settings += [
                    self.per_object_assignment,
                    self.measurements[0].measurement,
                    self.enclosing_object_name,
                    self.measurements[0].divider,
                ]
            elif self.filter_choice == FI_LIMITS:
                for i, group in enumerate(self.measurements):
                    visible_settings += [group.measurement, group.wants_minimum]
                    if group.wants_minimum:
                        visible_settings.append(group.min_limit)
                    visible_settings.append(group.wants_maximum)
                    if group.wants_maximum.value:
                        visible_settings.append(group.max_limit)
                    if i > 0:
                        visible_settings += [group.remover]
                    visible_settings += [group.divider]
                visible_settings += [self.add_measurement_button]
        visible_settings.append(self.spacer_3)
        for x in self.additional_objects:
            visible_settings += x.visible_settings()
        visible_settings += [self.additional_object_button]
        return visible_settings

    def validate_module(self, pipeline):
        """Make sure that the user has selected some limits when filtering"""
        if self.mode == MODE_MEASUREMENTS and self.filter_choice == FI_LIMITS:
            for group in self.measurements:
                if not (group.wants_minimum.value or group.wants_maximum.value):
                    raise ValidationError(
                        "Please enter a minimum and/or maximum limit for your measurement",
                        group.wants_minimum,
                    )
        if self.mode == MODE_RULES:
            try:
                rules = self.get_rules()
            except Exception as instance:
                logging.warning(
                    "Failed to load rules: %s", str(instance), exc_info=True
                )
                raise ValidationError(str(instance), self.rules_file_name)
            measurement_columns = pipeline.get_measurement_columns(self)
            for r in rules.rules:
                if not any(
                    [
                        mc[0] == r.object_name and mc[1] == r.feature
                        for mc in measurement_columns
                    ]
                ):
                    raise ValidationError(
                        (
                            "The rules file, %s, uses the measurement, %s "
                            "for object %s, but that measurement is not available "
                            "at this stage of the pipeline. Consider editing the "
                            "rules to match the available measurements or adding "
                            "measurement modules to produce the measurement."
                        )
                        % (self.rules_file_name, r.feature, r.object_name),
                        self.rules_file_name,
                    )
        elif self.mode == MODE_CLASSIFIERS:
            try:
                self.get_classifier()
                self.get_bin_labels()
                self.get_classifier_features()
            except IOError:
                raise ValidationError(
                    "Failed to load classifier file %s" % self.rules_file_name.value,
                    self.rules_file_name,
                )
            except:
                raise ValidationError(
                    "Unable to load %s as a classifier file"
                    % self.rules_file_name.value,
                    self.rules_file_name,
                )

    def run(self, workspace):
        """Filter objects for this image set, display results"""
        src_objects = workspace.get_objects(self.x_name.value)
        if self.mode == MODE_RULES:
            indexes = self.keep_by_rules(workspace, src_objects)
        elif self.mode == MODE_MEASUREMENTS:
            if self.filter_choice in (FI_MINIMAL, FI_MAXIMAL):
                indexes = self.keep_one(workspace, src_objects)
            if self.filter_choice in (FI_MINIMAL_PER_OBJECT, FI_MAXIMAL_PER_OBJECT):
                indexes = self.keep_per_object(workspace, src_objects)
            if self.filter_choice == FI_LIMITS:
                indexes = self.keep_within_limits(workspace, src_objects)
        elif self.mode == MODE_BORDER:
            indexes = self.discard_border_objects(src_objects)
        elif self.mode == MODE_CLASSIFIERS:
            indexes = self.keep_by_class(workspace, src_objects)
        else:
            raise ValueError("Unknown filter choice: %s" % self.mode.value)

        #
        # Create an array that maps label indexes to their new values
        # All labels to be deleted have a value in this array of zero
        #
        new_object_count = len(indexes)
        max_label = numpy.max(src_objects.segmented)
        label_indexes = numpy.zeros((max_label + 1,), int)
        label_indexes[indexes] = numpy.arange(1, new_object_count + 1)
        #
        # Loop over both the primary and additional objects
        #
        object_list = [(self.x_name.value, self.y_name.value)] + [
            (x.object_name.value, x.target_name.value) for x in self.additional_objects
        ]
        m = workspace.measurements
        for src_name, target_name in object_list:
            src_objects = workspace.get_objects(src_name)
            target_labels = src_objects.segmented.copy()
            #
            # Reindex the labels of the old source image
            #
            target_labels[target_labels > max_label] = 0
            target_labels = label_indexes[target_labels]
            #
            # Make a new set of objects - retain the old set's unedited
            # segmentation for the new and generally try to copy stuff
            # from the old to the new.
            #
            target_objects = cellprofiler_core.object.Objects()
            target_objects.segmented = target_labels
            target_objects.unedited_segmented = src_objects.unedited_segmented
            #
            # Remove the filtered objects from the small_removed_segmented
            # if present. "small_removed_segmented" should really be
            # "filtered_removed_segmented".
            #
            small_removed = src_objects.small_removed_segmented.copy()
            small_removed[(target_labels == 0) & (src_objects.segmented != 0)] = 0
            target_objects.small_removed_segmented = small_removed
            if src_objects.has_parent_image:
                target_objects.parent_image = src_objects.parent_image
            workspace.object_set.add_objects(target_objects, target_name)

            self.add_measurements(workspace, src_name, target_name)

        if self.show_window:
            workspace.display_data.src_objects_segmented = src_objects.segmented
            workspace.display_data.target_objects_segmented = target_objects.segmented
            workspace.display_data.dimensions = src_objects.dimensions

    def display(self, workspace, figure):
        """Display what was filtered"""
        src_name = self.x_name.value
        src_objects_segmented = workspace.display_data.src_objects_segmented
        target_objects_segmented = workspace.display_data.target_objects_segmented
        dimensions = workspace.display_data.dimensions

        target_name = self.y_name.value

        figure.set_subplots((2, 2), dimensions=dimensions)

        figure.subplot_imshow_labels(
            0, 0, src_objects_segmented, title="Original: %s" % src_name
        )

        figure.subplot_imshow_labels(
            1,
            0,
            target_objects_segmented,
            title="Filtered: %s" % target_name,
            sharexy=figure.subplot(0, 0),
        )

        statistics = [
            [numpy.max(src_objects_segmented)],
            [numpy.max(target_objects_segmented)],
        ]

        figure.subplot_table(
            0,
            1,
            statistics,
            row_labels=(
                "Number of objects pre-filtering",
                "Number of objects post-filtering",
            ),
        )

    def keep_one(self, workspace, src_objects):
        """Return an array containing the single object to keep

        workspace - workspace passed into Run
        src_objects - the Objects instance to be filtered
        """
        measurement = self.measurements[0].measurement.value
        src_name = self.x_name.value
        values = workspace.measurements.get_current_measurement(src_name, measurement)
        if len(values) == 0:
            return numpy.array([], int)
        best_idx = (
            numpy.argmax(values)
            if self.filter_choice == FI_MAXIMAL
            else numpy.argmin(values)
        ) + 1
        return numpy.array([best_idx], int)

    def keep_per_object(self, workspace, src_objects):
        """Return an array containing the best object per enclosing object

        workspace - workspace passed into Run
        src_objects - the Objects instance to be filtered
        """
        measurement = self.measurements[0].measurement.value
        src_name = self.x_name.value
        enclosing_name = self.enclosing_object_name.value
        src_objects = workspace.get_objects(src_name)
        enclosing_objects = workspace.get_objects(enclosing_name)
        enclosing_labels = enclosing_objects.segmented
        enclosing_max = enclosing_objects.count
        if enclosing_max == 0:
            return numpy.array([], int)
        enclosing_range = numpy.arange(1, enclosing_max + 1)
        #
        # Make a vector of the value of the measurement per label index.
        # We can then label each pixel in the image with the measurement
        # value for the object at that pixel.
        # For unlabeled pixels, put the minimum value if looking for the
        # maximum value and vice-versa
        #
        values = workspace.measurements.get_current_measurement(src_name, measurement)
        wants_max = self.filter_choice == FI_MAXIMAL_PER_OBJECT
        src_labels = src_objects.segmented
        src_count = src_objects.count
        if self.per_object_assignment == PO_PARENT_WITH_MOST_OVERLAP:
            #
            # Find the number of overlapping pixels in enclosing
            # and source objects
            #
            mask = enclosing_labels * src_labels != 0
            enclosing_labels = enclosing_labels[mask]
            src_labels = src_labels[mask]
            order = numpy.lexsort((enclosing_labels, src_labels))
            src_labels = src_labels[order]
            enclosing_labels = enclosing_labels[order]
            firsts = numpy.hstack(
                (
                    [0],
                    numpy.where(
                        (src_labels[:-1] != src_labels[1:])
                        | (enclosing_labels[:-1] != enclosing_labels[1:])
                    )[0]
                    + 1,
                    [len(src_labels)],
                )
            )
            areas = firsts[1:] - firsts[:-1]
            enclosing_labels = enclosing_labels[firsts[:-1]]
            src_labels = src_labels[firsts[:-1]]
            #
            # Re-sort by source label value and area descending
            #
            if wants_max:
                svalues = -values
            else:
                svalues = values
            order = numpy.lexsort((-areas, svalues[src_labels - 1]))
            src_labels, enclosing_labels, areas = [
                x[order] for x in (src_labels, enclosing_labels, areas)
            ]
            firsts = numpy.hstack(
                (
                    [0],
                    numpy.where(src_labels[:-1] != src_labels[1:])[0] + 1,
                    src_labels.shape[:1],
                )
            )
            counts = firsts[1:] - firsts[:-1]
            #
            # Process them in order. The maximal or minimal child
            # will be assigned to the most overlapping parent and that
            # parent will be excluded.
            #
            best_src_label = numpy.zeros(enclosing_max + 1, int)
            for idx, count in zip(firsts[:-1], counts):
                for i in range(count):
                    enclosing_object_number = enclosing_labels[idx + i]
                    if best_src_label[enclosing_object_number] == 0:
                        best_src_label[enclosing_object_number] = src_labels[idx]
                        break
            #
            # Remove best source labels = 0 and sort to get the list
            #
            best_src_label = best_src_label[best_src_label != 0]
            best_src_label.sort()
            return best_src_label
        else:
            tricky_values = numpy.zeros((len(values) + 1,))
            tricky_values[1:] = values
            if wants_max:
                tricky_values[0] = -numpy.Inf
            else:
                tricky_values[0] = numpy.Inf
            src_values = tricky_values[src_labels]
            #
            # Now find the location of the best for each of the enclosing objects
            #
            fn = (
                scipy.ndimage.maximum_position
                if wants_max
                else scipy.ndimage.minimum_position
            )
            best_pos = fn(src_values, enclosing_labels, enclosing_range)
            best_pos = numpy.array(
                (best_pos,) if isinstance(best_pos, tuple) else best_pos
            )
            best_pos = best_pos.astype(numpy.uint32)
            #
            # Get the label of the pixel at each location
            #
            indexes = src_labels[best_pos.transpose().tolist()]
            indexes = set(indexes)
            indexes = list(indexes)
            indexes.sort()
            return indexes[1:] if len(indexes) > 0 and indexes[0] == 0 else indexes

    def keep_within_limits(self, workspace, src_objects):
        """Return an array containing the indices of objects to keep

        workspace - workspace passed into Run
        src_objects - the Objects instance to be filtered
        """
        src_name = self.x_name.value
        hits = None
        m = workspace.measurements
        for group in self.measurements:
            measurement = group.measurement.value
            values = m.get_current_measurement(src_name, measurement)
            if hits is None:
                hits = numpy.ones(len(values), bool)
            elif len(hits) < len(values):
                temp = numpy.ones(len(values), bool)
                temp[~hits] = False
                hits = temp
            low_limit = group.min_limit.value
            high_limit = group.max_limit.value
            if group.wants_minimum.value:
                hits[values < low_limit] = False
            if group.wants_maximum.value:
                hits[values > high_limit] = False
        indexes = numpy.argwhere(hits)[:, 0]
        indexes = indexes + 1
        return indexes

    def discard_border_objects(self, src_objects):
        """Return an array containing the indices of objects to keep

        workspace - workspace passed into Run
        src_objects - the Objects instance to be filtered
        """
        labels = src_objects.segmented

        if src_objects.has_parent_image and src_objects.parent_image.has_mask:

            mask = src_objects.parent_image.mask

            interior_pixels = scipy.ndimage.binary_erosion(mask)

        else:

            interior_pixels = scipy.ndimage.binary_erosion(numpy.ones_like(labels))

        border_pixels = numpy.logical_not(interior_pixels)

        border_labels = set(labels[border_pixels])

        if (
            border_labels == {0}
            and src_objects.has_parent_image
            and src_objects.parent_image.has_mask
        ):
            # The assumption here is that, if nothing touches the border,
            # the mask is a large, elliptical mask that tells you where the
            # well is. That's the way the old Matlab code works and it's duplicated here
            #
            # The operation below gets the mask pixels that are on the border of the mask
            # The erosion turns all pixels touching an edge to zero. The not of this
            # is the border + formerly masked-out pixels.

            mask = src_objects.parent_image.mask

            interior_pixels = scipy.ndimage.binary_erosion(mask)

            border_pixels = numpy.logical_not(interior_pixels)

            border_labels = set(labels[border_pixels])

        return list(set(labels.ravel()).difference(border_labels))

    def get_rules(self):
        """Read the rules from a file"""
        rules_file = self.rules_file_name.value
        rules_directory = self.rules_directory.get_absolute_path()
        path = os.path.join(rules_directory, rules_file)
        if not os.path.isfile(path):
            raise ValidationError("No such rules file: %s" % path, self.rules_file_name)
        else:
            rules = cellprofiler.utilities.rules.Rules()
            rules.parse(path)
            return rules

    def load_classifier(self):
        """Load the classifier pickle if not cached

        returns classifier, bin_labels, name and features
        """
        d = self.get_dictionary()
        file_ = self.rules_file_name.value
        directory_ = self.rules_directory.get_absolute_path()
        path_ = os.path.join(directory_, file_)
        if path_ not in d:
            if not os.path.isfile(path_):
                raise ValidationError(
                    "No such classifier file: %s" % path_, self.rules_file_name
                )
            else:
                import joblib

                d[path_] = joblib.load(path_)
        return d[path_]

    def get_classifier(self):
        return self.load_classifier()[0]

    def get_bin_labels(self):
        return self.load_classifier()[1]

    def get_classifier_features(self):
        return self.load_classifier()[3]

    def keep_by_rules(self, workspace, src_objects):
        """Keep objects according to rules

        workspace - workspace holding the measurements for the rules
        src_objects - filter these objects (uses measurement indexes instead)

        Open the rules file indicated by the settings and score the
        objects by the rules. Return the indexes of the objects that pass.
        """
        rules = self.get_rules()
        rules_class = int(self.rules_class.value) - 1
        scores = rules.score(workspace.measurements)
        if len(scores) > 0:
            is_not_nan = numpy.any(~numpy.isnan(scores), 1)
            best_class = numpy.argmax(scores[is_not_nan], 1).flatten()
            hits = numpy.zeros(scores.shape[0], bool)
            hits[is_not_nan] = best_class == rules_class
            indexes = numpy.argwhere(hits).flatten() + 1
        else:
            indexes = numpy.array([], int)
        return indexes

    def keep_by_class(self, workspace, src_objects):
        """ Keep objects according to their predicted class
        :param workspace: workspace holding the measurements for the rules
        :param src_objects: filter these objects (uses measurement indexes instead)
        :return: indexes (base 1) of the objects that pass
        """
        classifier = self.get_classifier()
        target_idx = self.get_bin_labels().index(self.rules_class.value)
        target_class = classifier.classes_[target_idx]
        features = []
        for feature_name in self.get_classifier_features():
            feature_name = feature_name.split("_", 1)[1]
            if feature_name == "x_loc":
                feature_name = M_LOCATION_CENTER_X
            elif feature_name == "y_loc":
                feature_name = M_LOCATION_CENTER_Y
            features.append(feature_name)

        feature_vector = numpy.column_stack(
            [
                workspace.measurements[self.x_name.value, feature_name]
                for feature_name in features
            ]
        )
        predicted_classes = classifier.predict(feature_vector)
        hits = predicted_classes == target_class
        indexes = numpy.argwhere(hits) + 1
        return indexes.flatten()

    def get_measurement_columns(self, pipeline):
        return super(FilterObjects, self).get_measurement_columns(
            pipeline,
            additional_objects=[
                (x.object_name.value, x.target_name.value)
                for x in self.additional_objects
            ],
        )

    def prepare_to_create_batch(self, workspace, fn_alter_path):
        """Prepare to create a batch file

        This function is called when CellProfiler is about to create a
        file for batch processing. It will pickle the image set list's
        "legacy_fields" dictionary. This callback lets a module prepare for
        saving.

        pipeline - the pipeline to be saved
        image_set_list - the image set list to be saved
        fn_alter_path - this is a function that takes a pathname on the local
                        host and returns a pathname on the remote host. It
                        handles issues such as replacing backslashes and
                        mapping mountpoints. It should be called for every
                        pathname stored in the settings or legacy fields.
        """
        self.rules_directory.alter_for_create_batch_files(fn_alter_path)
        return True

    def upgrade_settings(self, setting_values, variable_revision_number, module_name):
        if variable_revision_number == 1:
            #
            # Added CPA rules
            #
            setting_values = (
                setting_values[:11]
                + [MODE_MEASUREMENTS, DEFAULT_INPUT_FOLDER_NAME, ".",]
                + setting_values[11:]
            )
            variable_revision_number = 2
        if variable_revision_number == 2:
            #
            # Forgot file name (???!!!)
            #
            setting_values = setting_values[:14] + ["rules.txt"] + setting_values[14:]
            variable_revision_number = 3
        if variable_revision_number == 3:
            #
            # Allowed multiple measurements
            # Structure changed substantially.
            #
            (
                target_name,
                object_name,
                measurement,
                filter_choice,
                enclosing_objects,
                wants_minimum,
                minimum_value,
                wants_maximum,
                maximum_value,
                wants_outlines,
                outlines_name,
                rules_or_measurements,
                rules_directory_choice,
                rules_path_name,
                rules_file_name,
            ) = setting_values[:15]
            additional_object_settings = setting_values[15:]
            additional_object_count = len(additional_object_settings) // 4

            setting_values = [
                target_name,
                object_name,
                rules_or_measurements,
                filter_choice,
                enclosing_objects,
                wants_outlines,
                outlines_name,
                rules_directory_choice,
                rules_path_name,
                rules_file_name,
                "1",
                str(additional_object_count),
                measurement,
                wants_minimum,
                minimum_value,
                wants_maximum,
                maximum_value,
            ] + additional_object_settings
            variable_revision_number = 4
        if variable_revision_number == 4:
            #
            # Used Directory to combine directory choice & custom path
            #
            rules_directory_choice = setting_values[7]
            rules_path_name = setting_values[8]
            if rules_directory_choice == DIR_CUSTOM:
                rules_directory_choice = ABSOLUTE_FOLDER_NAME
                if rules_path_name.startswith("."):
                    rules_directory_choice = DEFAULT_INPUT_SUBFOLDER_NAME
                elif rules_path_name.startswith("&"):
                    rules_directory_choice = DEFAULT_OUTPUT_SUBFOLDER_NAME
                    rules_path_name = "." + rules_path_name[1:]

            rules_directory = Directory.static_join_string(
                rules_directory_choice, rules_path_name
            )
            setting_values = setting_values[:7] + [rules_directory] + setting_values[9:]
            variable_revision_number = 5

        if variable_revision_number == 5:
            #
            # added rules class
            #
            setting_values = setting_values[:9] + ["1"] + setting_values[9:]
            variable_revision_number = 6

        if variable_revision_number == 6:
            #
            # Added per-object assignment
            #
            setting_values = (
                setting_values[:FIXED_SETTING_COUNT_V6]
                + [PO_BOTH]
                + setting_values[FIXED_SETTING_COUNT_V6:]
            )

            variable_revision_number = 7

        if variable_revision_number == 7:
            x_name = setting_values[1]

            y_name = setting_values[0]

            measurement_count = int(setting_values[10])

            additional_object_count = int(setting_values[11])

            n_measurement_settings = measurement_count * 5

            additional_object_settings = setting_values[13 + n_measurement_settings :]

            additional_object_names = additional_object_settings[::4]

            additional_target_names = additional_object_settings[1::4]

            new_additional_object_settings = sum(
                [
                    [object_name, target_name]
                    for object_name, target_name in zip(
                        additional_object_names, additional_target_names
                    )
                ],
                [],
            )

            setting_values = (
                [x_name, y_name]
                + setting_values[2:5]
                + setting_values[7 : 13 + n_measurement_settings]
                + new_additional_object_settings
            )

            variable_revision_number = 8

        slot_directory = 5

        setting_values[slot_directory] = Directory.upgrade_setting(
            setting_values[slot_directory]
        )

        return setting_values, variable_revision_number
Exemplo n.º 5
0
class RunImageJMacro(Module):
    module_name = "RunImageJMacro"
    variable_revision_number = 1
    category = "Advanced"

    def create_settings(self):

        self.executable_directory = Directory(
            "Executable directory",
            allow_metadata=False,
            doc="""\
Select the folder containing the executable. MacOS users should select the directory where Fiji.app lives. Windows users 
should select the directory containing ImageJ-win64.exe (usually corresponding to the Fiji.app folder).

{IO_FOLDER_CHOICE_HELP_TEXT}
""".format(**{"IO_FOLDER_CHOICE_HELP_TEXT": _help.IO_FOLDER_CHOICE_HELP_TEXT}))

        def set_directory_fn_executable(path):
            dir_choice, custom_path = self.executable_directory.get_parts_from_path(
                path)
            self.executable_directory.join_parts(dir_choice, custom_path)

        self.executable_file = Filename(
            "Executable",
            "ImageJ.exe",
            doc="Select your executable. MacOS users should select the Fiji.app "
            "application. Windows user should select the ImageJ-win64.exe executable",
            get_directory_fn=self.executable_directory.get_absolute_path,
            set_directory_fn=set_directory_fn_executable,
            browse_msg="Choose executable file")

        self.macro_directory = Directory(
            "Macro directory",
            allow_metadata=False,
            doc=f"""Select the folder containing the macro.
{_help.IO_FOLDER_CHOICE_HELP_TEXT}""")

        def set_directory_fn_macro(path):
            dir_choice, custom_path = self.macro_directory.get_parts_from_path(
                path)
            self.macro_directory.join_parts(dir_choice, custom_path)

        self.macro_file = Filename(
            "Macro",
            "macro.py",
            doc="Select your macro file.",
            get_directory_fn=self.macro_directory.get_absolute_path,
            set_directory_fn=set_directory_fn_macro,
            browse_msg="Choose macro file")

        self.add_directory = Text(
            "What variable in your macro defines the folder ImageJ should use?",
            "Directory",
            doc=
            """Because CellProfiler will save the output images in a temporary directory, this directory should be 
specified as a variable in the macro script. It is assumed that the macro will use this directory variable 
to obtain the full path to the inputted image. Enter the variable name here. CellProfiler will create a 
temporary directory and assign its path as a value to this variable.""")

        self.image_groups_in = []
        self.image_groups_out = []

        self.macro_variables_list = []

        self.image_groups_in_count = HiddenCount(self.image_groups_in)
        self.image_groups_out_count = HiddenCount(self.image_groups_out)
        self.macro_variable_count = HiddenCount(self.macro_variables_list)

        self.add_image_in(can_delete=False)
        self.add_image_button_in = DoSomething("", 'Add another input image',
                                               self.add_image_in)

        self.add_image_out(can_delete=False)
        self.add_image_button_out = DoSomething("", 'Add another output image',
                                                self.add_image_out)

        self.add_variable_button_out = DoSomething(
            "Does your macro expect variables?", "Add another variable",
            self.add_macro_variables)

    def add_macro_variables(self, can_delete=True):
        group = SettingsGroup()
        if can_delete:
            group.append("divider", Divider(line=False))
        group.append(
            "variable_name",
            Text('What variable name is your macro expecting?',
                 "None",
                 doc='Enter the variable name that your macro is expecting. '))
        group.append(
            "variable_value",
            Text("What value should this variable have?",
                 "None",
                 doc="Enter the desire value for this variable."),
        )
        if len(self.macro_variables_list
               ) == 0:  # Insert space between 1st two images for aesthetics
            group.append("extra_divider", Divider(line=False))

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton("", "Remove this variable",
                                    self.macro_variables_list, group))

        self.macro_variables_list.append(group)

    def add_image_in(self, can_delete=True):
        """Add an image to the image_groups collection
        can_delete - set this to False to keep from showing the "remove"
                     button for images that must be present.
        """
        group = SettingsGroup()
        if can_delete:
            group.append("divider", Divider(line=False))
        group.append(
            "image_name",
            ImageSubscriber('Select an image to send to your macro',
                            "None",
                            doc="Select an image to send to your macro. "))
        group.append(
            "output_filename",
            Text(
                "What should this image temporarily saved as?",
                "None.tiff",
                doc=
                'Enter the filename of the image to be used by the macro. This should be set to the name expected '
                'by the macro file.'),
        )
        if len(self.image_groups_in
               ) == 0:  # Insert space between 1st two images for aesthetics
            group.append("extra_divider", Divider(line=False))

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton("", "Remove this image",
                                    self.image_groups_in, group))

        self.image_groups_in.append(group)

    def add_image_out(self, can_delete=True):
        """Add an image to the image_groups collection
        can_delete - set this to False to keep from showing the "remove"
                     button for images that must be present.
        """
        group = SettingsGroup()
        if can_delete:
            group.append("divider", Divider(line=False))
        group.append(
            "input_filename",
            Text(
                "What is the image filename CellProfiler should load?",
                "None.tiff",
                doc=
                "Enter the image filename CellProfiler should load. This should be set to the output filename "
                "written in the macro file. The image written by the macro will be saved in a temporary directory "
                "and read by CellProfiler."),
        )

        group.append(
            "image_name",
            ImageName(
                r'What should CellProfiler call the loaded image?',
                "None",
                doc=
                'Enter a name to assign to the new image loaded by CellProfiler. This image will be added to your '
                'workspace. '))

        if len(self.image_groups_out
               ) == 0:  # Insert space between 1st two images for aesthetics
            group.append("extra_divider", Divider(line=False))

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton("", "Remove this image",
                                    self.image_groups_out, group))

        self.image_groups_out.append(group)

    def settings(self):
        result = [
            self.image_groups_in_count, self.image_groups_out_count,
            self.macro_variable_count
        ]
        result += [
            self.executable_directory, self.executable_file,
            self.macro_directory, self.macro_file, self.add_directory
        ]
        for image_group_in in self.image_groups_in:
            result += [
                image_group_in.image_name, image_group_in.output_filename
            ]
        for image_group_out in self.image_groups_out:
            result += [
                image_group_out.input_filename, image_group_out.image_name
            ]
        for macro_variable in self.macro_variables_list:
            result += [
                macro_variable.variable_name, macro_variable.variable_value
            ]
        return result

    def visible_settings(self):
        visible_settings = [
            self.executable_directory, self.executable_file,
            self.macro_directory, self.macro_file, self.add_directory
        ]
        for image_group_in in self.image_groups_in:
            visible_settings += image_group_in.visible_settings()
        visible_settings += [self.add_image_button_in]
        for image_group_out in self.image_groups_out:
            visible_settings += image_group_out.visible_settings()
        visible_settings += [self.add_image_button_out]
        for macro_variable in self.macro_variables_list:
            visible_settings += macro_variable.visible_settings()
        visible_settings += [self.add_variable_button_out]
        return visible_settings

    def prepare_settings(self, setting_values):
        image_groups_in_count = int(setting_values[0])
        image_groups_out_count = int(setting_values[1])
        macro_variable_count = int(setting_values[2])

        del self.image_groups_in[image_groups_in_count:]
        del self.image_groups_out[image_groups_out_count:]
        del self.macro_variables_list[macro_variable_count:]

        while len(self.image_groups_in) < image_groups_in_count:
            self.add_image_in()
        while len(self.image_groups_out) < image_groups_out_count:
            self.add_image_out()
        while len(self.macro_variables_list) < macro_variable_count:
            self.add_macro_variables()

    def stringify_metadata(self, dir):
        met_string = ""
        met_string += self.add_directory.value + "='" + dir + "', "
        for var in self.macro_variables_list:
            met_string += var.variable_name.value + "='" + var.variable_value.value + "', "
        return met_string[:-2]

    def run(self, workspace):
        default_output_directory = get_default_output_directory()
        tag = "runimagejmacro_" + str(random.randint(100000, 999999))
        tempdir = os.path.join(default_output_directory, tag)
        os.makedirs(tempdir, exist_ok=True)
        try:
            for image_group in self.image_groups_in:
                image = workspace.image_set.get_image(
                    image_group.image_name.value)
                image_pixels = image.pixel_data
                skimage.io.imsave(
                    os.path.join(tempdir, image_group.output_filename.value),
                    image_pixels)

            if self.executable_file.value[-4:] == ".app":
                executable = os.path.join(
                    default_output_directory,
                    self.executable_directory.value.split("|")[1],
                    self.executable_file.value, "Contents/MacOS/ImageJ-macosx")
            else:
                executable = os.path.join(
                    default_output_directory,
                    self.executable_directory.value.split("|")[1],
                    self.executable_file.value)
            cmd = [
                executable, "--headless", "console", "--run",
                os.path.join(default_output_directory,
                             self.macro_directory.value.split("|")[1],
                             self.macro_file.value)
            ]

            cmd += [self.stringify_metadata(tempdir)]

            subprocess.call(cmd,
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)

            for image_group in self.image_groups_out:
                if not os.path.exists(
                        os.path.join(tempdir,
                                     image_group.input_filename.value)):
                    msg = f"CellProfiler couldn't find the output expected from the ImageJ Macro," \
                          f"\n File {image_group.input_filename.value} was missing"
                    raise FileNotFoundError("Missing file", msg)
                image_pixels = skimage.io.imread(
                    os.path.join(tempdir, image_group.input_filename.value))
                workspace.image_set.add(image_group.image_name.value,
                                        Image(image_pixels, convert=False))
        finally:
            # Clean up temp directory regardless of macro success
            for subdir, dirs, files in os.walk(tempdir):
                for file in files:
                    os.remove(os.path.join(tempdir, file))
            os.removedirs(tempdir)

        pixel_data = []
        image_names = []

        if self.show_window:
            for x in itertools.chain(self.image_groups_in,
                                     self.image_groups_out):
                pixel_data.append(
                    workspace.image_set.get_image(
                        x.image_name.value).pixel_data)
                image_names.append(x.image_name.value)

        workspace.display_data.pixel_data = pixel_data
        workspace.display_data.display_names = image_names
        workspace.display_data.dimensions = workspace.image_set.get_image(
            self.image_groups_out[0].image_name.value).dimensions

    def display(self, workspace, figure):
        import matplotlib.cm

        pixel_data = workspace.display_data.pixel_data
        display_names = workspace.display_data.display_names

        columns = (len(pixel_data) + 1) // 2

        figure.set_subplots((columns, 2),
                            dimensions=workspace.display_data.dimensions)

        for i in range(len(pixel_data)):
            if pixel_data[i].shape[-1] in (3, 4):
                cmap = None
            elif pixel_data[i].dtype.kind == "b":
                cmap = matplotlib.cm.binary_r
            else:
                cmap = matplotlib.cm.Greys_r

            figure.subplot_imshow(
                i % columns,
                int(i / columns),
                pixel_data[i],
                title=display_names[i],
                sharexy=figure.subplot(0, 0),
                colormap=cmap,
            )
Exemplo n.º 6
0
class RunImageJMacro(Module):
    module_name = "RunImageJMacro"
    variable_revision_number = 1
    category = "Advanced"

    def create_settings(self):

        self.executable_directory = Directory(
            "Executable directory",
            allow_metadata=False,
            doc="""\
Select the folder containing the executable. MacOS users should select the directory where Fiji.app lives. Windows users 
should select the directory containing ImageJ-win64.exe (usually corresponding to the Fiji.app folder).

{IO_FOLDER_CHOICE_HELP_TEXT}
""".format(**{"IO_FOLDER_CHOICE_HELP_TEXT": _help.IO_FOLDER_CHOICE_HELP_TEXT}))

        def set_directory_fn_executable(path):
            dir_choice, custom_path = self.executable_directory.get_parts_from_path(
                path)
            self.executable_directory.join_parts(dir_choice, custom_path)

        self.executable_file = Filename(
            "Executable",
            "ImageJ.exe",
            doc="Select your executable. MacOS users should select the Fiji.app "
            "application. Windows user should select the ImageJ-win64.exe executable",
            get_directory_fn=self.executable_directory.get_absolute_path,
            set_directory_fn=set_directory_fn_executable,
            browse_msg="Choose executable file")

        self.macro_directory = Directory(
            "Macro directory",
            allow_metadata=False,
            doc=f"""Select the folder containing the macro.
{_help.IO_FOLDER_CHOICE_HELP_TEXT}""")

        def set_directory_fn_macro(path):
            dir_choice, custom_path = self.macro_directory.get_parts_from_path(
                path)
            self.macro_directory.join_parts(dir_choice, custom_path)

        self.macro_file = Filename(
            "Macro",
            "macro.py",
            doc="Select your macro file.",
            get_directory_fn=self.macro_directory.get_absolute_path,
            set_directory_fn=set_directory_fn_macro,
            browse_msg="Choose macro file")

        self.debug_mode = Binary(
            "Debug mode: Prevent deletion of temporary files",
            False,
            doc="This setting only applies when running in Test Mode."
            "If enabled, temporary folders used to communicate with ImageJ will not be cleared automatically."
            "You'll need to remove them manually. This can be helpful when trying to debug a macro."
            "Temporary folder location will be printed to the console.")

        self.add_directory = Text(
            "What variable in your macro defines the folder ImageJ should use?",
            "Directory",
            doc=
            """Because CellProfiler will save the output images in a temporary directory, this directory should be 
specified as a variable in the macro script. It is assumed that the macro will use this directory variable 
to obtain the full path to the inputted image. Enter the variable name here. CellProfiler will create a 
temporary directory and assign its path as a value to this variable.""")

        self.image_groups_in = []
        self.image_groups_out = []

        self.macro_variables_list = []

        self.image_groups_in_count = HiddenCount(self.image_groups_in)
        self.image_groups_out_count = HiddenCount(self.image_groups_out)
        self.macro_variable_count = HiddenCount(self.macro_variables_list)

        self.add_image_in(can_delete=False)
        self.add_image_button_in = DoSomething("", 'Add another input image',
                                               self.add_image_in)

        self.add_image_out(can_delete=False)
        self.add_image_button_out = DoSomething("", 'Add another output image',
                                                self.add_image_out)

        self.add_variable_button_out = DoSomething(
            "Does your macro expect variables?", "Add another variable",
            self.add_macro_variables)

    def add_macro_variables(self, can_delete=True):
        group = SettingsGroup()
        if can_delete:
            group.append("divider", Divider(line=False))
        group.append(
            "variable_name",
            Text('What variable name is your macro expecting?',
                 "None",
                 doc='Enter the variable name that your macro is expecting. '))
        group.append(
            "variable_value",
            Text("What value should this variable have?",
                 "None",
                 doc="Enter the desire value for this variable."),
        )
        if len(self.macro_variables_list
               ) == 0:  # Insert space between 1st two images for aesthetics
            group.append("extra_divider", Divider(line=False))

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton("", "Remove this variable",
                                    self.macro_variables_list, group))

        self.macro_variables_list.append(group)

    def add_image_in(self, can_delete=True):
        """Add an image to the image_groups collection
        can_delete - set this to False to keep from showing the "remove"
                     button for images that must be present.
        """
        group = SettingsGroup()
        if can_delete:
            group.append("divider", Divider(line=False))
        group.append(
            "image_name",
            ImageSubscriber('Select an image to send to your macro',
                            "None",
                            doc="Select an image to send to your macro. "))
        group.append(
            "output_filename",
            Text(
                "What should this image temporarily saved as?",
                "None.tiff",
                doc=
                'Enter the filename of the image to be used by the macro. This should be set to the name expected '
                'by the macro file.'),
        )
        if len(self.image_groups_in
               ) == 0:  # Insert space between 1st two images for aesthetics
            group.append("extra_divider", Divider(line=False))

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton("", "Remove this image",
                                    self.image_groups_in, group))

        self.image_groups_in.append(group)

    def add_image_out(self, can_delete=True):
        """Add an image to the image_groups collection
        can_delete - set this to False to keep from showing the "remove"
                     button for images that must be present.
        """
        group = SettingsGroup()
        if can_delete:
            group.append("divider", Divider(line=False))
        group.append(
            "input_filename",
            Text(
                "What is the image filename CellProfiler should load?",
                "None.tiff",
                doc=
                "Enter the image filename CellProfiler should load. This should be set to the output filename "
                "written in the macro file. The image written by the macro will be saved in a temporary directory "
                "and read by CellProfiler."),
        )

        group.append(
            "image_name",
            ImageName(
                r'What should CellProfiler call the loaded image?',
                "None",
                doc=
                'Enter a name to assign to the new image loaded by CellProfiler. This image will be added to your '
                'workspace. '))

        if len(self.image_groups_out
               ) == 0:  # Insert space between 1st two images for aesthetics
            group.append("extra_divider", Divider(line=False))

        if can_delete:
            group.append(
                "remover",
                RemoveSettingButton("", "Remove this image",
                                    self.image_groups_out, group))

        self.image_groups_out.append(group)

    def settings(self):
        result = [
            self.image_groups_in_count, self.image_groups_out_count,
            self.macro_variable_count
        ]
        result += [
            self.executable_directory, self.executable_file,
            self.macro_directory, self.macro_file, self.add_directory
        ]
        for image_group_in in self.image_groups_in:
            result += [
                image_group_in.image_name, image_group_in.output_filename
            ]
        for image_group_out in self.image_groups_out:
            result += [
                image_group_out.input_filename, image_group_out.image_name
            ]
        for macro_variable in self.macro_variables_list:
            result += [
                macro_variable.variable_name, macro_variable.variable_value
            ]
        return result

    def visible_settings(self):
        visible_settings = [
            self.executable_directory, self.executable_file,
            self.macro_directory, self.macro_file, self.debug_mode,
            self.add_directory
        ]
        for image_group_in in self.image_groups_in:
            visible_settings += image_group_in.visible_settings()
        visible_settings += [self.add_image_button_in]
        for image_group_out in self.image_groups_out:
            visible_settings += image_group_out.visible_settings()
        visible_settings += [self.add_image_button_out]
        for macro_variable in self.macro_variables_list:
            visible_settings += macro_variable.visible_settings()
        visible_settings += [self.add_variable_button_out]
        return visible_settings

    def prepare_settings(self, setting_values):
        image_groups_in_count = int(setting_values[0])
        image_groups_out_count = int(setting_values[1])
        macro_variable_count = int(setting_values[2])

        del self.image_groups_in[image_groups_in_count:]
        del self.image_groups_out[image_groups_out_count:]
        del self.macro_variables_list[macro_variable_count:]

        while len(self.image_groups_in) < image_groups_in_count:
            self.add_image_in()
        while len(self.image_groups_out) < image_groups_out_count:
            self.add_image_out()
        while len(self.macro_variables_list) < macro_variable_count:
            self.add_macro_variables()

    def stringify_metadata(self, dir):
        met_string = ""
        met_string += self.add_directory.value + "='" + dir + "', "
        for var in self.macro_variables_list:
            met_string += var.variable_name.value + "='" + var.variable_value.value + "', "
        return met_string[:-2]

    def run(self, workspace):
        default_output_directory = get_default_output_directory()
        tag = "runimagejmacro_" + str(random.randint(100000, 999999))
        tempdir = os.path.join(default_output_directory, tag)
        os.makedirs(tempdir, exist_ok=True)
        try:
            for image_group in self.image_groups_in:
                image = workspace.image_set.get_image(
                    image_group.image_name.value)
                image_pixels = image.pixel_data
                skimage.io.imsave(
                    os.path.join(tempdir, image_group.output_filename.value),
                    image_pixels)

            if self.executable_file.value[-4:] == ".app":
                executable = os.path.join(
                    default_output_directory,
                    self.executable_directory.value.split("|")[1],
                    self.executable_file.value, "Contents/MacOS/ImageJ-macosx")
            else:
                executable = os.path.join(
                    default_output_directory,
                    self.executable_directory.value.split("|")[1],
                    self.executable_file.value)
            cmd = [
                executable, "--headless", "console", "--run",
                os.path.join(default_output_directory,
                             self.macro_directory.value.split("|")[1],
                             self.macro_file.value)
            ]

            cmd += [self.stringify_metadata(tempdir)]

            result = subprocess.run(cmd,
                                    stdout=subprocess.PIPE,
                                    stderr=subprocess.STDOUT,
                                    text=True)
            for image_group in self.image_groups_out:
                if not os.path.exists(
                        os.path.join(tempdir,
                                     image_group.input_filename.value)):
                    # Cleanup the error logs for display, we want to remove less-useful lines to keep it succinct.
                    reject = ('console:', 'Java Hot', 'at org', 'at java',
                              '[WARNING]', '\t')
                    # ImageJ tends to report the same few lines over and over, so we'll use a dict as an ordered set.
                    err = {}
                    for line in result.stdout.splitlines():
                        if len(line.strip()) > 0 and not line.startswith(
                                reject):
                            err[line] = None
                    if len(err) > 1:
                        # Error appears when file loading fails, but can also show up if the macro failed to generate
                        # an output image. We remove this if it wasn't the only error, as it can be confusing.
                        err.pop('Unsupported format or not found', None)
                    err = "\n".join(err.keys())
                    msg = f"CellProfiler couldn't find the output expected from the ImageJ Macro," \
                          f"\n File {image_group.input_filename.value} was missing."
                    if err:
                        msg += f"\n\nImageJ logs contained the following: \n{err}"
                    raise FileNotFoundError("Missing file", msg)
                image_pixels = skimage.io.imread(
                    os.path.join(tempdir, image_group.input_filename.value))
                workspace.image_set.add(image_group.image_name.value,
                                        Image(image_pixels, convert=False))
        finally:
            want_delete = True
            # Optionally clean up temp directory regardless of macro success
            if workspace.pipeline.test_mode and self.debug_mode:
                want_delete = False
                if not get_headless():
                    import wx
                    message = f"Debugging was enabled.\nTemporary folder was not deleted automatically" \
                              f"\n\nTemporary subfolder is {os.path.split(tempdir)[-1]} in your Default Output Folder\n\nDo you want to delete it now?"
                    with wx.Dialog(None,
                                   title="RunImageJMacro Debug Mode") as dlg:
                        text_sizer = dlg.CreateTextSizer(message)
                        sizer = wx.BoxSizer(wx.VERTICAL)
                        dlg.SetSizer(sizer)
                        button_sizer = dlg.CreateStdDialogButtonSizer(
                            flags=wx.YES | wx.NO)
                        open_temp_folder_button = wx.Button(
                            dlg, -1, "Open temporary folder")
                        button_sizer.Insert(0, open_temp_folder_button)

                        def on_open_temp_folder(event):
                            import sys
                            if sys.platform == "win32":
                                os.startfile(tempdir)
                            else:
                                import subprocess
                                subprocess.call([
                                    "open",
                                    tempdir,
                                ])

                        open_temp_folder_button.Bind(wx.EVT_BUTTON,
                                                     on_open_temp_folder)
                        sizer.Add(text_sizer, 0, wx.EXPAND | wx.ALL, 10)
                        sizer.Add(button_sizer, 0, wx.EXPAND | wx.ALL, 10)
                        dlg.SetEscapeId(wx.ID_NO)
                        dlg.SetAffirmativeId(wx.ID_YES)
                        dlg.Fit()
                        dlg.CenterOnParent()
                        if dlg.ShowModal() == wx.ID_YES:
                            want_delete = True
            if want_delete:
                try:
                    for subdir, dirs, files in os.walk(tempdir):
                        for file in files:
                            os.remove(os.path.join(tempdir, file))
                    os.removedirs(tempdir)
                except:
                    logging.error(
                        "Unable to delete temporary directory, files may be in use by another program."
                    )
                    logging.error(
                        "Temp folder is subfolder {tempdir} in your Default Output Folder.\nYou may need to remove it manually."
                    )
            else:
                logging.error(
                    f"Debugging was enabled.\nDid not remove temporary folder at {tempdir}"
                )

        pixel_data = []
        image_names = []

        if self.show_window:
            for x in itertools.chain(self.image_groups_in,
                                     self.image_groups_out):
                pixel_data.append(
                    workspace.image_set.get_image(
                        x.image_name.value).pixel_data)
                image_names.append(x.image_name.value)

        workspace.display_data.pixel_data = pixel_data
        workspace.display_data.display_names = image_names
        workspace.display_data.dimensions = workspace.image_set.get_image(
            self.image_groups_out[0].image_name.value).dimensions

    def display(self, workspace, figure):
        import matplotlib.cm

        pixel_data = workspace.display_data.pixel_data
        display_names = workspace.display_data.display_names

        columns = (len(pixel_data) + 1) // 2

        figure.set_subplots((columns, 2),
                            dimensions=workspace.display_data.dimensions)

        for i in range(len(pixel_data)):
            if pixel_data[i].shape[-1] in (3, 4):
                cmap = None
            elif pixel_data[i].dtype.kind == "b":
                cmap = matplotlib.cm.binary_r
            else:
                cmap = matplotlib.cm.Greys_r

            figure.subplot_imshow(
                i % columns,
                int(i / columns),
                pixel_data[i],
                title=display_names[i],
                sharexy=figure.subplot(0, 0),
                colormap=cmap,
            )