View Javadoc

1   /**
2    * Copyright (c) 2011, The University of Southampton and the individual contributors.
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without modification,
6    * are permitted provided that the following conditions are met:
7    *
8    *   * 	Redistributions of source code must retain the above copyright notice,
9    * 	this list of conditions and the following disclaimer.
10   *
11   *   *	Redistributions in binary form must reproduce the above copyright notice,
12   * 	this list of conditions and the following disclaimer in the documentation
13   * 	and/or other materials provided with the distribution.
14   *
15   *   *	Neither the name of the University of Southampton nor the names of its
16   * 	contributors may be used to endorse or promote products derived from this
17   * 	software without specific prior written permission.
18   *
19   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20   * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21   * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22   * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23   * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26   * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29   */
30  package org.openimaj.image.renderer;
31  
32  import org.openimaj.image.FImage;
33  import org.openimaj.image.pixel.Pixel;
34  import org.openimaj.image.renderer.ScanRasteriser.ScanLineListener;
35  import org.openimaj.math.geometry.line.Line2d;
36  import org.openimaj.math.geometry.point.Point2d;
37  import org.openimaj.math.geometry.point.Point2dImpl;
38  import org.openimaj.math.geometry.shape.Polygon;
39  
40  /**
41   * {@link ImageRenderer} for {@link FImage} images. Supports both anti-aliased
42   * and fast rendering.
43   *
44   * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
45   *
46   */
47  public class FImageRenderer extends ImageRenderer<Float, FImage> {
48  
49  	/**
50  	 * Construct with given target image.
51  	 *
52  	 * @param targetImage
53  	 *            the target image.
54  	 */
55  	public FImageRenderer(final FImage targetImage) {
56  		super(targetImage);
57  	}
58  
59  	/**
60  	 * Construct with given target image and rendering hints.
61  	 *
62  	 * @param targetImage
63  	 *            the target image.
64  	 * @param hints
65  	 *            the render hints
66  	 */
67  	public FImageRenderer(final FImage targetImage, final RenderHints hints) {
68  		super(targetImage, hints);
69  	}
70  
71  	@Override
72  	public Float defaultForegroundColour() {
73  		return 1f;
74  	}
75  
76  	@Override
77  	public Float defaultBackgroundColour() {
78  		return 0f;
79  	}
80  
81  	/**
82  	 * {@inheritDoc}
83  	 *
84  	 * @see org.openimaj.image.renderer.ImageRenderer#drawLine(int, int, double,
85  	 *      int, int, java.lang.Object)
86  	 */
87  	@Override
88  	public void drawLine(final int x1, final int y1, final double theta, final int length, final int thickness,
89  			final Float grey)
90  	{
91  		final int x2 = x1 + (int) Math.round(Math.cos(theta) * length);
92  		final int y2 = y1 + (int) Math.round(Math.sin(theta) * length);
93  
94  		this.drawLine(x1, y1, x2, y2, thickness, grey);
95  	}
96  
97  	/**
98  	 * {@inheritDoc}
99  	 *
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 }