def upgrade_settings(self, setting_values, variable_revision_number, module_name): PC_DEFAULT = "Default output folder" PC_WITH_IMAGE = "Same folder as image" if variable_revision_number == 1: # # Minor change: Default output directory -> folder # new_setting_values = [setting_values[0]] for offset in range(1, len(setting_values), 6): dir_choice = setting_values[offset + 4] custom_path = setting_values[offset + 5] if dir_choice == PC_CUSTOM: if custom_path[0] == ".": dir_choice = DEFAULT_OUTPUT_SUBFOLDER_NAME elif custom_path[0] == "&": dir_choice = DEFAULT_OUTPUT_SUBFOLDER_NAME custom_path = "." + custom_path[1:] else: dir_choice = ABSOLUTE_FOLDER_NAME directory = Directory.static_join_string(dir_choice, custom_path) new_setting_values += setting_values[offset : (offset + 4)] new_setting_values += [directory] setting_values = new_setting_values variable_revision_number = 2 # Standardize input/output directory name references setting_values = list(setting_values) for offset in range(5, len(setting_values), VARIABLE_SETTING_COUNT): setting_values[offset] = Directory.upgrade_setting(setting_values[offset]) return setting_values, variable_revision_number
def upgrade_settings(self, setting_values, variable_revision_number, module_name): """Provide backwards compatibility for old pipelines setting_values - the strings to be fed to settings variable_revision_number - the version number at time of saving module_name - name of original module """ if variable_revision_number == 1: # # Added hole size questions # setting_values = setting_values + ["Yes", "10"] variable_revision_number = 2 if variable_revision_number == 2: # # Added graph stuff # setting_values = setting_values + [ "No", "None", Directory.static_join_string(DEFAULT_OUTPUT_FOLDER_NAME, "None"), "None", "None", ] variable_revision_number = 3 return setting_values, variable_revision_number
def create_settings(self): super(Noise2Void, self).create_settings() self.ml_model = Directory("Path to ML Model", doc="""\ Select the folder containing the machine learning model to be used. This model has to be generated via the noise2void training. See https://github.com/juglab/n2v/blob/master/examples/2D/denoising2D_RGB/01_training.ipynb for an example of training. """ ) self.color = Binary("Process as color image?", value=False, doc="""\ Select whether your image should be processed as a color image or not. """) self.manual_slicing = Binary("Slice Image manually?", value=False, doc="""\ If necessary, **Noise2Void** will slice your image into tiles automatically for a better memory fit. If you want to manually determine the size of the said tiles, check this setting. Colored images **do not** support custom slicing as of right now! """) self.slicing_configuration = Text("Tile size", value="(2,2,2)", doc="""\ You can provide an image slicing configuration for Noise2Void for a better memory fit. Specify your custom slicing configuration as follows: - (x,y) for 2D Images - (x,y,z) for 3D Images, whereas x,y and z are positive integers. If your input cannot be parsed, no slicing configuration will be provided to n2v. """) self.axes_configuration = Text(text="N2V Axes", value=N2V_AXES_3D, doc="""\ For internal use only. Communicates axes configuration (2D or 3D, color or not) to n2v. """) self.x_name.doc = """\
def create_settings(self): self.export_option = Choice( "Do you want to save cropped images or object masks?", [SAVE_PER_OBJECT, SAVE_MASK], doc="""\ Choose the way you want the per-object crops to be exported. The choices are: - *{SAVE_PER_OBJECT}*: Save a per-object crop from the original image based on the object's bounding box. - *{SAVE_MASK}*: Export a per-object mask.""".format( SAVE_PER_OBJECT=SAVE_PER_OBJECT, SAVE_MASK=SAVE_MASK), ) self.objects_name = LabelSubscriber( "Objects", doc="Select the objects you want to export as per-object crops.") self.image_name = ImageSubscriber("Image", doc="Select the image to crop") self.directory = Directory( "Directory", doc="Enter the directory where object crops are saved.", value=DEFAULT_OUTPUT_FOLDER_NAME, ) self.file_format = Choice( "Saved file format", [O_PNG, O_TIFF_8, O_TIFF_16], value=O_TIFF_8, doc="""\ **{O_PNG}** files do not support 3D. **{O_TIFF_8}** files use zlib compression level 6.""" .format(O_PNG=O_PNG, O_TIFF_8=O_TIFF_8, O_TIFF_16=O_TIFF_16), )
def upgrade_settings(self, setting_values, variable_revision_number, module_name): """Adjust the setting values based on the version that saved them """ if variable_revision_number == 1: setting_values = (setting_values[:4] + [NO, ""] + setting_values[4:]) variable_revision_number = 2 # Standardize input/output directory name references SLOT_DIRCHOICE = 1 directory = setting_values[SLOT_DIRCHOICE] directory = Directory.upgrade_setting(directory) setting_values = (setting_values[:SLOT_DIRCHOICE] + [directory] + setting_values[SLOT_DIRCHOICE + 1:]) return setting_values, variable_revision_number
def test_copy_image(): x, y = numpy.mgrid[0:10, 0:10] input_image = (x / 100.0 + y / 10.0).astype(numpy.float32) module, workspace = make_workspace() module.script_directory = Directory("Script directory") module.script_file = Filename( "ImageJ Script", "./../resources/modules/runimagejscript/dummyscript.py") module.get_parameters_from_script() workspace.image_set.add("None", cellprofiler_core.image.Image(input_image)) module.run(workspace) output_image = workspace.image_set.get_image("copy") assert numpy.all(output_image.pixel_data == input_image)
def test_parse_parameters(): module, workspace = make_workspace() module.script_directory = Directory("Script directory") module.script_file = Filename( "ImageJ Script", "./../resources/modules/runimagejscript/dummyscript.py") module.get_parameters_from_script() assert len(module.script_parameter_list) > 0 assert module.script_parameter_list[0].name.value == "image" assert module.script_parameter_list[1].name.value == "copy" assert isinstance(module.script_parameter_list[0].setting, cellprofiler_core.setting.subscriber.ImageSubscriber) assert isinstance( module.script_parameter_list[1].setting, cellprofiler_core.setting. text.alphanumeric.name.image_name._image_name.ImageName)
class SaveCroppedObjects(Module): category = "File Processing" module_name = "SaveCroppedObjects" variable_revision_number = 2 def create_settings(self): self.export_option = Choice( "Do you want to save cropped images or object masks?", [SAVE_PER_OBJECT, SAVE_MASK], doc="""\ Choose the way you want the per-object crops to be exported. The choices are: - *{SAVE_PER_OBJECT}*: Save a per-object crop from the original image based on the object's bounding box. - *{SAVE_MASK}*: Export a per-object mask.""".format( SAVE_PER_OBJECT=SAVE_PER_OBJECT, SAVE_MASK=SAVE_MASK), ) self.objects_name = LabelSubscriber( "Objects", doc="Select the objects you want to export as per-object crops.") self.image_name = ImageSubscriber("Image", doc="Select the image to crop") self.directory = Directory( "Directory", doc="Enter the directory where object crops are saved.", value=DEFAULT_OUTPUT_FOLDER_NAME, ) self.file_format = Choice( "Saved file format", [O_PNG, O_TIFF_8, O_TIFF_16], value=O_TIFF_8, doc="""\ **{O_PNG}** files do not support 3D. **{O_TIFF_8}** files use zlib compression level 6.""" .format(O_PNG=O_PNG, O_TIFF_8=O_TIFF_8, O_TIFF_16=O_TIFF_16), ) def display(self, workspace, figure): figure.set_subplots((1, 1)) figure.subplot_table(0, 0, [["\n".join(workspace.display_data.filenames)]]) def run(self, workspace): objects = workspace.object_set.get_objects(self.objects_name.value) directory = self.directory.get_absolute_path(workspace.measurements) if not os.path.exists(directory): os.makedirs(directory) labels = objects.segmented unique_labels = numpy.unique(labels) if unique_labels[0] == 0: unique_labels = unique_labels[1:] filenames = [] if self.export_option == SAVE_PER_OBJECT: images = workspace.image_set x = images.get_image(self.image_name.value) if len(x.pixel_data.shape) == len( labels.shape) + 1 and not x.volumetric: # Color 2D image, repeat mask for all channels labels = numpy.repeat(labels[:, :, numpy.newaxis], x.pixel_data.shape[-1], axis=2) for label in unique_labels: if self.export_option == SAVE_MASK: mask = labels == label elif self.export_option == SAVE_PER_OBJECT: mask_in = labels == label properties = skimage.measure.regionprops( mask_in.astype(int), intensity_image=x.pixel_data) mask = properties[0].intensity_image if self.file_format.value == O_PNG: filename = os.path.join( directory, "{}_{}.{}".format(self.objects_name.value, label, O_PNG)) skimage.io.imsave(filename, skimage.img_as_ubyte(mask), check_contrast=False) elif self.file_format.value == O_TIFF_8: filename = os.path.join( directory, "{}_{}.{}".format(self.objects_name.value, label, "tiff")) skimage.io.imsave( filename, skimage.img_as_ubyte(mask), compress=6, check_contrast=False, ) elif self.file_format.value == O_TIFF_16: filename = os.path.join( directory, "{}_{}.{}".format(self.objects_name.value, label, "tiff")) skimage.io.imsave( filename, skimage.img_as_uint(mask), compress=6, check_contrast=False, ) filenames.append(filename) if self.show_window: workspace.display_data.filenames = filenames def settings(self): settings = [ self.objects_name, self.directory, self.file_format, self.export_option, self.image_name, ] return settings def visible_settings(self): result = [ self.export_option, self.objects_name, self.directory, self.file_format, ] if self.export_option.value == SAVE_PER_OBJECT: result += [self.image_name] return result def volumetric(self): return True
def upgrade_settings(self, setting_values, variable_revision_number, module_name): if variable_revision_number == 1: new_setting_values = [setting_values[0]] idx = 1 for flag_idx in range(int(setting_values[0])): new_setting_values += setting_values[idx:idx + 4] + ["No"] meas_count = int(setting_values[idx]) idx += 4 for meas_idx in range(meas_count): measurement_source = setting_values[idx] if (measurement_source.startswith("Measurement for all") or measurement_source == "All objects"): measurement_source = S_ALL_OBJECTS elif measurement_source == "Average for objects": measurement_source = S_AVERAGE_OBJECT elif measurement_source == "Image": measurement_source = S_IMAGE new_setting_values += [measurement_source] new_setting_values += setting_values[(idx + 1):(idx + 7)] idx += 7 setting_values = new_setting_values variable_revision_number = 2 if variable_revision_number == 2: # Added rules new_setting_values = [setting_values[0]] idx = 1 for flag_idx in range(int(setting_values[0])): new_setting_values += setting_values[idx:idx + N_FIXED_SETTINGS_PER_FLAG] meas_count = int(setting_values[idx]) idx += N_FIXED_SETTINGS_PER_FLAG for meas_idx in range(meas_count): measurement_source = setting_values[idx] new_setting_values += [measurement_source] new_setting_values += setting_values[ (idx + 1):(idx + N_SETTINGS_PER_MEASUREMENT_V2)] + [ Directory.static_join_string( DEFAULT_INPUT_FOLDER_NAME, "None"), "rules.txt", ] idx += N_SETTINGS_PER_MEASUREMENT_V2 setting_values = new_setting_values variable_revision_number = 3 if variable_revision_number == 3: # Added rules_class new_setting_values = setting_values[:1] idx = 1 for flag_idx in range(int(setting_values[0])): new_setting_values += setting_values[idx:( idx + N_FIXED_SETTINGS_PER_FLAG)] meas_count = int(setting_values[idx]) idx += N_FIXED_SETTINGS_PER_FLAG for meas_idx in range(meas_count): new_setting_values += setting_values[idx:( idx + N_SETTINGS_PER_MEASUREMENT_V3)] new_setting_values += ["1"] idx += N_SETTINGS_PER_MEASUREMENT_V3 setting_values = new_setting_values variable_revision_number = 4 return setting_values, variable_revision_number
def add_measurement(self, flag_settings, can_delete=True): measurement_settings = flag_settings.measurement_settings group = SettingsGroup() group.append("divider1", Divider(line=False)) group.append( "source_choice", Choice( "Flag is based on", S_ALL, doc="""\ - *%(S_IMAGE)s:* A per-image measurement, such as intensity or granularity. - *%(S_AVERAGE_OBJECT)s:* The average of all object measurements in the image. - *%(S_ALL_OBJECTS)s:* All the object measurements in an image, without averaging. In other words, if *any* of the objects meet the criteria, the image will be flagged. - *%(S_RULES)s:* Use a text file of rules produced by CellProfiler Analyst. With this option, you will have to ensure that this pipeline produces every measurement in the rules file upstream of this module. - *%(S_CLASSIFIER)s:* Use a classifier built by CellProfiler Analyst. """ % globals(), ), ) group.append( "object_name", LabelSubscriber( "Select the object to be used for flagging", "None", doc="""\ *(Used only when flag is based on an object measurement)* Select the objects whose measurements you want to use for flagging. """, ), ) def object_fn(): if group.source_choice == S_IMAGE: return IMAGE return group.object_name.value group.append( "rules_directory", Directory( "Rules file location", doc="""\ *(Used only when flagging using "{rules}")* Select the location of the rules file that will be used for flagging images. {folder_choice} """.format(rules=S_RULES, folder_choice=IO_FOLDER_CHOICE_HELP_TEXT), ), ) def get_directory_fn(): """Get the directory for the rules file name""" return group.rules_directory.get_absolute_path() def set_directory_fn(path): dir_choice, custom_path = group.rules_directory.get_parts_from_path( path) group.rules_directory.join_parts(dir_choice, custom_path) group.append( "rules_file_name", Filename( "Rules file name", "rules.txt", get_directory_fn=get_directory_fn, set_directory_fn=set_directory_fn, doc="""\ *(Used only when flagging using "%(S_RULES)s")* The name of the rules file, most commonly from CellProfiler Analyst's Classifier. This file should be a plain text file containing the complete set of rules. Each line of this file should be a rule naming a measurement to be made on an image, for instance: IF (Image_ImageQuality_PowerLogLogSlope_DNA < -2.5, [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 images whose power log slope is less than -2.5 pixels and will score the opposite for images whose slope is larger. The filter adds positive and negative and flags the images whose positive score is higher than the negative score. """ % globals(), ), ) def get_rules_class_choices(group=group): """Get the available choices from the rules file""" try: if group.source_choice == S_CLASSIFIER: return self.get_bin_labels(group) elif group.source_choice == S_RULES: rules = self.get_rules(group) nclasses = len(rules.rules[0].weights[0]) return [str(i) for i in range(1, nclasses + 1)] else: return ["None"] rules = self.get_rules(group) 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)] group.append( "rules_class", MultiChoice( "Class number", choices=["1", "2"], doc="""\ *(Used only when flagging using "%(S_RULES)s")* Select which classes to flag when filtering. The CellProfiler Analyst Classifier user interface lists the names of the classes in order. By default, these are the positive (class 1) and negative (class 2) classes. **FlagImage** uses the first class from CellProfiler Analyst if you choose “1”, etc. Please note the following: - The flag is set if the image falls into the selected class. - You can make multiple class selections. If you do so, the module will set the flag if the image falls into any of the selected classes. """ % globals(), ), ) group.rules_class.get_choices = get_rules_class_choices group.append( "measurement", Measurement( "Which measurement?", object_fn, doc="""Choose the measurement to be used as criteria.""", ), ) group.append( "wants_minimum", Binary( "Flag images based on low values?", True, doc="""\ Select *Yes* to flag images with measurements below the specified cutoff. If the measurement evaluates to Not-A-Number (NaN), then the image is not flagged. """ % globals(), ), ) group.append( "minimum_value", Float("Minimum value", 0, doc="""Set a value as a lower limit."""), ) group.append( "wants_maximum", Binary( "Flag images based on high values?", True, doc="""\ Select *Yes* to flag images with measurements above the specified cutoff. If the measurement evaluates to Not-A-Number (NaN), then the image is not flagged. """ % globals(), ), ) group.append( "maximum_value", Float("Maximum value", 1, doc="""Set a value as an upper limit."""), ) if can_delete: group.append( "remover", RemoveSettingButton("", "Remove this measurement", measurement_settings, group), ) group.append("divider2", Divider(line=True)) measurement_settings.append(group)
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<Plate>". 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)
class Noise2Void(ImageProcessing): module_name = "Noise2Void" variable_revision_number = 1 def create_settings(self): super(Noise2Void, self).create_settings() self.ml_model = Directory("Path to ML Model", doc="""\ Select the folder containing the machine learning model to be used. This model has to be generated via the noise2void training. See https://github.com/juglab/n2v/blob/master/examples/2D/denoising2D_RGB/01_training.ipynb for an example of training. """ ) self.color = Binary("Process as color image?", value=False, doc="""\ Select whether your image should be processed as a color image or not. """) self.manual_slicing = Binary("Slice Image manually?", value=False, doc="""\ If necessary, **Noise2Void** will slice your image into tiles automatically for a better memory fit. If you want to manually determine the size of the said tiles, check this setting. Colored images **do not** support custom slicing as of right now! """) self.slicing_configuration = Text("Tile size", value="(2,2,2)", doc="""\ You can provide an image slicing configuration for Noise2Void for a better memory fit. Specify your custom slicing configuration as follows: - (x,y) for 2D Images - (x,y,z) for 3D Images, whereas x,y and z are positive integers. If your input cannot be parsed, no slicing configuration will be provided to n2v. """) self.axes_configuration = Text(text="N2V Axes", value=N2V_AXES_3D, doc="""\ For internal use only. Communicates axes configuration (2D or 3D, color or not) to n2v. """) self.x_name.doc = """\ This is the image that the module operates on. You can choose any image that is made available by a prior module. **Noise2Void** will denoise this image using a tensorflow based neural network. """ def settings(self): settings = super(Noise2Void, self).settings() return settings + [self.ml_model, self.slicing_configuration, self.color, self.axes_configuration] def visible_settings(self): visible_settings = super(Noise2Void, self).visible_settings() visible_settings += [self.ml_model, self.color] if not self.color: visible_settings += [self.manual_slicing] if self.manual_slicing: visible_settings += [self.slicing_configuration] return visible_settings # # This is the function that gets called during "run" to create the output image. # def denoise(self, pixels, ml_model, final_tile_choice, color, axes): path = self.ml_model.get_absolute_path() (basedir, model_name) = split(path) try: model = N2V(config=None, name=model_name, basedir=basedir) except FileNotFoundError as e: raise FileNotFoundError( 'Path ' + path + ' doesn\'t lead to valid model') from e if self.manual_slicing: tile_tuple = self.convert_string_to_tuple(final_tile_choice) if color or not self.manual_slicing or tile_tuple == None: axes = self.adjust_for_color(axes) pred = model.predict(pixels, axes=axes) else: pred = model.predict(pixels, axes=axes, n_tiles=tile_tuple) return pred def run(self, workspace): image = workspace.image_set.get_image(self.x_name.value) self.adjust_settings_for_dimensionality(image.volumetric) self.function = self.denoise super(Noise2Void, self).run(workspace) def volumetric(self): return True def adjust_settings_for_dimensionality(self, image_is_3d_in_workspace): if image_is_3d_in_workspace: self.axes_configuration.value = N2V_AXES_3D else: self.axes_configuration.value = N2V_AXES_2D def adjust_for_color(self, axes): axes.replace(N2V_AXES_COLOR, '') if self.color: axes += N2V_AXES_COLOR return axes def convert_string_to_tuple(self, text): try: text = text.strip() text = text.replace('(', '') text = text.replace(')', '') text = text.replace(' ', '') return tuple(map(int, text.split(','))) except ValueError: return None
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
def create_settings(self): self.save_image_or_figure = Choice( "Select the type of image to save", IF_ALL, IF_IMAGE, doc="""\ The following types of images can be saved as a file on the hard drive: - *{IF_IMAGE}:* Any of the images produced upstream of **SaveImages** can be selected for saving. Outlines of objects created by other modules such as **Identify** modules, **Watershed**, and various object processing modules can also be saved with this option, but you must use the **OverlayOutlines** module to create them prior to saving images. Likewise, if you wish to save the objects themselves, you must use the **ConvertObjectsToImage** module to create a savable image. - *{IF_MASK}:* Relevant only if a module that produces masks has been used such as **Crop**, **MaskImage**, or **MaskObjects**. These modules create a mask of the pixels of interest in the image. Saving the mask will produce a binary image in which the pixels of interest are set to 1; all other pixels are set to 0. - *{IF_CROPPING}:* Relevant only if the **Crop** module is used. The **Crop** module also creates a cropping image which is typically the same size as the original image. However, since **Crop** permits removal of the rows and columns that are left blank, the cropping can be of a different size than the mask. - *{IF_MOVIE}:* A sequence of images can be saved as a TIFF stack. """.format( **{ "IF_CROPPING": IF_CROPPING, "IF_IMAGE": IF_IMAGE, "IF_MASK": IF_MASK, "IF_MOVIE": IF_MOVIE, } ), ) self.image_name = ImageSubscriber( "Select the image to save", doc="Select the image you want to save." ) self.file_name_method = Choice( "Select method for constructing file names", [FN_FROM_IMAGE, FN_SEQUENTIAL, FN_SINGLE_NAME], FN_FROM_IMAGE, doc="""\ *(Used only if saving non-movie files)* Several choices are available for constructing the image file name: - *{FN_FROM_IMAGE}:* The filename will be constructed based on the original filename of an input image specified in **NamesAndTypes**. You will have the opportunity to prefix or append additional text. If you have metadata associated with your images, you can append text to the image filename using a metadata tag. This is especially useful if you want your output given a unique label according to the metadata corresponding to an image group. The name of the metadata to substitute can be provided for each image for each cycle using the **Metadata** module. - *{FN_SEQUENTIAL}:* Same as above, but in addition, each filename will have a number appended to the end that corresponds to the image cycle number (starting at 1). - *{FN_SINGLE_NAME}:* A single name will be given to the file. Since the filename is fixed, this file will be overwritten with each cycle. In this case, you would probably want to save the image on the last cycle (see the *Select how often to save* setting). The exception to this is to use a metadata tag to provide a unique label, as mentioned in the *{FN_FROM_IMAGE}* option. {USING_METADATA_TAGS_REF} {USING_METADATA_HELP_REF} """.format( **{ "FN_FROM_IMAGE": FN_FROM_IMAGE, "FN_SEQUENTIAL": FN_SEQUENTIAL, "FN_SINGLE_NAME": FN_SINGLE_NAME, "USING_METADATA_HELP_REF": _help.USING_METADATA_HELP_REF, "USING_METADATA_TAGS_REF": _help.USING_METADATA_TAGS_REF, } ), ) self.file_image_name = FileImageSubscriber( "Select image name for file prefix", "None", doc="""\ *(Used only when “{FN_FROM_IMAGE}” is selected for constructing the filename)* Select an image loaded using **NamesAndTypes**. The original filename will be used as the prefix for the output filename.""".format( **{"FN_FROM_IMAGE": FN_FROM_IMAGE} ), ) self.single_file_name = Text( SINGLE_NAME_TEXT, "OrigBlue", metadata=True, doc="""\ *(Used only when “{FN_SEQUENTIAL}” or “{FN_SINGLE_NAME}” are selected for constructing the filename)* Specify the filename text here. If you have metadata associated with your images, enter the filename text with the metadata tags. {USING_METADATA_TAGS_REF} Do not enter the file extension in this setting; it will be appended automatically.""".format( **{ "FN_SEQUENTIAL": FN_SEQUENTIAL, "FN_SINGLE_NAME": FN_SINGLE_NAME, "USING_METADATA_TAGS_REF": _help.USING_METADATA_TAGS_REF, } ), ) self.number_of_digits = Integer( "Number of digits", 4, doc="""\ *(Used only when “{FN_SEQUENTIAL}” is selected for constructing the filename)* Specify the number of digits to be used for the sequential numbering. Zeros will be used to left-pad the digits. If the number specified here is less than that needed to contain the number of image sets, the latter will override the value entered.""".format( **{"FN_SEQUENTIAL": FN_SEQUENTIAL} ), ) self.wants_file_name_suffix = Binary( "Append a suffix to the image file name?", False, doc="""\ Select "*{YES}*" to add a suffix to the image’s file name. Select "*{NO}*" to use the image name as-is. """.format( **{"NO": "No", "YES": "Yes"} ), ) self.file_name_suffix = Text( "Text to append to the image name", "", metadata=True, doc="""\ *(Used only when constructing the filename from the image filename)* Enter the text that should be appended to the filename specified above. If you have metadata associated with your images, you may use metadata tags. {USING_METADATA_TAGS_REF} Do not enter the file extension in this setting; it will be appended automatically. """.format( **{"USING_METADATA_TAGS_REF": _help.USING_METADATA_TAGS_REF} ), ) self.file_format = Choice( "Saved file format", [FF_JPEG, FF_NPY, FF_PNG, FF_TIFF, FF_H5], value=FF_TIFF, doc="""\ *(Used only when saving non-movie files)* Select the format to save the image(s). Only *{FF_TIFF}* supports saving as 16-bit or 32-bit. *{FF_TIFF}* is a "lossless" file format. *{FF_PNG}* is also a "lossless" file format and it tends to produce smaller files without losing any image data. *{FF_JPEG}* is also small but is a "lossy" file format and should not be used for any images that will undergo further quantitative analysis. Select *{FF_NPY}* to save an illumination correction image generated by **CorrectIlluminationCalculate**. Select *{FF_H5}* to save files to be used for Ilastik pixel classificaiton. The images should be correctly recognized as yxcz images.""".format( **{ "FF_NPY": FF_NPY, "FF_TIFF": FF_TIFF, "FF_PNG": FF_PNG, "FF_JPEG": FF_JPEG, "FF_H5": FF_H5, } ), ) self.pathname = SaveImagesDirectoryPath( "Output file location", self.file_image_name, doc="""\ This setting lets you choose the folder for the output files. {IO_FOLDER_CHOICE_HELP_TEXT} An additional option is the following: - *Same folder as image*: Place the output file in the same folder that the source image is located. {IO_WITH_METADATA_HELP_TEXT} If the subfolder does not exist when the pipeline is run, CellProfiler will create it. If you are creating nested subfolders using the sub-folder options, you can specify the additional folders separated with slashes. For example, “Outlines/Plate1” will create a “Plate1” folder in the “Outlines” folder, which in turn is under the Default Input/Output Folder. The use of a forward slash (“/”) as a folder separator will avoid ambiguity between the various operating systems. """.format( **{ "IO_FOLDER_CHOICE_HELP_TEXT": _help.IO_FOLDER_CHOICE_HELP_TEXT, "IO_WITH_METADATA_HELP_TEXT": _help.IO_WITH_METADATA_HELP_TEXT, } ), ) self.bit_depth = Choice( "Image bit depth", [BIT_DEPTH_8, BIT_DEPTH_16, BIT_DEPTH_FLOAT, BIT_DEPTH_RAW], doc=f"""\ Select the bit-depth at which you want to save the images. *{BIT_DEPTH_FLOAT}* saves the image as floating-point decimals with 32-bit precision. When the input data is integer or binary type, pixel values are scaled within the range (0, 1). Floating point data is not rescaled. *{BIT_DEPTH_16}* and *{BIT_DEPTH_FLOAT}* images are supported only for TIFF formats. Data is normally checked and transformed to ensure that it matches the selected format's requirements. Selecting *{BIT_DEPTH_RAW}* will attempt to automatically save to a compatible format without applying any transformations to the data. This could be used to save integer labels in 32-bit float format if you had more labels than the 16-bit format can handle (without rescaling to the 0-1 range of *{BIT_DEPTH_FLOAT}*). Note that because the data validation step is skipped some images may fail to save if they contain unusable data. Note: Opening exported multichannel 16-bit TIFF stacks in ImageJ may require the BioFormats Importer plugin due to the compression method used by CellProfiler.""", ) self.tiff_compress = Binary( "Save with lossless compression?", value=True, doc="""\ *(Used only when saving 2D images as file type tiff)* Choose whether or not to use lossless compression when saving images. This will lead to smaller file sizes, but somewhat longer module execution time. Note that the value of this setting will be ignored when saving 3D tiff images, which have been saved by default with compression since CellProfiler 3.1. Do not use for multichannel tiff images created as Stacks in GrayToColor.""" ) self.stack_axis = Choice( "How to save the series", [AXIS_T, AXIS_Z], value=AXIS_T, doc="""\ *(Used only when saving movie/stack files)* This setting determines how planes are saved into a movie/stack. Selecting "T" will save planes as a time series. Selecting "Z" will save planes as slices in a 3D z-axis. """, ) self.overwrite = Binary( "Overwrite existing files without warning?", False, doc="""\ Select "*{YES}*" to automatically overwrite a file if it already exists. Select "*{NO}*" to be prompted for confirmation first. If you are running the pipeline on a computing cluster, select "*{YES}*" since you will not be able to intervene and answer the confirmation prompt.""".format( **{"NO": "No", "YES": "Yes"} ), ) self.when_to_save = Choice( "When to save", [WS_EVERY_CYCLE, WS_FIRST_CYCLE, WS_LAST_CYCLE], WS_EVERY_CYCLE, doc="""\ *(Used only when saving non-movie files)* Specify at what point during pipeline execution to save file(s). - *{WS_EVERY_CYCLE}:* Useful for when the image of interest is created every cycle and is not dependent on results from a prior cycle. - *{WS_FIRST_CYCLE}:* Useful for when you are saving an aggregate image created on the first cycle, e.g., **CorrectIlluminationCalculate** with the *All* setting used on images obtained directly from **NamesAndTypes**. - *{WS_LAST_CYCLE}:* Useful for when you are saving an aggregate image completed on the last cycle, e.g., **CorrectIlluminationCalculate** with the *All* setting used on intermediate images generated during each cycle.""".format( **{ "WS_EVERY_CYCLE": WS_EVERY_CYCLE, "WS_FIRST_CYCLE": WS_FIRST_CYCLE, "WS_LAST_CYCLE": WS_LAST_CYCLE, } ), ) self.update_file_names = Binary( "Record the file and path information to the saved image?", False, doc="""\ Select "*{YES}*" to store filename and pathname data for each of the new files created via this module as a per-image measurement. Instances in which this information may be useful include: - Exporting measurements to a database, allowing access to the saved image. If you are using the machine-learning tools or image viewer in CellProfiler Analyst, for example, you will want to enable this setting if you want the saved images to be displayed along with the original images.""".format( **{"YES": "Yes"} ), ) self.create_subdirectories = Binary( "Create subfolders in the output folder?", False, doc=""" Select "*{YES}*" to create subfolders to match the input image folder structure. For example, if your input images are organized into subfolders (e.g., for each plate, well, animal, etc.), this option allows you to mirror some or all of that nested folder structure in the output folder.""".format( **{"YES": "Yes"} ), ) self.root_dir = Directory( "Base image folder", doc="""\ *Used only if creating subfolders in the output folder* In subfolder mode, **SaveImages** determines the folder for an output image file by examining the path of the matching input file. You should choose as **Base image folder** the input folder that has the structure you'd like to mirror in the output folder. Consider an example where your input images are stored in a nested folder structure of "images\/experiment-name\/plate-name" (i.e., your files are in folders for each plate, nested inside of folders for each experiment, nested in a parent folder called "images"). If you select the base image folder to be **images**, **SaveImages** will go to your "Output file location" and save images in subfolders "experiment-name\/plate-name" that corresponds to each input image. If the base image folder chosen is one level deeper at "images\/experiment-name", **SaveImages** will store images in subfolders for each "plate-name" they belong to. **Warning**: Do not select the same folder you selected for "Output file location" as this can lead to unexpected behavior like saving in the original input file directory. For safety, ensure "Overwrite existing files without warning?" is set to "No" while testing this option. """, )
class ExportToACC(Module): module_name = 'ExportToACC' category = ["File Processing", "Data Tools"] variable_revision_number = 2 def create_settings(self): self.directory = Directory("Output file location", dir_choices=[ ABSOLUTE_FOLDER_NAME, DEFAULT_OUTPUT_FOLDER_NAME, DEFAULT_OUTPUT_SUBFOLDER_NAME, DEFAULT_INPUT_FOLDER_NAME, DEFAULT_INPUT_SUBFOLDER_NAME ], doc=""" This setting lets you choose the folder for the output files. %(IO_FOLDER_CHOICE_HELP_TEXT)s %(IO_WITH_METADATA_HELP_TEXT)s """ % globals()) self.directory.dir_choice = DEFAULT_OUTPUT_FOLDER_NAME self.wants_file_name_suffix = Binary( "Append a suffix to the file name?", False, doc=""" Select *"YES"* to add a suffix to the file name. Select *"NO"* to use the file name as-is. """ % globals()) self.file_name_suffix = Text("Text to append to the file name", "", metadata=True, doc=""" "*(Used only when constructing the filename from the image filename)*" Enter the text that should be appended to the filename specified above. """) self.wants_overwrite_without_warning = Binary( "Overwrite without warning?", False, doc="""This setting either prevents or allows overwriting of old .txt files by **ExportToACC** without confirmation. Select "*YES*" to overwrite without warning any .txt file that already exists. Select "*NO*" to prompt before overwriting when running CellProfiler in the GUI and to fail when running headless. """ % globals()) self.nan_representation = Choice("Representation of Nan/Inf", [NANS_AS_NANS, NANS_AS_NULLS], doc=""" This setting controls the output for numeric fields if the calculated value is infinite (*"Inf"*) or undefined (*"NaN*"). CellProfiler will produce Inf or NaN values under certain rare circumstances, for instance when calculating the mean intensity of an object within a masked region of an image. - "*%(NANS_AS_NULLS)s:*" Output these values as empty fields. - "*%(NANS_AS_NANS)s:*" Output them as the strings "NaN", "Inf" or "-Inf". """ % globals()) self.pick_columns = Binary("Select the measurements to export", False, doc=""" Select *"YES"* to provide a button that allows you to select which measurements you want to export. This is useful if you know exactly what measurements you want included in the final spreadheet(s). """ % globals()) self.columns = MeasurementMultiChoice( "Press button to select measurements to export", doc=""" "*(Used only when selecting the columns of measurements to export)*" This setting controls the columns to be exported. Press the button and check the measurements or categories to export.""") self.file_image_name = FileImageSubscriber( "Select image name for file prefix", "None", doc=""" Select an image loaded using **NamesAndTypes**. The original filename will be used as the prefix for the output filename.""" % globals()) def settings(self): """Return the settings in the order used when storing """ result = [ self.pick_columns, self.directory, self.columns, self.nan_representation, self.wants_file_name_suffix, self.file_name_suffix, self.wants_overwrite_without_warning, self.file_image_name ] return result def visible_settings(self): """Return the settings as seen by the user""" result = [ self.directory, self.file_image_name, self.wants_file_name_suffix ] if self.wants_file_name_suffix: result += [self.file_name_suffix] result += [ self.wants_overwrite_without_warning, self.nan_representation, self.pick_columns ] if self.pick_columns: result += [self.columns] return result def validate_module(self, pipeline): '''Test the module settings to make sure they are internally consistent''' '''Make sure metadata tags exist''' if self.wants_file_name_suffix.value: text_str = self.file_name_suffix.value undefined_tags = pipeline.get_undefined_metadata_tags(text_str) if len(undefined_tags) > 0: raise ValidationError( "%s is not a defined metadata tag. Check the metadata specifications in your load modules" % undefined_tags[0], self.file_name_suffix) def validate_module_warnings(self, pipeline): '''Warn user re: Test mode ''' if pipeline.test_mode: raise ValidationError( "ExportToACC will not produce output in Test Mode", self.directory) def prepare_run(self, workspace): '''Prepare an image set to be run workspace - workspace with image set populated (at this point) returns False if analysis can't be done ''' return self.check_overwrite(workspace) def run(self, workspace): # all of the work is done in post_run() if self.show_window: image_set_number = workspace.measurements.image_set_number header = ["Filename"] columns = [] path = self.make_image_file_name(workspace, image_set_number) columns.append((path, )) workspace.display_data.header = header workspace.display_data.columns = columns def display(self, workspace, figure): figure.set_subplots(( 1, 1, )) if workspace.display_data.columns is None: figure.subplot_table(0, 0, [["Data written to acc files"]]) elif workspace.pipeline.test_mode: figure.subplot_table( 0, 0, [["Data not written to acc files in test mode"]]) else: figure.subplot_table(0, 0, workspace.display_data.columns, col_labels=workspace.display_data.header) def run_as_data_tool(self, workspace): '''Run the module as a data tool For ExportToACC, we do the "post_run" method in order to write out the .txt files as if the experiment had just finished. ''' # # Set the measurements to the end of the list to mimic the state # at the end of the run. # m = workspace.measurements m.image_set_number = m.image_set_count self.post_run(workspace) def post_run(self, workspace): '''Save measurements at end of run''' # # Don't export in test mode # #if workspace.pipeline.test_mode: # return object_names = self.filter_object_names( workspace.measurements.get_object_names()) self.run_objects(object_names, workspace) def should_stop_writing_measurements(self): '''All subsequent modules should not write measurements''' return True def get_metadata_groups(self, workspace, settings_group=None): '''Find the metadata groups that are relevant for creating the file name workspace - the workspace with the image set metadata elements and grouping measurements populated. settings_group - if saving individual objects, this is the settings group that controls naming the files. ''' if settings_group is None or settings_group.wants_automatic_file_name: tags = [] else: tags = find_metadata_tokens(settings_group.file_name.value) if self.directory.is_custom_choice: tags += find_metadata_tokens(self.directory.custom_path) metadata_groups = workspace.measurements.group_by_metadata(tags) return metadata_groups def run_objects(self, object_names, workspace, settings_group=None): """Create a file based on the object names object_names - a sequence of object names (or Image or Experiment) which tell us which objects get piled into each file workspace - get the images from here. settings_group - if present, use the settings group for naming. """ metadata_groups = self.get_metadata_groups(workspace, settings_group) for metadata_group in metadata_groups: self.make_object_file(object_names, metadata_group.image_numbers, workspace, settings_group) def make_full_filename(self, file_name, workspace=None, image_set_number=None): """Convert a file name into an absolute path We do a few things here: * apply metadata from an image set to the file name if an image set is specified * change the relative path into an absolute one using the "." and "&" convention * Create any directories along the path """ if image_set_number is not None and workspace is not None: file_name = workspace.measurements.apply_metadata( file_name, image_set_number) measurements = None if workspace is None else workspace.measurements path_name = self.directory.get_absolute_path(measurements, image_set_number) file_name = os.path.join(path_name, file_name) path, fname = os.path.split(file_name) if not os.path.isdir(path): os.makedirs(path) return os.path.join(path, fname) def extension(self): '''Return the appropriate extension for the txt file name The appropriate extension is "txt" ''' return "txt" def make_image_file_name(self, workspace, image_set_number, settings_group=None): '''Make file name for objects measured from an image :param workspace: the current workspace :param image_set_number: the current image set number :param settings_group: the settings group used to name the file ''' imagename = workspace.measurements.get_measurement( IMAGE, "FileName_" + self.file_image_name.value, image_set_number) filename = "%s" % os.path.splitext(imagename)[0] if self.wants_file_name_suffix: suffix = self.file_name_suffix.value suffix = workspace.measurements.apply_metadata( suffix, image_set_number) filename += suffix filename = "%s.%s" % (filename, self.extension()) return self.make_full_filename(filename, workspace, image_set_number) def check_overwrite(self, workspace): """Make sure it's ok to overwrite any existing files before starting run workspace - workspace with all image sets already populated returns True if ok to proceed, False if user cancels """ if self.wants_overwrite_without_warning: return True files_to_check = [] metadata_groups = self.get_metadata_groups(workspace) for metadata_group in metadata_groups: image_number = metadata_group.image_numbers[0] files_to_check.append( self.make_image_file_name(workspace, image_number)) files_to_overwrite = filter(os.path.isfile, files_to_check) if len(files_to_overwrite) > 0: if get_headless(): logger.error( "ExportToACC is configured to refrain from overwriting files and the following file(s) already exist: %s" % ", ".join(files_to_overwrite)) return False msg = "Overwrite the following file(s)?\n" +\ "\n".join(files_to_overwrite) import wx result = wx.MessageBox( msg, caption="ExportToACC: Overwrite existing files", style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION) if result != wx.YES: return False return True def filter_columns(self, features, object_name): if self.pick_columns: columns = [ self.columns.get_measurement_feature(x) for x in self.columns.selections if self.columns.get_measurement_object(x) == object_name ] columns = set(columns) features = [x for x in features if x in columns] return features def filter_object_names(self, object_names): object_names.remove('Image') object_names.remove('Experiment') return object_names def make_object_file(self, object_names, image_set_numbers, workspace, settings_group=None): """Make a file containing object measurements object_names - sequence of names of the objects whose measurements will be included image_set_numbers - the image sets whose data gets extracted workspace - workspace containing the measurements settings_group - the settings group used to choose to make the file """ m = workspace.measurements acc_file_name = os.path.join( os.path.dirname( self.make_image_file_name(workspace, image_set_numbers[0], settings_group)), ACC_FILE_NAME) features = [] objects_with_selected_features = [] center_x = ("", "") center_y = ("", "") for object_name in object_names: if not object_name in m.get_object_names(): continue rfeatures = m.get_feature_names(object_name) rfeatures = self.filter_columns(rfeatures, object_name) ofeatures = [ x for x in rfeatures if not [y for y in REMOVE_FEAT if y in x] ] ofeatures = [(object_name, feature_name) for feature_name in ofeatures] ofeatures.sort() features += ofeatures # Haggish way to find feature to use as object coordinates if ofeatures: objects_with_selected_features.append(object_name) coord = [feat for feat in rfeatures if "Location_Center_" in feat] for feat in coord: if (not center_x or "Nuclei" == object_name) and "Center_X" in feat: center_x = (object_name, feat) if (not center_y or "Nuclei" == object_name) and "Center_Y" in feat: center_y = (object_name, feat) features.insert(0, center_y) features.insert(0, center_x) # Write ACC file try: fd = open(acc_file_name, "w") for feat in features: fd.write(feat[0] + "_" + feat[1] + "\n") fd.close() except: pass for img_number in image_set_numbers: try: file_name = self.make_image_file_name(workspace, img_number, settings_group) fd = open(file_name, "w") writer = csv.writer(fd, delimiter=DELIMITER) object_count =\ np.max([m.get_measurement(IMAGE, "Count_%s"%name, img_number) for name in objects_with_selected_features]) object_count = int(object_count) if object_count else 0 columns = [ np.repeat(img_number, object_count) if feature_name == IMAGE_NUMBER else np.arange(1, object_count + 1) if feature_name == OBJECT_NUMBER else np. repeat(m.get_measurement(IMAGE, feature_name, img_number), object_count) if object_name == IMAGE else m.get_measurement(object_name, feature_name, img_number) for object_name, feature_name in features ] for obj_index in range(object_count): row = [ column[obj_index] if (column is not None and obj_index < column.shape[0]) else np.NAN for column in columns ] if self.nan_representation == NANS_AS_NULLS: row = [ "" if (field is None) or (np.isreal(field) and not np.isfinite(field)) else field for field in row ] writer.writerow(row) fd.close() except: pass 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. ExportToACC has to convert the path to file names to something that can be used on the cluster. ''' self.directory.alter_for_create_batch_files(fn_alter_path) return True def upgrade_settings(self, setting_values, variable_revision_number, module_name): """Adjust the setting values based on the version that saved them """ if variable_revision_number == 1: setting_values = (setting_values[:4] + [NO, ""] + setting_values[4:]) variable_revision_number = 2 # Standardize input/output directory name references SLOT_DIRCHOICE = 1 directory = setting_values[SLOT_DIRCHOICE] directory = Directory.upgrade_setting(directory) setting_values = (setting_values[:SLOT_DIRCHOICE] + [directory] + setting_values[SLOT_DIRCHOICE + 1:]) return setting_values, variable_revision_number
def add_dose_value(self, can_remove=True): """Add a dose value measurement to the list can_delete - set this to False to keep from showing the "remove" button for images that must be present.""" group = SettingsGroup() group.append( "measurement", Measurement( "Select the image measurement describing the treatment dose", lambda: IMAGE, doc="""\ The V and Z’ factors, metrics of assay quality, and the EC50, indicating dose-response, are calculated by this module based on each image being specified as a particular treatment dose. Choose a measurement that gives the dose of some treatment for each of your images. See the help for the previous setting for details.""", ), ) group.append( "log_transform", Binary( "Log-transform the dose values?", False, doc="""\ Select *Yes* if you have dose-response data and you want to log-transform the dose values before fitting a sigmoid curve. Select *No* if your data values indicate only positive vs. negative controls. """ % globals(), ), ) group.append( "wants_save_figure", Binary( """Create dose-response plots?""", False, doc="""Select *Yes* if you want to create and save dose-response plots. You will be asked for information on how to save the plots.""" % globals(), ), ) group.append( "figure_name", Text( "Figure prefix", "", doc="""\ *(Used only when creating dose-response plots)* CellProfiler will create a file name by appending the measurement name to the prefix you enter here. For instance, if you specify a prefix of “Dose\_”, when saving a file related to objects you have chosen (for example, *Cells*) and a particular measurement (for example, *AreaShape_Area*), CellProfiler will save the figure as *Dose_Cells_AreaShape_Area.m*. Leave this setting blank if you do not want a prefix. """, ), ) group.append( "pathname", Directory( "Output file location", dir_choices=[ DEFAULT_OUTPUT_FOLDER_NAME, DEFAULT_INPUT_FOLDER_NAME, ABSOLUTE_FOLDER_NAME, DEFAULT_OUTPUT_SUBFOLDER_NAME, DEFAULT_INPUT_SUBFOLDER_NAME, ], doc="""\ *(Used only when creating dose-response plots)* This setting lets you choose the folder for the output files. {fcht} {mht} """.format( fcht=IO_FOLDER_CHOICE_HELP_TEXT, mht=IO_WITH_METADATA_HELP_TEXT ), ), ) group.append("divider", Divider()) group.append( "remover", RemoveSettingButton( "", "Remove this dose measurement", self.dose_values, group ), ) self.dose_values.append(group)
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.""", )
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
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, )
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, )
class SaveImages(Module): module_name = "SaveImages" variable_revision_number = 16 category = "File Processing" def create_settings(self): self.save_image_or_figure = Choice( "Select the type of image to save", IF_ALL, IF_IMAGE, doc="""\ The following types of images can be saved as a file on the hard drive: - *{IF_IMAGE}:* Any of the images produced upstream of **SaveImages** can be selected for saving. Outlines of objects created by other modules such as **Identify** modules, **Watershed**, and various object processing modules can also be saved with this option, but you must use the **OverlayOutlines** module to create them prior to saving images. Likewise, if you wish to save the objects themselves, you must use the **ConvertObjectsToImage** module to create a savable image. - *{IF_MASK}:* Relevant only if a module that produces masks has been used such as **Crop**, **MaskImage**, or **MaskObjects**. These modules create a mask of the pixels of interest in the image. Saving the mask will produce a binary image in which the pixels of interest are set to 1; all other pixels are set to 0. - *{IF_CROPPING}:* Relevant only if the **Crop** module is used. The **Crop** module also creates a cropping image which is typically the same size as the original image. However, since **Crop** permits removal of the rows and columns that are left blank, the cropping can be of a different size than the mask. - *{IF_MOVIE}:* A sequence of images can be saved as a TIFF stack. """.format( **{ "IF_CROPPING": IF_CROPPING, "IF_IMAGE": IF_IMAGE, "IF_MASK": IF_MASK, "IF_MOVIE": IF_MOVIE, } ), ) self.image_name = ImageSubscriber( "Select the image to save", doc="Select the image you want to save." ) self.file_name_method = Choice( "Select method for constructing file names", [FN_FROM_IMAGE, FN_SEQUENTIAL, FN_SINGLE_NAME], FN_FROM_IMAGE, doc="""\ *(Used only if saving non-movie files)* Several choices are available for constructing the image file name: - *{FN_FROM_IMAGE}:* The filename will be constructed based on the original filename of an input image specified in **NamesAndTypes**. You will have the opportunity to prefix or append additional text. If you have metadata associated with your images, you can append text to the image filename using a metadata tag. This is especially useful if you want your output given a unique label according to the metadata corresponding to an image group. The name of the metadata to substitute can be provided for each image for each cycle using the **Metadata** module. - *{FN_SEQUENTIAL}:* Same as above, but in addition, each filename will have a number appended to the end that corresponds to the image cycle number (starting at 1). - *{FN_SINGLE_NAME}:* A single name will be given to the file. Since the filename is fixed, this file will be overwritten with each cycle. In this case, you would probably want to save the image on the last cycle (see the *Select how often to save* setting). The exception to this is to use a metadata tag to provide a unique label, as mentioned in the *{FN_FROM_IMAGE}* option. {USING_METADATA_TAGS_REF} {USING_METADATA_HELP_REF} """.format( **{ "FN_FROM_IMAGE": FN_FROM_IMAGE, "FN_SEQUENTIAL": FN_SEQUENTIAL, "FN_SINGLE_NAME": FN_SINGLE_NAME, "USING_METADATA_HELP_REF": _help.USING_METADATA_HELP_REF, "USING_METADATA_TAGS_REF": _help.USING_METADATA_TAGS_REF, } ), ) self.file_image_name = FileImageSubscriber( "Select image name for file prefix", "None", doc="""\ *(Used only when “{FN_FROM_IMAGE}” is selected for constructing the filename)* Select an image loaded using **NamesAndTypes**. The original filename will be used as the prefix for the output filename.""".format( **{"FN_FROM_IMAGE": FN_FROM_IMAGE} ), ) self.single_file_name = Text( SINGLE_NAME_TEXT, "OrigBlue", metadata=True, doc="""\ *(Used only when “{FN_SEQUENTIAL}” or “{FN_SINGLE_NAME}” are selected for constructing the filename)* Specify the filename text here. If you have metadata associated with your images, enter the filename text with the metadata tags. {USING_METADATA_TAGS_REF} Do not enter the file extension in this setting; it will be appended automatically.""".format( **{ "FN_SEQUENTIAL": FN_SEQUENTIAL, "FN_SINGLE_NAME": FN_SINGLE_NAME, "USING_METADATA_TAGS_REF": _help.USING_METADATA_TAGS_REF, } ), ) self.number_of_digits = Integer( "Number of digits", 4, doc="""\ *(Used only when “{FN_SEQUENTIAL}” is selected for constructing the filename)* Specify the number of digits to be used for the sequential numbering. Zeros will be used to left-pad the digits. If the number specified here is less than that needed to contain the number of image sets, the latter will override the value entered.""".format( **{"FN_SEQUENTIAL": FN_SEQUENTIAL} ), ) self.wants_file_name_suffix = Binary( "Append a suffix to the image file name?", False, doc="""\ Select "*{YES}*" to add a suffix to the image’s file name. Select "*{NO}*" to use the image name as-is. """.format( **{"NO": "No", "YES": "Yes"} ), ) self.file_name_suffix = Text( "Text to append to the image name", "", metadata=True, doc="""\ *(Used only when constructing the filename from the image filename)* Enter the text that should be appended to the filename specified above. If you have metadata associated with your images, you may use metadata tags. {USING_METADATA_TAGS_REF} Do not enter the file extension in this setting; it will be appended automatically. """.format( **{"USING_METADATA_TAGS_REF": _help.USING_METADATA_TAGS_REF} ), ) self.file_format = Choice( "Saved file format", [FF_JPEG, FF_NPY, FF_PNG, FF_TIFF, FF_H5], value=FF_TIFF, doc="""\ *(Used only when saving non-movie files)* Select the format to save the image(s). Only *{FF_TIFF}* supports saving as 16-bit or 32-bit. *{FF_TIFF}* is a "lossless" file format. *{FF_PNG}* is also a "lossless" file format and it tends to produce smaller files without losing any image data. *{FF_JPEG}* is also small but is a "lossy" file format and should not be used for any images that will undergo further quantitative analysis. Select *{FF_NPY}* to save an illumination correction image generated by **CorrectIlluminationCalculate**. Select *{FF_H5}* to save files to be used for Ilastik pixel classificaiton. The images should be correctly recognized as yxcz images.""".format( **{ "FF_NPY": FF_NPY, "FF_TIFF": FF_TIFF, "FF_PNG": FF_PNG, "FF_JPEG": FF_JPEG, "FF_H5": FF_H5, } ), ) self.pathname = SaveImagesDirectoryPath( "Output file location", self.file_image_name, doc="""\ This setting lets you choose the folder for the output files. {IO_FOLDER_CHOICE_HELP_TEXT} An additional option is the following: - *Same folder as image*: Place the output file in the same folder that the source image is located. {IO_WITH_METADATA_HELP_TEXT} If the subfolder does not exist when the pipeline is run, CellProfiler will create it. If you are creating nested subfolders using the sub-folder options, you can specify the additional folders separated with slashes. For example, “Outlines/Plate1” will create a “Plate1” folder in the “Outlines” folder, which in turn is under the Default Input/Output Folder. The use of a forward slash (“/”) as a folder separator will avoid ambiguity between the various operating systems. """.format( **{ "IO_FOLDER_CHOICE_HELP_TEXT": _help.IO_FOLDER_CHOICE_HELP_TEXT, "IO_WITH_METADATA_HELP_TEXT": _help.IO_WITH_METADATA_HELP_TEXT, } ), ) self.bit_depth = Choice( "Image bit depth", [BIT_DEPTH_8, BIT_DEPTH_16, BIT_DEPTH_FLOAT, BIT_DEPTH_RAW], doc=f"""\ Select the bit-depth at which you want to save the images. *{BIT_DEPTH_FLOAT}* saves the image as floating-point decimals with 32-bit precision. When the input data is integer or binary type, pixel values are scaled within the range (0, 1). Floating point data is not rescaled. *{BIT_DEPTH_16}* and *{BIT_DEPTH_FLOAT}* images are supported only for TIFF formats. Data is normally checked and transformed to ensure that it matches the selected format's requirements. Selecting *{BIT_DEPTH_RAW}* will attempt to automatically save to a compatible format without applying any transformations to the data. This could be used to save integer labels in 32-bit float format if you had more labels than the 16-bit format can handle (without rescaling to the 0-1 range of *{BIT_DEPTH_FLOAT}*). Note that because the data validation step is skipped some images may fail to save if they contain unusable data. Note: Opening exported multichannel 16-bit TIFF stacks in ImageJ may require the BioFormats Importer plugin due to the compression method used by CellProfiler.""", ) self.tiff_compress = Binary( "Save with lossless compression?", value=True, doc="""\ *(Used only when saving 2D images as file type tiff)* Choose whether or not to use lossless compression when saving images. This will lead to smaller file sizes, but somewhat longer module execution time. Note that the value of this setting will be ignored when saving 3D tiff images, which have been saved by default with compression since CellProfiler 3.1. Do not use for multichannel tiff images created as Stacks in GrayToColor.""" ) self.stack_axis = Choice( "How to save the series", [AXIS_T, AXIS_Z], value=AXIS_T, doc="""\ *(Used only when saving movie/stack files)* This setting determines how planes are saved into a movie/stack. Selecting "T" will save planes as a time series. Selecting "Z" will save planes as slices in a 3D z-axis. """, ) self.overwrite = Binary( "Overwrite existing files without warning?", False, doc="""\ Select "*{YES}*" to automatically overwrite a file if it already exists. Select "*{NO}*" to be prompted for confirmation first. If you are running the pipeline on a computing cluster, select "*{YES}*" since you will not be able to intervene and answer the confirmation prompt.""".format( **{"NO": "No", "YES": "Yes"} ), ) self.when_to_save = Choice( "When to save", [WS_EVERY_CYCLE, WS_FIRST_CYCLE, WS_LAST_CYCLE], WS_EVERY_CYCLE, doc="""\ *(Used only when saving non-movie files)* Specify at what point during pipeline execution to save file(s). - *{WS_EVERY_CYCLE}:* Useful for when the image of interest is created every cycle and is not dependent on results from a prior cycle. - *{WS_FIRST_CYCLE}:* Useful for when you are saving an aggregate image created on the first cycle, e.g., **CorrectIlluminationCalculate** with the *All* setting used on images obtained directly from **NamesAndTypes**. - *{WS_LAST_CYCLE}:* Useful for when you are saving an aggregate image completed on the last cycle, e.g., **CorrectIlluminationCalculate** with the *All* setting used on intermediate images generated during each cycle.""".format( **{ "WS_EVERY_CYCLE": WS_EVERY_CYCLE, "WS_FIRST_CYCLE": WS_FIRST_CYCLE, "WS_LAST_CYCLE": WS_LAST_CYCLE, } ), ) self.update_file_names = Binary( "Record the file and path information to the saved image?", False, doc="""\ Select "*{YES}*" to store filename and pathname data for each of the new files created via this module as a per-image measurement. Instances in which this information may be useful include: - Exporting measurements to a database, allowing access to the saved image. If you are using the machine-learning tools or image viewer in CellProfiler Analyst, for example, you will want to enable this setting if you want the saved images to be displayed along with the original images.""".format( **{"YES": "Yes"} ), ) self.create_subdirectories = Binary( "Create subfolders in the output folder?", False, doc=""" Select "*{YES}*" to create subfolders to match the input image folder structure. For example, if your input images are organized into subfolders (e.g., for each plate, well, animal, etc.), this option allows you to mirror some or all of that nested folder structure in the output folder.""".format( **{"YES": "Yes"} ), ) self.root_dir = Directory( "Base image folder", doc="""\ *Used only if creating subfolders in the output folder* In subfolder mode, **SaveImages** determines the folder for an output image file by examining the path of the matching input file. You should choose as **Base image folder** the input folder that has the structure you'd like to mirror in the output folder. Consider an example where your input images are stored in a nested folder structure of "images\/experiment-name\/plate-name" (i.e., your files are in folders for each plate, nested inside of folders for each experiment, nested in a parent folder called "images"). If you select the base image folder to be **images**, **SaveImages** will go to your "Output file location" and save images in subfolders "experiment-name\/plate-name" that corresponds to each input image. If the base image folder chosen is one level deeper at "images\/experiment-name", **SaveImages** will store images in subfolders for each "plate-name" they belong to. **Warning**: Do not select the same folder you selected for "Output file location" as this can lead to unexpected behavior like saving in the original input file directory. For safety, ensure "Overwrite existing files without warning?" is set to "No" while testing this option. """, ) def settings(self): """Return the settings in the order to use when saving""" return [ self.save_image_or_figure, self.image_name, self.file_name_method, self.file_image_name, self.single_file_name, self.number_of_digits, self.wants_file_name_suffix, self.file_name_suffix, self.file_format, self.pathname, self.bit_depth, self.overwrite, self.when_to_save, self.update_file_names, self.create_subdirectories, self.root_dir, self.stack_axis, self.tiff_compress, ] def visible_settings(self): """Return only the settings that should be shown""" result = [self.save_image_or_figure, self.image_name, self.file_name_method] if self.file_name_method == FN_FROM_IMAGE: result += [self.file_image_name, self.wants_file_name_suffix] if self.wants_file_name_suffix: result.append(self.file_name_suffix) elif self.file_name_method == FN_SEQUENTIAL: self.single_file_name.text = SEQUENTIAL_NUMBER_TEXT # XXX - Change doc, as well! result.append(self.single_file_name) result.append(self.number_of_digits) elif self.file_name_method == FN_SINGLE_NAME: self.single_file_name.text = SINGLE_NAME_TEXT result.append(self.single_file_name) else: raise NotImplementedError( "Unhandled file name method: %s" % self.file_name_method ) if self.save_image_or_figure != IF_MOVIE: result.append(self.file_format) supports_16_bit = ( self.file_format in (FF_TIFF, FF_H5) and self.save_image_or_figure == IF_IMAGE ) or self.save_image_or_figure == IF_MOVIE if supports_16_bit: # TIFF supports 8 & 16-bit, all others are written 8-bit result.append(self.bit_depth) if self.file_format == FF_TIFF: result.append(self.tiff_compress) if self.save_image_or_figure == IF_MOVIE: result.append(self.stack_axis) result.append(self.pathname) result.append(self.overwrite) if self.save_image_or_figure != IF_MOVIE: result.append(self.when_to_save) result.append(self.update_file_names) if self.file_name_method == FN_FROM_IMAGE: result.append(self.create_subdirectories) if self.create_subdirectories: result.append(self.root_dir) return result @property def module_key(self): return "%s_%d" % (self.module_name, self.module_num) def prepare_group(self, workspace, grouping, image_numbers): d = self.get_dictionary(workspace.image_set_list) if self.save_image_or_figure == IF_MOVIE: d["N_FRAMES"] = len(image_numbers) d["CURRENT_FRAME"] = 0 return True def prepare_to_create_batch(self, workspace, fn_alter_path): self.pathname.alter_for_create_batch_files(fn_alter_path) if self.create_subdirectories: self.root_dir.alter_for_create_batch_files(fn_alter_path) def run(self, workspace): """Run the module pipeline - instance of cellprofiler_core.pipeline for this run workspace - the workspace contains: image_set - the images in the image set being processed object_set - the objects (labeled masks) in this image set measurements - the measurements for this run frame - display within this frame (or None to not display) """ if self.save_image_or_figure.value in (IF_IMAGE, IF_MASK, IF_CROPPING): should_save = self.run_image(workspace) elif self.save_image_or_figure == IF_MOVIE: self.run_movie(workspace) else: raise NotImplementedError( ("Saving a %s is not yet supported" % self.save_image_or_figure) ) workspace.display_data.filename = self.get_filename( workspace, make_dirs=False, check_overwrite=False ) def is_aggregation_module(self): """SaveImages is an aggregation module when it writes movies""" return ( self.save_image_or_figure == IF_MOVIE or self.when_to_save == WS_LAST_CYCLE ) def display(self, workspace, figure): if self.show_window: if self.save_image_or_figure == IF_MOVIE: return figure.set_subplots((1, 1)) outcome = ( "Wrote %s" if workspace.display_data.wrote_image else "Did not write %s" ) figure.subplot_table(0, 0, [[outcome % workspace.display_data.filename]]) def run_image(self, workspace): """Handle saving an image""" # # First, check to see if we should save this image # if self.when_to_save == WS_FIRST_CYCLE: d = self.get_dictionary(workspace.image_set_list) if workspace.measurements["Image", "Group_Index",] > 1: workspace.display_data.wrote_image = False self.save_filename_measurements(workspace) return d["FIRST_IMAGE"] = False elif self.when_to_save == WS_LAST_CYCLE: workspace.display_data.wrote_image = False self.save_filename_measurements(workspace) return self.save_image(workspace) return True def run_movie(self, workspace): out_file = self.get_filename(workspace, check_overwrite=False) # overwrite checks are made only for first frame. d = self.get_dictionary(workspace.image_set_list) if d["CURRENT_FRAME"] == 0 and os.path.exists(out_file): if not self.check_overwrite(out_file, workspace): d["CURRENT_FRAME"] = "Ignore" return else: # Have to delete the old movie before making the new one os.remove(out_file) elif d["CURRENT_FRAME"] == "Ignore": return image = workspace.image_set.get_image(self.image_name.value) pixels = image.pixel_data if self.get_bit_depth() == BIT_DEPTH_8: pixels = skimage.util.img_as_ubyte(pixels) pixel_type = bioformats.omexml.PT_UINT8 elif self.get_bit_depth() == BIT_DEPTH_16: pixels = skimage.util.img_as_uint(pixels) pixel_type = bioformats.omexml.PT_UINT16 elif self.get_bit_depth() == BIT_DEPTH_FLOAT: pixels = skimage.util.img_as_float32(pixels) pixel_type = bioformats.omexml.PT_FLOAT else: raise ValueError("Bit depth unsupported in movie mode") frames = d["N_FRAMES"] current_frame = d["CURRENT_FRAME"] d["CURRENT_FRAME"] += 1 if self.stack_axis == AXIS_T: self.do_save_image( workspace, out_file, pixels, pixel_type, t=current_frame, size_t=frames, ) else: self.do_save_image( workspace, out_file, pixels, pixel_type, z=current_frame, size_z=frames, ) def post_group(self, workspace, *args): if self.when_to_save == WS_LAST_CYCLE and self.save_image_or_figure != IF_MOVIE: try: self.save_image(workspace) except ValueError: raise ValueError( "You have tried to save %s on the last cycle but that cycle failed FlagImages. Please adjust the FlagImages settings and rerun" % (self.image_name.value) ) def do_save_image( self, workspace, filename, pixels, pixel_type, c=0, z=0, t=0, size_c=1, size_z=1, size_t=1, channel_names=None, ): """Save image using bioformats workspace - the current workspace filename - save to this filename pixels - the image to save pixel_type - save using this pixel type c - the image's channel index z - the image's z index t - the image's t index sizeC - # of channels in the stack sizeZ - # of z stacks sizeT - # of timepoints in the stack channel_names - names of the channels (make up names if not present """ bioformats.formatwriter.write_image( filename, pixels, pixel_type, c=c, z=z, t=t, size_c=size_c, size_z=size_z, size_t=size_t, channel_names=channel_names, ) def save_image(self, workspace): if self.show_window: workspace.display_data.wrote_image = False filename = self.get_filename(workspace) if filename is None: # failed overwrite check return image = workspace.image_set.get_image(self.image_name.value) volumetric_extensions = [FF_NPY, FF_TIFF, FF_H5] if image.volumetric and self.file_format.value not in volumetric_extensions: raise RuntimeError( "Unsupported file format {} for 3D pipeline. Use {} format when processing images as 3D.".format( self.file_format.value, ", or ".join(volumetric_extensions) ) ) if self.save_image_or_figure.value == IF_IMAGE: pixels = image.pixel_data elif self.save_image_or_figure.value == IF_MASK: pixels = image.mask elif self.save_image_or_figure.value == IF_CROPPING: pixels = image.crop_mask if self.file_format == FF_NPY: numpy.save(filename, pixels) else: save_kwargs = {} if self.get_bit_depth() == BIT_DEPTH_8: pixels = skimage.util.img_as_ubyte(pixels) elif self.get_bit_depth() == BIT_DEPTH_16: pixels = skimage.util.img_as_uint(pixels) elif self.get_bit_depth() == BIT_DEPTH_FLOAT: pixels = skimage.util.img_as_float32(pixels) elif self.get_bit_depth() == BIT_DEPTH_RAW: # No bit depth transformation pass # skimage will save out color images (M,N,3) or (M,N,4) appropriately # but any more than that will need to be transposed so they conform to the # CYX convention rather than YXC # http://scikit-image.org/docs/dev/api/skimage.io.html#skimage.io.imsave if ( not image.volumetric and len(pixels.shape) > 2 and image.channelstack and self.file_format.value == FF_TIFF ): pixels = numpy.transpose(pixels, (2, 0, 1)) save_kwargs.update({'imagej':True}) if (image.volumetric or self.tiff_compress.value) and self.file_format.value == FF_TIFF: save_kwargs.update({"compress": 6}) if self.file_format.value == FF_H5: save_h5(filename, pixels, volumetric=image.volumetric) else: skimage.io.imsave(filename, pixels, **save_kwargs) if self.show_window: workspace.display_data.wrote_image = True if self.when_to_save != WS_LAST_CYCLE: self.save_filename_measurements(workspace) def check_overwrite(self, filename, workspace): """Check to see if it's legal to overwrite a file Throws an exception if can't overwrite and no interaction available. Returns False if can't overwrite, otherwise True. """ if not self.overwrite.value and os.path.isfile(filename): try: return ( workspace.interaction_request( self, workspace.measurements.image_set_number, filename ) == "Yes" ) except workspace.NoInteractionException: raise ValueError( 'SaveImages: trying to overwrite %s in headless mode, but Overwrite files is set to "No"' % filename ) return True def handle_interaction(self, image_set_number, filename): """handle an interaction request from check_overwrite()""" import wx dlg = wx.MessageDialog( wx.GetApp().TopWindow, "%s #%d, set #%d - Do you want to overwrite %s?" % (self.module_name, self.module_num, image_set_number, filename), "Warning: overwriting file", wx.YES_NO | wx.ICON_QUESTION, ) result = dlg.ShowModal() == wx.ID_YES return "Yes" if result else "No" def save_filename_measurements(self, workspace): if self.update_file_names.value: filename = self.get_filename( workspace, make_dirs=False, check_overwrite=False ) pn, fn = os.path.split(filename) url = cellprofiler_core.utilities.pathname.pathname2url(filename) workspace.measurements.add_measurement( "Image", self.file_name_feature, fn, ) workspace.measurements.add_measurement( "Image", self.path_name_feature, pn, ) workspace.measurements.add_measurement( "Image", self.url_feature, url, ) @property def file_name_feature(self): return "_".join((C_FILE_NAME, self.image_name.value)) @property def path_name_feature(self): return "_".join((C_PATH_NAME, self.image_name.value)) @property def url_feature(self): return "_".join((C_URL, self.image_name.value)) @property def source_file_name_feature(self): """The file name measurement for the exemplar disk image""" return "_".join((C_FILE_NAME, self.file_image_name.value)) def source_path(self, workspace): """The path for the image data, or its first parent with a path""" if self.file_name_method.value == FN_FROM_IMAGE: path_feature = "%s_%s" % (C_PATH_NAME, self.file_image_name.value,) assert workspace.measurements.has_feature("Image", path_feature), ( "Image %s does not have a path!" % self.file_image_name.value ) return workspace.measurements.get_current_image_measurement(path_feature) # ... otherwise, chase the cpimage hierarchy looking for an image with a path cur_image = workspace.image_set.get_image(self.image_name.value) while cur_image.path_name is None: cur_image = cur_image.parent_image assert ( cur_image is not None ), "Could not determine source path for image %s' % (self.image_name.value)" return cur_image.path_name def get_measurement_columns(self, pipeline): if self.update_file_names.value: return [ ("Image", self.file_name_feature, COLTYPE_VARCHAR_FILE_NAME,), ("Image", self.path_name_feature, COLTYPE_VARCHAR_PATH_NAME,), ] else: return [] def get_filename(self, workspace, make_dirs=True, check_overwrite=True): """Concoct a filename for the current image based on the user settings""" measurements = workspace.measurements if self.file_name_method == FN_SINGLE_NAME: filename = self.single_file_name.value filename = workspace.measurements.apply_metadata(filename) elif self.file_name_method == FN_SEQUENTIAL: filename = self.single_file_name.value filename = workspace.measurements.apply_metadata(filename) n_image_sets = workspace.measurements.image_set_count ndigits = int(numpy.ceil(numpy.log10(n_image_sets + 1))) ndigits = max((ndigits, self.number_of_digits.value)) padded_num_string = str(measurements.image_set_number).zfill(ndigits) filename = "%s%s" % (filename, padded_num_string) else: file_name_feature = self.source_file_name_feature filename = measurements.get_current_measurement("Image", file_name_feature) filename = os.path.splitext(filename)[0] if self.wants_file_name_suffix: suffix = self.file_name_suffix.value suffix = workspace.measurements.apply_metadata(suffix) filename += suffix filename = "%s.%s" % (filename, self.get_file_format()) pathname = self.pathname.get_absolute_path(measurements) if self.create_subdirectories: image_path = self.source_path(workspace) subdir = os.path.relpath(image_path, self.root_dir.get_absolute_path()) pathname = os.path.join(pathname, subdir) if len(pathname) and not os.path.isdir(pathname) and make_dirs: try: os.makedirs(pathname) except: # # On cluster, this can fail if the path was created by # another process after this process found it did not exist. # if not os.path.isdir(pathname): raise result = os.path.join(pathname, filename) if check_overwrite and not self.check_overwrite(result, workspace): return if check_overwrite and os.path.isfile(result): try: os.remove(result) except: import bioformats bioformats.clear_image_reader_cache() os.remove(result) return result def get_file_format(self): """Return the file format associated with the extension in self.file_format """ if self.save_image_or_figure == IF_MOVIE: return FF_TIFF return self.file_format.value def get_bit_depth(self): if self.save_image_or_figure in ( IF_IMAGE, IF_MOVIE, ) and self.get_file_format() in (FF_TIFF, FF_H5): return self.bit_depth.value else: return BIT_DEPTH_8 def upgrade_settings(self, setting_values, variable_revision_number, module_name): if variable_revision_number == 11: if setting_values[0] == "Objects": raise NotImplementedError( "Unsupported image type: Objects. Use <i>ConvertObjectsToImage</i> to create an image." ) if setting_values[10] in ("bmp", "mat"): raise NotImplementedError( "Unsupported file format: {}".format(setting_values[10]) ) elif setting_values[10] == "tif": setting_values[10] = FF_TIFF elif setting_values[10] == "jpg": setting_values[10] = FF_JPEG new_setting_values = setting_values[:2] new_setting_values += setting_values[4:15] new_setting_values += setting_values[18:-1] setting_values = new_setting_values if setting_values[10] == "8": setting_values[10] = BIT_DEPTH_8 elif setting_values[10] == "16": setting_values[10] = BIT_DEPTH_16 variable_revision_number = 12 if variable_revision_number == 12: if setting_values[10] == "64-bit floating point": setting_values[10] = BIT_DEPTH_FLOAT variable_revision_number = 13 if variable_revision_number == 13: variable_revision_number = 14 if variable_revision_number == 14: # Renamed "Movie" to "Movie/Stack" if setting_values[0] == "Movie": setting_values[0] = IF_MOVIE # Added movie save axis setting_values.append(AXIS_T) variable_revision_number = 15 if variable_revision_number == 15: setting_values.append(False) variable_revision_number == 16 return setting_values, variable_revision_number def validate_module(self, pipeline): if self.save_image_or_figure in ( IF_IMAGE, IF_MASK, IF_CROPPING, ) and self.when_to_save in (WS_FIRST_CYCLE, WS_EVERY_CYCLE): # # Make sure that the image name is available on every cycle # for setting in get_name_providers(pipeline, self.image_name): if setting.provided_attributes.get("available_on_last"): # # If we fell through, then you can only save on the last cycle # raise ValidationError( "%s is only available after processing all images in an image group" % self.image_name.value, self.when_to_save, ) # XXX - should check that if file_name_method is # FN_FROM_IMAGE, that the named image actually has the # required path measurement # Make sure metadata tags exist if self.file_name_method == FN_SINGLE_NAME or ( self.file_name_method == FN_FROM_IMAGE and self.wants_file_name_suffix.value ): text_str = ( self.single_file_name.value if self.file_name_method == FN_SINGLE_NAME else self.file_name_suffix.value ) undefined_tags = pipeline.get_undefined_metadata_tags(text_str) if len(undefined_tags) > 0: raise ValidationError( "%s is not a defined metadata tag. Check the metadata specifications in your load modules" % undefined_tags[0], self.single_file_name if self.file_name_method == FN_SINGLE_NAME else self.file_name_suffix, ) def volumetric(self): return True
class RunStarDist(ImageSegmentation): category = "Object Processing" module_name = "RunStarDist" variable_revision_number = 1 def create_settings(self): super(RunStarDist, self).create_settings() self.model = Choice( text="Model", choices=MODEL_OPTIONS, value=GREY_1, doc="""\ StarDist comes with models for detecting nuclei. Alternatively, you can supply a custom-trained model generated outside of CellProfiler within Python. Custom models can be useful if working with unusual cell types. The inbuilt fluorescent and DSB models expect greyscale images. The H&E model expects a color image as input (from brightfield). Custom models will require images of the type they were trained with. It should be noted that the models supplied with StarDist do not support 3D images, but it's possible to train and use your own. """, ) self.tile_image = Binary( text="Tile input image?", value=False, doc="""\ If enabled, the input image will be broken down into overlapping tiles. This can help to conserve memory when working with large images. The image is split into a set number of vertical and horizontal tiles. The total number of tiles will be the result of multiplying the horizontal and vertical tile number.""", ) self.n_tiles_x = Integer(text="Horizontal tiles", value=1, minval=1, doc="""\ Specify the number of tiles to break the image down into along the x-axis (horizontal).""" ) self.n_tiles_y = Integer(text="Vertical tiles", value=1, minval=1, doc="""\ Specify the number of tiles to break the image down into along the y-axis (vertical).""" ) 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 scales from 0-1, with 1 representing absolute certainty of a pixel being in a cell. You may want to use a custom 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("Model folder", doc=f"""\ *(Used only when using a custom pre-trained model)* Select the folder containing your StarDist model. This should have the config, threshold and weights files exported after training.""") 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. Make sure you followed the setup instructions here: https://www.tensorflow.org/install/gpu If you don't have a GPU or it's not configured, StarDist will instead run on the CPU. This will be slower but should work on any system. """, ) def settings(self): return [ self.x_name, self.model, self.y_name, self.tile_image, self.n_tiles_x, self.n_tiles_y, self.save_probabilities, self.probabilities_name, self.model_directory, ] def visible_settings(self): vis_settings = [self.x_name, self.model] if self.model.value == MODEL_CUSTOM: vis_settings += [self.model_directory] vis_settings += [self.y_name, self.save_probabilities] if self.save_probabilities.value: vis_settings += [self.probabilities_name] vis_settings += [self.tile_image] if self.tile_image.value: vis_settings += [self.n_tiles_x, self.n_tiles_y] vis_settings += [self.gpu_test] return vis_settings def run(self, workspace): images = workspace.image_set x = images.get_image(self.x_name.value) dimensions = x.dimensions x_data = x.pixel_data # Validate some settings if self.model.value in (GREY_1, GREY_2) and x.multichannel: raise ValueError( "Color images are not supported by this model. Please provide greyscale images." ) elif self.model.value == COLOR_1 and not x.multichannel: raise ValueError( "Greyscale images are not supported by this model. Please provide a color overlay." ) if self.model.value != MODEL_CUSTOM: if x.volumetric: raise NotImplementedError( "StarDist's inbuilt models do not currently support 3D images" ) model = StarDist2D.from_pretrained(self.model.value) else: model_directory, model_name = os.path.split( self.model_directory.get_absolute_path()) if x.volumetric: from stardist.models import StarDist3D model = StarDist3D(config=None, basedir=model_directory, name=model_name) else: model = StarDist2D(config=None, basedir=model_directory, name=model_name) tiles = None if self.tile_image.value: tiles = [] if x.volumetric: tiles += [1] tiles += [self.n_tiles_x.value, self.n_tiles_y.value] # Handle colour channels tiles += [1] * max(0, x.pixel_data.ndim - len(tiles)) print(x.pixel_data.shape, x.pixel_data.ndim, tiles) if not self.save_probabilities.value: # Probabilities aren't wanted, things are simple data = model.predict_instances(normalize(x.pixel_data), return_predict=False, n_tiles=tiles) y_data = data[0] else: data, probs = model.predict_instances(normalize(x.pixel_data), return_predict=True, sparse=False, n_tiles=tiles) y_data = data[0] # Scores aren't at the same resolution as the input image. # We need to slightly resize to match the original image. size_corrected = resize(probs[0], 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 y = Objects() y.segmented = y_data y.parent_image = x.parent_image objects = workspace.object_set objects.add_objects(y, self.y_name.value) self.add_measurements(workspace) if self.show_window: 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 tensorflow if len(tensorflow.config.list_physical_devices('GPU')) > 0: message = "GPU appears to be working correctly!" print("GPUs:", tensorflow.config.list_physical_devices('GPU')) else: message = "GPU test failed. There may be something wrong with your configuration." import wx wx.MessageBox(message, caption="GPU Test")
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
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 create_settings(self): self.directory = Directory("Output file location", dir_choices=[ ABSOLUTE_FOLDER_NAME, DEFAULT_OUTPUT_FOLDER_NAME, DEFAULT_OUTPUT_SUBFOLDER_NAME, DEFAULT_INPUT_FOLDER_NAME, DEFAULT_INPUT_SUBFOLDER_NAME ], doc=""" This setting lets you choose the folder for the output files. %(IO_FOLDER_CHOICE_HELP_TEXT)s %(IO_WITH_METADATA_HELP_TEXT)s """ % globals()) self.directory.dir_choice = DEFAULT_OUTPUT_FOLDER_NAME self.wants_file_name_suffix = Binary( "Append a suffix to the file name?", False, doc=""" Select *"YES"* to add a suffix to the file name. Select *"NO"* to use the file name as-is. """ % globals()) self.file_name_suffix = Text("Text to append to the file name", "", metadata=True, doc=""" "*(Used only when constructing the filename from the image filename)*" Enter the text that should be appended to the filename specified above. """) self.wants_overwrite_without_warning = Binary( "Overwrite without warning?", False, doc="""This setting either prevents or allows overwriting of old .txt files by **ExportToACC** without confirmation. Select "*YES*" to overwrite without warning any .txt file that already exists. Select "*NO*" to prompt before overwriting when running CellProfiler in the GUI and to fail when running headless. """ % globals()) self.nan_representation = Choice("Representation of Nan/Inf", [NANS_AS_NANS, NANS_AS_NULLS], doc=""" This setting controls the output for numeric fields if the calculated value is infinite (*"Inf"*) or undefined (*"NaN*"). CellProfiler will produce Inf or NaN values under certain rare circumstances, for instance when calculating the mean intensity of an object within a masked region of an image. - "*%(NANS_AS_NULLS)s:*" Output these values as empty fields. - "*%(NANS_AS_NANS)s:*" Output them as the strings "NaN", "Inf" or "-Inf". """ % globals()) self.pick_columns = Binary("Select the measurements to export", False, doc=""" Select *"YES"* to provide a button that allows you to select which measurements you want to export. This is useful if you know exactly what measurements you want included in the final spreadheet(s). """ % globals()) self.columns = MeasurementMultiChoice( "Press button to select measurements to export", doc=""" "*(Used only when selecting the columns of measurements to export)*" This setting controls the columns to be exported. Press the button and check the measurements or categories to export.""") self.file_image_name = FileImageSubscriber( "Select image name for file prefix", "None", doc=""" Select an image loaded using **NamesAndTypes**. The original filename will be used as the prefix for the output filename.""" % globals())
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
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 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 create_settings(self): super(RunStarDist, self).create_settings() self.model = Choice( text="Model", choices=MODEL_OPTIONS, value=GREY_1, doc="""\ StarDist comes with models for detecting nuclei. Alternatively, you can supply a custom-trained model generated outside of CellProfiler within Python. Custom models can be useful if working with unusual cell types. The inbuilt fluorescent and DSB models expect greyscale images. The H&E model expects a color image as input (from brightfield). Custom models will require images of the type they were trained with. It should be noted that the models supplied with StarDist do not support 3D images, but it's possible to train and use your own. """, ) self.tile_image = Binary( text="Tile input image?", value=False, doc="""\ If enabled, the input image will be broken down into overlapping tiles. This can help to conserve memory when working with large images. The image is split into a set number of vertical and horizontal tiles. The total number of tiles will be the result of multiplying the horizontal and vertical tile number.""", ) self.n_tiles_x = Integer(text="Horizontal tiles", value=1, minval=1, doc="""\ Specify the number of tiles to break the image down into along the x-axis (horizontal).""" ) self.n_tiles_y = Integer(text="Vertical tiles", value=1, minval=1, doc="""\ Specify the number of tiles to break the image down into along the y-axis (vertical).""" ) 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 scales from 0-1, with 1 representing absolute certainty of a pixel being in a cell. You may want to use a custom 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("Model folder", doc=f"""\ *(Used only when using a custom pre-trained model)* Select the folder containing your StarDist model. This should have the config, threshold and weights files exported after training.""") 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. Make sure you followed the setup instructions here: https://www.tensorflow.org/install/gpu If you don't have a GPU or it's not configured, StarDist will instead run on the CPU. This will be slower but should work on any system. """, )
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<Plate>". 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()