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.processing.transform;
031
032import java.util.ArrayList;
033import java.util.List;
034
035import org.openimaj.citation.annotation.Reference;
036import org.openimaj.citation.annotation.ReferenceType;
037import org.openimaj.image.FImage;
038import org.openimaj.image.Image;
039import org.openimaj.image.processing.convolution.FGaussianConvolve;
040import org.openimaj.image.processing.convolution.FImageConvolveSeparable;
041import org.openimaj.image.processor.SinglebandImageProcessor;
042import org.openimaj.math.geometry.point.Point2d;
043import org.openimaj.math.geometry.transforms.TransformUtilities;
044
045/**
046 * Utility methods to simulate affine transformations defined by a rotation and
047 * tilt, or series of rotations and tilts.
048 * 
049 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
050 * 
051 * @param <I>
052 *            Concrete subclass of {@link Image}
053 * @param <P>
054 *            Pixel type
055 */
056@Reference(
057                type = ReferenceType.Article,
058                author = { "Morel, Jean-Michel", "Yu, Guoshen" },
059                title = "{ASIFT: A New Framework for Fully Affine Invariant Image Comparison}",
060                year = "2009",
061                journal = "SIAM J. Img. Sci.",
062                publisher = "Society for Industrial and Applied Mathematics")
063public abstract class AffineSimulation<I extends Image<P, I> & SinglebandImageProcessor.Processable<Float, FImage, I>, P>
064{
065        protected static final float PI = 3.141592654f;
066        protected static final float InitialAntiAliasingSigma = 1.6f;
067
068        private AffineSimulation() {
069        }
070
071        /**
072         * Compute the position of a point in an image given the position in the
073         * transformed image and the transform parameters.
074         * 
075         * @param pt
076         *            the point in the transformed image
077         * @param width
078         *            the width of the untransformed image
079         * @param height
080         *            the height of the untransformed image
081         * @param theta
082         *            the rotation
083         * @param t
084         *            the tilt
085         * @return the point mapped to the untransformed image
086         */
087        public static Point2d transformToOriginal(Point2d pt, int width, int height, float theta, float t) {
088                if (t == 1)
089                        return pt;
090
091                return internalTransformToOriginal(pt, width, height, theta, t);
092        }
093
094        /**
095         * Compute the position of a point in an image given the position in the
096         * transformed image and the transform parameters.
097         * 
098         * @param pt
099         *            the point in the transformed image
100         * @param original
101         *            the original untransformed image
102         * @param theta
103         *            the rotation
104         * @param t
105         *            the tilt
106         * @return the point mapped to the untransformed image
107         */
108        public static Point2d transformToOriginal(Point2d pt, Image<?, ?> original, float theta, float t) {
109                if (t == 1)
110                        return pt;
111
112                return internalTransformToOriginal(pt, original.getWidth(), original.getHeight(), theta, t);
113        }
114
115        /**
116         * Compute the position of a point in an image given the position in the
117         * transformed image and the transform parameters.
118         * 
119         * @param pt
120         *            the point in the transformed image
121         * @param width
122         *            the width of the untransformed image
123         * @param height
124         *            the height of the untransformed image
125         * @param params
126         *            the simulation parameters
127         * @return the point mapped to the untransformed image
128         */
129        public static Point2d transformToOriginal(Point2d pt, int width, int height, AffineParams params) {
130                return transformToOriginal(pt, width, height, params.theta, params.tilt);
131        }
132
133        /**
134         * Compute the position of a point in an image given the position in the
135         * transformed image and the transform parameters.
136         * 
137         * @param pt
138         *            the point in the transformed image
139         * @param original
140         *            the original untransformed image
141         * @param params
142         *            the simulation parameters
143         * @return the point mapped to the untransformed image
144         */
145        public static Point2d transformToOriginal(Point2d pt, Image<?, ?> original, AffineParams params) {
146                return transformToOriginal(pt, original.getWidth(), original.getHeight(), params.theta, params.tilt);
147        }
148
149        protected static Point2d internalTransformToOriginal(Point2d pt, int width, int height, float Rtheta, float t1)
150        {
151                float x_ori, y_ori;
152                Rtheta = Rtheta * PI / 180;
153
154                if (Rtheta <= PI / 2) {
155                        x_ori = 0;
156                        y_ori = (float) ((width) * Math.sin(Rtheta) / t1);
157                } else {
158                        x_ori = (float) (-(width) * Math.cos(Rtheta) / 1);
159                        y_ori = (float) (((width) * Math.sin(Rtheta) + (height) * Math.sin(Rtheta - PI / 2)) / t1);
160                }
161
162                final float sin_Rtheta = (float) Math.sin(Rtheta);
163                final float cos_Rtheta = (float) Math.cos(Rtheta);
164
165                final Point2d ptout = pt.copy();
166
167                /*
168                 * project the coordinates of im1 to original image before tilt-rotation
169                 * transform; get the coordinates with respect to the 'origin' of the
170                 * original image before transform
171                 */
172                ptout.setX(pt.getX() - x_ori);
173                ptout.setY(pt.getY() - y_ori);
174
175                /* Invert tilt */
176                ptout.setX(ptout.getX() * 1);
177                ptout.setY(ptout.getY() * t1);
178
179                /*
180                 * Invert rotation (Note that the y direction (vertical) is inverse to
181                 * the usual concention. Hence Rtheta instead of -Rtheta to inverse the
182                 * rotation.)
183                 */
184                final float tx = cos_Rtheta * ptout.getX() - sin_Rtheta * ptout.getY();
185                final float ty = sin_Rtheta * ptout.getX() + cos_Rtheta * ptout.getY();
186
187                ptout.setX(tx);
188                ptout.setY(ty);
189
190                return ptout;
191        }
192
193        /**
194         * Transform the coordinates of the given points from a transformed image to
195         * the original space.
196         * 
197         * @param <Q>
198         *            Type of interest point list
199         * @param <T>
200         *            Type of interest point
201         * @param <I>
202         *            Type of {@link Image}
203         * @param points
204         *            the points
205         * @param original
206         *            the original untransformed image
207         * @param theta
208         *            the rotation
209         * @param tilt
210         *            the tilt
211         */
212        public static <Q extends List<T>, T extends Point2d, I extends Image<?, I>> void transformToOriginal(Q points,
213                        I original, float theta, float tilt)
214        {
215                final List<T> keys_to_remove = new ArrayList<T>();
216                float x_ori, y_ori;
217
218                if (theta <= PI / 2) {
219                        x_ori = 0;
220                        y_ori = (float) ((original.getWidth()) * Math.sin(theta) / tilt);
221                } else {
222                        x_ori = (float) (-(original.getWidth()) * Math.cos(theta) / 1);
223                        y_ori = (float) (((original.getWidth()) * Math.sin(theta) + (original.getHeight())
224                                        * Math.sin(theta - PI / 2)) / tilt);
225                }
226
227                final float sin_Rtheta = (float) Math.sin(theta);
228                final float cos_Rtheta = (float) Math.cos(theta);
229
230                for (final T k : points) {
231                        /*
232                         * project the coordinates of im1 to original image before
233                         * tilt-rotation transform
234                         */
235                        /*
236                         * Get the coordinates with respect to the 'origin' of the original
237                         * image before transform
238                         */
239                        k.setX(k.getX() - x_ori);
240                        k.setY(k.getY() - y_ori);
241                        /* Invert tilt */
242                        k.setX(k.getX() * 1);
243                        k.setY(k.getY() * tilt);
244                        /*
245                         * Invert rotation (Note that the y direction (vertical) is inverse
246                         * to the usual concention. Hence Rtheta instead of -Rtheta to
247                         * inverse the rotation.)
248                         */
249                        final float tx = cos_Rtheta * k.getX() - sin_Rtheta * k.getY();
250                        final float ty = sin_Rtheta * k.getX() + cos_Rtheta * k.getY();
251
252                        k.setX(tx);
253                        k.setY(ty);
254
255                        if (tx <= 0 || ty <= 0 || tx >= original.getWidth() || ty >= original.getHeight()) {
256                                keys_to_remove.add(k);
257                        }
258                }
259                points.removeAll(keys_to_remove);
260        }
261
262        /**
263         * Compute the transformed images based on the given number of tilts.
264         * 
265         * @param image
266         *            the image to transform.
267         * @param numTilts
268         *            the number of tilts to simulate.
269         * @return the transformed images
270         * @throws IllegalArgumentException
271         *             if the number of tilts is < 1
272         */
273        public static <I extends Image<P, I> & SinglebandImageProcessor.Processable<Float, FImage, I>, P>
274                        List<I> transformImage(I image, int numTilts)
275        {
276                if (numTilts < 1) {
277                        throw new IllegalArgumentException("Number of tilts num_tilt should be equal or larger than 1.");
278                }
279
280                final List<I> transformed = new ArrayList<I>();
281                int num_rot1 = 0;
282
283                final int num_rot_t2 = 10;
284                final float t_min = 1;
285                final float t_k = (float) Math.sqrt(2);
286
287                for (int tt = 1; tt <= numTilts; tt++) {
288                        final float t = t_min * (float) Math.pow(t_k, tt - 1);
289
290                        if (t == 1) {
291                                transformed.add(image.clone());
292                        } else {
293                                num_rot1 = Math.round(num_rot_t2 * t / 2);
294
295                                if (num_rot1 % 2 == 1) {
296                                        num_rot1 = num_rot1 + 1;
297                                }
298                                num_rot1 = num_rot1 / 2;
299
300                                final float delta_theta = PI / num_rot1;
301
302                                for (int rr = 1; rr <= num_rot1; rr++) {
303                                        final float theta = delta_theta * (rr - 1);
304
305                                        transformed.add(transformImage(image, theta, t));
306                                }
307                        }
308                }
309
310                return transformed;
311        }
312
313        /**
314         * Compute a single transformed image for a given rotation and tilt.
315         * 
316         * @param image
317         *            the image
318         * @param theta
319         *            the rotation angle
320         * @param t
321         *            the tilt amount
322         * @return the transformed image
323         */
324        public static <I extends Image<P, I> & SinglebandImageProcessor.Processable<Float, FImage, I>, P> I transformImage(
325                        I image, float theta, float t)
326        {
327                final float t1 = 1;
328                final float t2 = 1 / t;
329
330                // Perform rotation
331                final I image_rotated = ProjectionProcessor.project(image, TransformUtilities.rotationMatrix(-theta));
332
333                // Perform anti-aliasing filtering by convolving with a Gaussian in the
334                // vertical direction
335                final float sigma_aa = InitialAntiAliasingSigma * t / 2;
336                image_rotated.processInplace(new FImageConvolveSeparable(null, FGaussianConvolve.makeKernel(sigma_aa)));
337
338                // Squash the image in the x and y direction by t1 and t2 to mimic tilt
339                return ProjectionProcessor.project(image_rotated, TransformUtilities.scaleMatrix(t1, t2));
340        }
341
342        /**
343         * Compute a single transformed image for a given rotation and tilt.
344         * 
345         * @param image
346         *            the image
347         * @param params
348         *            the simulation parameters
349         * @return the transformed image
350         */
351        public static <I extends Image<P, I> & SinglebandImageProcessor.Processable<Float, FImage, I>, P> I transformImage(
352                        I image, AffineParams params)
353        {
354                return transformImage(image, params.theta, params.tilt);
355        }
356
357}