diff --git a/2d_spots_in_fibers.py b/2d_spots_in_fibers.py new file mode 100644 index 0000000000000000000000000000000000000000..4e0a81636e3d9dec4e242499d9a2948488d06244 --- /dev/null +++ b/2d_spots_in_fibers.py @@ -0,0 +1,479 @@ +#@ OpService ops +#@ CommandService command +#@ RoiManager rm +#@ File (label="select image", description="select your input image", style=file) path_to_image +#@ Integer (label="select image file series", description="leave 1 if not needed", value=1, min=1, max=20, stepSize=1, persist=false, style=slider) series_number +#@ File (label="Fiber Segmentation", description="select the Fiber Segmentation RoiSet .zip file", style=file) fiber_segmentation_roiset +#@ Integer (label="DAPI channel", description="select the DAPI channel", min=1, max=20, stepSize=1, persist=true, style=slider) dapi_channel_number +#@ Integer (label="DAPI threshold", description="0 = Auto", min=0) threshold +#@ String (label="processing channels", description="comma separated list of channels, e.g. 2,3,6,7", value="1,2,3,4") processing_channels_string +#@ String (label="Spot detection quality threshold", description="comma separated list of values, one per channel", value="80,80,80,80" ) quality_thresholds_string +#@ Double (label="Minimum spot diameter", description="smaller spots will be discarded", value=0.3, min=1, max=10, stepSize=0.1) spot_diameter + +# trackmate imports +from fiji.plugin.trackmate import Settings +from fiji.plugin.trackmate import TrackMate +from fiji.plugin.trackmate.detection import DogDetectorFactory +from fiji.plugin.trackmate.action import LabelImgExporter +# ij imports +from ij import IJ +from ij.plugin import ImageCalculator +from ij.measure import ResultsTable +from ij.measure import Measurements as M +from ij.plugin.filter import Analyzer +# Bio-Formats imports +from loci.plugins import BF +from loci.common import Region +from loci.plugins.in import ImporterOptions +from loci.formats import ImageReader, MetadataTools, Memoizer +# MorpholibJ imports +from inra.ijpb.binary import BinaryImages +# BIOP imports +from ch.epfl.biop.ij2command import Labels2Rois +# python imports +import os + + +def get_parentdir_filename_ext_from_path(path): + """ returns parent directory, filename, and file extension from a given path + + Parameters + ---------- + path : String + the full path to a file + + Returns + ------- + String + the parent directory, the filename without extension and the extension split after the dot + """ + parent_dir = os.path.dirname(path) + base = os.path.basename(path) + (filename, ext) = os.path.splitext(base) + + return (parent_dir, filename, ext) + + +def write_bf_memoryfile(path_to_file): + """Write a BF memo-file so subsequent access to the same file is faster. + + The Bio-Formats memo-file is written next to the image file (i.e. in the + same folder as the given file). + + Parameters + ---------- + string + The full path to the image file. + """ + reader = Memoizer(ImageReader()) + reader.setId(path_to_file) + reader.close() + + +def get_ome_metadata(path_to_image, series_number): + """Get some metadata fields using BF from an image on disk from a given series. + Gets number of channels, the image width, image height and bit depth. + + Parameters + ---------- + path_to_image : str + full path to the image + series_number : int + the Bio-Formats series number + + Returns + ------- + int + n_channels : the number of channels + int + image_width : the image width in px + int + image_height : the image height in px + int + bit_depth : the image bit depth + """ + series_number -= 1 # python starts counting from 0 + reader = ImageReader() + ome_meta = MetadataTools.createOMEXMLMetadata() + reader.setMetadataStore(ome_meta) + reader.setId(path_to_image) + reader.setSeries(series_number) + n_channels = reader.getSizeC() + image_width = reader.getSizeX() + image_height = reader.getSizeY() + bit_depth = reader.getBitsPerPixel() + + reader.close() + + return n_channels, image_width, image_height, bit_depth + + +def BFopen_image(path_to_image, channel_number, series_number, region=None, z_slice_number=None): + """Use Bio-Formats to open a given image or subset. + + Parameters + ---------- + path_to_image : str + full path to the image file on disk + channel_number : int + the number of the channel to open + series_number : int + the Bio-Formats series number to open + region : list, optional + Bio-Formats crop region, by default None. format = [start_x, start_y, width in px, height in px] + z_slice_number : int, optional + the number of the z-slice to open, by default None + + Returns + ------- + ImagePlus + the requested image + """ + series_number -= 1 # python starts counting from 0 + channel_number -= 1 # python starts counting from 0 + options = ImporterOptions() + options.setId(path_to_image) + options.setColorMode(ImporterOptions.COLOR_MODE_GRAYSCALE) + options.setSplitChannels(False) + options.setSplitFocalPlanes(False) + options.setSplitTimepoints(False) + options.setSeriesOn(series_number, True) + + if region is not None: + options.setCrop(True) + options.setCropRegion(series_number, Region(region[0], region[1], region[2], region[3])) # x,y,w,h + + options.setSpecifyRanges(True) + options.setCBegin(series_number, channel_number) + options.setCEnd(series_number, channel_number) + options.setCStep(series_number, 1) + + if z_slice_number is not None: + z_slice_number -= 1 # python starts counting from 0 + options.setZBegin(series_number, z_slice_number) + options.setZEnd(series_number, z_slice_number) + options.setZStep(series_number, 1) + + imps = BF.openImagePlus(options) + + return imps[0] + + +def run_trackmate_dog_spot_detector(imp, spot_diameter, quality_threshold): + """Run TrackMates DoG detector with a given quality threshold + on a target image + + Parameters + ---------- + imp : ImagePlus + the input image + quality_threshold : float or double + the spot detection quality threshold + + Returns + ------- + ImagePlus + a label image of the detected spots. + """ + + cal = imp.getCalibration() + settings = Settings(imp) + + settings.detectorFactory = DogDetectorFactory() + settings.detectorSettings['DO_SUBPIXEL_LOCALIZATION'] = True + settings.detectorSettings['RADIUS'] = spot_diameter / 2 # type = double + settings.detectorSettings['TARGET_CHANNEL'] = 0 + settings.detectorSettings['THRESHOLD'] = quality_threshold # type = double + settings.detectorSettings['DO_MEDIAN_FILTERING'] = False + + trackmate = TrackMate(settings) + trackmate.computeSpotFeatures(False) + trackmate.computeTrackFeatures(False) + + if not trackmate.checkInput(): + print( str( trackmate.getErrorMessage() ) ) + + if not trackmate.process(): + print( str( trackmate.getErrorMessage() ) ) + + exportSpotsAsDots = True + exportTracksOnly = False + labelIdPainting = LabelImgExporter.LabelIdPainting.LABEL_IS_INDEX_MOVIE_UNIQUE + label_imp = LabelImgExporter.createLabelImagePlus(trackmate, exportSpotsAsDots, exportTracksOnly, labelIdPainting) + label_imp.setCalibration(cal) + imp.close() + + return label_imp + + +def get_threshold_from_method(imp, method): + """returns the threshold value of chosen IJ AutoThreshold method from a stack (hyperstack?) + + Parameters + ---------- + imp : ImagePlus (maybe also accepts ij2 dataset, img?) + the image from which to get the threshold value + method : string + the AutoThreshold method to use, e.g. "otsu", "yen", ... + + Returns + ------- + int + """ + histogram = ops.run("image.histogram", imp) + threshold_value = ops.run("threshold.%s" % method, histogram) + threshold_value = int(round(threshold_value.get())) + + return threshold_value + + +def convert_to_binary(imp, threshold=1, scale_binary=True): + """Convert grayscale image to a mask / binary image + + Parameters + ---------- + imp : ImagePlus + the input image + threshold : int, optional + the threshold above which pixels are considered foreground, by default 1 + scale_binary : bool, optional + scale the mask image from 0/255 to 0/1, by default True + + Returns + ------- + ImagePlus + the mask / binary image + """ + mask_imp = imp.duplicate() + bitdepth = mask_imp.getBitDepth() + if bitdepth > 16.0: + IJ.run("Conversions...", " ") + IJ.run(mask_imp, "16-bit", "") + IJ.run("Conversions...", "scale") + + IJ.setRawThreshold(mask_imp, threshold, (2**bitdepth)-1, None) + IJ.run(mask_imp, "Convert to Mask", "") + if scale_binary == True: + IJ.run(mask_imp, "Divide...", "value=255") + IJ.run(mask_imp, "Enhance Contrast", "saturated=0.35") + mask_imp.changes = False + + return mask_imp + + +def save_image_as_IJtif(imp, filename, suffix, target): + """Save the image as ImageJ-Tiff + + Parameters + ---------- + imp : ImagePlus + the input Image to be saved + filename : str + the image filename without extension + suffix : int, float or str + a suffix that will be added to the filename as a string + target : str + the target directory in with to save the image + """ + + savename = filename + "_" + str(suffix) + ".tif" + savepath = os.path.join(target, savename) + IJ.log("now saving: " + str(savepath)) + IJ.saveAs(imp, "Tiff", savepath) + + +def convert_labelimage_to_binary(label_imp, scale_binary=True): + """Convert a label image to a mask / binary image + + Parameters + ---------- + label_imp : ImagePlus + the input label image + scale_binary : bool, optional + scale the mask image from 0/255 to 0/1, by default True + + Returns + ------- + ImagePlus + the mask / binary image + """ + binary_imp = BinaryImages.binarize(label_imp) + if scale_binary == True: + IJ.run(binary_imp, "Divide...", "value=255") + + return binary_imp + + +def measure_intensity_sum(imp, rm): + """ + Measure theraw integrated intensity for all rois in target imp. + + Parameters + ---------- + imp : ImagePlus + The image from which the intensity will be measured. + rm : RoiManager + The ROI Manager containing the regions of interest to be analyzed. + + Returns + ------- + label_id : list of str + A list of labels corresponding to each ROI. + sum_intensity : list of int + A list of summed integrated intensities for each ROI. + + Notes + ----- + The results are stored in a `ij.ResultsTable`, + from which the raw integrated density values are extracted. + + Example + ------- + >>> labels, intensities = measure_intensity_sum(imp, roi_manager) + """ + + rt_ = ResultsTable() + options = M.INTEGRATED_DENSITY + an = Analyzer (imp, options, rt_) + an.setPrecision(0) + + label_id = [] + sum_intensity = [] + + for index, roi in enumerate(rm.getRoisAsArray()): + imp.setRoi(roi) + an.measure() + label_id.append(roi.getName()) + sum_intensity.append(int(rt_.getColumn("RawIntDen")[index])) + + return label_id, sum_intensity + + +def add_results_to_resultstable(results_table, column, values): + """Add values to the ResultsTable starting from row 0 of a given column. + + Parameters + ---------- + results_table : ij.measure.ResultsTable + a reference of the IJ-ResultsTable + column : string + the column in which to add the values + values : list(int, double or float) + array with values to be added + """ + for index, value in enumerate(values): + results_table.setValue(column, index, value) + + +def save_results_table(results_table, filename, suffix, target): + """Save an IJ-ResultsTable as txt to a given location + + Parameters + ---------- + results_table : IJ-ResultsTable + an IJ-ResultsTable + filename : str + filename to include in the tables name + suffix : str, int or float + a suffix to be included in the tables name + target : str + the target directory in which to save the table + """ + + savename = filename + "_" + str(suffix) + ".txt" + savepath = os.path.join(target, savename) + results_table.save(savepath) + + +def save_labelimage_as_ijroiset(label_imp, rm, filename, suffix, target): + """Save all labels i a label Images as IJ-ROIset to target location. + Only 2D, requires PTBIOP update site. + + Parameters + ---------- + label_imp : ImagePlus + the input label image + filename : str + the filename to include in the ROIset name + suffix : str, int or float + a suffix to be included in the ROIset name + target : str + the target directory in which to save the table + """ + command.run( Labels2Rois , False , 'imp' , label_imp , 'rm', rm).get() + savename = filename + "_" + str(suffix) + ".zip" + savepath = os.path.join(target, savename) + rm.runCommand("Save", savepath) + rm.reset() + + +def load_rois_from_zip(path, rm): + """Load ROIs from the given zip file and add them to the RoiManager. + + Parameters + ---------- + path : string + Path to the ROI zip file. + rm : RoiManager + The ROI Manager. + """ + rm.runCommand("Open", path) + + +def close_images(list_of_imps): + """Close given ImagePlus images + + Parameters + ---------- + list_of_imps : list of ImagePlus + list of ImagePlus images to be closed + """ + for imp in list_of_imps: + imp.changes = False + imp.close() + + +path_to_image = str(path_to_image).replace("\\","/") +fiber_segmentation_roiset = str(fiber_segmentation_roiset).replace("\\","/") +parent_dir, filename, ext = get_parentdir_filename_ext_from_path(path_to_image) +write_bf_memoryfile(path_to_image) +n_channels, image_width, image_height, bit_depth = get_ome_metadata(path_to_image, series_number) +processing_channels_string = processing_channels_string.replace(" ", "") +processing_channels = processing_channels_string.split(",") +quality_thresholds_string = quality_thresholds_string.replace(" ", "") +quality_thresholds = quality_thresholds_string.split(",") + +# threshold DAPI channel and convert to binary +dapi_channel = BFopen_image(path_to_image, dapi_channel_number, series_number) +if threshold <= 0: + threshold = get_threshold_from_method(dapi_channel, "otsu") +dapi_binary = convert_to_binary(dapi_channel, threshold) + +# detect spots and count them per fiber +results_table = ResultsTable() +for index, channel in enumerate(processing_channels): + channel = int(channel) + quality_threshold = float(quality_thresholds[index]) + spots_channel = BFopen_image(path_to_image, channel, series_number) + spots_label_imp = run_trackmate_dog_spot_detector(spots_channel, spot_diameter, quality_threshold) + save_labelimage_as_ijroiset(spots_label_imp, rm, filename, "spots_ch" + str(channel), parent_dir) + spots_binary_imp = convert_labelimage_to_binary(spots_label_imp) + dapi_positive_spots_binary = ImageCalculator.run(spots_binary_imp, dapi_binary, "Multiply create") + dapi_negative_spots_binary = ImageCalculator.run(spots_binary_imp, dapi_positive_spots_binary, "Subtract create") + + load_rois_from_zip(fiber_segmentation_roiset, rm) + fiber_label_IDs, n_spots_per_fiber = measure_intensity_sum(spots_binary_imp, rm) + _, n_dapi_positive_spots_per_fiber = measure_intensity_sum(dapi_positive_spots_binary, rm) + _, n_dapi_negative_spots_per_fiber = measure_intensity_sum(dapi_negative_spots_binary, rm) + + # TODO: the column fiber label ID needs to be added only one time. Maybe check before if column exists. + add_results_to_resultstable(results_table, "fiber label ID", fiber_label_IDs) + add_results_to_resultstable(results_table, "n spots channel " + str(channel), n_spots_per_fiber) + add_results_to_resultstable(results_table, "n spots dapi positive channel " + str(channel), n_dapi_positive_spots_per_fiber) + add_results_to_resultstable(results_table, "n spots dapi negative channel " + str(channel), n_dapi_negative_spots_per_fiber) + close_images([spots_channel, spots_label_imp, spots_binary_imp, spots_binary_imp, dapi_binary, dapi_positive_spots_binary, dapi_negative_spots_binary]) + +results_table.show("Spots") +save_results_table(results_table, filename, "Spots", parent_dir) +IJ.run("Collect Garbage", "") +IJ.log("DONE") \ No newline at end of file