#### to quantify F2H czi images ### automatically identifies nucleus and array and outputs the area, mean and median intensity of each feature identified to a csv file ### the ROIs for each respective image are saved to the output directory to allow confirmation of accurate feature calling #@ File[] (label="Select the input directories", style="directories") inputDir #ImageJ stuff from ij import IJ, ImagePlus, Prefs, WindowManager from ij.process import ImageStatistics as IS from ij.io import FileSaver from ij.plugin import ImageCalculator, filter from ij.plugin.frame import RoiManager from ij.measure import ResultsTable from ij.gui import Overlay, Roi, ShapeRoi, GenericDialog # czi and image import stuff from loci.plugins import BF from loci.plugins.in import ImporterOptions from loci.formats.in import ZeissCZIReader, DynamicMetadataOptions #python stuff (regular expressions etc.) import re import collections #file management from java.io import File from java.lang import System from java.text import SimpleDateFormat #GUI stuff from java.awt import GridBagLayout, GridBagConstraints from javax.swing import JDialog, JFrame, JPanel, JLabel, JTextField, BorderFactory, JButton class frameMaker(): def __init__(self): self.Imageinfo = {} self.dialog = JDialog() self.panel = JPanel(border = BorderFactory.createEmptyBorder(10,10,10,10)) self.out = {} def okayPressed(self, event): length = len(self.panel.getComponents()) upperFinder = re.compile("^upper.*") for i in range(1, length): item = self.panel.getComponents()[i] if not isinstance(item, JTextField): continue label = self.panel.getComponents()[i-1].getText() #labels are located one index behind their respective textboxes in the panel if label in ["nuclear", "prey", "bait"]: bounds = [1, self.Imageinfo['SizeC']] elif label in ["lower nuclear area", "upper nuclear area"]: bounds = [1, self.Imageinfo['SizeX']*self.Imageinfo['SizeY']] elif label in ["lower array area", "upper array area"]: bounds = [1, self.out['lower nuclear area']] elif label in ["lower thresh", "upper thresh"]: bounds = [0, 255] try: val = int(item.getText()) except: IJ.log("Non-numeric input for {0}. Please input an integer between {1} and {2}.".format(label, bounds[0], bounds[1])) IJ.error("Non-numeric input for {0}. Please input an integer between {1} and {2}.".format(label, bounds[0], bounds[1])) return if upperFinder.match(label): #check if label begins with upper lowerVal = int(self.panel.getComponents()[i-2].getText()) if val <= lowerVal:#the upper value of any pair of upper and lower values cannot be less than or equal to the lower value sectionLabel = label.split("upper ")[1] IJ.log("Input value outside permitted range. Upper {0} must be greater than lower {0}.".format(sectionLabel)) IJ.error("Input value outside permitted range. Upper {0} must be greater than lower {0}.".format(sectionLabel)) return if bounds[0] <= val <= bounds[1]: self.out[label] = val else: IJ.log("Input value {0} outside of range. Please input an integer between {1} and {2} in {3}".format(val, bounds[0], bounds[1], label)) IJ.error("Input value {0} outside of range. Please input an integer between {1} and {2} in {3}".format(val,bounds[0], bounds[1], label)) return print self.out, "okaypressed" self.dialog.dispose() def cancelPressed(self, event): IJ.error("Parameter selection cancelled. Exiting.") self.dialog.dispose() def buttonMaker(self, gb): gbc = GridBagConstraints() okayButton = JButton("okay", actionPerformed = self.okayPressed) gbc.gridx = 0 gbc.anchor = GridBagConstraints.WEST gb.setConstraints(okayButton, gbc) self.panel.add(okayButton) cancelButton = JButton("cancel", actionPerformed = self.cancelPressed) gbc.gridx = 1 gb.setConstraints(cancelButton, gbc) self.panel.add(cancelButton) def inputMaker(self, gb, vals): """add the labels and text input fields for the respective sections""" gbc = GridBagConstraints() for val in vals: gbc.anchor = GridBagConstraints.WEST gbc.gridx = 0 label = JLabel(val[0]) gb.setConstraints(label, gbc) self.panel.add(label) gbc.gridx = 1 gb.setConstraints(val[1], gbc) self.panel.add(val[1]) def sectionLabel(self, gb, text): """create labels 2 columns wide to separate types of input""" gbc = GridBagConstraints() gbc.gridx = 0 gbc.anchor = GridBagConstraints.WEST gbc.gridwidth = 2 sectionLabel = JLabel(text) gb.setConstraints(sectionLabel, gbc) self.panel.add(sectionLabel) def idBuilder(self, IDs): IDs['channel'] = [("nuclear", JTextField("3", 5)), ("prey", JTextField("2", 5)), ("bait", JTextField("1", 5)), "Input the indices of the indicated channels."] IDs['nuc'] = [("lower nuclear area", JTextField("4000", 5)), ("upper nuclear area", JTextField("20000", 5)), "
Input the minimum and maximum nuclear area
(in pixels) for nucleus calling."] IDs['array'] = [("lower array area", JTextField("5", 5)), ("upper array area", JTextField("200", 5)), "
Input the minimum and maximum array area
(in pixels) for array calling."] IDs['thresh'] = [("lower threshold", JTextField("97", 5)), ("upper threshold", JTextField("195", 5)), "
Input the values (between 0 and 255) to threshold
the bait images for array calling."] return IDs def dialogBuilder(self, ImageInfo): self.Imageinfo = ImageInfo """set up basic panel with slight border and establish the gb layout""" gb = GridBagLayout() self.panel.setLayout(gb) """fill the self.IDs hash table of labels and input boxes""" IDs = self.idBuilder({}) """as dictionaries are not ordered in python 2, create separate list of keys to allow looping through self.IDs in the desired order""" idList = ['channel', 'nuc', 'array', 'thresh'] """use idList to loop through self.IDs and add an instruction label and labelled text boxes for each section""" for item in idList: temp = IDs[item] instructions = temp.pop() self.sectionLabel(gb, instructions) self.inputMaker(gb, temp) """add the okay and cancel buttons for the dialog box""" self.buttonMaker(gb) """setup the frame""" self.dialog.add(self.panel) self.dialog.pack() self.dialog.setLocationRelativeTo(None) self.dialog.setModal(True) self.dialog.setVisible(True) return self.out def CZIopener(imagefile): """import czi info incl. image dimensions and series length""" options = ImporterOptions() options.setAutoscale(True) options.setColorMode(ImporterOptions.COLOR_MODE_GRAYSCALE) options.setId(imagefile) imp = BF.openImagePlus(options)[0] return imp def findnucleus(imp, parameters): """call nuclei""" IJ.run(imp, "Gaussian Blur...", "sigma=3") IJ.run(imp, "Enhance Contrast", "saturated=0.35") IJ.run(imp, "Apply LUT", "") IJ.run(imp, "Auto Threshold", "method=Default white") IJ.run(imp, "Make Binary", "BlackBackground") IJ.run(imp, "Analyze Particles...", "size=" + str(parameters['lower nuclear area']) + "-" + str(parameters['upper nuclear area']) + " circularity=0.50-1.00 show=Overlay include") DAPIoverlay = imp.getOverlay() return DAPIoverlay def findarray(images, DAPIoverlay, totalnuclei, parameters): """use the nuclear channel to make a mask of all non-nuclear areas in the image, then identify the arrays""" nucShape = ShapeRoi(DAPIoverlay.get(0)) i = 1 while i < totalnuclei: shape = ShapeRoi(DAPIoverlay.get(i)) nucShape = nucShape.or(shape) i += 1 bgShape = ShapeRoi(Roi(0, 0, images['bait'].getWidth(), images['bait'].getHeight())) bgShape = bgShape.not(nucShape) ip = images['bait'].getProcessor() ip.setValue(0) ip.setRoi(bgShape) ip.fill(ip.getMask()) IJ.run(images['bait'], "8-bit", "") IJ.setThreshold(images['bait'], parameters['lower threshold'], parameters['upper threshold'], "Black & White") IJ.run(images['bait'], "Analyze Particles...", "size=" + str(parameters['lower array area']) + "-" + str(parameters['upper array area']) + "pixel circularity=0.50-1.00 show=Overlay include") baitoverlay = images['bait'].getOverlay() return baitoverlay def getCZIinfo(imagefile): """import czi info incl. image dimensions and series length""" CZIinfo = {} options = DynamicMetadataOptions() options.setBoolean("zeissczi.autostitch", False) options.setBoolean("zeissczi.attachments", False) czireader = ZeissCZIReader() czireader.setFlattenedResolutions(False) czireader.setMetadataOptions(options) czireader.setId(imagefile) CZIinfo['seriesCount'] = czireader.getSeriesCount() CZIinfo['SizeC'] = czireader.getSizeC() CZIinfo['SizeX'] = czireader.getSizeX() CZIinfo['SizeY'] = czireader.getSizeY() czireader.close() return CZIinfo def imageProcessor(imagefile, parameters, outputDir): imp = CZIopener(imagefile) images, imageLabels = {}, {} imageLabels['snapName'] = imp.getTitle().split(".")[0] imageLabels['imagefile'] = imagefile imageLabels['date'] = SimpleDateFormat("yyyy/MM/dd").format(File(imagefile).lastModified()) IJ.log("processing {0}".format(imageLabels['snapName'])) images['nuclear'] = ImagePlus('nuclear', imp.getImageStack().getProcessor(parameters["nuclear"])).duplicate() images['bait'] = ImagePlus('bait', imp.getImageStack().getProcessor(parameters["bait"])).duplicate() """use the DAPI channel to call nuclei""" DAPIoverlay = findnucleus(images['nuclear'], parameters) if not DAPIoverlay: IJ.log("no nuclei called in {0}".format(imageLabels['snapName'])) return None totalnuclei = Overlay.size(DAPIoverlay) IJ.log("{0} nuclei found in {1}".format(totalnuclei, imageLabels['snapName'])) baitoverlay = findarray(images, DAPIoverlay, totalnuclei, parameters) if not baitoverlay: IJ.log("no arrays coincident with called nuclei in {0}".format(imageLabels['snapName'])) return None totalarray = Overlay.size(baitoverlay) IJ.log("{0} array(s) found in {1}".format(totalarray, imageLabels['snapName'])) DAPIoverlay, totalnuclei = nucFilter(DAPIoverlay, baitoverlay, totalarray, totalnuclei) finalOverlay = nucArraypairer(DAPIoverlay,baitoverlay, totalarray, totalnuclei) if Overlay.size(finalOverlay) == 0: IJ.log("no single coincident arrays and nuclei identified in snap {0}".format(imageLabels['snapName'])) return None table = measureImage(imp, finalOverlay, parameters, imageLabels) if len(table) > 0: """save rois to output directory so can check success of array/nucleus caller and see which specific arrays & nuclei were identified""" roiSaver(finalOverlay, outputDir, imageLabels['snapName']) return table else: IJ.log("table not generated for {0} even though transfected cells overlapped specific nuclei.".format(imageLabels['snapName'])) return None def measureImage(imp, overlay, parameters, imageLabels): overlaySize = Overlay.size(overlay) imp.setOverlay(overlay) table = [] channels = {"prey":parameters["prey"], "bait": parameters["bait"]} for label, channel in channels.items(): imp.setSlice(channel) temp = [] for i in range(overlaySize): roi = overlay.get(i) roi.setImage(imp) roi.setPosition(imp) Rname = Roi.getName(roi) roiStat = roi.getStatistics() temp.append(imageLabels['imagefile']) temp.append(imageLabels['date']) temp.append(imageLabels['snapName']) temp.append(label) temp.append(Rname) temp.append(roiStat.area) temp.append(roiStat.mean) temp.append(roiStat.median) table.append(temp) return table def nucArraypairer(DAPIoverlay, baitoverlay, totalarray, totalnuclei): finalOverlay = Overlay() j = list(range(totalarray)) for i in range(totalnuclei): tempNo = str(i + 1) roi = DAPIoverlay.get(i) nucPoints = roi.getContainedPoints() breakVal = 0 for k, element in enumerate(j): roi2 = baitoverlay.get(element) arrayPoints = roi2.getContainedPoints() overlapTest = bool(set(arrayPoints) & set(nucPoints)) if overlapTest == False: continue nucleoplasm = ShapeRoi(roi).not(ShapeRoi(roi2)) Roi.setName(roi, "nucleus_" + tempNo) Roi.setName(roi2, "array_" + tempNo) Roi.setName(nucleoplasm, "nucleoplasm_" + tempNo) finalOverlay.add(roi) finalOverlay.add(roi2) finalOverlay.add(nucleoplasm) j.remove(element) break return finalOverlay def nucFilter(DAPIoverlay, baitoverlay, totalarray, totalnuclei): """remove all nuclei that do not contain precisely one array from DAPIoverlay""" completed = [] j = list(range(totalarray)) i = 0 while i < totalnuclei: roi = DAPIoverlay.get(i) nucPoints = roi.getContainedPoints() overlapCounter = 0 for k, element in enumerate(j): roi2 = baitoverlay.get(element) arrayPoints = roi2.getContainedPoints() overlapTest = bool(set(arrayPoints) & set(nucPoints)) if overlapTest == True: j.remove(element) overlapCounter += 1 if overlapCounter != 1: DAPIoverlay.remove(roi) totalnuclei -= 1 else: i += 1 return DAPIoverlay, totalnuclei def processDirectory(inputDir): sep = System.getProperty("file.separator") outputDir = inputDir[0].getParent() + sep + "output" + sep cziFinder = re.compile(".*czi$") pathList = [image.getAbsolutePath() for directory in inputDir for image in directory.listFiles() if cziFinder.match(image.getAbsolutePath())] if pathList == []: IJ.log("No .czi files found in input directories. Exiting") IJ.error("No .czi files found in input directories. Exiting") return pathLen = len(pathList) CZIinfo = getCZIinfo(pathList[0]) print CZIinfo if CZIinfo['SizeC'] < 2: IJ.log("F2H processing requires a minimum of 2 channels. Exiting") IJ.error("F2H processing requires a minimum of 2 channels. Exiting") return else: parameters = frameMaker().dialogBuilder(CZIinfo) if parameters == {}: return for key, value in parameters.items(): print "{0}: {1}".format(key, value) if File(outputDir).exists() == False: File(outputDir).mkdir() IJ.log("Output directory created at {0}".format(outputDir)) else: IJ.log("Output directory exists at {0}".format(outputDir)) outputArray = [] for i, image in enumerate(pathList): IJ.log("processing {0}. Image {1} out of {2}".format(image, i + 1, pathLen)) outputArray.append(imageProcessor(image, parameters, outputDir)) IJ.log("finished processing {0}. Image {1} out of {2}".format(image, i + 1, pathLen)) IJ.log("image processing finished") log = IJ.getLog() resultsSaver(log, outputDir, "F2H_log", ".txt") table = resultsTablemaker(outputArray) if ResultsTable.size(table) == 0: IJ.log("No transfected cells found. Bye.") return resultsSaver(table, outputDir, "F2H_results", ".csv") WindowManager.closeAllWindows() def resultsSaver(item, output, name, extension): """save results table and log files""" filepath = output + name + extension level = 0 while File(filepath).exists(): level += 1 filepath = output + name + str(level) + extension if extension == ".csv": item.save(filepath) elif extension == ".txt": with open(filepath, 'w') as logSaver: logSaver.write(item) elif extension == ".zip": rm = RoiManager.getInstance() rm.runCommand("save selected", filepath) IJ.log("Output saved to {0}".format(filepath)) def resultsTablemaker(outputArray): """fill results table""" IJ.log("filling results table") table = ResultsTable() colnames = collections.deque(["Path", "Date", "Name", "Channel", "ROI", "Area", "Mean", "Median"]) colLen = len(colnames) for image in outputArray: if image is None: continue for cell in image: for i, value in enumerate(cell): if i%colLen == 0: table.incrementCounter() col = colnames.popleft() table.addValue(col, value) colnames.append(col) return table def roiSaver(overlay, output, name): """move overlay to roimanager and save""" rm = RoiManager.getInstance() if not rm: rm = RoiManager() rm.reset() for roi in overlay: rm.addRoi(roi) rm.deselect() resultsSaver("roi", output, name + "_rois", ".zip") #rm.runCommand("save selected", outputDir + name + "_rois.zip") if __name__ in ["__builtin__", "__main__"]: processDirectory(inputDir)