package gdsc.foci; import java.awt.Canvas; /*----------------------------------------------------------------------------- * GDSC Plugins for ImageJ * * Copyright (C) 2016 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 3 of the License, or * (at your option) any later version. *---------------------------------------------------------------------------*/ import java.awt.Color; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import gdsc.UsageTracker; import gdsc.core.ij.Utils; import gdsc.core.match.MatchCalculator; import gdsc.core.match.PointPair; import gdsc.core.utils.Maths; import ij.IJ; import ij.ImagePlus; import ij.ImageStack; import ij.WindowManager; import ij.gui.GenericDialog; import ij.gui.ImageCanvas; import ij.gui.Line; import ij.gui.Overlay; import ij.gui.PointRoi; import ij.gui.PolygonRoi; import ij.gui.Roi; import ij.gui.TextRoi; import ij.gui.Toolbar; import ij.plugin.PlugIn; import ij.plugin.tool.PlugInTool; import ij.process.ByteProcessor; import ij.text.TextPanel; import ij.text.TextWindow; /** * Find translocations using markers for colocalisation. * * <P> * Run a pairwise analysis of 3 channels. Find triplets where the two markers from channel 2 & 3 matching a foci in * channel 1 are also a matching pair. Draw a bounding box round the triplet and output the distances between the * centres. Output a guess for a translocation where channel 13 distance << 12|23, no transolcation where 12 << 13|23. */ public class TranslocationFinder implements PlugIn { public static String TITLE = "Translocation Finder"; private static TextWindow resultsWindow = null; private static final int UNKNOWN = 0; private static final int NO_TRANSLOCATION = 1; private static final int TRANSLOCATION = 2; private static final String[] CLASSIFICATION = { "Unknown", "No translocation", "Translocation" }; private static String resultsName1 = ""; private static String resultsName2 = ""; private static String resultsName3 = ""; private static String image = ""; private static double distance = 50; private static double minDistance = 8; private static double factor = 2; private static boolean showMatches = false; // Static fields hold information to draw the overlay and update the results table // The foci private static AssignedPoint[] foci1, foci2, foci3; // Image to draw overlay private static ImagePlus imp; // Current set of triplets private static ArrayList<int[]> triplets = new ArrayList<int[]>(); // Image to draw overlay for the manual triplets private static ImagePlus manualImp; // Current set of manual triplets private static ArrayList<AssignedPoint[]> manualTriplets = new ArrayList<AssignedPoint[]>(); public void run(String arg) { UsageTracker.recordPlugin(this.getClass(), arg); if ("tool".equals(arg)) { addPluginTool(); return; } // List the foci results String[] names = FindFoci.getResultsNames(); if (names == null || names.length < 3) { IJ.error(TITLE, "3 sets of Foci must be stored in memory using the " + FindFoci.TITLE + " plugin"); return; } // Build a list of the open images to add an overlay String[] imageList = Utils.getImageList(Utils.GREY_8_16 | Utils.NO_IMAGE, null); GenericDialog gd = new GenericDialog(TITLE); gd.addMessage("Analyses spots within a mask/ROI region\nand computes density and closest distances."); gd.addChoice("Results_name_1", names, resultsName1); gd.addChoice("Results_name_2", names, resultsName2); gd.addChoice("Results_name_3", names, resultsName3); gd.addChoice("Overlay_on_image", imageList, image); gd.addNumericField("Distance", distance, 2, 6, "pixels"); gd.addNumericField("Min_distance", minDistance, 2, 6, "pixels"); gd.addNumericField("Factor", factor, 2); gd.addCheckbox("Show_matches", showMatches); gd.addHelp(gdsc.help.URL.FIND_FOCI); gd.showDialog(); if (!gd.wasOKed()) return; resultsName1 = gd.getNextChoice(); resultsName2 = gd.getNextChoice(); resultsName3 = gd.getNextChoice(); image = gd.getNextChoice(); distance = gd.getNextNumber(); minDistance = gd.getNextNumber(); factor = gd.getNextNumber(); showMatches = gd.getNextBoolean(); // Get the foci foci1 = getFoci(resultsName1); foci2 = getFoci(resultsName2); foci3 = getFoci(resultsName3); if (foci1 == null || foci2 == null || foci3 == null) return; analyse(); } private AssignedPoint[] getFoci(String resultsName) { ArrayList<FindFociResult> results = FindFoci.getResults(resultsName); if (results == null) { IJ.showMessage("Error", "No foci with the name " + resultsName); return null; } if (results.size() == 0) { IJ.showMessage("Error", "Zero foci in the results with the name " + resultsName); return null; } AssignedPoint[] foci = new AssignedPoint[results.size()]; int i = 0; for (FindFociResult result : results) { foci[i] = new AssignedPoint(result.x, result.y, result.z + 1, i); i++; } return foci; } /** * For all foci in set 1, compare to set 2 and output a histogram of the average density around each foci (pair * correlation) and the minimum distance to another foci. */ private void analyse() { // Compute pairwise matches final boolean is3D = is3D(foci1) || is3D(foci2) || is3D(foci3); List<PointPair> matches12 = new ArrayList<PointPair>(Math.min(foci1.length, foci2.length)); List<PointPair> matches13 = new ArrayList<PointPair>(Math.min(foci1.length, foci3.length)); List<PointPair> matches23 = new ArrayList<PointPair>(Math.min(foci2.length, foci3.length)); if (is3D) { MatchCalculator.analyseResults3D(foci1, foci2, distance, null, null, null, matches12); MatchCalculator.analyseResults3D(foci1, foci3, distance, null, null, null, matches13); MatchCalculator.analyseResults3D(foci2, foci3, distance, null, null, null, matches23); } else { MatchCalculator.analyseResults2D(foci1, foci2, distance, null, null, null, matches12); MatchCalculator.analyseResults2D(foci1, foci3, distance, null, null, null, matches13); MatchCalculator.analyseResults2D(foci2, foci3, distance, null, null, null, matches23); } // Use for debugging imp = WindowManager.getImage(image); if (imp != null && showMatches) { // DEBUG : Show the matches ImageStack stack = new ImageStack(imp.getWidth(), imp.getHeight()); stack.addSlice("12", new ByteProcessor(imp.getWidth(), imp.getHeight())); stack.addSlice("13", new ByteProcessor(imp.getWidth(), imp.getHeight())); stack.addSlice("23", new ByteProcessor(imp.getWidth(), imp.getHeight())); Overlay ov = new Overlay(); add(ov, matches12, 1); add(ov, matches13, 2); add(ov, matches23, 3); Utils.display(TITLE, stack).setOverlay(ov); } // Find triplets with mutual closest neighbours triplets.clear(); for (PointPair pair12 : matches12) { final int id1 = ((AssignedPoint) pair12.getPoint1()).id; final int id2 = ((AssignedPoint) pair12.getPoint2()).id; // Find match in channel 3 int id3 = -1; for (PointPair pair13 : matches13) { if (id1 == ((AssignedPoint) pair13.getPoint1()).id) { id3 = ((AssignedPoint) pair13.getPoint2()).id; break; } } if (id3 != -1) { // Find if the same pair match in channel 23 for (PointPair pair23 : matches23) { if (id2 == ((AssignedPoint) pair23.getPoint1()).id) { if (id3 == ((AssignedPoint) pair23.getPoint2()).id) { // Add an extra int to store the classification triplets.add(new int[] { id1, id2, id3, UNKNOWN }); } break; } } } } // Table of results createResultsWindow(); int count = 0; for (int[] triplet : triplets) { count++; addResult(count, triplet); } overlayTriplets(); } private boolean is3D(AssignedPoint[] foci) { final int z = foci[0].z; for (int i = 1; i < foci.length; i++) if (foci[i].z != z) return true; return false; } private void add(Overlay ov, List<PointPair> matches, int n) { for (PointPair pair : matches) { AssignedPoint p1 = (AssignedPoint) pair.getPoint1(); AssignedPoint p2 = (AssignedPoint) pair.getPoint2(); Line line = new Line(p1.x, p1.y, p2.x, p2.y); line.setPosition(n); ov.add(line); } } private String name = ""; private void createResultsWindow() { // Get the name. // We are expecting FindFoci to be run on 3 channels of the same image: // 1=ImageTitle (c1,t1) // 2=ImageTitle (c2,t1) // 3=ImageTitle (c3,t1) // Look for this and then output: // ImageTitle (c1,t1); (c2,t1); (c3,t1) final int len = Maths.min(resultsName1.length(), resultsName2.length(), resultsName3.length()); int i = 0; while (i < len) { // Find common prefix if (resultsName1.charAt(i) != resultsName2.charAt(i) || resultsName1.charAt(i) != resultsName3.charAt(i) || resultsName1.charAt(i) == '(') // First character of FindFoci results suffix { break; } i++; } // Common prefix plus the FindFoci suffix name = resultsName1 + "; " + resultsName2.substring(i).trim() + "; " + resultsName3.substring(i).trim(); if (resultsWindow == null || !resultsWindow.isShowing()) { resultsWindow = new TextWindow(TITLE + " Results", createResultsHeader(), "", 1000, 300); // Allow the results to be manually changed resultsWindow.getTextPanel().addMouseListener(new MouseListener() { public void mouseClicked(MouseEvent e) { if (e.getClickCount() < 2) return; TextPanel tp = null; if (e.getSource() instanceof TextPanel) { tp = (TextPanel) e.getSource(); } else if (e.getSource() instanceof Canvas && ((Canvas) e.getSource()).getParent() instanceof TextPanel) { tp = (TextPanel) ((Canvas) e.getSource()).getParent(); } final String line = tp.getLine(tp.getSelectionStart()); final String[] fields = line.split("\t"); final GenericDialog gd = new GenericDialog(TITLE + " Results Update"); // Get the current classification: label = count + classification char final String label = fields[fields.length - 1]; int index = 0; final char c = label.charAt(label.length() - 1); while (index < CLASSIFICATION.length && CLASSIFICATION[index].charAt(0) != c) index++; if (index == CLASSIFICATION.length) index = 0; // Prompt the user to change it gd.addMessage("Update the classification for " + label); gd.addChoice("Class", CLASSIFICATION, CLASSIFICATION[index]); gd.showDialog(); if (gd.wasCanceled()) return; final int newIndex = gd.getNextChoiceIndex(); final boolean noChange = (newIndex == index); index = newIndex; // Update the table fields so we capture the manual edit final String sCount = label.substring(0, label.length() - 1); fields[fields.length - 3] = "Manual"; fields[fields.length - 2] = CLASSIFICATION[index]; fields[fields.length - 1] = sCount + CLASSIFICATION[index].charAt(0); StringBuilder sb = new StringBuilder(fields[0]); for (int i = 1; i < fields.length; i++) sb.append('\t').append(fields[i]); tp.setLine(tp.getSelectionStart(), sb.toString()); // Update the overlay if we can if (noChange) return; if (imp == null && manualImp == null) return; if (triplets.isEmpty() && manualTriplets.isEmpty()) return; // Get the triplet count from the label int count = 0; try { count = Integer.parseInt(sCount); } catch (NumberFormatException ex) { return; } if (count == 0) return; if (count > 0) { // Triplet added by the plugin if (triplets.size() < count) return; // Find if the selection is from the current set of triplets final int[] triplet = triplets.get(count - 1); final AssignedPoint p1 = foci1[triplet[0]]; final AssignedPoint p2 = foci2[triplet[1]]; final AssignedPoint p3 = foci3[triplet[2]]; if (p1.x != Integer.parseInt(fields[1]) || p1.y != Integer.parseInt(fields[2]) || p1.z != Integer.parseInt(fields[3]) || p2.x != Integer.parseInt(fields[4]) || p2.y != Integer.parseInt(fields[5]) || p2.z != Integer.parseInt(fields[6]) || p3.x != Integer.parseInt(fields[7]) || p3.y != Integer.parseInt(fields[8]) || p3.z != Integer.parseInt(fields[9])) return; triplet[3] = index; } else { // Manual triplet count = -count; if (manualTriplets.size() < count) return; // Find if the selection is from the current set of manual triplets final AssignedPoint[] triplet = manualTriplets.get(count - 1); final AssignedPoint p1 = triplet[0]; final AssignedPoint p2 = triplet[1]; final AssignedPoint p3 = triplet[2]; if (p1.x != Integer.parseInt(fields[1]) || p1.y != Integer.parseInt(fields[2]) || p1.z != Integer.parseInt(fields[3]) || p2.x != Integer.parseInt(fields[4]) || p2.y != Integer.parseInt(fields[5]) || p2.z != Integer.parseInt(fields[6]) || p3.x != Integer.parseInt(fields[7]) || p3.y != Integer.parseInt(fields[8]) || p3.z != Integer.parseInt(fields[9])) return; triplet[0].id = index; } overlayTriplets(); } public void mouseEntered(MouseEvent arg0) { } public void mouseExited(MouseEvent arg0) { } public void mousePressed(MouseEvent arg0) { } public void mouseReleased(MouseEvent arg0) { } }); } } private String createResultsHeader() { StringBuilder sb = new StringBuilder(); sb.append("Name"); sb.append("\t1x\t1y\t1z"); sb.append("\t2x\t2y\t2z"); sb.append("\t3x\t3y\t3z"); sb.append("\tD12"); sb.append("\tD13"); sb.append("\tD23"); sb.append("\tMode"); sb.append("\tClass"); sb.append("\tLabel"); return sb.toString(); } private void addResult(int count, int[] triplet) { AssignedPoint p1 = foci1[triplet[0]]; AssignedPoint p2 = foci2[triplet[1]]; AssignedPoint p3 = foci3[triplet[2]]; triplet[3] = addResult(count, name, p1, p2, p3); } private int addResult(int count, String name, AssignedPoint p1, AssignedPoint p2, AssignedPoint p3) { return addResult(count, name, p1, p2, p3, -1); } /** * Adds the result. * * @param count * the count * @param name * the name * @param p1 * the p1 * @param p2 * the p2 * @param p3 * the p3 * @param classification * the classification (set to -1 to auto compute) * @return the classification */ private int addResult(int count, String name, AssignedPoint p1, AssignedPoint p2, AssignedPoint p3, int classification) { StringBuilder sb = new StringBuilder(); sb.append(name); addTriplet(sb, p1); addTriplet(sb, p2); addTriplet(sb, p3); final double d12 = p1.distanceXYZ(p2); final double d13 = p1.distanceXYZ(p3); final double d23 = p2.distanceXYZ(p3); sb.append("\t").append(Utils.rounded(d12)); sb.append("\t").append(Utils.rounded(d13)); sb.append("\t").append(Utils.rounded(d23)); // Compute classification if (classification >= CLASSIFICATION.length || classification < 0) { classification = 0; if (isSeparated(d12, d13, d23)) classification = NO_TRANSLOCATION; else if (isSeparated(d13, d12, d23, minDistance)) classification = TRANSLOCATION; sb.append("\tAuto"); } else { sb.append("\tManual"); } sb.append('\t').append(CLASSIFICATION[classification]); sb.append('\t').append(count).append(CLASSIFICATION[classification].charAt(0)); resultsWindow.append(sb.toString()); return classification; } private void addTriplet(StringBuilder sb, AssignedPoint p) { sb.append("\t").append(p.x); sb.append("\t").append(p.y); sb.append("\t").append(p.z); } /** * Check if distance 12 is much smaller than distance 13 and 23. It must be a given factor smaller than the other * two distances. i.e. foci 3 is separated from foci 1 and 2. * * @param d12 * the d12 * @param d13 * the d13 * @param d23 * the d23 * @return true, if successful */ private boolean isSeparated(double d12, double d13, double d23) { return d13 / d12 > factor && d23 / d12 > factor; } /** * Check if distance 12 is much smaller than distance 13 and 23. It must be a given factor smaller than the other * two distances. The other two distances must also be above the min distance threshold, i.e. foci 3 is separated * from foci 1 and 2. * * @param d12 * the d12 * @param d13 * the d13 * @param d23 * the d23 * @param minDistance * the min distance * @return true, if successful */ private boolean isSeparated(double d12, double d13, double d23, double minDistance) { return d13 > minDistance && d23 > minDistance && d13 / d12 > factor && d23 / d12 > factor; } /** * Overlay triplets on image */ private void overlayTriplets() { Overlay o = null; if (imp != null) { o = new Overlay(); int count = 0; for (int[] triplet : triplets) { count++; AssignedPoint p1 = foci1[triplet[0]]; AssignedPoint p2 = foci2[triplet[1]]; AssignedPoint p3 = foci3[triplet[2]]; addTriplet(count, o, p1, p2, p3, triplet[3]); } imp.setOverlay(o); } if (manualImp != null) { // New overlay if the two images are different if (o == null || (imp != null && imp.getID() != manualImp.getID())) o = new Overlay(); int count = 0; for (AssignedPoint[] triplet : manualTriplets) { count--; AssignedPoint p1 = triplet[0]; AssignedPoint p2 = triplet[1]; AssignedPoint p3 = triplet[2]; // We store the classification in the id of the first point addTriplet(count, o, p1, p2, p3, triplet[0].id); } manualImp.setOverlay(o); } } private void addTriplet(int count, Overlay o, AssignedPoint p1, AssignedPoint p2, AssignedPoint p3, int classification) { float[] x = new float[3]; float[] y = new float[3]; x[0] = p1.x; x[1] = p2.x; x[2] = p3.x; y[0] = p1.y; y[1] = p2.y; y[2] = p3.y; PolygonRoi roi = new PolygonRoi(x, y, 3, Roi.POLYGON); Color color; switch (classification) { case TRANSLOCATION: color = Color.CYAN; break; case NO_TRANSLOCATION: color = Color.MAGENTA; break; case UNKNOWN: default: color = Color.YELLOW; } roi.setStrokeColor(color); o.add(roi); TextRoi text = new TextRoi(Maths.max(x) + 1, Maths.min(y), Integer.toString(count) + CLASSIFICATION[classification].charAt(0)); text.setStrokeColor(color); o.add(text); } /** * Provide a tool on the ImageJ toolbar that responds to a user clicking on the same image to identify * foci for potential translocations. */ public class TranslocationFinderPluginTool extends PlugInTool { final String[] labels = { "C1", "C2", "C3" }; final String[] items = Arrays.copyOf(CLASSIFICATION, CLASSIFICATION.length + 1); int imageId = 0; int[] ox = new int[3], oy = new int[3], oz = new int[3]; int points = 0; boolean prompt = true; TranslocationFinderPluginTool() { items[items.length - 1] = "Auto"; } @Override public String getToolName() { return "Manual Translocation Finder Tool"; } @Override public String getToolIcon() { return "Cf00o4233C0f0o6933C00foa644C000Ta508M"; } @Override public void showOptionsDialog() { GenericDialog gd = new GenericDialog(TITLE + " Tool Options"); gd.addNumericField("Min_distance", minDistance, 2, 6, "pixels"); gd.addNumericField("Factor", factor, 2); gd.addCheckbox("Show_record_dialog", prompt); gd.showDialog(); if (gd.wasCanceled()) return; minDistance = gd.getNextNumber(); factor = gd.getNextNumber(); prompt = gd.getNextBoolean(); } @Override public void mouseClicked(ImagePlus imp, MouseEvent e) { // Ensure rapid mouse click does not break things synchronized (this) { if (imageId != imp.getID()) { points = 0; } imageId = imp.getID(); ImageCanvas ic = imp.getCanvas(); ox[points] = ic.offScreenX(e.getX()); oy[points] = ic.offScreenY(e.getY()); oz[points] = imp.getSlice(); //System.out.printf("click %d,%d\n", ox[points], oy[points]); points++; // Draw points as an ROI. if (points < 3) { PointRoi roi = new PointRoi(ox, oy, points); roi.setShowLabels(true); imp.setRoi(roi); } else { imp.setRoi((Roi) null); points = 0; int classification = CLASSIFICATION.length; // Auto // Q. Ask user if they want to add the point? if (prompt) { GenericDialog gd = new GenericDialog(TITLE + " Tool"); gd.addMessage("Record manual translocation"); gd.addChoice("Class", items, items[classification]); gd.addNumericField("Min_distance", minDistance, 2, 6, "pixels"); gd.addNumericField("Factor", factor, 2); gd.showDialog(); if (gd.wasCanceled()) return; classification = gd.getNextChoiceIndex(); minDistance = gd.getNextNumber(); factor = gd.getNextNumber(); } // If a new image for a triplet then reset the manual triplets for the overlay if (manualImp != null && manualImp.getID() != imp.getID()) manualTriplets.clear(); manualImp = imp; createResultsWindow(); AssignedPoint p1 = new AssignedPoint(ox[0], oy[0], oz[0], 1); AssignedPoint p2 = new AssignedPoint(ox[1], oy[1], oz[1], 2); AssignedPoint p3 = new AssignedPoint(ox[2], oy[2], oz[2], 3); manualTriplets.add(new AssignedPoint[] { p1, p2, p3 }); int count = -manualTriplets.size(); classification = addResult(count, imp.getTitle() + " (Manual)", p1, p2, p3, classification); p1.id = classification; overlayTriplets(); } } e.consume(); } } private static TranslocationFinder instance = null; private static TranslocationFinderPluginTool toolInstance = null; /** * Initialise the manual translocation finder tool. This is to allow support for calling within macro toolsets. */ public static void addPluginTool() { if (instance == null) { instance = new TranslocationFinder(); toolInstance = instance.new TranslocationFinderPluginTool(); } // Add the tool Toolbar.addPlugInTool(toolInstance); IJ.showStatus("Added " + TITLE + " Tool"); } }