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.image.renderer; 031 032import org.openimaj.image.FImage; 033import org.openimaj.image.pixel.Pixel; 034import org.openimaj.image.renderer.ScanRasteriser.ScanLineListener; 035import org.openimaj.math.geometry.line.Line2d; 036import org.openimaj.math.geometry.point.Point2d; 037import org.openimaj.math.geometry.point.Point2dImpl; 038import org.openimaj.math.geometry.shape.Polygon; 039 040/** 041 * {@link ImageRenderer} for {@link FImage} images. Supports both anti-aliased 042 * and fast rendering. 043 * 044 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk) 045 * 046 */ 047public class FImageRenderer extends ImageRenderer<Float, FImage> { 048 049 /** 050 * Construct with given target image. 051 * 052 * @param targetImage 053 * the target image. 054 */ 055 public FImageRenderer(final FImage targetImage) { 056 super(targetImage); 057 } 058 059 /** 060 * Construct with given target image and rendering hints. 061 * 062 * @param targetImage 063 * the target image. 064 * @param hints 065 * the render hints 066 */ 067 public FImageRenderer(final FImage targetImage, final RenderHints hints) { 068 super(targetImage, hints); 069 } 070 071 @Override 072 public Float defaultForegroundColour() { 073 return 1f; 074 } 075 076 @Override 077 public Float defaultBackgroundColour() { 078 return 0f; 079 } 080 081 /** 082 * {@inheritDoc} 083 * 084 * @see org.openimaj.image.renderer.ImageRenderer#drawLine(int, int, double, 085 * int, int, java.lang.Object) 086 */ 087 @Override 088 public void drawLine(final int x1, final int y1, final double theta, final int length, final int thickness, 089 final Float grey) 090 { 091 final int x2 = x1 + (int) Math.round(Math.cos(theta) * length); 092 final int y2 = y1 + (int) Math.round(Math.sin(theta) * length); 093 094 this.drawLine(x1, y1, x2, y2, thickness, grey); 095 } 096 097 /** 098 * {@inheritDoc} 099 * 100 * @see org.openimaj.image.renderer.ImageRenderer#drawLine(int, int, int, 101 * int, int, java.lang.Object) 102 */ 103 @Override 104 public void drawLine(final int x0, final int y0, final int x1, final int y1, final int thickness, final Float grey) { 105 this.drawLine((float) x0, (float) y0, (float) x1, (float) y1, thickness, grey); 106 } 107 108 @Override 109 public void drawLine(final float x0, final float y0, final float x1, final float y1, final int thickness, 110 final Float grey) 111 { 112 switch (this.hints.drawingAlgorithm) { 113 case ANTI_ALIASED: 114 if (thickness <= 1) { 115 this.drawLineXiaolinWu(x0, y0, x1, y1, grey); 116 } else { 117 final double theta = Math.atan2(y1 - y0, x1 - x0); 118 final double t = thickness / 2; 119 final double sin = t * Math.sin(theta); 120 final double cos = t * Math.cos(theta); 121 122 final Polygon p = new Polygon(); 123 p.addVertex(new Point2dImpl((float) (x0 - sin), (float) (y0 + cos))); 124 p.addVertex(new Point2dImpl((float) (x0 + sin), (float) (y0 - cos))); 125 p.addVertex(new Point2dImpl((float) (x1 + sin), (float) (y1 - cos))); 126 p.addVertex(new Point2dImpl((float) (x1 - sin), (float) (y1 + cos))); 127 128 this.drawPolygonFilled(p, grey); 129 } 130 break; 131 default: 132 this.drawLineBresenham(Math.round(x0), Math.round(y0), Math.round(x1), Math.round(y1), thickness, grey); 133 } 134 } 135 136 private float fpart(final float f) { 137 return f - (int) f; 138 } 139 140 private float rfpart(final float f) { 141 return 1 - this.fpart(f); 142 } 143 144 private void plot(final int a, final int b, final float c, final float grey, final boolean reversed) { 145 int x, y; 146 if (reversed) { 147 y = a; 148 x = b; 149 } else { 150 x = a; 151 y = b; 152 } 153 154 if (x >= 0 && x < this.targetImage.width && y >= 0 && y < this.targetImage.height && !Float.isNaN(c)) { 155 this.targetImage.pixels[y][x] = c * grey + (1 - c) * this.targetImage.pixels[y][x]; 156 } 157 } 158 159 /* 160 * Implementation of Xiaolin Wu's anti-aliased line drawing algorithm. Based 161 * on the wikipedia article: 162 * http://en.wikipedia.org/wiki/Xiaolin_Wu's_line_algorithm 163 */ 164 protected void drawLineXiaolinWu(float x1, float y1, float x2, float y2, final Float grey) { 165 float dx = x2 - x1; 166 float dy = y2 - y1; 167 boolean reversed = false; 168 169 if (Math.abs(dx) < Math.abs(dy)) { 170 float tmp; 171 tmp = x1; 172 x1 = y1; 173 y1 = tmp; 174 tmp = x2; 175 x2 = y2; 176 y2 = tmp; 177 tmp = dx; 178 dx = dy; 179 dy = tmp; 180 reversed = true; 181 } 182 183 if (x2 < x1) { 184 float tmp; 185 tmp = x1; 186 x1 = x2; 187 x2 = tmp; 188 tmp = y1; 189 y1 = y2; 190 y2 = tmp; 191 } 192 193 final float gradient = dy / dx; 194 195 // handle first endpoint 196 int xend = Math.round(x1); 197 float yend = y1 + gradient * (xend - x1); 198 float xgap = this.rfpart(x1 + 0.5f); 199 final int xpxl1 = xend; // this will be used in the main loop 200 final int ypxl1 = (int) (yend); 201 this.plot(xpxl1, ypxl1, this.rfpart(yend) * xgap, grey, reversed); 202 this.plot(xpxl1, ypxl1 + 1, this.fpart(yend) * xgap, grey, reversed); 203 float intery = yend + gradient; // first y-intersection for the main 204 // loop 205 206 // handle second endpoint 207 xend = Math.round(x2); 208 yend = y2 + gradient * (xend - x2); 209 xgap = this.fpart(x2 + 0.5f); 210 final int xpxl2 = xend; // this will be used in the main loop 211 final int ypxl2 = (int) (yend); 212 this.plot(xpxl2, ypxl2, this.rfpart(yend) * xgap, grey, reversed); 213 this.plot(xpxl2, ypxl2 + 1, this.fpart(yend) * xgap, grey, reversed); 214 215 // main loop 216 for (int x = xpxl1 + 1; x <= xpxl2 - 1; x++) { 217 this.plot(x, (int) (intery), this.rfpart(intery), grey, reversed); 218 this.plot(x, (int) (intery) + 1, this.fpart(intery), grey, reversed); 219 intery += gradient; 220 } 221 } 222 223 /* 224 * Implementation of Bresenham's fast line drawing algorithm. Based on the 225 * wikipedia article: 226 * http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 227 */ 228 protected void drawLineBresenham(int x0, int y0, int x1, int y1, int thickness, final Float grey) { 229 final Line2d line = new Line2d(new Point2dImpl(x0, y0), new Point2dImpl(x1, y1)) 230 .lineWithinSquare(this.targetImage 231 .getBounds()); 232 if (line == null) 233 return; 234 235 x0 = (int) line.begin.getX(); 236 y0 = (int) line.begin.getY(); 237 x1 = (int) line.end.getX(); 238 y1 = (int) line.end.getY(); 239 240 final double theta = Math.atan2(y1 - y0, x1 - x0); 241 thickness = (int) Math.round(thickness * Math.max(Math.abs(Math.cos(theta)), Math.abs(Math.sin(theta)))); 242 243 final int offset = thickness / 2; 244 final int extra = thickness % 2; 245 246 // implementation of Bresenham's algorithm from Wikipedia. 247 int Dx = x1 - x0; 248 int Dy = y1 - y0; 249 final boolean steep = (Math.abs(Dy) >= Math.abs(Dx)); 250 if (steep) { 251 int tmp; 252 // SWAP(x0, y0); 253 tmp = x0; 254 x0 = y0; 255 y0 = tmp; 256 // SWAP(x1, y1); 257 tmp = x1; 258 x1 = y1; 259 y1 = tmp; 260 261 // recompute Dx, Dy after swap 262 Dx = x1 - x0; 263 Dy = y1 - y0; 264 } 265 int xstep = 1; 266 if (Dx < 0) { 267 xstep = -1; 268 Dx = -Dx; 269 } 270 int ystep = 1; 271 if (Dy < 0) { 272 ystep = -1; 273 Dy = -Dy; 274 } 275 final int TwoDy = 2 * Dy; 276 final int TwoDyTwoDx = TwoDy - 2 * Dx; // 2*Dy - 2*Dx 277 int E = TwoDy - Dx; // 2*Dy - Dx 278 int y = y0; 279 int xDraw, yDraw; 280 for (int x = x0; x != x1; x += xstep) { 281 if (steep) { 282 xDraw = y; 283 yDraw = x; 284 } else { 285 xDraw = x; 286 yDraw = y; 287 } 288 // plot 289 if (xDraw >= 0 && xDraw < this.targetImage.width && yDraw >= 0 && yDraw < this.targetImage.height) { 290 if (thickness == 1) { 291 this.targetImage.pixels[yDraw][xDraw] = grey; 292 } else if (thickness > 1) { 293 for (int yy = yDraw - offset; yy < yDraw + offset + extra; yy++) 294 for (int xx = xDraw - offset; xx < xDraw + offset + extra; xx++) 295 if (xx >= 0 && yy >= 0 && xx < this.targetImage.width && yy < this.targetImage.height) 296 this.targetImage.pixels[yy][xx] = grey; 297 } 298 } 299 300 // next 301 if (E > 0) { 302 E += TwoDyTwoDx; // E += 2*Dy - 2*Dx; 303 y = y + ystep; 304 } else { 305 E += TwoDy; // E += 2*Dy; 306 } 307 } 308 } 309 310 /** 311 * {@inheritDoc} 312 * 313 * @see org.openimaj.image.renderer.ImageRenderer#drawPoint(org.openimaj.math.geometry.point.Point2d, 314 * java.lang.Object, int) 315 */ 316 @Override 317 public void drawPoint(final Point2d p, final Float grey, final int size) { 318 319 if (!this.targetImage.getBounds().isInside(p)) 320 return; 321 final int halfsize = (size + 1) / 2; // 3 == 2, 4 = 2, 5 = 3, 6 = 3 etc. 322 // TODO anti-aliased point rendering 323 final int x = Math.round(p.getX()); 324 final int y = Math.round(p.getY()); 325 final int startx = Math.max(0, x - (halfsize - 1)); 326 final int starty = Math.max(0, y - (halfsize - 1)); 327 final int endx = Math.min(this.targetImage.width, x + halfsize); 328 final int endy = Math.min(this.targetImage.height, y + halfsize); 329 330 for (int j = starty; j < endy; j++) { 331 for (int i = startx; i < endx; i++) { 332 this.targetImage.pixels[j][i] = grey; 333 } 334 } 335 } 336 337 /** 338 * {@inheritDoc} 339 * 340 * @see org.openimaj.image.renderer.ImageRenderer#drawPolygon(org.openimaj.math.geometry.shape.Polygon, 341 * int, java.lang.Object) 342 */ 343 @Override 344 public void drawPolygon(final Polygon p, final int thickness, final Float grey) { 345 if (p.nVertices() < 2) 346 return; 347 348 Point2d p1, p2; 349 for (int i = 0; i < p.nVertices() - 1; i++) { 350 p1 = p.getVertices().get(i); 351 p2 = p.getVertices().get(i + 1); 352 this.drawLine(p1.getX(), p1.getY(), p2.getX(), p2.getY(), thickness, grey); 353 } 354 355 p1 = p.getVertices().get(p.nVertices() - 1); 356 p2 = p.getVertices().get(0); 357 this.drawLine(p1.getX(), p1.getY(), p2.getX(), p2.getY(), thickness, grey); 358 359 for (final Polygon i : p.getInnerPolys()) 360 drawPolygon(i, thickness, grey); 361 } 362 363 @Override 364 protected void drawHorizLine(final int x1, final int x2, final int y, final Float col) { 365 if (y < 0 || y > this.targetImage.getHeight() - 1) 366 return; 367 368 final int startx = Math.max(0, Math.min(x1, x2)); 369 final int stopx = Math.min(Math.max(x1, x2), this.targetImage.getWidth() - 1); 370 final float[][] img = this.targetImage.pixels; 371 final float c = col; 372 373 for (int x = startx; x <= stopx; x++) { 374 img[y][x] = c; 375 } 376 } 377 378 @Override 379 protected Float sanitise(final Float colour) { 380 return colour; 381 } 382 383 @Override 384 public void drawPolygonFilled(Polygon p, final Float col) { 385 // clip to the frame 386 // p = p.intersect(this.targetImage.getBounds().asPolygon()); 387 388 this.drawPolygon(p, col); 389 390 if (p.getNumInnerPoly() == 1) { 391 ScanRasteriser.scanFill(p.points, new ScanLineListener() { 392 @Override 393 public void process(final int x1, final int x2, final int y) { 394 FImageRenderer.this.drawHorizLine(x1, x2, y, col); 395 } 396 }); 397 } else { 398 final int minx = Math.max(0, (int) Math.round(p.minX())); 399 final int maxx = Math.min((int) Math.round(p.maxX()), targetImage.width - 1); 400 final int miny = Math.max(0, (int) Math.round(p.minY())); 401 final int maxy = Math.min((int) Math.round(p.maxY()), targetImage.height - 1); 402 403 final Pixel tmp = new Pixel(); 404 for (tmp.y = miny; tmp.y <= maxy; tmp.y++) { 405 for (tmp.x = minx; tmp.x <= maxx; tmp.x++) { 406 if (p.isInside(tmp)) 407 this.targetImage.pixels[tmp.y][tmp.x] = col; 408 } 409 } 410 } 411 } 412}