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),
205                                        (float) affine
206                                                        .get(1, 0), (float) affine.get(1, 1) };
207                        return pos[dimension];
208                }
209        }
210
211        private static final long serialVersionUID = 1L;
212
213        /**
214         * The location of the point
215         */
216        public SIFTGeoLocation location = new SIFTGeoLocation();
217
218        /**
219         * The descriptor
220         */
221        public byte[] descriptor;
222
223        /**
224         * Construct with the location set to zero, and with an empty descriptor of
225         * the given length.
226         *
227         * @param len
228         *            the descriptor length
229         */
230        public SIFTGeoKeypoint(int len) {
231                descriptor = new byte[len];
232        }
233
234        /**
235         * Construct with the given parameters
236         *
237         * @param x
238         *            x-ordinate of feature
239         *
240         * @param y
241         *            y-ordinate of feature
242         *
243         * @param scale
244         *            scale of feature
245         *
246         * @param orientation
247         *            orientation of feature
248         *
249         * @param cornerness
250         *            the saliency of the interest point
251         *
252         * @param affine
253         *            affine parameters
254         * @param descriptor
255         *            the descriptor
256         */
257        public SIFTGeoKeypoint(float x, float y, float orientation, float scale, float cornerness, Matrix affine,
258                        byte[] descriptor)
259        {
260                this.location.x = x;
261                this.location.y = y;
262                this.location.orientation = orientation;
263                this.location.scale = scale;
264                this.location.cornerness = cornerness;
265                this.location.affine = affine;
266                this.descriptor = descriptor;
267        }
268
269        @Override
270        public void readASCII(Scanner in) throws IOException {
271                location.readASCII(in);
272                final int len = in.nextInt();
273
274                descriptor = new byte[len];
275                for (int i = 0; i < len; i++)
276                        descriptor[i] = (byte) (in.nextInt() - 128);
277        }
278
279        @Override
280        public String asciiHeader() {
281                return "";
282        }
283
284        @Override
285        public void readBinary(DataInput in) throws IOException {
286                location.readBinary(in);
287
288                final byte[] array = new byte[4];
289                in.readFully(array);
290
291                final ByteBuffer buffer = ByteBuffer.wrap(array);
292
293                buffer.order(ByteOrder.LITTLE_ENDIAN);
294                final int len = buffer.getInt();
295
296                descriptor = new byte[len];
297                for (int i = 0; i < descriptor.length; i++)
298                        descriptor[i] = (byte) (in.readUnsignedByte() - 128);
299        }
300
301        @Override
302        public byte[] binaryHeader() {
303                return new byte[0]; // legacy files are "headerless"
304        }
305
306        @Override
307        public void writeASCII(PrintWriter out) throws IOException {
308                location.writeASCII(out);
309                out.format("%d\n", descriptor.length);
310                for (int i = 0; i < descriptor.length; i++)
311                        out.format("%d ", descriptor[i] + 128);
312                out.append("\n");
313        }
314
315        @Override
316        public void writeBinary(DataOutput out) throws IOException {
317                final ByteBuffer buffer = ByteBuffer.allocate(SIFTGeoLocation.NUM_BYTES + 4 + descriptor.length);
318                buffer.order(ByteOrder.LITTLE_ENDIAN);
319
320                location.writeBinary(buffer);
321                buffer.putInt(descriptor.length);
322
323                for (int i = 0; i < descriptor.length; i++)
324                        buffer.put((byte) ((descriptor[i] + 128) & 0xFF));
325
326                out.write(buffer.array());
327        }
328
329        @Override
330        public ByteFV getFeatureVector() {
331                return new ByteFV(descriptor);
332        }
333
334        @Override
335        public SIFTGeoLocation getLocation() {
336                return location;
337        }
338
339        @Override
340        public String toString() {
341                final StringWriter sw = new StringWriter();
342
343                try {
344                        writeASCII(new PrintWriter(sw));
345                } catch (final IOException e) {
346                }
347
348                return sw.toString();
349        }
350
351        /**
352         * Read a .siftgeo file.
353         *
354         * @param file
355         *            the file
356         * @return the list of read {@link SIFTGeoKeypoint}s
357         * @throws IOException
358         *             if an error occurs during reading
359         */
360        public static LocalFeatureList<SIFTGeoKeypoint> read(File file) throws IOException {
361                return read(new BufferedInputStream(new FileInputStream(file)));
362        }
363
364        /**
365         * Read .siftgeo file from a stream.
366         *
367         * @param stream
368         *            the stream
369         * @return the list of read {@link SIFTGeoKeypoint}s
370         * @throws IOException
371         *             if an error occurs during reading
372         */
373        public static LocalFeatureList<SIFTGeoKeypoint> read(InputStream stream) throws IOException {
374                return read(new DataInputStream(stream));
375        }
376
377        /**
378         * Read .siftgeo file from a stream.
379         *
380         * @param stream
381         *            the stream
382         * @return the list of read {@link SIFTGeoKeypoint}s
383         * @throws IOException
384         *             if an error occurs during reading
385         */
386        public static LocalFeatureList<SIFTGeoKeypoint> read(DataInputStream stream) throws IOException {
387                final MemoryLocalFeatureList<SIFTGeoKeypoint> keys = new MemoryLocalFeatureList<SIFTGeoKeypoint>();
388
389                while (true) {
390                        try {
391                                final SIFTGeoKeypoint kp = new SIFTGeoKeypoint(0);
392                                kp.readBinary(stream);
393                                keys.add(kp);
394                        } catch (final EOFException eof) {
395                                // end of stream
396                                return keys;
397                        }
398                }
399        }
400
401        /**
402         * Write a .siftgeo file
403         *
404         * @param keys
405         *            the {@link SIFTGeoKeypoint}s to write
406         * @param file
407         *            the file
408         * @throws IOException
409         *             if an error occurs whilst writing
410         */
411        public static void write(List<SIFTGeoKeypoint> keys, File file) throws IOException {
412                write(keys, new FileOutputStream(file));
413        }
414
415        /**
416         * Write a .siftgeo stream
417         *
418         * @param keys
419         *            the {@link SIFTGeoKeypoint}s to write
420         * @param stream
421         *            the stream
422         * @throws IOException
423         *             if an error occurs whilst writing
424         */
425        public static void write(List<SIFTGeoKeypoint> keys, OutputStream stream) throws IOException {
426                write(keys, new DataOutputStream(stream));
427        }
428
429        /**
430         * Write a .siftgeo stream
431         *
432         * @param keys
433         *            the {@link SIFTGeoKeypoint}s to write
434         * @param stream
435         *            the stream
436         * @throws IOException
437         *             if an error occurs whilst writing
438         */
439        public static void write(List<SIFTGeoKeypoint> keys, DataOutputStream stream) throws IOException {
440                for (final SIFTGeoKeypoint k : keys) {
441                        k.writeBinary(stream);
442                }
443        }
444}