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 064/** 065 * A ternary plot draws a triangle simplex. The values of the triangle are interpolated from 066 * a few {@link TernaryData} points provided. 067 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 068 */ 069public class TernaryPlot { 070 private static final float ONE_OVER_ROOT3 = (float) (1f/Math.sqrt(3)); 071 /** 072 * Holds an a value for the 3 ternary dimensions and a value 073 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 074 */ 075 public static class TernaryData extends DoubleFV { 076 /** 077 * 078 */ 079 private static final long serialVersionUID = 4560404458888209082L; 080 081 /** 082 * @param a 083 * @param b 084 * @param c 085 * @param value 086 */ 087 public TernaryData(float a, float b, float c, float value) { 088 this.values = new double[]{a,b,c}; 089 this.value = value; 090 091 } 092 093 /** 094 * @return the ternary point projected into 2D 095 */ 096 public Point2d asPoint(){ 097 double a = this.values[0]; double b = this.values[1]; double c = this.values[2]; 098 double x = 0.5 * (2 * b + c ) / (a + b + c); 099 double y = (Math.sqrt(3) / 2) * ( c ) / (a + b + c); 100 101 return new Point2dImpl((float)x,(float)y); 102 } 103 104 /** 105 * the value at a,b,c 106 */ 107 public float value; 108 109 @Override 110 public int hashCode() { 111 return Arrays.hashCode(values); 112 } 113 @Override 114 public boolean equals(Object obj) { 115 return obj instanceof TernaryData && this.hashCode() == obj.hashCode() && this.value == ((TernaryData)obj).value; 116 } 117 } 118 119 /** 120 * A hash of triangles created from a list of 121 * @author Sina Samangooei (ss@ecs.soton.ac.uk) 122 */ 123 private static class TrenaryDataTriangles { 124 private HashMap<Triangle, List<TernaryData>> triToData; 125 private HashMap<Point2d, TernaryData> pointToTre; 126 127 public TrenaryDataTriangles(List<TernaryData> data){ 128 this.pointToTre = new HashMap<Point2d,TernaryData>(); 129 for (TernaryData trenaryData : data) { 130 pointToTre.put(trenaryData.asPoint(), trenaryData); 131 } 132 this.triToData = new HashMap<Triangle,List<TernaryData>>(); 133 List<Triangle> triangles = DelaunayTriangulator.triangulate(new ArrayList<Point2d>(pointToTre.keySet())); 134 for (Triangle triangle : triangles) { 135 List<TernaryData> triangleData = new ArrayList<TernaryData>(); 136 triangleData.add(pointToTre.get(triangle.vertices[0])); 137 triangleData.add(pointToTre.get(triangle.vertices[1])); 138 triangleData.add(pointToTre.get(triangle.vertices[2])); 139 140 triToData.put(triangle, triangleData); 141 } 142 } 143 144 public Triangle getHoldingTriangle(Point2d point) { 145 for (Triangle t : this.triToData.keySet()) { 146 if(t.isInsideOnLine(point)){ 147 return t; 148 } 149 } 150 return null; 151 } 152 153 public TernaryData getPointData(Point2d point) { 154 return pointToTre.get(point); 155 } 156 } 157 private Triangle tri; 158 private float height; 159 private float width; 160 private List<TernaryData> data; 161 private Point2dImpl pointA; 162 private Point2dImpl pointB; 163 private Point2dImpl pointC; 164 private TrenaryDataTriangles dataTriangles; 165 /** 166 * @param width 167 * @param data 168 */ 169 public TernaryPlot(float width, List<TernaryData> data) { 170 this.width = width; 171 this.height = (float) Math.sqrt( (width*width) - ((width*width)/4) ); 172 pointA = new Point2dImpl(0, height); 173 pointB = new Point2dImpl(width, height); 174 pointC = new Point2dImpl(width/2, 0); 175 176 177 this.tri = new Triangle(new Point2d[]{ 178 pointA, 179 pointB, 180 pointC, 181 }); 182 183 184 this.data = data; 185 if(data.size() > 2){ 186 this.dataTriangles = new TrenaryDataTriangles(data); 187 } 188 } 189 190 /** 191 * @return {@link #draw(TernaryParams)} with the defaults of {@link TernaryParams} 192 */ 193 public MBFImage draw() { 194 return draw(new TernaryParams()); 195 196 } 197 198 /** 199 * @param params 200 * @return draw the plot 201 */ 202 public MBFImage draw(TernaryParams params) { 203 204 int padding = (Integer) params.getTyped(TernaryParams.PADDING); 205 Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR); 206 207 208 MBFImage ret = new MBFImage((int)width + padding*2,(int)height + padding*2,ColourSpace.RGB); 209 ret.fill(bgColour); 210 drawTernaryPlot(ret,params); 211 drawTriangle(ret,params); 212 drawBorder(ret,params); 213 drawScale(ret,params); 214 drawLabels(ret,params); 215 216 return ret; 217 } 218 219 private void drawScale(MBFImage ret, TernaryParams params) { 220 boolean drawScale = (Boolean) params.getTyped(TernaryParams.DRAW_SCALE); 221 if(!drawScale) return; 222 223 Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.SCALE_FONT); 224 FontStyle<Float[]> fs = FontStyle.parseAttributes(typed,ret.createRenderer()); 225 226 int padding = (Integer) params.getTyped(TernaryParams.PADDING); 227 ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP); 228 Rectangle r = ret.getBounds(); 229 r.width = r.width/2.f; 230 r.height = r.height * 2.f; 231 r.scale(0.15f); 232 r.x = width * TernaryParams.TOP_RIGHT_X; 233 r.y = height * TernaryParams.TOP_RIGHT_Y; 234 r.translate(padding, padding); 235 ret.drawShape(r, 2, RGBColour.BLACK); 236 for (float i = r.y; i < r.y + r.height; i++) { 237 Float[] col = cm.apply(((i - r.y) / r.height)); 238 ret.drawLine((int)r.x, (int)i, (int)(r.x +r.width), (int)i, col); 239 } 240 fs.setVerticalAlignment(VerticalAlignment.VERTICAL_BOTTOM); 241 String minText = params.getTyped(TernaryParams.SCALE_MIN); 242 ret.drawText(minText , (int)r.x - 3, (int)(r.y + r.height), fs); 243 fs.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP); 244 String maxText = params.getTyped(TernaryParams.SCALE_MAX); 245 ret.drawText(maxText , (int)r.x - 3, (int)r.y, fs); 246 } 247 248 private void drawBorder(MBFImage ret, TernaryParams params) { 249 int padding = (Integer) params.getTyped(TernaryParams.PADDING); 250 boolean drawTicks = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICKS); 251 Map<Attribute, Object> fontParams = params.getTyped(TernaryParams.TICK_FONT); 252 FontStyle<Float[]> style = FontStyle.parseAttributes(fontParams, ret.createRenderer()); 253 if(drawTicks){ 254 Triangle drawTri = tri.transform(TransformUtilities.translateMatrix(padding, padding)); 255 256 257 for (int i = 0; i < 3; i++) { 258 int paddingx = 0; 259 int paddingy = 0; 260 switch (i){ 261 case 0: 262 // the bottom line 263 style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_CENTER); 264 style.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP); 265 paddingy = 5; 266 break; 267 case 1: 268 // the right line 269 style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_LEFT); 270 style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF); 271 paddingx = 5; 272 paddingy = -5; 273 break; 274 case 2: 275 // the left line 276 style.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_RIGHT); 277 style.setVerticalAlignment(VerticalAlignment.VERTICAL_HALF); 278 paddingx = -5; 279 paddingy = -5; 280 break; 281 } 282 Point2d start = drawTri.vertices[i]; 283 Point2d end = drawTri.vertices[(i+1) % 3]; 284 int nTicks = 10; 285 for (int j = 0; j < nTicks + 1; j++) { 286 Line2d tickLine = new Line2d(start, end); 287 double length = tickLine.calculateLength(); 288 // bring its end to the correct position 289 double desired = length - j * (length / nTicks); 290 if(desired == 0) desired = 0.001; 291 double scale = desired / length; 292 double overallScale = scale; 293 tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, start)); 294 // make it 10 pixels long 295 scale = 5f / tickLine.calculateLength(); 296 tickLine = tickLine.transform(TransformUtilities.scaleMatrixAboutPoint(scale, scale, tickLine.end)); 297 // Now rotate it by 90 degrees 298 tickLine = tickLine.transform(TransformUtilities.rotationMatrixAboutPoint(-Math.PI/2, tickLine.end.getX(), tickLine.end.getY())); 299 int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_TICK_THICKNESS); 300 Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR); 301 ret.drawLine(tickLine, thickness, col); 302 303 Point2d textPoint = tickLine.begin.copy(); 304 textPoint.translate(paddingx, paddingy); 305// ret.drawText(String.format("%2.2f",overallScale), textPoint, style); 306 } 307 308 } 309 } 310 } 311 312 private void drawTriangle(MBFImage ret,TernaryParams params) { 313 int padding = (Integer) params.getTyped(TernaryParams.PADDING); 314 boolean drawTriangle = (Boolean) params.getTyped(TernaryParams.TRIANGLE_BORDER); 315 if(drawTriangle){ 316 int thickness = (Integer) params.getTyped(TernaryParams.TRIANGLE_BORDER_THICKNESS); 317 Float[] col = params.getTyped(TernaryParams.TRIANGLE_BORDER_COLOUR); 318 ret.drawShape(this.tri.transform(TransformUtilities.translateMatrix(padding, padding)), thickness, col); 319 } 320 } 321 322 private void drawLabels( MBFImage ret, TernaryParams params) { 323 int padding = (Integer) params.getTyped(TernaryParams.PADDING); 324 List<IndependentPair<TernaryData,String>> labels = params.getTyped(TernaryParams.LABELS); 325 Map<? extends Attribute, Object> typed = params.getTyped(TernaryParams.LABEL_FONT); 326 FontStyle<Float[]> fs = FontStyle.parseAttributes(typed,ret.createRenderer()); 327 Float[] labelBackground = params.getTyped(TernaryParams.LABEL_BACKGROUND); 328 Float[] labelBorder = params.getTyped(TernaryParams.LABEL_BORDER); 329 int labelPadding = (Integer) params.getTyped(TernaryParams.LABEL_PADDING); 330 FontRenderer<Float[], FontStyle<Float[]>> fontRenderer = fs.getRenderer(ret.createRenderer()); 331 if(labels != null){ 332 for (IndependentPair<TernaryData, String> labelPoint: labels) { 333 TernaryData ternaryData = labelPoint.firstObject(); 334 Point2d point = ternaryData.asPoint(); 335 point.setX(point.getX() * width + padding ); 336 point.setY(height - (point.getY() * width ) + padding); 337 Point2d p = point.copy(); 338 if(point.getY() < height/2){ 339 point.setY(point.getY() - 10); 340 } 341 else{ 342 point.setY(point.getY() + 35); 343 } 344 Rectangle rect = fontRenderer.getBounds(labelPoint.getSecondObject(), (int)point.getX(), (int)point.getY(), fs); 345 rect.x -= labelPadding; 346 rect.y -= labelPadding; 347 rect.width += labelPadding*2; 348 rect.height += labelPadding*2; 349 if(labelBackground!=null){ 350 ret.drawShapeFilled(rect, labelBackground); 351 } 352 if(labelBorder!=null){ 353 ret.drawShape(rect, labelBorder); 354 } 355 ret.drawText(labelPoint.getSecondObject(), point, fs); 356 ret.drawPoint(p , RGBColour.RED, (int) ternaryData.value); 357 } 358 } 359 } 360 361 private void drawTernaryPlot(MBFImage ret, TernaryParams params) { 362 ColourMap cm = params.getTyped(TernaryParams.COLOUR_MAP); 363 int padding = (Integer) params.getTyped(TernaryParams.PADDING); 364 Float[] bgColour = params.getTyped(TernaryParams.BG_COLOUR); 365 for (int y = 0; y < height + padding; y++) { 366 for (int x = 0; x < width + padding; x++) { 367 int xp = x - padding; 368 int yp = y - padding; 369 Point2dImpl point = new Point2dImpl(xp,yp); 370 if (this.tri.isInside(point)){ 371 TernaryData closest = weightThreeClosest(point); 372 Float[] apply = null; 373 if(cm!=null) 374 apply = cm.apply(1-closest.value); 375 else{ 376 apply = new Float[]{closest.value,closest.value,closest.value}; 377 } 378 379 ret.setPixel(x, y, apply); 380 } 381 else{ 382 ret.setPixel(x, y, bgColour); 383 } 384 } 385 } 386 } 387 388 /** 389 * @return draw the triangles generated from the data 390 */ 391 public MBFImage drawTriangles() { 392 MBFImage img = new MBFImage((int)width,(int)height,ColourSpace.RGB); 393 for (Triangle tri : this.dataTriangles.triToData.keySet()) { 394 img.drawShape(tri.transform(TransformUtilities.scaleMatrix(width, height)), RGBColour.RED); 395 } 396 return img; 397 } 398 399 class DistanceToPointComparator implements Comparator<TernaryData>{ 400 401 402 private TernaryData terneryPoint; 403 404 public DistanceToPointComparator(TernaryData point) { 405 406 this.terneryPoint = point; 407 } 408 409 410 411 @Override 412 public int compare(TernaryData o1, TernaryData o2) { 413 double o1d = DoubleFVComparison.EUCLIDEAN.compare(o1, this.terneryPoint); 414 double o2d = DoubleFVComparison.EUCLIDEAN.compare(o2, this.terneryPoint); 415 return Double.compare(o1d, o2d); 416 } 417 418 } 419 private float calcBfromXY(float xn, float yn) { 420 return xn - ONE_OVER_ROOT3 * yn; 421 } 422 423 private float calcCfromXY(float xn, float yn) { 424 return 2 * ONE_OVER_ROOT3 * yn; 425 } 426 427 private float calcAfromXY(float xn, float yn) { 428 return 1f - xn - ONE_OVER_ROOT3 * yn; 429 } 430 private TernaryData weightThreeClosest(Point2dImpl point) { 431 float xn = (point.x - pointA.x)/width; 432 float yn = (pointA.y - point.y )/width; 433 434 float a = calcAfromXY(xn,yn); 435 float b = calcBfromXY(xn,yn); 436 float c = calcCfromXY(xn,yn); 437 TernaryData trenData = new TernaryData(a, b, c, 0f); 438 if(data.size() == 1){ 439 return data.get(0); 440 } else if (data.size() == 2){ 441 TernaryData tpa = data.get(0); 442 TernaryData tpb = data.get(1); 443 double da = DoubleFVComparison.EUCLIDEAN.compare(tpa, trenData); 444 double db = DoubleFVComparison.EUCLIDEAN.compare(tpb, trenData); 445 double sumd = da + db; 446 trenData.value = (float) ((1 - (da / sumd)) * tpa.value + (1-(db/sumd)) * tpb.value); 447 } 448 else{ 449 Triangle t = dataTriangles.getHoldingTriangle(new Point2dImpl(xn,yn)); 450 if(t == null) { 451 return new TernaryData(a, b, c, 0f); 452 } 453 Map<Line2d, Point2d> points = t.intersectionSides( 454 new Line2d( 455 new Point2dImpl(0,yn), 456 new Point2dImpl(1,yn) 457 ) 458 ); 459 460 if(points.size() == 2){ 461 Iterator<Line2d> liter = points.keySet().iterator(); 462 Line2d l1 = liter.next(); 463 Line2d l2 = liter.next(); 464 Point2d p1 = points.get(l1); 465 Point2d p2 = points.get(l2); 466 467 double p1Value = linePointInterp(l1, p1); 468 double p2Value = linePointInterp(l2, p2); 469 470 double pointValue = linePointInterp(new Line2d(p1,p2), new Point2dImpl(xn,yn),p1Value,p2Value); 471 472// if((l1.begin.getX() == l1.end.getX() || l2.begin.getX() == l2.end.getX() ) && pointValue <0.5){ 473// System.out.println("A vertical line created a 0 value"); 474// } 475 476 trenData.value = (float) pointValue; 477 478 } 479 else{ // 0, 1 or more than 2 480 System.out.println("Found 3 or 0 lines: " + points.size()); 481 return new TernaryData(a, b, c, 0f); 482 } 483 } 484 return trenData; 485 } 486 487 private double linePointInterp(Line2d line, Point2d point) { 488 TernaryData l1p1data = dataTriangles.getPointData(line.begin); 489 TernaryData l1p2data = dataTriangles.getPointData(line.end); 490 float l1p1datav = l1p1data.value; 491 float l1p2datav = l1p2data.value; 492 493 return linePointInterp(line, point, l1p1datav, l1p2datav); 494 } 495 496 private double linePointInterp(Line2d line, Point2d point, double lineBeginValue,double lineEndValue) { 497 double l1Len = line.calculateLength(); 498 double l1Prop = Line2d.distance(line.begin, point); 499 double p1Value = Interpolation.lerp(l1Prop, 0, lineBeginValue, l1Len, lineEndValue); 500 return p1Value; 501 } 502 503 /** 504 * @param args 505 */ 506 public static void main(String[] args) { 507 List<TernaryData> data = new ArrayList<TernaryData>(); 508 data.add(new TernaryData(1/3f+0.1f,1/3f-0.1f,1/3f,0.8f)); 509 data.add(new TernaryData(1/3f-0.1f,1/3f+0.1f,1/3f,0.2f)); 510 data.add(new TernaryData(1f,0,0,0)); 511 data.add(new TernaryData(0,1f,0,0)); 512 data.add(new TernaryData(0,0,1f,0)); 513 TernaryPlot plot = new TernaryPlot(500, data); 514 DisplayUtilities.display(plot.draw()); 515 DisplayUtilities.display(plot.drawTriangles()); 516 } 517 518 519 520}