/**
TrakEM2 plugin for ImageJ(C).
Copyright (C) 2009 Albert Cardona.
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 (http://www.gnu.org/licenses/gpl.txt )
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
You may contact Albert Cardona at acardona at ini.phys.ethz.ch
Institute of Neuroinformatics, University of Zurich / ETH, Switzerland.
**/
package ini.trakem2.display;
import ij.gui.GenericDialog;
import ini.trakem2.display.graphics.GraphicsSource;
import ini.trakem2.utils.Bureaucrat;
import ini.trakem2.utils.IJError;
import ini.trakem2.utils.Utils;
import ini.trakem2.utils.Worker;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import mpicbg.models.AbstractAffineModel2D;
import mpicbg.models.AffineModel2D;
import mpicbg.models.Point;
import mpicbg.models.PointMatch;
import mpicbg.models.RigidModel2D;
import mpicbg.models.SimilarityModel2D;
import mpicbg.models.TranslationModel2D;
import mpicbg.trakem2.align.Align;
import mpicbg.trakem2.align.AlignTask;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
public class ManualAlignMode implements Mode {
final private Display display;
private final HashMap<Layer,Landmarks> m = new HashMap<Layer,Landmarks>();
final private GraphicsSource gs = new GraphicsSource() {
/** Returns the list given as argument without any modification. */
@Override
public List<? extends Paintable> asPaintable(final List<? extends Paintable> ds) {
return ds;
}
@Override
public void paintOnTop(final Graphics2D g, final Display display, final Rectangle srcRect, final double magnification) {
final Landmarks lm = m.get(display.getLayer());
// Set clean canvas
final AffineTransform t = g.getTransform();
g.setTransform(new AffineTransform());
final Stroke stroke = g.getStroke();
g.setStroke(new BasicStroke(1.0f));
if (null != lm) {
lm.paint(g, srcRect, magnification);
}
// Restore
g.setTransform(t);
g.setStroke(stroke);
}
};
public ManualAlignMode(final Display display) {
this.display = display;
}
@Override
public GraphicsSource getGraphicsSource() {
return gs;
}
@Override
public boolean canChangeLayer() { return true; }
@Override
public boolean canZoom() { return true; }
@Override
public boolean canPan() { return true; }
@Override
public boolean isDragging() {
return false;
}
public class Landmarks {
Layer layer;
ArrayList<Point> points = new ArrayList<Point>();
public Landmarks(final Layer layer) {
this.layer = layer;
}
/** Returns the index of the newly added point, or -1 if the layer doesn't match. */
synchronized public int add(final Layer layer, final double x_p, final double y_p) {
if (this.layer != layer) return -1;
points.add(new Point(new double[]{x_p, y_p}));
return points.size() -1;
}
/** Returns the index of the closest point, with accuracy depending on magnification. */
synchronized public int find(final double x_p, final double y_p, final double mag) {
int index = -1;
double d = 10 / mag;
if (d < 2) d = 2;
double min_dist = Integer.MAX_VALUE;
int i = 0;
final Point ref = new Point(new double[]{x_p, y_p});
for (final Point p : points) {
final double dist = Point.distance(ref, p);
if (dist <= d && dist <= min_dist) {
min_dist = dist;
index = i;
}
i++;
}
return index;
}
/** Sets the point at @param index to the new location. */
synchronized public void set(final int index, final double x_d, final double y_d) {
if (index < 0 || index >= points.size()) return;
points.remove(index);
points.add(index, new Point(new double[]{x_d, y_d}));
}
synchronized public void remove(final int index) {
if (index < 0 || index >= points.size()) return;
points.remove(index);
}
synchronized public void paint(final Graphics2D g, final Rectangle srcRect, final double mag) {
g.setColor(Color.yellow);
g.setFont(new Font("SansSerif", Font.BOLD, 14));
int i = 1;
for (final Point p : points) {
final double[] w = p.getW();
final int x = (int)((w[0] - srcRect.x) * mag);
final int y = (int)((w[1] - srcRect.y) * mag);
// draw a cross at the exact point
g.setColor(Color.black);
g.drawLine(x-4, y+1, x+4, y+1);
g.drawLine(x+1, y-4, x+1, y+4);
g.setColor(Color.yellow);
g.drawLine(x-4, y, x+4, y);
g.drawLine(x, y-4, x, y+4);
// draw the index
g.drawString(Integer.toString(i), x+5, y+5);
i++;
}
}
}
private final void addPoint(final Layer layer, final double x, final double y) {
Landmarks lm = m.get(layer);
if (null == lm) {
lm = new Landmarks(layer);
m.put(layer, lm);
}
lm.add(layer, x, y);
}
private Layer current_layer = null;
private int index = -1;
@Override
public void mousePressed(final MouseEvent me, final int x_p, final int y_p, final double magnification) {
current_layer = display.getLayer();
// Find an existing Landmarks object for the current layer
Landmarks lm = m.get(current_layer);
if (null == lm) {
lm = new Landmarks(current_layer);
m.put(current_layer, lm);
}
// Find the point in it
index = lm.find(x_p, y_p, magnification);
if (-1 == index) {
index = lm.add(current_layer, x_p, y_p);
} else if (Utils.isControlDown(me) && me.isShiftDown()) {
lm.remove(index);
if (0 == lm.points.size()) {
m.remove(current_layer);
}
}
display.repaintAll3();
}
@Override
public void mouseDragged(final MouseEvent me, final int x_p, final int y_p, final int x_d, final int y_d, final int x_d_old, final int y_d_old) {
//Utils.log2("index is " + index);
if (-1 != index && current_layer == display.getLayer()) {
final Landmarks lm = m.get(current_layer);
//Utils.log2("lm is " + lm);
if (null != lm) {
lm.set(index, x_d, y_d);
Display.repaint();
}
}
}
@Override
public void mouseReleased(final MouseEvent me, final int x_p, final int y_p, final int x_d, final int y_d, final int x_r, final int y_r) {
// Do nothing
}
@Override
public void undoOneStep() {}
@Override
public void redoOneStep() {}
@Override
public boolean apply() {
// Check there's more than one layer
if (m.size() < 2) {
Utils.showMessage("Need more than one layer to align!");
return false;
}
// Check that the current layer is one of the layers with landmarks.
// Will be used as reference
final Layer ref_layer = display.getLayer();
if (null == m.get(ref_layer)) {
Utils.showMessage("Please scroll to a layer with landmarks,\nto be used as reference.");
return false;
}
// Check that all layers have the same number of landmarks
int n_landmarks = -1;
for (final Map.Entry<Layer,Landmarks> e : m.entrySet()) {
final Landmarks lm = e.getValue();
if (-1 == n_landmarks) {
n_landmarks = lm.points.size();
continue;
}
if (n_landmarks != lm.points.size()) {
Utils.showMessage("Can't apply: there are different amounts of landmarks per layer.\nSee the log window.");
for (final Map.Entry<Layer,Landmarks> ee : m.entrySet()) {
Utils.log(ee.getValue().points.size() + " landmarks in layer " + ee.getKey());
}
return false;
}
}
// Sort Layers by Z
final TreeMap<Layer,Landmarks> sorted = new TreeMap<Layer,Landmarks>(new Comparator<Layer>() {
@Override
public boolean equals(final Object ob) {
return this == ob;
}
@Override
public int compare(final Layer l1, final Layer l2) {
// Ascending order
final double dz = l1.getZ() - l2.getZ();
if (dz < 0) return -1;
else if (dz > 0) return 1;
else return 0;
}
});
sorted.putAll(m);
int iref = 0;
for (final Layer la : sorted.keySet()) {
if (la != ref_layer) iref++;
else break;
}
// Ok, now ask for a model
final GenericDialog gd = new GenericDialog("Model");
gd.addChoice("Model:", Align.Param.modelStrings, Align.Param.modelStrings[1]);
gd.addCheckbox("Propagate to first layer", 0 != iref);
((Component)gd.getCheckboxes().get(0)).setEnabled(0 != iref);
gd.addCheckbox("Propagate to last layer", sorted.size()-1 != iref);
((Component)gd.getCheckboxes().get(1)).setEnabled(sorted.size()-1 != iref);
gd.showDialog();
if (gd.wasCanceled()) return false;
final int model_index = gd.getNextChoiceIndex();
final boolean propagate_to_first = gd.getNextBoolean();
final boolean propagate_to_last = gd.getNextBoolean();
int min;
// Create a model as desired
final AbstractAffineModel2D< ? > model;
switch ( model_index ) {
case 0:
min = 1;
model = new TranslationModel2D();
break;
case 1:
min = 2;
model = new RigidModel2D();
break;
case 2:
min = 2;
model = new SimilarityModel2D();
break;
case 3:
min = 3;
model = new AffineModel2D();
break;
default:
Utils.log("Unknown model index!");
return false;
}
if (n_landmarks < min) {
Utils.showMessage("Need at least " + min + " landmarks for a " + Align.Param.modelStrings[model_index] + " model");
return false;
}
Bureaucrat.createAndStart(new Worker.Task("Aligning layers with landmarks") {
@Override
public void exec() {
// Find layers with landmarks, in increasing Z.
// Match in pairs.
// So, get two submaps: from ref_layer to first, and from ref_layer to last
final SortedMap<Layer,Landmarks> first_chunk_ = new TreeMap<Layer,Landmarks>(sorted.headMap(ref_layer)); // strictly lower Z than ref_layer
first_chunk_.put(ref_layer, m.get(ref_layer)); // .. so add ref_layer
final SortedMap<Layer,Landmarks> second_chunk = sorted.tailMap(ref_layer); // equal or larger Z than ref_layer
final SortedMap<Layer,Landmarks> first_chunk;
// Reverse order of first_chunk
if (first_chunk_.size() > 1) {
final SortedMap<Layer,Landmarks> fc = new TreeMap<Layer,Landmarks>(new Comparator<Layer>() {
@Override
public boolean equals(final Object ob) {
return this == ob;
}
@Override
public int compare(final Layer l1, final Layer l2) {
// Descending order
final double dz = l2.getZ() - l1.getZ();
if (dz < 0) return -1;
else if (dz > 0) return 1;
else return 0;
}
});
fc.putAll(first_chunk_);
first_chunk = fc;
} else {
first_chunk = first_chunk_;
}
final LayerSet ls = ref_layer.getParent();
final Collection<Layer> affected_layers = new HashSet<Layer>(m.keySet());
// Gather all Patch instances that will be affected
final ArrayList<Patch> patches = new ArrayList<Patch>();
for (final Layer la : m.keySet()) patches.addAll(la.getAll(Patch.class));
if (propagate_to_first && first_chunk.size() > 1) {
final Collection<Layer> affected = ls.getLayers().subList(0, ls.indexOf(first_chunk.lastKey()));
for (final Layer la : affected) {
patches.addAll(la.getAll(Patch.class));
}
affected_layers.addAll(affected);
}
if (propagate_to_last && second_chunk.size() > 1) {
final Collection<Layer> affected = ls.getLayers().subList(ls.indexOf(second_chunk.lastKey()) + 1, ls.size());
for (final Layer la : affected) {
patches.addAll(la.getAll(Patch.class));
}
}
// Transform segmentations along with patches
AlignTask.transformPatchesAndVectorData(patches, new Runnable() { @Override
public void run() {
// Apply!
// TODO: when adding non-linear transforms, use this single line for undo instead of all below:
// (these transforms may be non-linear as well, which alter mipmaps.)
//ls.addTransformStepWithData(affected_layers);
// Setup undo:
// Find all images in the range of affected layers,
// plus all Displayable of those layers (but Patch instances in a separate DoTransforms step,
// to avoid adding a "data" undo for them, which would recreate mipmaps when undone).
// plus all ZDisplayable that paint in those layers
final HashSet<Displayable> ds = new HashSet<Displayable>();
final ArrayList<Displayable> patches = new ArrayList<Displayable>();
for (final Layer layer : affected_layers) {
for (final Displayable d : layer.getDisplayables()) {
if (d.getClass() == Patch.class) {
patches.add(d);
} else {
ds.add(d);
}
}
}
for (final ZDisplayable zd : ls.getZDisplayables()) {
for (final Layer layer : affected_layers) {
if (zd.paintsAt(layer)) {
ds.add((Displayable)zd);
break;
}
}
}
if (ds.size() > 0) {
final Displayable.DoEdits step = ls.addTransformStepWithData(ds);
if (patches.size() > 0) {
final ArrayList<DoStep> a = new ArrayList<DoStep>();
a.add(new Displayable.DoTransforms().addAll(patches));
step.addDependents(a);
}
}
if (first_chunk.size() > 1) {
final AffineTransform aff = align(first_chunk, model);
if (propagate_to_first) {
for (final Layer la : ls.getLayers().subList(0, ls.indexOf(first_chunk.lastKey()))) { // exclusive last
la.apply(Patch.class, aff);
}
}
}
if (second_chunk.size() > 1) {
final AffineTransform aff = align(second_chunk, model);
if (propagate_to_last) {
for (final Layer la : ls.getLayers().subList(ls.indexOf(second_chunk.lastKey()) + 1, ls.size())) { // exclusive last
la.apply(Patch.class, aff);
}
}
}
Display.repaint();
// Store current state
if (ds.size() > 0) {
final Displayable.DoEdits step2 = ls.addTransformStepWithData(ds);
if (patches.size() > 0) {
final ArrayList<DoStep> a2 = new ArrayList<DoStep>();
a2.add(new Displayable.DoTransforms().addAll(patches));
step2.addDependents(a2);
}
}
}});
}}, display.getProject());
return true;
}
private AffineTransform align(final SortedMap<Layer,Landmarks> sm, final AbstractAffineModel2D< ? > model) {
Layer layer1 = sm.firstKey();
Landmarks lm1 = sm.get(sm.firstKey());
final AffineTransform accum = new AffineTransform();
for (final Map.Entry<Layer,Landmarks> e : sm.entrySet()) {
final Layer layer2 = e.getKey();
if (layer1 == layer2) continue;
final Landmarks lm2 = e.getValue();
// Create pointmatches
final ArrayList<PointMatch> matches = new ArrayList<PointMatch>();
for (int i=0; i<lm1.points.size(); i++) {
matches.add(new PointMatch(lm2.points.get(i), lm1.points.get(i)));
}
final AbstractAffineModel2D< ? > mod = model.copy();
try {
mod.fit(matches);
} catch (final Throwable t) {
IJError.print(t);
// continue happily
}
accum.preConcatenate(mod.createAffine());
layer2.apply(Patch.class, accum);
layer1 = layer2;
lm1 = lm2;
}
return accum;
}
@Override
public boolean cancel() {
return true;
}
@Override
public Rectangle getRepaintBounds() {
return (Rectangle) display.getCanvas().getSrcRect().clone();
}
@Override
public void srcRectUpdated(final Rectangle srcRect, final double magnification) {}
@Override
public void magnificationUpdated(final Rectangle srcRect, final double magnification) {}
/** Export landmarks into XML file, in patch coordinates. */
public boolean exportLandmarks() {
if (m.isEmpty()) {
Utils.log("No landmarks to export!");
return false;
}
final StringBuilder sb = new StringBuilder("<landmarks>\n");
for (final Map.Entry<Layer,Landmarks> e : new TreeMap<Layer,Landmarks>(m).entrySet()) { // sorted by Layer z
sb.append(" <layer id=\"").append(e.getKey().getId()).append("\">\n");
for (final Point p : e.getValue().points) {
final double[] w = p.getW();
double x = w[0],
y = w[1];
// Find the point in a patch, and inverseTransform it into the patch local coords
final Collection<Displayable> under = e.getKey().find(Patch.class, (int)x, (int)y, true);
if (!under.isEmpty()) {
final Patch patch = (Patch)under.iterator().next();
final Point2D.Double po = patch.inverseTransformPoint(x, y);
x = po.x;
y = po.y;
sb.append(" <point patch_id=\"").append(patch.getId()).append('\"');
} else {
// Store the point as absolute
sb.append(" <point ");
}
sb.append(" x=\"").append(x).append("\" y=\"").append(y).append("\" />\n");
}
sb.append(" </layer>\n");
}
sb.append("</landmarks>");
final File f = Utils.chooseFile(null, "landmarks", ".xml");
if (null != f && Utils.saveToFile(f, sb.toString())) {
return true;
}
return false;
}
/** Import landmarks from XML file. */
public boolean importLandmarks() {
if (!this.m.isEmpty() && !Utils.checkYN("Remove current landmarks and import new landmarks from a file?")) return false;
// copy for restore in case of failure:
final HashMap<Layer,Landmarks> current = new HashMap<Layer,Landmarks>(this.m);
InputStream istream = null;
try {
final String[] fs = Utils.selectFile("Choose landmarks XML file");
if (null == fs || null == fs[0] || null == fs[1]) return false;
final File f = new File(fs[0] + fs[1]);
if (!f.exists() || !f.canRead()) {
Utils.log("ERROR: cannot read file at " + f.getAbsolutePath());
return false;
}
// Clear current landmarks and parse from file:
this.m.clear();
final SAXParserFactory ft = SAXParserFactory.newInstance();
ft.setValidating(false);
final SAXParser parser = ft.newSAXParser();
istream = Utils.createStream(fs[0] + fs[1]);
parser.parse(new InputSource(istream), new LandmarksParser());
// Warn on inconsistencies
final HashSet<Integer> sizes = new HashSet<Integer>();
for (final Landmarks lm : this.m.values()) {
sizes.add(lm.points.size());
}
if (sizes.size() > 1) {
Utils.log("WARNING: different number of landmarks in at least one layer.");
}
Display.repaint();
} catch (final Throwable t) {
IJError.print(t);
Utils.log("ERROR: did not import any landmarks.");
this.m.clear();
this.m.putAll(current);
return false;
} finally {
try {
if (null != istream) istream.close();
} catch (final Exception e) {}
}
return true;
}
private final class LandmarksParser extends DefaultHandler {
Layer layer = null;
@Override
public void startElement(final String uri, final String localName, final String qName, final Attributes attributes) {
final HashMap<String,String> a = new HashMap<String,String>();
for (int i=attributes.getLength() -1; i>-1; i--) {
a.put(attributes.getQName(i).toLowerCase(), attributes.getValue(i));
}
final String tag = qName.toLowerCase();
if ("point".equals(tag)) {
if (null == this.layer) return; // ignore!
final String sid = a.get("patch_id");
final String sX = a.get("x");
final String sY = a.get("y");
if (null == sX || null == sY) {
Utils.log("ERROR: ignoring point with x, y : " + sX + ", " + sY);
return;
}
if (null == sid) {
// Assume absolute coords
addPoint(layer, Double.parseDouble(sX), Double.parseDouble(sY));
} else {
// Find the patch
final Patch p = (Patch)layer.findById(Long.parseLong(sid));
if (null == p) {
Utils.log("ERROR: ignoring point for layer " + layer + "\n Reason: could not find Patch with id " + sid);
return;
}
// ... and add the point transformed to world
final Point2D.Double po = p.transformPoint(Double.parseDouble(sX), Double.parseDouble(sY));
addPoint(layer, (float)po.x, (float)po.y);
}
} else if ("layer".equals(tag)) {
final String sid = a.get("id");
this.layer = null;
if (null == sid) {
Utils.log("ERROR: could not parse a layer that lacks an id!");
return;
}
this.layer = display.getLayerSet().getLayer(Long.parseLong(sid));
if (null == this.layer) {
Utils.log("ERROR: could not find layer with id " + sid);
return;
}
}
}
@Override
public void endElement(final String namespace_URI, final String local_name, final String qualified_name) {
if ("layer".equals(qualified_name)) {
final Landmarks lm = m.get(this.layer);
Utils.log("Loaded " + lm.points.size() + " landmarks for layer " + (layer.getParent().indexOf(layer) + 1) + ": " + layer);
}
}
}
}