package gdsc.foci; import gdsc.UsageTracker; /*----------------------------------------------------------------------------- * GDSC Plugins for ImageJ * * Copyright (C) 2011 Alex Herbert * Genome Damage and Stability Centre * University of Sussex, UK * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. *---------------------------------------------------------------------------*/ import gdsc.core.ij.Utils; import gdsc.core.match.Coordinate; import gdsc.core.match.MatchCalculator; import gdsc.core.match.MatchResult; import gdsc.core.match.PointPair; import gnu.trove.set.hash.TIntHashSet; import ij.IJ; import ij.ImagePlus; import ij.ImageStack; import ij.WindowManager; import ij.gui.GenericDialog; import ij.gui.Overlay; import ij.gui.PointRoi; import ij.io.OpenDialog; import ij.plugin.PlugIn; import ij.process.ImageProcessor; import ij.text.TextWindow; import java.awt.Color; import java.awt.Point; import java.awt.TextField; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedList; import java.util.List; /** * Compares the coordinates in two files and computes the match statistics. * <p> * Can read QuickPALM xls files; STORMJ xls files; and a generic CSV file. * <p> * The generic CSV file has records of the following:<br/> * ID,T,X,Y,Z,Value<br/> * Z and Value can be missing. The generic file can also be tab delimited. */ public class FileMatchCalculator implements PlugIn, MouseListener { public class IdTimeValuedPoint extends TimeValuedPoint { public int id; public IdTimeValuedPoint(int id, TimeValuedPoint p) { super(p.getX(), p.getY(), p.getZ(), p.time, p.value); this.id = id; } } private static String TITLE = "Match Calculator"; private static String title1 = ""; private static String title2 = ""; private static double dThreshold = 1; private static String mask = ""; private static double beta = 4; private static boolean showPairs = false; private static boolean savePairs = false; private static boolean savePairsSingleFile = false; private static String filename = ""; private static String filenameSingle = ""; private static String image1 = ""; private static String image2 = ""; private static boolean showComposite = false; private static boolean ignoreFrames = false; private static boolean useSlicePosition = false; private String myMask, myImage1, myImage2; private static boolean writeHeader = true; private static TextWindow resultsWindow = null; private static TextWindow pairsWindow = null; private TextField text1; private TextField text2; // flag indicating the pairs have values that should be output private boolean valued = false; /* * (non-Javadoc) * * @see ij.plugin.PlugIn#run(java.lang.String) */ public void run(String arg) { UsageTracker.recordPlugin(this.getClass(), arg); if (!showDialog()) { return; } TimeValuedPoint[] actualPoints = null; TimeValuedPoint[] predictedPoints = null; double d = 0; try { actualPoints = TimeValuePointManager.loadPoints(title1); predictedPoints = TimeValuePointManager.loadPoints(title2); } catch (IOException e) { IJ.error("Failed to load the points: " + e.getMessage()); return; } d = dThreshold; // Optionally filter points using a mask if (myMask != null) { ImagePlus maskImp = WindowManager.getImage(myMask); if (maskImp != null) { actualPoints = filter(actualPoints, maskImp.getProcessor()); predictedPoints = filter(predictedPoints, maskImp.getProcessor()); } } compareCoordinates(actualPoints, predictedPoints, d); } private TimeValuedPoint[] filter(TimeValuedPoint[] points, ImageProcessor processor) { int ok = 0; for (int i = 0; i < points.length; i++) { if (processor.get(points[i].getXint(), points[i].getYint()) > 0) { points[ok++] = points[i]; } } return Arrays.copyOf(points, ok); } /** * Adds an ROI point overlay to the image using the specified colour * * @param imp * @param list * @param color */ public static void addOverlay(ImagePlus imp, List<? extends Coordinate> list, Color color) { if (list.isEmpty()) return; Color strokeColor = color; Color fillColor = color; Overlay o = imp.getOverlay(); PointRoi roi = (PointRoi) PointManager.createROI(list); roi.setStrokeColor(strokeColor); roi.setFillColor(fillColor); roi.setShowLabels(false); if (o == null) { imp.setOverlay(roi, strokeColor, 2, fillColor); } else { o.add(roi); imp.setOverlay(o); } } private boolean showDialog() { ArrayList<String> newImageList = buildMaskList(); final boolean haveImages = !newImageList.isEmpty(); GenericDialog gd = new GenericDialog(TITLE); gd.addMessage( "Compare the points in two files\nand compute the match statistics\n(Double click input fields to use a file chooser)"); gd.addStringField("Input_1", title1, 30); gd.addStringField("Input_2", title2, 30); if (haveImages) { ArrayList<String> maskImageList = new ArrayList<String>(newImageList.size() + 1); maskImageList.add("[None]"); maskImageList.addAll(newImageList); gd.addChoice("mask", maskImageList.toArray(new String[0]), mask); } gd.addNumericField("Distance", dThreshold, 2); gd.addNumericField("Beta", beta, 2); gd.addCheckbox("Show_pairs", showPairs); gd.addCheckbox("Save_pairs", savePairs); gd.addMessage("Use this option to save the pairs to a single file,\nappending if it exists"); gd.addCheckbox("Save_pairs_single_file", savePairsSingleFile); if (!newImageList.isEmpty()) { gd.addCheckbox("Show_composite_image", showComposite); gd.addCheckbox("Ignore_file_frames", ignoreFrames); String[] items = newImageList.toArray(new String[newImageList.size()]); gd.addChoice("Image_1", items, image1); gd.addChoice("Image_2", items, image2); gd.addCheckbox("Use_slice_position", useSlicePosition); } // Dialog to allow double click to select files using a file chooser if (!java.awt.GraphicsEnvironment.isHeadless()) { text1 = (TextField) gd.getStringFields().get(0); text2 = (TextField) gd.getStringFields().get(1); text1.addMouseListener(this); text2.addMouseListener(this); } gd.showDialog(); if (gd.wasCanceled()) return false; title1 = gd.getNextString(); title2 = gd.getNextString(); if (haveImages) { myMask = mask = gd.getNextChoice(); } dThreshold = gd.getNextNumber(); beta = gd.getNextNumber(); showPairs = gd.getNextBoolean(); savePairs = gd.getNextBoolean(); savePairsSingleFile = gd.getNextBoolean(); if (haveImages) { showComposite = gd.getNextBoolean(); ignoreFrames = gd.getNextBoolean(); myImage1 = image1 = gd.getNextChoice(); myImage2 = image2 = gd.getNextChoice(); useSlicePosition = gd.getNextBoolean(); } return true; } public static ArrayList<String> buildMaskList() { ArrayList<String> newImageList = new ArrayList<String>(); for (int id : gdsc.core.ij.Utils.getIDList()) { ImagePlus imp = WindowManager.getImage(id); // Ignore RGB images if (imp.getBitDepth() == 24) continue; newImageList.add(imp.getTitle()); } return newImageList; } private void compareCoordinates(TimeValuedPoint[] actualPoints, TimeValuedPoint[] predictedPoints, double dThreshold) { int tp = 0, fp = 0, fn = 0; double rmsd = 0; final boolean is3D = is3D(actualPoints) && is3D(predictedPoints); final boolean computePairs = showPairs || savePairs || savePairsSingleFile || (showComposite && myImage1 != null && myImage2 != null); final List<PointPair> pairs = (computePairs) ? new LinkedList<PointPair>() : null; // Process each timepoint for (Integer t : getTimepoints(actualPoints, predictedPoints)) { Coordinate[] actual = getCoordinates(actualPoints, t); Coordinate[] predicted = getCoordinates(predictedPoints, t); List<Coordinate> TP = null; List<Coordinate> FP = null; List<Coordinate> FN = null; List<PointPair> matches = null; if (computePairs) { TP = new LinkedList<Coordinate>(); FP = new LinkedList<Coordinate>(); FN = new LinkedList<Coordinate>(); matches = new LinkedList<PointPair>(); } MatchResult result = (is3D) ? MatchCalculator.analyseResults3D(actual, predicted, dThreshold, TP, FP, FN, matches) : MatchCalculator.analyseResults2D(actual, predicted, dThreshold, TP, FP, FN, matches); // Aggregate tp += result.getTruePositives(); fp += result.getFalsePositives(); fn += result.getFalseNegatives(); rmsd += (result.getRMSD() * result.getRMSD()) * result.getTruePositives(); if (computePairs) { pairs.addAll(matches); for (Coordinate c : FN) pairs.add(new PointPair(c, null)); for (Coordinate c : FP) pairs.add(new PointPair(null, c)); } } if (showPairs || savePairs || savePairsSingleFile) { // Check if these are valued points valued = isValued(actualPoints) && isValued(predictedPoints); } if (!java.awt.GraphicsEnvironment.isHeadless()) { if (resultsWindow == null || !resultsWindow.isShowing()) { resultsWindow = new TextWindow(TITLE + " Results", createResultsHeader(), "", 900, 300); } if (showPairs) { String header = createPairsHeader(title1, title2); if (pairsWindow == null || !pairsWindow.isShowing()) { pairsWindow = new TextWindow(TITLE + " Pairs", header, "", 900, 300); Point p = resultsWindow.getLocation(); p.y += resultsWindow.getHeight(); pairsWindow.setLocation(p); } for (PointPair pair : pairs) addPairResult(pair, is3D); } } else { if (writeHeader) { writeHeader = false; IJ.log(createResultsHeader()); } } if (tp > 0) rmsd = Math.sqrt(rmsd / tp); MatchResult result = new MatchResult(tp, fp, fn, rmsd); addResult(title1, title2, dThreshold, result); if (savePairs || savePairsSingleFile) { savePairs(pairs, is3D); } if (java.awt.GraphicsEnvironment.isHeadless()) return; // If input images and a mask have been selected then we can produce an output // that draws the points on a composite image. produceComposite(pairs); } /** * Checks if there is more than one z-value in the coordinates * * @param points * @return */ private boolean is3D(TimeValuedPoint[] points) { if (points.length == 0) return false; final float z = points[0].getZ(); for (TimeValuedPoint p : points) if (p.getZ() != z) return true; return false; } /** * Checks if there is a non-zero value within the points * * @param points * @return */ private boolean isValued(TimeValuedPoint[] points) { if (points.length == 0) return false; for (TimeValuedPoint p : points) if (p.getValue() != 0) return true; return false; } private int[] getTimepoints(TimeValuedPoint[] points, TimeValuedPoint[] points2) { TIntHashSet set = new TIntHashSet(); for (TimeValuedPoint p : points) set.add(p.getTime()); for (TimeValuedPoint p : points2) set.add(p.getTime()); int[] data = set.toArray(); if (showPairs) // Sort so the table order is nice Arrays.sort(data); return data; } private Coordinate[] getCoordinates(TimeValuedPoint[] points, int t) { LinkedList<Coordinate> coords = new LinkedList<Coordinate>(); int id = 1; for (TimeValuedPoint p : points) if (p.getTime() == t) coords.add(new IdTimeValuedPoint(id++, p)); return coords.toArray(new Coordinate[coords.size()]); } private String createResultsHeader() { StringBuilder sb = new StringBuilder(); sb.append("Image 1\t"); sb.append("Image 2\t"); sb.append("Distance (px)\t"); sb.append("N\t"); sb.append("TP\t"); sb.append("FP\t"); sb.append("FN\t"); sb.append("Jaccard\t"); sb.append("RMSD\t"); sb.append("Precision\t"); sb.append("Recall\t"); sb.append("F0.5\t"); sb.append("F1\t"); sb.append("F2\t"); sb.append("F-beta"); return sb.toString(); } private void addResult(String i1, String i2, double dThrehsold, MatchResult result) { StringBuilder sb = new StringBuilder(); sb.append(i1).append("\t"); sb.append(i2).append("\t"); sb.append(IJ.d2s(dThrehsold, 2)).append("\t"); sb.append(result.getNumberPredicted()).append("\t"); sb.append(result.getTruePositives()).append("\t"); sb.append(result.getFalsePositives()).append("\t"); sb.append(result.getFalseNegatives()).append("\t"); sb.append(IJ.d2s(result.getJaccard(), 4)).append("\t"); sb.append(IJ.d2s(result.getRMSD(), 4)).append("\t"); sb.append(IJ.d2s(result.getPrecision(), 4)).append("\t"); sb.append(IJ.d2s(result.getRecall(), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(0.5), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(1.0), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(2.0), 4)).append("\t"); sb.append(IJ.d2s(result.getFScore(beta), 4)); if (java.awt.GraphicsEnvironment.isHeadless()) { IJ.log(sb.toString()); } else { resultsWindow.append(sb.toString()); } } private String pairsPrefix; private String createPairsHeader(String i1, String i2) { StringBuilder sb = new StringBuilder(); sb.append(i1).append("\t"); sb.append(i2).append("\t"); pairsPrefix = sb.toString(); sb.setLength(0); sb.append("Image 1\t"); sb.append("Image 2\t"); sb.append("T\t"); sb.append("ID1\t"); sb.append("X1\t"); sb.append("Y1\t"); sb.append("Z1\t"); if (valued) sb.append("Value\t"); sb.append("ID2\t"); sb.append("X2\t"); sb.append("Y2\t"); sb.append("Z2\t"); if (valued) sb.append("Value\t"); sb.append("Distance\t"); sb.append("Outcome"); return sb.toString(); } private void addPairResult(PointPair pair, boolean is3D) { pairsWindow.append(createPairResult(pair, is3D)); } private String createPairResult(PointPair pair, boolean is3D) { StringBuilder sb = new StringBuilder(pairsPrefix); IdTimeValuedPoint p1 = (IdTimeValuedPoint) pair.getPoint1(); IdTimeValuedPoint p2 = (IdTimeValuedPoint) pair.getPoint2(); int t = (p1 != null) ? p1.getTime() : p2.getTime(); sb.append(t).append("\t"); addPoint(sb, p1); addPoint(sb, p2); double d = (is3D) ? pair.getXYZDistance() : pair.getXYDistance(); if (d >= 0) sb.append(d).append("\t"); else sb.append("\t"); // Added for colocalisation analysis: // C = Colocalised (i.e. a match) // F = First dataset has foci // S = Second dataset has foci final char outcome = (p1 != null) ? (p2 != null) ? 'C' : 'F' : 'S'; sb.append(outcome); return sb.toString(); } private void addPoint(StringBuilder sb, IdTimeValuedPoint p) { if (p == null) { if (valued) sb.append("\t\t\t\t\t"); else sb.append("\t\t\t\t"); } else { sb.append(p.id).append("\t"); sb.append(p.getXint()).append("\t"); sb.append(p.getYint()).append("\t"); sb.append(p.getZint()).append("\t"); if (valued) sb.append(p.getValue()).append("\t"); } } private void savePairs(List<PointPair> pairs, boolean is3D) { boolean fileSelected = false; if (savePairs) { filename = getFilename("Pairs_filename", filename); fileSelected = filename != null; } boolean fileSingleSelected = false; if (savePairsSingleFile) { filenameSingle = getFilename("Pairs_filename_single", filenameSingle); fileSingleSelected = filenameSingle != null; } if (!(fileSelected || fileSingleSelected)) return; OutputStreamWriter out = null; OutputStreamWriter outSingle = null; try { final String newLine = System.getProperty("line.separator"); // Always create the header as it sets up the pairs prefix for each pair result final String header = createPairsHeader(title1, title2); if (fileSelected) { FileOutputStream fos = new FileOutputStream(filename); out = new OutputStreamWriter(fos, "UTF-8"); out.write(header); out.write(newLine); } if (fileSingleSelected) { File file = new File(filenameSingle); boolean append = (file.length() != 0); FileOutputStream fos = new FileOutputStream(file, append); outSingle = new OutputStreamWriter(fos, "UTF-8"); if (!append) { outSingle.write(header); outSingle.write(newLine); } } for (PointPair pair : pairs) { final String result = createPairResult(pair, is3D); if (out != null) { out.write(result); out.write(newLine); } if (outSingle != null) { outSingle.write(result); outSingle.write(newLine); } } } catch (Exception e) { IJ.log("Unable to save the matches to file: " + e.getMessage()); } finally { if (out != null) { try { out.close(); } catch (IOException e) { // Ignore } } if (outSingle != null) { try { outSingle.close(); } catch (IOException e) { // Ignore } } } } private String getFilename(String title, String filename) { final String[] path = Utils.decodePath(filename); final OpenDialog chooser = new OpenDialog(title, path[0], path[1]); if (chooser.getFileName() == null) return null; return Utils.replaceExtension(chooser.getDirectory() + chooser.getFileName(), ".xls"); } public void mouseClicked(MouseEvent e) { if (e.getClickCount() > 1) // Double-click { TextField text; String title; if (e.getSource() == text1) { text = text1; title = "Coordinate_file_1"; } else { text = text2; title = "Coordinate_file_2"; } String[] path = decodePath(text.getText()); OpenDialog chooser = new OpenDialog(title, path[0], path[1]); if (chooser.getFileName() != null) { text.setText(chooser.getDirectory() + chooser.getFileName()); } } } private String[] decodePath(String path) { String[] result = new String[2]; int i = path.lastIndexOf('/'); if (i == -1) i = path.lastIndexOf('\\'); if (i > 0) { result[0] = path.substring(0, i + 1); result[1] = path.substring(i + 1); } else { result[0] = OpenDialog.getDefaultDirectory(); result[1] = path; } return result; } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } private void produceComposite(List<PointPair> pairs) { if (!showComposite) return; if (myImage1 == null || myImage2 == null) return; ImagePlus imp1 = WindowManager.getImage(myImage1); ImagePlus imp2 = WindowManager.getImage(myImage2); ImagePlus impM = WindowManager.getImage(myMask); if (myImage1 == null || myImage2 == null) { IJ.error(TITLE, "No images specified for composite"); return; } // Images must be the same XYZ dimensions final int w = imp1.getWidth(); final int h = imp1.getHeight(); final int nSlices = imp1.getNSlices(); if (w != imp2.getWidth() || h != imp2.getHeight() || nSlices != imp2.getNSlices()) { IJ.error(TITLE, "Composite images must have the same XYZ dimensions"); return; } if (impM != null && (w != impM.getWidth() || h != impM.getHeight())) { IJ.error(TITLE, "Composite image mask must have the same XY dimensions"); return; } final boolean addMask = impM != null; final ImageStack stack = new ImageStack(w, h); final ImageStack s1 = imp1.getImageStack(); final ImageStack s2 = imp2.getImageStack(); final ImageStack sm = (addMask) ? impM.getImageStack() : null; // Get the bit-depth for the output stack int depth = imp1.getBitDepth(); depth = Math.max(depth, imp2.getBitDepth()); if (addMask) depth = Math.max(depth, impM.getBitDepth()); final int nChannels = (addMask) ? 3 : 2; int nFrames = 0; // Produce a composite for each time point. // The input list will have pairs in order of time. // So move through the list noting each new time. ArrayList<Integer> upper = new ArrayList<Integer>(); int time = -1; final IdTimeValuedPoint[] p1 = new IdTimeValuedPoint[pairs.size()]; final IdTimeValuedPoint[] p2 = new IdTimeValuedPoint[pairs.size()]; for (int i = 0; i < pairs.size(); i++) { p1[i] = (IdTimeValuedPoint) pairs.get(i).getPoint1(); p2[i] = (IdTimeValuedPoint) pairs.get(i).getPoint2(); final int newTime = (p1[i] != null) ? p1[i].time : p2[i].time; if (time != newTime) { if (time != -1) upper.add(i); time = newTime; } } upper.add(pairs.size()); // Check if the input image has only one time frame but the points have multiple time points boolean singleFrame = false; if (upper.size() > 1 && imp1.getNFrames() == 1 && imp2.getNFrames() == 1) { if (ignoreFrames) { singleFrame = true; upper.clear(); upper.add(pairs.size()); } } // Create an overlay to show the pairs Overlay overlay = new Overlay(); final Color colorf = MatchPlugin.UNMATCH1; final Color colors = MatchPlugin.UNMATCH2; final Color colorc = MatchPlugin.MATCH; int l = 0; for (int u : upper) { nFrames++; time = (p1[l] != null) ? p1[l].time : p2[l].time; //System.out.printf("%d - %d : Time %d\n", l, u, time); if (singleFrame) time = 1; // Extract the images for the specified time for (int slice = 1; slice <= nSlices; slice++) { stack.addSlice("Image1, t=" + time, convert(s1.getProcessor(imp1.getStackIndex(imp1.getC(), slice, time)), depth)); stack.addSlice("Image2, t=" + time, convert(s2.getProcessor(imp2.getStackIndex(imp2.getC(), slice, time)), depth)); if (addMask) stack.addSlice("Mask, t=" + time, convert(sm.getProcessor(impM.getStackIndex(impM.getC(), slice, time)), depth)); } // Count number of First, Second, Colocalised. int f = 0, s = 0, c = 0; float[] fx = new float[u - l]; float[] fy = new float[fx.length]; float[] sx = new float[fx.length]; float[] sy = new float[fx.length]; float[] cx = new float[fx.length]; float[] cy = new float[fx.length]; int[] fz = new int[fx.length]; int[] sz = new int[fx.length]; int[] cz = new int[fx.length]; for (int j = l; j < u; j++) { if (p1[j] == null) { sx[s] = p2[j].getX(); sy[s] = p2[j].getY(); sz[s] = p2[j].getZint(); s++; } else if (p2[j] == null) { fx[f] = p1[j].getX(); fy[f] = p1[j].getY(); fz[f] = p1[j].getZint(); f++; } else { cx[c] = (p1[j].getX() + p2[j].getX()) * 0.5f; cy[c] = (p1[j].getY() + p2[j].getY()) * 0.5f; cz[c] = (p1[j].getZint() + p2[j].getZint()) / 2; c++; } } add(overlay, fx, fy, fz, f, colorf, nFrames); add(overlay, sx, sy, sz, s, colors, nFrames); add(overlay, cx, cy, cz, c, colorc, nFrames); l = u; } String title = "Match Composite"; ImagePlus imp = new ImagePlus(title, stack); imp.setDimensions(nChannels, nSlices, nFrames); imp.setOverlay(overlay); imp.setOpenAsHyperStack(true); imp2 = WindowManager.getImage(title); if (imp2 != null) { if (imp2.isDisplayedHyperStack()) { imp2.setImage(imp); imp2.setOverlay(overlay); imp2.getWindow().toFront(); return; } // Figure out how to convert back to a hyperstack if it is not already. // Currently we just close the image and show a new one. imp2.close(); } imp.show(); } private ImageProcessor convert(ImageProcessor processor, int depth) { if (depth == 8) return processor.convertToByte(true); if (depth == 16) return processor.convertToShort(true); return processor.convertToFloat(); } private void add(Overlay overlay, float[] x, float[] y, int[] z, int n, Color color, int frame) { if (n != 0) { // Option to position the ROI points on the z-slice. // This requires an ROI for each slice that contains a point. if (useSlicePosition) { // Sort points by slice position float[][] data = new float[n][3]; for (int i = n; i-- > 0;) { data[i][0] = z[i]; data[i][1] = x[i]; data[i][2] = y[i]; } Comparator<float[]> comp = new Comparator<float[]>() { public int compare(float[] o1, float[] o2) { // smallest first if (o1[0] < o2[0]) return -1; if (o1[0] > o2[0]) return 1; return 0; } }; Arrays.sort(data, comp); // Find blocks in the same slice int l = 0; ArrayList<Integer> upper = new ArrayList<Integer>(n); for (int i = 0; i < n; i++) { if (data[l][0] != data[i][0]) { upper.add(i); l = i; } } upper.add(n); // Process each block l = 0; for (int u : upper) { int nPoints = u - l; for (int i = l, j = 0; i < u; i++, j++) { x[j] = data[i][1]; y[j] = data[i][2]; } add(overlay, new PointRoi(x, y, nPoints), (int) data[l][0], frame, color); l = u; } } else { add(overlay, new PointRoi(x, y, n), 0, frame, color); } } } private void add(Overlay overlay, PointRoi roi, int slice, int frame, Color color) { // Tie position to the frame but not the channel or slice //System.out.printf("Add %d to z=%d,t=%d\n", roi.getNCoordinates(), slice, frame); roi.setPosition(0, slice, frame); roi.setStrokeColor(color); roi.setFillColor(color); roi.setShowLabels(false); overlay.add(roi); } }