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.feature.local.keypoints;
031
032import java.io.BufferedInputStream;
033import java.io.DataInput;
034import java.io.DataInputStream;
035import java.io.DataOutput;
036import java.io.DataOutputStream;
037import java.io.EOFException;
038import java.io.File;
039import java.io.FileInputStream;
040import java.io.FileOutputStream;
041import java.io.IOException;
042import java.io.InputStream;
043import java.io.OutputStream;
044import java.io.PrintWriter;
045import java.io.Serializable;
046import java.io.StringWriter;
047import java.nio.ByteBuffer;
048import java.nio.ByteOrder;
049import java.util.List;
050import java.util.Scanner;
051
052import org.openimaj.feature.ByteFV;
053import org.openimaj.feature.local.LocalFeature;
054import org.openimaj.feature.local.list.LocalFeatureList;
055import org.openimaj.feature.local.list.MemoryLocalFeatureList;
056import org.openimaj.image.feature.local.keypoints.SIFTGeoKeypoint.SIFTGeoLocation;
057import org.openimaj.io.IOUtils;
058import org.openimaj.io.VariableLength;
059
060import Jama.Matrix;
061
062/**
063 * Implementation of a {@link LocalFeature} based on the .siftgeo format
064 * developed by Krystian Mikolajczyk for his tools.
065 * <p>
066 * Because the .siftgeo file-format is custom, it isn't directly compatible with
067 * that read by
068 * {@link MemoryLocalFeatureList#read(java.io.BufferedInputStream, Class)} or
069 * written with {@link IOUtils}. To work-around these issues, this class
070 * implements a set of static I/O methods for reading and writing multiple
071 * features to/from a standard .siftgeo file.
072 * 
073 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
074 */
075public class SIFTGeoKeypoint implements LocalFeature<SIFTGeoLocation, ByteFV>, VariableLength, Cloneable, Serializable {
076        /**
077         * The location of a {@link SIFTGeoKeypoint}.
078         * 
079         * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
080         * 
081         */
082        public class SIFTGeoLocation extends KeypointLocation {
083                private static final long serialVersionUID = 1L;
084
085                // number of bytes when written as binary
086                private static final int NUM_BYTES = 36;
087
088                /**
089                 * The saliency of the interest point
090                 */
091                public float cornerness;
092
093                /**
094                 * affine parameters of the interest point
095                 */
096                public Matrix affine;
097
098                /**
099                 * Construct with the given parameters
100                 * 
101                 * @param x
102                 *            x-ordinate of feature
103                 * @param y
104                 *            y-ordinate of feature
105                 * @param scale
106                 *            scale of feature
107                 * @param orientation
108                 *            orientation of feature
109                 * @param cornerness
110                 *            the saliency of the interest point
111                 * @param affine
112                 *            affine parameters
113                 */
114                public SIFTGeoLocation(float x, float y, float orientation, float scale, float cornerness, Matrix affine) {
115                        super(x, y, scale, orientation);
116                        this.cornerness = cornerness;
117                        this.affine = affine;
118                }
119
120                /**
121                 * Default constructor. Everything set to zero with the exception of the
122                 * affine parameters which are set to the identity matrix.
123                 */
124                public SIFTGeoLocation() {
125                        affine = Matrix.identity(2, 2);
126                }
127
128                @Override
129                public void writeBinary(DataOutput out) throws IOException {
130                        final ByteBuffer buffer = ByteBuffer.allocate(SIFTGeoLocation.NUM_BYTES);
131                        buffer.order(ByteOrder.LITTLE_ENDIAN);
132
133                        writeBinary(buffer);
134                }
135
136                private void writeBinary(ByteBuffer buffer) {
137                        buffer.putFloat(x);
138                        buffer.putFloat(y);
139                        buffer.putFloat(scale);
140                        buffer.putFloat(orientation);
141                        buffer.putFloat((float) affine.get(0, 0));
142                        buffer.putFloat((float) affine.get(0, 1));
143                        buffer.putFloat((float) affine.get(1, 0));
144                        buffer.putFloat((float) affine.get(1, 1));
145                        buffer.putFloat(cornerness);
146                }
147
148                @Override
149                public void writeASCII(PrintWriter out) throws IOException {
150                        out.format("%4.2f %4.2f %4.2f %4.3f %4.3f %4.3f %4.3f %4.3f", x, y, scale, orientation,
151                                        (float) affine.get(0, 0), (float) affine.get(0, 1), (float) affine
152                                                        .get(1, 0), (float) affine.get(1, 1));
153                        out.println();
154                }
155
156                @Override
157                public void readBinary(DataInput in) throws IOException {
158                        final byte[] array = new byte[NUM_BYTES];
159                        in.readFully(array);
160
161                        final ByteBuffer buffer = ByteBuffer.wrap(array);
162                        buffer.order(ByteOrder.LITTLE_ENDIAN);
163
164                        readBinary(buffer);
165                }
166
167                private void readBinary(ByteBuffer buffer) {
168                        x = buffer.getFloat();
169                        y = buffer.getFloat();
170                        scale = buffer.getFloat();
171                        orientation = buffer.getFloat();
172                        affine.set(0, 0, buffer.getFloat());
173                        affine.set(0, 1, buffer.getFloat());
174                        affine.set(1, 0, buffer.getFloat());
175                        affine.set(1, 1, buffer.getFloat());
176                        cornerness = buffer.getFloat();
177                }
178
179                @Override
180                public void readASCII(Scanner in) throws IOException {
181                        x = Float.parseFloat(in.next());
182                        y = Float.parseFloat(in.next());
183                        scale = Float.parseFloat(in.next());
184                        orientation = Float.parseFloat(in.next());
185                        affine.set(0, 0, Float.parseFloat(in.next()));
186                        affine.set(0, 1, Float.parseFloat(in.next()));
187                        affine.set(1, 0, Float.parseFloat(in.next()));
188                        affine.set(1, 1, Float.parseFloat(in.next()));
189                        cornerness = Float.parseFloat(in.next());
190                }
191
192                @Override
193                public byte[] binaryHeader() {
194                        return "".getBytes();
195                }
196
197                @Override
198                public String asciiHeader() {
199                        return "";
200                }
201
202                @Override
203                public Float getOrdinate(int dimension) {
204                        final float[] pos = { x, y, scale, orientation, (float) affine.get(0, 0), (float) affine.get(0, 1), (float) affine
205                                        .get(1, 0), (float) affine.get(1, 1) };
206                        return pos[dimension];
207                }
208        }
209
210        private static final long serialVersionUID = 1L;
211
212        /**
213         * The location of the point
214         */
215        public SIFTGeoLocation location = new SIFTGeoLocation();
216
217        /**
218         * The descriptor
219         */
220        public byte[] descriptor;
221
222        /**
223         * Construct with the location set to zero, and with an empty descriptor of
224         * the given length.
225         * 
226         * @param len
227         *            the descriptor length
228         */
229        public SIFTGeoKeypoint(int len) {
230                descriptor = new byte[len];
231        }
232
233        /**
234         * Construct with the given parameters
235         * 
236         * @param x
237         *            x-ordinate of feature
238         * 
239         * @param y
240         *            y-ordinate of feature
241         * 
242         * @param scale
243         *            scale of feature
244         * 
245         * @param orientation
246         *            orientation of feature
247         * 
248         * @param cornerness
249         *            the saliency of the interest point
250         * 
251         * @param affine
252         *            affine parameters
253         * @param descriptor
254         *            the descriptor
255         */
256        public SIFTGeoKeypoint(float x, float y, float orientation, float scale, float cornerness, Matrix affine,
257                        byte[] descriptor)
258        {
259                this.location.x = x;
260                this.location.y = y;
261                this.location.orientation = orientation;
262                this.location.scale = scale;
263                this.location.cornerness = cornerness;
264                this.location.affine = affine;
265                this.descriptor = descriptor;
266        }
267
268        @Override
269        public void readASCII(Scanner in) throws IOException {
270                location.readASCII(in);
271                final int len = in.nextInt();
272
273                descriptor = new byte[len];
274                for (int i = 0; i < len; i++)
275                        descriptor[i] = (byte) (in.nextInt() - 128);
276        }
277
278        @Override
279        public String asciiHeader() {
280                return "";
281        }
282
283        @Override
284        public void readBinary(DataInput in) throws IOException {
285                location.readBinary(in);
286
287                final byte[] array = new byte[4];
288                in.readFully(array);
289
290                final ByteBuffer buffer = ByteBuffer.wrap(array);
291
292                buffer.order(ByteOrder.LITTLE_ENDIAN);
293                final int len = buffer.getInt();
294
295                descriptor = new byte[len];
296                for (int i = 0; i < descriptor.length; i++)
297                        descriptor[i] = (byte) (in.readUnsignedByte() - 128);
298        }
299
300        @Override
301        public byte[] binaryHeader() {
302                return new byte[0]; // legacy files are "headerless"
303        }
304
305        @Override
306        public void writeASCII(PrintWriter out) throws IOException {
307                location.writeASCII(out);
308                out.format("%d\n", descriptor.length);
309                for (int i = 0; i < descriptor.length; i++)
310                        out.format("%d ", descriptor[i] + 128);
311                out.append("\n");
312        }
313
314        @Override
315        public void writeBinary(DataOutput out) throws IOException {
316                final ByteBuffer buffer = ByteBuffer.allocate(SIFTGeoLocation.NUM_BYTES + 4 + descriptor.length);
317                buffer.order(ByteOrder.LITTLE_ENDIAN);
318
319                location.writeBinary(buffer);
320                buffer.putInt(descriptor.length);
321
322                for (int i = 0; i < descriptor.length; i++)
323                        buffer.put((byte) ((descriptor[i] + 128) & 0xFF));
324
325                out.write(buffer.array());
326        }
327
328        @Override
329        public ByteFV getFeatureVector() {
330                return new ByteFV(descriptor);
331        }
332
333        @Override
334        public SIFTGeoLocation getLocation() {
335                return location;
336        }
337
338        @Override
339        public String toString() {
340                final StringWriter sw = new StringWriter();
341
342                try {
343                        writeASCII(new PrintWriter(sw));
344                } catch (final IOException e) {
345                }
346
347                return sw.toString();
348        }
349
350        /**
351         * Read a .siftgeo file.
352         * 
353         * @param file
354         *            the file
355         * @return the list of read {@link SIFTGeoKeypoint}s
356         * @throws IOException
357         *             if an error occurs during reading
358         */
359        public static LocalFeatureList<SIFTGeoKeypoint> read(File file) throws IOException {
360                return read(new BufferedInputStream(new FileInputStream(file)));
361        }
362
363        /**
364         * Read .siftgeo file from a stream.
365         * 
366         * @param stream
367         *            the stream
368         * @return the list of read {@link SIFTGeoKeypoint}s
369         * @throws IOException
370         *             if an error occurs during reading
371         */
372        public static LocalFeatureList<SIFTGeoKeypoint> read(InputStream stream) throws IOException {
373                return read(new DataInputStream(stream));
374        }
375
376        /**
377         * Read .siftgeo file from a stream.
378         * 
379         * @param stream
380         *            the stream
381         * @return the list of read {@link SIFTGeoKeypoint}s
382         * @throws IOException
383         *             if an error occurs during reading
384         */
385        public static LocalFeatureList<SIFTGeoKeypoint> read(DataInputStream stream) throws IOException {
386                final MemoryLocalFeatureList<SIFTGeoKeypoint> keys = new MemoryLocalFeatureList<SIFTGeoKeypoint>();
387
388                while (true) {
389                        try {
390                                final SIFTGeoKeypoint kp = new SIFTGeoKeypoint(0);
391                                kp.readBinary(stream);
392                                keys.add(kp);
393                        } catch (final EOFException eof) {
394                                // end of stream
395                                return keys;
396                        }
397                }
398        }
399
400        /**
401         * Write a .siftgeo file
402         * 
403         * @param keys
404         *            the {@link SIFTGeoKeypoint}s to write
405         * @param file
406         *            the file
407         * @throws IOException
408         *             if an error occurs whilst writing
409         */
410        public static void write(List<SIFTGeoKeypoint> keys, File file) throws IOException {
411                write(keys, new FileOutputStream(file));
412        }
413
414        /**
415         * Write a .siftgeo stream
416         * 
417         * @param keys
418         *            the {@link SIFTGeoKeypoint}s to write
419         * @param stream
420         *            the stream
421         * @throws IOException
422         *             if an error occurs whilst writing
423         */
424        public static void write(List<SIFTGeoKeypoint> keys, OutputStream stream) throws IOException {
425                write(keys, new DataOutputStream(stream));
426        }
427
428        /**
429         * Write a .siftgeo stream
430         * 
431         * @param keys
432         *            the {@link SIFTGeoKeypoint}s to write
433         * @param stream
434         *            the stream
435         * @throws IOException
436         *             if an error occurs whilst writing
437         */
438        public static void write(List<SIFTGeoKeypoint> keys, DataOutputStream stream) throws IOException {
439                for (final SIFTGeoKeypoint k : keys) {
440                        k.writeBinary(stream);
441                }
442        }
443}