/**
* Copyright (c) 2013 Oculus Info Inc.
* http://www.oculusinfo.com/
*
* Released under the MIT License.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package spimedb.cluster.tracks;
import junit.framework.Assert;
import org.junit.Ignore;
import org.junit.Test;
import spimedb.cluster.DataSet;
import spimedb.cluster.Instance;
import spimedb.cluster.feature.spatial.TrackFeature;
import spimedb.cluster.feature.spatial.centroid.TrackCentroid;
import spimedb.cluster.feature.spatial.distance.TrackDistance;
import spimedb.cluster.unsupervised.cluster.Cluster;
import spimedb.cluster.unsupervised.cluster.ClusterResult;
import spimedb.cluster.unsupervised.cluster.kmeans.KMeans;
import spimedb.cluster.validation.unsupervised.external.BCubed;
import spimedb.util.geom.cartesian.CubicBSpline;
import spimedb.util.geom.geodesic.Position;
import spimedb.util.geom.geodesic.PositionCalculationParameters;
import spimedb.util.geom.geodesic.PositionCalculationType;
import spimedb.util.geom.geodesic.Track;
import spimedb.util.geom.geodesic.tracks.Cartesian3DTrack;
import spimedb.util.geom.geodesic.tracks.GeodeticTrack;
import spimedb.util.math.linearalgebra.Vector;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.logging.Logger;
public class TrackClusteringTests {
private static final Logger LOGGER = Logger.getLogger(TrackClusteringTests.class.getName());
private static final PositionCalculationParameters GEODETIC_PARAMETERS =
new PositionCalculationParameters(PositionCalculationType.Geodetic, 0.01, 1E-10, false);
private static final PositionCalculationParameters CARTESIAN_PARAMETERS =
new PositionCalculationParameters(PositionCalculationType.Cartesian3D, 0.01, 1E-10, false);
@Test
public void testIncrementalMean () {
TrackCentroid centroid = new TrackCentroid();
Track A = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 0, 0, true),
new Position(6000002, 0, 0, true),
new Position(6000003, 1, 0, true));
TrackFeature fA = new TrackFeature("a");
fA.setValue(A);
Track B = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 1, 0, true),
new Position(6000002, 1, 0, true),
new Position(6000003, 2, 0, true));
TrackFeature fB = new TrackFeature("b");
fB.setValue(B);
Track C = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 2, 0, true),
new Position(6000002, 2, 0, true),
new Position(6000003, 3, 0, true));
TrackFeature fC = new TrackFeature("c");
fC.setValue(C);
Track AB = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 0.5, 0, true),
new Position(6000002, 0.5, 0, true),
new Position(6000003, 1.5, 0, true));
Track BC = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 1.5, 0, true),
new Position(6000002, 1.5, 0, true),
new Position(6000003, 2.5, 0, true));
centroid.add(fA);
Assert.assertEquals(A, centroid.getCentroid().getValue());
centroid.add(fB);
Assert.assertEquals(AB, centroid.getCentroid().getValue());
centroid.add(fC);
Assert.assertEquals(B, centroid.getCentroid().getValue());
centroid.remove(fA);
Assert.assertEquals(BC, centroid.getCentroid().getValue());
centroid.remove(fB);
Assert.assertEquals(C, centroid.getCentroid().getValue());
centroid.remove(fC);
Assert.assertNull(centroid.getCentroid().getValue());
}
@Test
public void testIncrementalMeansWithOnePointTrack () {
TrackCentroid centroid = new TrackCentroid();
Track A = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 0, 0, true),
new Position(6000002, 0, 0, true),
new Position(6000003, 0, 0, true));
TrackFeature fA = new TrackFeature("a");
fA.setValue(A);
Track B = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000002, 3, 0, true));
TrackFeature fB = new TrackFeature("b");
fB.setValue(B);
Track C = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000000, 0, 0, true),
new Position(6000002, 0, 0, true),
new Position(6000004, 0, 0, true));
TrackFeature fC = new TrackFeature("c");
fC.setValue(C);
Track AB = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001.5, 1.5, 0, true),
new Position(6000002.0, 1.5, 0, true),
new Position(6000002.5, 1.5, 0, true));
Track BC = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001, 1.5, 0, true),
new Position(6000002, 1.5, 0, true),
new Position(6000003, 1.5, 0, true));
Track ABC = new Cartesian3DTrack(CARTESIAN_PARAMETERS,
new Position(6000001.0, 1.0, 0, true),
new Position(6000002.0, 1.0, 0, true),
new Position(6000003.0, 1.0, 0, true));
centroid.add(fA);
Assert.assertEquals(A, centroid.getCentroid().getValue());
centroid.add(fB);
Assert.assertEquals(AB, centroid.getCentroid().getValue());
centroid.add(fC);
Assert.assertEquals(ABC, centroid.getCentroid().getValue());
centroid.remove(fA);
Assert.assertEquals(BC, centroid.getCentroid().getValue());
centroid.remove(fB);
Assert.assertEquals(C, centroid.getCentroid().getValue());
centroid.remove(fC);
Assert.assertNull(centroid.getCentroid().getValue());
}
@Ignore
@Test
public void interactiveClusteringTest () {
clusterRandomTracks(5, 10, 30, true);
}
public void clusterRandomTracks (int N, int T, int P, boolean visible) {
TrackFrame frame = new TrackFrame();
DataSet ds = new DataSet();
LOGGER.info("Creating "+N+" track splines as bases");
// Create N track bases
Rectangle2D bounds = new Rectangle2D.Double(12.5, 6.172, 1.521, 2.5);
frame.setDrawingBounds(bounds);
CubicBSpline[] trackBases = new CubicBSpline[N];
for (int i=0; i<N; ++i) {
trackBases[i] = randomBoundedSpline(bounds);
frame.addSpline(trackBases[i]);
}
LOGGER.info("Creating "+T+" track per track bases");
// Now create a hundred random tracks from each
List<Track> tracks = new ArrayList<Track>();
for (int j=0; j<T; ++j) {
for (int i=0; i<N; ++i) {
Track track = new GeodeticTrack(GEODETIC_PARAMETERS, randomPoints(trackBases[i], P));
// add the track as an instance to the dataset
Instance inst = new Instance();
TrackFeature feature = new TrackFeature("track");
feature.setValue(track);
inst.addFeature(feature);
inst.setClassLabel(Integer.toString(i)); // set the generating spline as the class label for verification
ds.add(inst);
tracks.add(track);
}
}
LOGGER.info("Clustering "+ds.size()+" tracks");
// Cluster the tracks in N clusters.
KMeans clusterer = new KMeans(N, 10, false);
clusterer.registerFeatureType(
"track",
TrackCentroid.class,
new TrackDistance(1.0));
ClusterResult clusterResults = clusterer.doCluster(ds);
frame.addClusters(clusterResults);
LOGGER.info("Validating track clusters");
// validate the clustering
BCubed validator = new BCubed();
validator.validate(frame.getClusters());
LOGGER.info("Precision: "+validator.getPrecision());
LOGGER.info("Recall: "+validator.getRecall());
LOGGER.info("BCubed: "+validator.getBCubed());
if (visible)
frame.showAndWait();
}
private CubicBSpline randomBoundedSpline (Rectangle2D bounds) {
double width = bounds.getWidth();
double height = bounds.getHeight();
double minX = bounds.getMinX();
double minY = bounds.getMinY();
int N = (int) Math.round(3 + Math.random() * 3);
// Get our times
double[] times = new double[N];
for (int i=0; i<N; ++i) {
if (0 == i) times[i] = 0.0;
else times[i] = times[i-1] + Math.random();
}
double totalTime = times[N-1];
List<Vector> points = new ArrayList<Vector>();
for (int i = 0; i < N; ++i) {
times[i] = times[i] / totalTime;
points.add(new Vector(minX + Math.random() * width,
minY + Math.random() * height));
}
return CubicBSpline.fit(times, points);
}
private List<Position> randomPoints (CubicBSpline basis, int N) {
N = (int) Math.round(N + N*(Math.random()-Math.random())/2.0);
double[] times = new double[N];
for (int i=0; i<N; ++i) {
if (0 == i) times[i] = 0;
else times[i] = times[i-1]+Math.random();
}
double totalTime = times[N-1];
List<Position> points = new ArrayList<Position>(N);
for (int i=1; i<N-1; ++i) {
double t = times[i]/totalTime;
Vector point = basis.getPoint(t);
points.add(new Position(point.coord(0), point.coord(1)));
}
return points;
}
private class TrackFrame extends TestFrame {
private static final long serialVersionUID = 1L;
private final List<CubicBSpline> _splines;
private final List<Cluster> _clusters;
private Rectangle2D _drawingBounds;
public TrackFrame () {
_splines = new ArrayList<CubicBSpline>();
_clusters = new ArrayList<Cluster>();
_drawingBounds = null;
}
public List<Cluster> getClusters () {
return _clusters;
}
public void setDrawingBounds (Rectangle2D bounds) {
_drawingBounds = bounds;
}
public void addSpline (CubicBSpline spline) {
_splines.add(spline);
}
public void addClusters (Iterable<Cluster> clusters) {
for (Cluster cluster: clusters) {
_clusters.add(cluster);
}
}
private Rectangle2D getDrawingBounds () {
if (null != _drawingBounds)
return _drawingBounds;
double minX = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
// For now, brute-force it; we'll put in max/min functions later.
for (CubicBSpline s: _splines) {
for (double t=0; t<=1.0; t += 0.01) {
Vector v = s.getPoint(t);
if (v.coord(0) < minX) minX = v.coord(0);
if (v.coord(0) > maxX) maxX = v.coord(0);
if (v.coord(1) < minY) minY = v.coord(1);
if (v.coord(1) > maxY) maxY = v.coord(1);
}
}
return new Rectangle2D.Double(minX, minY, maxX-minX, maxY-minY);
}
@Override
public void paint (Graphics g) {
Color[] colors = new Color[_splines.size()];
Random rnd = new Random();
// randomly associate a color with each spline
for (int i = 0; i < colors.length; i++) {
colors[i] = new Color(rnd.nextInt(255), rnd.nextInt(255), rnd.nextInt(255));
}
Graphics2D g2 = (Graphics2D) g;
Dimension size = getSize();
Rectangle2D bounds = getDrawingBounds();
bounds = new Rectangle2D.Double(bounds.getMinX()-bounds.getWidth()/10.0,
bounds.getMinY()-bounds.getHeight()/10.0,
bounds.getWidth()*1.2,
bounds.getHeight()*1.2);
double pixelsPerUnit = Math.min(size.width/bounds.getWidth(),
size.height/bounds.getHeight());
double zeroX = bounds.getMinX();
double zeroY = bounds.getMaxY();
g2.setColor(Color.WHITE);
g2.fillRect(0, 0, size.width, size.height);
// draw the ground truth splines for reference
for (int i=0; i<_splines.size(); ++i) {
g2.setColor( colors[i] );
g2.setStroke(new BasicStroke(4.0f));
CubicBSpline spline = _splines.get(i);
spline.draw(g2, zeroX, zeroY, pixelsPerUnit, 100);
}
int i = 0;
// draw the actual tracks - color by cluster they are assigned to
for (Cluster cluster : _clusters) {
Color color = colors[i];
i++;
for (Instance inst: cluster.getMembers()) {
Track track = ((TrackFeature)inst.getFeature("track")).getValue();
g2.setColor(color.darker());
g2.setStroke(new BasicStroke(1.0f));
Point ptLast = null;
List<?> points = track.getPoints();
for (int j=0; j<points.size(); ++j) {
Position p = (Position) points.get(j);
Point pt = new Point((int) Math.round((p.getLongitude()-zeroX)*pixelsPerUnit),
(int) Math.round((zeroY-p.getLatitude())*pixelsPerUnit));
if (null != ptLast)
g.drawLine(ptLast.x, ptLast.y, pt.x, pt.y);
ptLast = pt;
}
}
}
}
}
}