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}