001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package org.openimaj.vis.ternary; 031 032import java.text.AttributedCharacterIterator.Attribute; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.Comparator; 036import java.util.HashMap; 037import java.util.Iterator; 038import java.util.List; 039import java.util.Map; 040 041import org.openimaj.feature.DoubleFV; 042import org.openimaj.feature.DoubleFVComparison; 043import org.openimaj.image.DisplayUtilities; 044import org.openimaj.image.MBFImage; 045import org.openimaj.image.colour.ColourMap; 046import org.openimaj.image.colour.ColourSpace; 047import org.openimaj.image.colour.RGBColour; 048import org.openimaj.image.typography.FontRenderer; 049import org.openimaj.image.typography.FontStyle; 050import org.openimaj.image.typography.FontStyle.HorizontalAlignment; 051import org.openimaj.image.typography.FontStyle.VerticalAlignment; 052import org.openimaj.math.geometry.line.Line2d; 053import org.openimaj.math.geometry.point.Point2d; 054import org.openimaj.math.geometry.point.Point2dImpl; 055import org.openimaj.math.geometry.shape.Rectangle; 056import org.openimaj.math.geometry.shape.Triangle; 057import org.openimaj.math.geometry.transforms.TransformUtilities; 058import org.openimaj.math.geometry.triangulation.DelaunayTriangulator; 059import org.openimaj.math.util.Interpolation; 060import org.openimaj.util.pair.IndependentPair; 061 062/** 063 * A ternary plot draws a triangle simplex. The values of the triangle are 064 * interpolated from a few {@link TernaryData} points provided. 065 * 066 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 067 */ 068public class TernaryPlot { 069 private static final float ONE_OVER_ROOT3 = (float) (1f / Math.sqrt(3)); 070 071 /** 072 * Holds an a value for the 3 ternary dimensions and a value 073 * 074 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 075 */ 076 public static class TernaryData extends DoubleFV { 077 /** 078 * 079 */ 080 private static final long serialVersionUID = 4560404458888209082L; 081 082 /** 083 * @param a 084 * @param b 085 * @param c 086 * @param value 087 */ 088 public TernaryData(float a, float b, float c, float value) { 089 this.values = new double[] { a, b, c }; 090 this.value = value; 091 092 } 093 094 /** 095 * @return the ternary point projected into 2D 096 */ 097 public Point2d asPoint() { 098 final double a = this.values[0]; 099 final double b = this.values[1]; 100 final double c = this.values[2]; 101 final double x = 0.5 * (2 * b + c) / (a + b + c); 102 final double y = (Math.sqrt(3) / 2) * (c) / (a + b + c); 103 104 return new Point2dImpl((float) x, (float) y); 105 } 106 107 /** 108 * the value at a,b,c 109 */ 110 public float value; 111 112 @Override 113 public int hashCode() { 114 return Arrays.hashCode(values); 115 } 116 117 @Override 118 public boolean equals(Object obj) { 119 return obj instanceof TernaryData && this.hashCode() == obj.hashCode() 120 && this.value == ((TernaryData) obj).value; 121 } 122 } 123 124 /** 125 * A hash of triangles created from a list of 126 * 127 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 128 */ 129 private static class TrenaryDataTriangles { 130 private HashMap<Triangle, List<TernaryData>> triToData; 131 private HashMap<Point2d, TernaryData> pointToTre; 132 133 public TrenaryDataTriangles(List<TernaryData> data) { 134 this.pointToTre = new HashMap<Point2d, TernaryData>(); 135 for (final TernaryData trenaryData : data) { 136 pointToTre.put(trenaryData.asPoint(), trenaryData); 137 } 138 this.triToData = new HashMap<Triangle, List<TernaryData>>(); 139 final List<Triangle> triangles = DelaunayTriangulator 140 .triangulate(new ArrayList<Point2d>(pointToTre.keySet())); 141 for (final Triangle triangle : triangles) { 142 final List<TernaryData> triangleData = new ArrayList<TernaryData>(); 143 triangleData.add(pointToTre.get(triangle.vertices[0])); 144 triangleData.add(pointToTre.get(triangle.vertices[1])); 145 triangleData.add(pointToTre.get(triangle.vertices[2])); 146 147 triToData.put(triangle, triangleData); 148 } 149 } 150 151 public Triangle getHoldingTriangle(Point2d point) { 152 for (final Triangle t : this.triToData.keySet()) { 153 if (t.isInsideOnLine(point)) { 154 return t; 155 } 156 } 157 return null; 158 } 159 160 public TernaryData getPointData(Point2d point) { 161 return pointToTre.get(point); 162 } 163 } 164 165 private Triangle tri; 166 private float height; 167 private float width; 168 private List<TernaryData> data; 169 private Point2dImpl pointA; 170 private Point2dImpl pointB; 171 private Point2dImpl pointC; 172 private TrenaryDataTriangles dataTriangles; 173 174 /** 175 * @param width 176 * @param data 177 */ 178 public TernaryPlot(float width, List<TernaryData> data) { 179 this.width = width; 180 this.height = (float) Math.sqrt((width * width) - ((width * width) / 4)); 181 pointA = new Point2dImpl(0, height); 182 pointB = new Point2dImpl(width, height); 183 pointC = new Point2dImpl(width / 2, 0); 184 185 this.tri = new Triangle(new Point2d[] { 186 pointA, 187 pointB, 188 pointC, 189 }); 190 191 this.data = data; 192 if (data.size() > 2) { 193 this.dataTriangles = new TrenaryDataTriangles(data); 194 } 195 } 196 197 /** 198 * @return {@link #draw(TernaryParams)} with the defaults of 199 * {@link TernaryParams} 200 */ 201 public MBFImage draw() { 202 return draw(new TernaryParams()); 203 204 } 205 206 /** 207 * @param params 208 * @return draw the plot 209 */ 210 public MBFImage draw(TernaryParams params) { 211 212 final int padding = (Integer) params.getTyped(TernaryParams.PADDING); 213 final Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR); 214 215 final MBFImage ret = new MBFImage((int) width + padding * 2, (int) height + padding * 2, ColourSpace.RGB); 216 ret.fill(bgColour); 217 drawTernaryPlot(ret, params); 218 drawTriangle(ret, params); 219 drawBorder(ret, params); 220 drawScale(ret, params); 221 drawLabels(ret, params); 222 223 return ret; 224 } 225 226 private void drawScale(MBFImage ret, TernaryParams params) { 227 final boolean drawScale = (Boolean) params.getTyped(TernaryParams.DRAW_SCALE); 228 if (!drawScale) 229 return; 230 231 final Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.SCALE_FONT); 232 final FontStyle<Float[]> fs = FontStyle.parseAttributes(typed, ret.createRenderer()); 233 234 final int padding = (Integer) params.getTyped(TernaryParams.PADDING); 235 final ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP); 236 final Rectangle r = ret.getBounds(); 237 r.width = r.width / 2.f; 238 r.height = r.height * 2.f; 239 r.scale(0.15f); 240 r.x = width * TernaryParams.TOP_RIGHT_X; 241 r.y = height * TernaryParams.TOP_RIGHT_Y; 242 r.translate(padding, padding); 243 ret.drawShape(r, 2, RGBColour.BLACK); 244 for (float i = r.y; i < r.y + r.height; i++) { 245 final Float[] col = cm.apply(((i - r.y) / r.height)); 246 ret.drawLine((int) r.x, (int) i, (int) (r.x + r.width), (int) i, col); 247 } 248 fs.setVerticalAlignment(VerticalAlignment.VERTICAL_BOTTOM); 249 final String minText = params.getTyped(TernaryParams.SCALE_MIN); 250 ret.drawText(minText, (int) r.x - 3, (int) (r.y + r.height), fs); 251 fs.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP); 252 final String maxText = params.getTyped(TernaryParams.SCALE_MAX); 253 ret.drawText(maxText, (int) r.x - 3, (int) r.y, fs); 254 } 255 256 private void drawBorder(MBFImage ret, TernaryParams params) { 257 final int padding = (Integer) params.getTyped(TernaryParams.PADDING); 258 final boolean drawTicks = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICKS); 259 final Map<Attribute, Object> fontParams = params.getTyped(TernaryParams.TICK_FONT); 260 final FontStyle<Float[]> style = FontStyle.parseAttributes(fontParams, ret.createRenderer()); 261 if (drawTicks) { 262 final Triangle drawTri = tri.transform(TransformUtilities.translateMatrix(padding, padding)); 263 264 for (int i = 0; i < 3; i++) { 265 int paddingx = 0; 266 int paddingy = 0; 267 switch (i) { 268 case 0: 269 // the bottom line 270 style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_CENTER); 271 style.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP); 272 paddingy = 5; 273 break; 274 case 1: 275 // the right line 276 style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_LEFT); 277 style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF); 278 paddingx = 5; 279 paddingy = -5; 280 break; 281 case 2: 282 // the left line 283 style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_RIGHT); 284 style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF); 285 paddingx = -5; 286 paddingy = -5; 287 break; 288 } 289 final Point2d start = drawTri.vertices[i]; 290 final Point2d end = drawTri.vertices[(i + 1) % 3]; 291 final int nTicks = 10; 292 for (int j = 0; j < nTicks + 1; j++) { 293 Line2d tickLine = new Line2d(start, end); 294 final double length = tickLine.calculateLength(); 295 // bring its end to the correct position 296 double desired = length - j * (length / nTicks); 297 if (desired == 0) 298 desired = 0.001; 299 double scale = desired / length; 300 // double overallScale = scale; 301 tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, start)); 302 // make it 10 pixels long 303 scale = 5f / tickLine.calculateLength(); 304 tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, tickLine.end)); 305 // Now rotate it by 90 degrees 306 tickLine = tickLine.transform(TransformUtilities.rotationMatrixAboutPoint(-Math.PI / 2, 307 tickLine.end.getX(), tickLine.end.getY())); 308 final int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICK_THICKNESS); 309 final Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR); 310 ret.drawLine(tickLine, thickness, col); 311 312 final Point2d textPoint = tickLine.begin.copy(); 313 textPoint.translate(paddingx, paddingy); 314 // ret.drawText(String.format("%2.2f",overallScale), 315 // textPoint, style); 316 } 317 318 } 319 } 320 } 321 322 private void drawTriangle(MBFImage ret, TernaryParams params) { 323 final int padding = (Integer) params.getTyped(TernaryParams.PADDING); 324 final boolean drawTriangle = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER); 325 if (drawTriangle) { 326 final int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_THICKNESS); 327 final Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR); 328 ret.drawShape(this.tri.transform(TransformUtilities.translateMatrix(padding, padding)), thickness, col); 329 } 330 } 331 332 private void drawLabels(MBFImage ret, TernaryParams params) { 333 final int padding = (Integer) params.getTyped(TernaryParams.PADDING); 334 final List<IndependentPair<TernaryData, String>> labels = params.getTyped(TernaryParams.LABELS); 335 final Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.LABEL_FONT); 336 final FontStyle<Float[]> fs = FontStyle.parseAttributes(typed, ret.createRenderer()); 337 final Float[] labelBackground = params.getTyped(TernaryParams.LABEL_BACKGROUND); 338 final Float[] labelBorder = params.getTyped(TernaryParams.LABEL_BORDER); 339 final int labelPadding = (Integer) params.getTyped(TernaryParams.LABEL_PADDING); 340 final FontRenderer<Float[], FontStyle<Float[]>> fontRenderer = fs.getRenderer(ret.createRenderer()); 341 if (labels != null) { 342 for (final IndependentPair<TernaryData, String> labelPoint : labels) { 343 final TernaryData ternaryData = labelPoint.firstObject(); 344 final Point2d point = ternaryData.asPoint(); 345 point.setX(point.getX() * width + padding); 346 point.setY(height - (point.getY() * width) + padding); 347 final Point2d p = point.copy(); 348 if (point.getY() < height / 2) { 349 point.setY(point.getY() - 10); 350 } 351 else { 352 point.setY(point.getY() + 35); 353 } 354 final Rectangle rect = fontRenderer.getBounds(labelPoint.getSecondObject(), (int) point.getX(), 355 (int) point.getY(), fs); 356 rect.x -= labelPadding; 357 rect.y -= labelPadding; 358 rect.width += labelPadding * 2; 359 rect.height += labelPadding * 2; 360 if (labelBackground != null) { 361 ret.drawShapeFilled(rect, labelBackground); 362 } 363 if (labelBorder != null) { 364 ret.drawShape(rect, labelBorder); 365 } 366 ret.drawText(labelPoint.getSecondObject(), point, fs); 367 ret.drawPoint(p, RGBColour.RED, (int) ternaryData.value); 368 } 369 } 370 } 371 372 private void drawTernaryPlot(MBFImage ret, TernaryParams params) { 373 final ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP); 374 final int padding = (Integer) params.getTyped(TernaryParams.PADDING); 375 final Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR); 376 for (int y = 0; y < height + padding; y++) { 377 for (int x = 0; x < width + padding; x++) { 378 final int xp = x - padding; 379 final int yp = y - padding; 380 final Point2dImpl point = new Point2dImpl(xp, yp); 381 if (this.tri.isInside(point)) { 382 final TernaryData closest = weightThreeClosest(point); 383 Float[] apply = null; 384 if (cm != null) 385 apply = cm.apply(1 - closest.value); 386 else { 387 apply = new Float[] { closest.value, closest.value, closest.value }; 388 } 389 390 ret.setPixel(x, y, apply); 391 } 392 else { 393 ret.setPixel(x, y, bgColour); 394 } 395 } 396 } 397 } 398 399 /** 400 * @return draw the triangles generated from the data 401 */ 402 public MBFImage drawTriangles() { 403 final MBFImage img = new MBFImage((int) width, (int) height, ColourSpace.RGB); 404 for (final Triangle tri : this.dataTriangles.triToData.keySet()) { 405 img.drawShape(tri.transform(TransformUtilities.scaleMatrix(width, height)), RGBColour.RED); 406 } 407 return img; 408 } 409 410 class DistanceToPointComparator implements Comparator<TernaryData> { 411 412 private TernaryData terneryPoint; 413 414 public DistanceToPointComparator(TernaryData point) { 415 416 this.terneryPoint = point; 417 } 418 419 @Override 420 public int compare(TernaryData o1, TernaryData o2) { 421 final double o1d = DoubleFVComparison.EUCLIDEAN.compare(o1, this.terneryPoint); 422 final double o2d = DoubleFVComparison.EUCLIDEAN.compare(o2, this.terneryPoint); 423 return Double.compare(o1d, o2d); 424 } 425 426 } 427 428 private float calcBfromXY(float xn, float yn) { 429 return xn - ONE_OVER_ROOT3 * yn; 430 } 431 432 private float calcCfromXY(float xn, float yn) { 433 return 2 * ONE_OVER_ROOT3 * yn; 434 } 435 436 private float calcAfromXY(float xn, float yn) { 437 return 1f - xn - ONE_OVER_ROOT3 * yn; 438 } 439 440 private TernaryData weightThreeClosest(Point2dImpl point) { 441 final float xn = (point.x - pointA.x) / width; 442 final float yn = (pointA.y - point.y) / width; 443 444 final float a = calcAfromXY(xn, yn); 445 final float b = calcBfromXY(xn, yn); 446 final float c = calcCfromXY(xn, yn); 447 final TernaryData trenData = new TernaryData(a, b, c, 0f); 448 if (data.size() == 1) { 449 return data.get(0); 450 } else if (data.size() == 2) { 451 final TernaryData tpa = data.get(0); 452 final TernaryData tpb = data.get(1); 453 final double da = DoubleFVComparison.EUCLIDEAN.compare(tpa, trenData); 454 final double db = DoubleFVComparison.EUCLIDEAN.compare(tpb, trenData); 455 final double sumd = da + db; 456 trenData.value = (float) ((1 - (da / sumd)) * tpa.value + (1 - (db / sumd)) * tpb.value); 457 } 458 else { 459 final Triangle t = dataTriangles.getHoldingTriangle(new Point2dImpl(xn, yn)); 460 if (t == null) { 461 return new TernaryData(a, b, c, 0f); 462 } 463 final Map<Line2d, Point2d> points = t.intersectionSides( 464 new Line2d( 465 new Point2dImpl(0, yn), 466 new Point2dImpl(1, yn) 467 ) 468 ); 469 470 if (points.size() == 2) { 471 final Iterator<Line2d> liter = points.keySet().iterator(); 472 final Line2d l1 = liter.next(); 473 final Line2d l2 = liter.next(); 474 final Point2d p1 = points.get(l1); 475 final Point2d p2 = points.get(l2); 476 477 final double p1Value = linePointInterp(l1, p1); 478 final double p2Value = linePointInterp(l2, p2); 479 480 final double pointValue = linePointInterp(new Line2d(p1, p2), new Point2dImpl(xn, yn), p1Value, p2Value); 481 482 // if((l1.begin.getX() == l1.end.getX() || l2.begin.getX() == 483 // l2.end.getX() ) && pointValue <0.5){ 484 // System.out.println("A vertical line created a 0 value"); 485 // } 486 487 trenData.value = (float) pointValue; 488 489 } 490 else { // 0, 1 or more than 2 491 System.out.println("Found 3 or 0 lines: " + points.size()); 492 return new TernaryData(a, b, c, 0f); 493 } 494 } 495 return trenData; 496 } 497 498 private double linePointInterp(Line2d line, Point2d point) { 499 final TernaryData l1p1data = dataTriangles.getPointData(line.begin); 500 final TernaryData l1p2data = dataTriangles.getPointData(line.end); 501 final float l1p1datav = l1p1data.value; 502 final float l1p2datav = l1p2data.value; 503 504 return linePointInterp(line, point, l1p1datav, l1p2datav); 505 } 506 507 private double linePointInterp(Line2d line, Point2d point, double lineBeginValue, double lineEndValue) { 508 final double l1Len = line.calculateLength(); 509 final double l1Prop = Line2d.distance(line.begin, point); 510 final double p1Value = Interpolation.lerp(l1Prop, 0, lineBeginValue, l1Len, lineEndValue); 511 return p1Value; 512 } 513 514 /** 515 * @param args 516 */ 517 public static void main(String[] args) { 518 final List<TernaryData> data = new ArrayList<TernaryData>(); 519 data.add(new TernaryData(1 / 3f + 0.1f, 1 / 3f - 0.1f, 1 / 3f, 0.8f)); 520 data.add(new TernaryData(1 / 3f - 0.1f, 1 / 3f + 0.1f, 1 / 3f, 0.2f)); 521 data.add(new TernaryData(1f, 0, 0, 0)); 522 data.add(new TernaryData(0, 1f, 0, 0)); 523 data.add(new TernaryData(0, 0, 1f, 0)); 524 final TernaryPlot plot = new TernaryPlot(500, data); 525 DisplayUtilities.display(plot.draw()); 526 DisplayUtilities.display(plot.drawTriangles()); 527 } 528 529}