31 | * A decoder capable of processing a GIF data stream to render the graphics
32 | * contained in it. This implementation follows the official
33 | * GIF
34 | * specification.
35 | *
36 | *
37 | *
38 | * Example usage:
39 | *
40 | *
41 | *
42 | *
43 | *
44 | * final GifImage gifImage = GifDecoder.read(int[] data);
45 | * final int width = gifImage.getWidth();
46 | * final int height = gifImage.getHeight();
47 | * final int frameCount = gifImage.getFrameCount();
48 | * for (int i = 0; i < frameCount; i++) {
49 | * final BufferedImage image = gifImage.getFrame(i);
50 | * final int delay = gif.getDelay(i);
51 | * }
52 | *
53 | *
54 | *
55 | *
56 | * @author Dhyan Blum
57 | * @version 1.09 November 2017
58 | *
59 | */
60 | public final class GifDecoder {
61 | static final class BitReader {
62 | private int bitPos; // Next bit to read
63 | private int numBits; // Number of bits to read
64 | private int bitMask; // Use to kill unwanted higher bits
65 | private byte[] in; // Data array
66 |
67 | // To avoid costly bounds checks, 'in' needs 2 more 0-bytes at the end
68 | private final void init(final byte[] in) {
69 | this.in = in;
70 | bitPos = 0;
71 | }
72 |
73 | private final int read() {
74 | // Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
75 | int i = bitPos >>> 3; // Byte = bit / 8
76 | // Bits we'll shift to the right, AND 7 is the same as MODULO 8
77 | final int rBits = bitPos & 7;
78 | // Byte 0 to 2, AND to get their unsigned values
79 | final int b0 = in[i++] & 0xFF, b1 = in[i++] & 0xFF, b2 = in[i] & 0xFF;
80 | // Glue the bytes together, don't do more shifting than necessary
81 | final int buf = ((b2 << 8 | b1) << 8 | b0) >>> rBits;
82 | bitPos += numBits;
83 | return buf & bitMask; // Kill the unwanted higher bits
84 | }
85 |
86 | private final void setNumBits(final int numBits) {
87 | this.numBits = numBits;
88 | bitMask = (1 << numBits) - 1;
89 | }
90 | }
91 |
92 | static final class CodeTable {
93 | private final int[][] tbl; // Maps codes to lists of colors
94 | private int initTableSize; // Number of colors +2 for CLEAR + EOI
95 | private int initCodeSize; // Initial code size
96 | private int initCodeLimit; // First code limit
97 | private int codeSize; // Current code size, maximum is 12 bits
98 | private int nextCode; // Next available code for a new entry
99 | private int nextCodeLimit; // Increase codeSize when nextCode == limit
100 | private BitReader br; // Notify when code sizes increases
101 |
102 | public CodeTable() {
103 | tbl = new int[4096][1];
104 | }
105 |
106 | private final int add(final int[] indices) {
107 | if (nextCode < 4096) {
108 | if (nextCode == nextCodeLimit && codeSize < 12) {
109 | codeSize++; // Max code size is 12
110 | br.setNumBits(codeSize);
111 | nextCodeLimit = (1 << codeSize) - 1; // 2^codeSize - 1
112 | }
113 | tbl[nextCode++] = indices;
114 | }
115 | return codeSize;
116 | }
117 |
118 | private final int clear() {
119 | codeSize = initCodeSize;
120 | br.setNumBits(codeSize);
121 | nextCodeLimit = initCodeLimit;
122 | nextCode = initTableSize; // Don't recreate table, reset pointer
123 | return codeSize;
124 | }
125 |
126 | private final void init(final GifFrame fr, final int[] activeColTbl, final BitReader br) {
127 | this.br = br;
128 | final int numColors = activeColTbl.length;
129 | initCodeSize = fr.firstCodeSize;
130 | initCodeLimit = (1 << initCodeSize) - 1; // 2^initCodeSize - 1
131 | initTableSize = fr.endOfInfoCode + 1;
132 | nextCode = initTableSize;
133 | for (int c = numColors - 1; c >= 0; c--) {
134 | tbl[c][0] = activeColTbl[c]; // Translated color
135 | } // A gap may follow with no colors assigned if numCols < CLEAR
136 | tbl[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR
137 | tbl[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
138 | // Locate transparent color in code table and set to 0
139 | if (fr.transpColFlag && fr.transpColIndex < numColors) {
140 | tbl[fr.transpColIndex][0] = 0;
141 | }
142 | }
143 | }
144 |
145 | final class GifFrame {
146 | // Graphic control extension (optional)
147 | // Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
148 | private int disposalMethod; // 0-3 as above, 4-7 undefined
149 | private boolean transpColFlag; // 1 Bit
150 | private int delay; // Unsigned, LSByte first, n * 1/100 * s
151 | private int transpColIndex; // 1 Byte
152 | // Image descriptor
153 | private int x; // Position on the canvas from the left
154 | private int y; // Position on the canvas from the top
155 | private int w; // May be smaller than the base image
156 | private int h; // May be smaller than the base image
157 | private int wh; // width * height
158 | private boolean hasLocColTbl; // Has local color table? 1 Bit
159 | private boolean interlaceFlag; // Is an interlace image? 1 Bit
160 | @SuppressWarnings("unused")
161 | private boolean sortFlag; // True if local colors are sorted, 1 Bit
162 | private int sizeOfLocColTbl; // Size of the local color table, 3 Bits
163 | private int[] localColTbl; // Local color table (optional)
164 | // Image data
165 | private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
166 | private int clearCode;
167 | private int endOfInfoCode;
168 | private byte[] data; // Holds LZW encoded data
169 | private BufferedImage img; // Full drawn image, not just the frame area
170 | }
171 |
172 | public final class GifImage {
173 | public String header; // Bytes 0-5, GIF87a or GIF89a
174 | private int w; // Unsigned 16 Bit, least significant byte first
175 | private int h; // Unsigned 16 Bit, least significant byte first
176 | private int wh; // Image width * image height
177 | public boolean hasGlobColTbl; // 1 Bit
178 | public int colorResolution; // 3 Bits
179 | public boolean sortFlag; // True if global colors are sorted, 1 Bit
180 | public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
181 | public int bgColIndex; // Background color index, 1 Byte
182 | public int pxAspectRatio; // Pixel aspect ratio, 1 Byte
183 | public int[] globalColTbl; // Global color table
184 | private final List frames = new ArrayList(64);
185 | public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
186 | public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
187 | public int repetitions = 0; // 0: infinite loop, N: number of loops
188 | private BufferedImage img = null; // Currently drawn frame
189 | private int[] prevPx = null; // Previous frame's pixels
190 | private final BitReader bits = new BitReader();
191 | private final CodeTable codes = new CodeTable();
192 | private Graphics2D g;
193 |
194 | private final int[] decode(final GifFrame fr, final int[] activeColTbl) {
195 | codes.init(fr, activeColTbl, bits);
196 | bits.init(fr.data); // Incoming codes
197 | final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
198 | final int[] out = new int[wh]; // Target image pixel array
199 | final int[][] tbl = codes.tbl; // Code table
200 | int outPos = 0; // Next pixel position in the output image array
201 | codes.clear(); // Init code table
202 | bits.read(); // Skip leading clear code
203 | int code = bits.read(); // Read first code
204 | if (code == clearCode) { // Skip leading clear code
205 | code = bits.read();
206 | }
207 | int[] pixels = tbl[code]; // Output pixel for first code
208 | arraycopy(pixels, 0, out, outPos, pixels.length);
209 | outPos += pixels.length;
210 | try {
211 | while (true) {
212 | final int prevCode = code;
213 | code = bits.read(); // Get next code in stream
214 | if (code == clearCode) { // After a CLEAR table, there is
215 | codes.clear(); // no previous code, we need to read
216 | code = bits.read(); // a new one
217 | pixels = tbl[code]; // Output pixels
218 | arraycopy(pixels, 0, out, outPos, pixels.length);
219 | outPos += pixels.length;
220 | continue; // Back to the loop with a valid previous code
221 | } else if (code == endCode) {
222 | break;
223 | }
224 | final int[] prevVals = tbl[prevCode];
225 | final int[] prevValsAndK = new int[prevVals.length + 1];
226 | arraycopy(prevVals, 0, prevValsAndK, 0, prevVals.length);
227 | if (code < codes.nextCode) { // Code table contains code
228 | pixels = tbl[code]; // Output pixels
229 | arraycopy(pixels, 0, out, outPos, pixels.length);
230 | outPos += pixels.length;
231 | prevValsAndK[prevVals.length] = tbl[code][0]; // K
232 | } else {
233 | prevValsAndK[prevVals.length] = prevVals[0]; // K
234 | arraycopy(prevValsAndK, 0, out, outPos, prevValsAndK.length);
235 | outPos += prevValsAndK.length;
236 | }
237 | codes.add(prevValsAndK); // Previous indices + K
238 | }
239 | } catch (final ArrayIndexOutOfBoundsException e) {
240 | }
241 | return out;
242 | }
243 |
244 | private final int[] deinterlace(final int[] src, final GifFrame fr) {
245 | final int w = fr.w, h = fr.h, wh = fr.wh;
246 | final int[] dest = new int[src.length];
247 | // Interlaced images are organized in 4 sets of pixel lines
248 | final int set2Y = (h + 7) >>> 3; // Line no. = ceil(h/8.0)
249 | final int set3Y = set2Y + ((h + 3) >>> 3); // ceil(h-4/8.0)
250 | final int set4Y = set3Y + ((h + 1) >>> 2); // ceil(h-2/4.0)
251 | // Sets' start indices in source array
252 | final int set2 = w * set2Y, set3 = w * set3Y, set4 = w * set4Y;
253 | // Line skips in destination array
254 | final int w2 = w << 1, w4 = w2 << 1, w8 = w4 << 1;
255 | // Group 1 contains every 8th line starting from 0
256 | int from = 0, to = 0;
257 | for (; from < set2; from += w, to += w8) {
258 | arraycopy(src, from, dest, to, w);
259 | } // Group 2 contains every 8th line starting from 4
260 | for (to = w4; from < set3; from += w, to += w8) {
261 | arraycopy(src, from, dest, to, w);
262 | } // Group 3 contains every 4th line starting from 2
263 | for (to = w2; from < set4; from += w, to += w4) {
264 | arraycopy(src, from, dest, to, w);
265 | } // Group 4 contains every 2nd line starting from 1 (biggest group)
266 | for (to = w; from < wh; from += w, to += w2) {
267 | arraycopy(src, from, dest, to, w);
268 | }
269 | return dest; // All pixel lines have now been rearranged
270 | }
271 |
272 | private final void drawFrame(final GifFrame fr) {
273 | // Determine the color table that will be active for this frame
274 | final int[] activeColTbl = fr.hasLocColTbl ? fr.localColTbl : globalColTbl;
275 | // Get pixels from data stream
276 | int[] pixels = decode(fr, activeColTbl);
277 | if (fr.interlaceFlag) {
278 | pixels = deinterlace(pixels, fr); // Rearrange pixel lines
279 | }
280 | // Create image of type 2=ARGB for frame area
281 | final BufferedImage frame = new BufferedImage(fr.w, fr.h, 2);
282 | arraycopy(pixels, 0, ((DataBufferInt) frame.getRaster().getDataBuffer()).getData(), 0, fr.wh);
283 | // Draw frame area on top of working image
284 | g.drawImage(frame, fr.x, fr.y, null);
285 |
286 | // Visualize frame boundaries during testing
287 | // if (DEBUG_MODE) {
288 | // if (prev != null) {
289 | // g.setColor(Color.RED); // Previous frame color
290 | // g.drawRect(prev.x, prev.y, prev.w - 1, prev.h - 1);
291 | // }
292 | // g.setColor(Color.GREEN); // New frame color
293 | // g.drawRect(fr.x, fr.y, fr.w - 1, fr.h - 1);
294 | // }
295 |
296 | // Keep one copy as "previous frame" in case we need to restore it
297 | prevPx = new int[wh];
298 | arraycopy(((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, prevPx, 0, wh);
299 |
300 | // Create another copy for the end user to not expose internal state
301 | fr.img = new BufferedImage(w, h, 2); // 2 = ARGB
302 | arraycopy(prevPx, 0, ((DataBufferInt) fr.img.getRaster().getDataBuffer()).getData(), 0, wh);
303 |
304 | // Handle disposal of current frame
305 | if (fr.disposalMethod == 2) {
306 | // Restore to background color (clear frame area only)
307 | g.clearRect(fr.x, fr.y, fr.w, fr.h);
308 | } else if (fr.disposalMethod == 3 && prevPx != null) {
309 | // Restore previous frame
310 | arraycopy(prevPx, 0, ((DataBufferInt) img.getRaster().getDataBuffer()).getData(), 0, wh);
311 | }
312 | }
313 |
314 | /**
315 | * Returns the background color of the first frame in this GIF image. If
316 | * the frame has a local color table, the returned color will be from
317 | * that table. If not, the color will be from the global color table.
318 | * Returns 0 if there is neither a local nor a global color table.
319 | *
320 | * @return 32 bit ARGB color in the form 0xAARRGGBB
321 | */
322 | public final int getBackgroundColor() {
323 | final GifFrame frame = frames.get(0);
324 | if (frame.hasLocColTbl) {
325 | return frame.localColTbl[bgColIndex];
326 | } else if (hasGlobColTbl) {
327 | return globalColTbl[bgColIndex];
328 | }
329 | return 0;
330 | }
331 |
332 | /**
333 | * If not 0, the delay specifies how many hundredths (1/100) of a second
334 | * to wait before displaying the frame after the current frame.
335 | *
336 | * @param index
337 | * Index of the current frame, 0 to N-1
338 | * @return Delay as number of hundredths (1/100) of a second
339 | */
340 | public final int getDelay(final int index) {
341 | return frames.get(index).delay;
342 | }
343 |
344 | /**
345 | * @param index
346 | * Index of the frame to return as image, starting from 0.
347 | * For incremental calls such as [0, 1, 2, ...] the method's
348 | * run time is O(1) as only one frame is drawn per call. For
349 | * random access calls such as [7, 12, ...] the run time is
350 | * O(N+1) with N being the number of previous frames that
351 | * need to be drawn before N+1 can be drawn on top. Once a
352 | * frame has been drawn it is being cached and the run time
353 | * is more or less O(0) to retrieve it from the list.
354 | * @return A BufferedImage for the specified frame.
355 | */
356 | public final BufferedImage getFrame(final int index) {
357 | if (img == null) { // Init
358 | img = new BufferedImage(w, h, 2); // 2 = ARGB
359 | g = img.createGraphics();
360 | g.setBackground(new Color(0, true)); // Transparent color
361 | }
362 | GifFrame fr = frames.get(index);
363 | if (fr.img == null) {
364 | // Draw all frames until and including the requested frame
365 | for (int i = 0; i <= index; i++) {
366 | fr = frames.get(i);
367 | if (fr.img == null) {
368 | drawFrame(fr);
369 | }
370 | }
371 | }
372 | return fr.img;
373 | }
374 |
375 | /**
376 | * @return The number of frames contained in this GIF image
377 | */
378 | public final int getFrameCount() {
379 | return frames.size();
380 | }
381 |
382 | /**
383 | * @return The height of the GIF image
384 | */
385 | public final int getHeight() {
386 | return h;
387 | }
388 |
389 | /**
390 | * @return The width of the GIF image
391 | */
392 | public final int getWidth() {
393 | return w;
394 | }
395 | }
396 |
397 | static final boolean DEBUG_MODE = false;
398 |
399 | /**
400 | * @param in
401 | * Raw image data as a byte[] array
402 | * @return A GifImage object exposing the properties of the GIF image.
403 | * @throws IOException
404 | * If the image violates the GIF specification or is truncated.
405 | */
406 | public static final GifImage read(final byte[] in) throws IOException {
407 | final GifDecoder decoder = new GifDecoder();
408 | final GifImage img = decoder.new GifImage();
409 | GifFrame frame = null; // Currently open frame
410 | int pos = readHeader(in, img); // Read header, get next byte position
411 | pos = readLogicalScreenDescriptor(img, in, pos);
412 | if (img.hasGlobColTbl) {
413 | img.globalColTbl = new int[img.sizeOfGlobColTbl];
414 | pos = readColTbl(in, img.globalColTbl, pos);
415 | }
416 | while (pos < in.length) {
417 | final int block = in[pos] & 0xFF;
418 | switch (block) {
419 | case 0x21: // Extension introducer
420 | if (pos + 1 >= in.length) {
421 | throw new IOException("Unexpected end of file.");
422 | }
423 | switch (in[pos + 1] & 0xFF) {
424 | case 0xFE: // Comment extension
425 | pos = readTextExtension(in, pos);
426 | break;
427 | case 0xFF: // Application extension
428 | pos = readAppExt(img, in, pos);
429 | break;
430 | case 0x01: // Plain text extension
431 | frame = null; // End of current frame
432 | pos = readTextExtension(in, pos);
433 | break;
434 | case 0xF9: // Graphic control extension
435 | if (frame == null) {
436 | frame = decoder.new GifFrame();
437 | img.frames.add(frame);
438 | }
439 | pos = readGraphicControlExt(frame, in, pos);
440 | break;
441 | default:
442 | throw new IOException("Unknown extension at " + pos);
443 | }
444 | break;
445 | case 0x2C: // Image descriptor
446 | if (frame == null) {
447 | frame = decoder.new GifFrame();
448 | img.frames.add(frame);
449 | }
450 | pos = readImgDescr(frame, in, pos);
451 | if (frame.hasLocColTbl) {
452 | frame.localColTbl = new int[frame.sizeOfLocColTbl];
453 | pos = readColTbl(in, frame.localColTbl, pos);
454 | }
455 | pos = readImgData(frame, in, pos);
456 | frame = null; // End of current frame
457 | break;
458 | case 0x3B: // GIF Trailer
459 | return img; // Found trailer, finished reading.
460 | default:
461 | // Unknown block. The image is corrupted. Strategies: a) Skip
462 | // and wait for a valid block. Experience: It'll get worse. b)
463 | // Throw exception. c) Return gracefully if we are almost done
464 | // processing. The frames we have so far should be error-free.
465 | final double progress = 1.0 * pos / in.length;
466 | if (progress < 0.9) {
467 | throw new IOException("Unknown block at: " + pos);
468 | }
469 | pos = in.length; // Exit loop
470 | }
471 | }
472 | return img;
473 | }
474 |
475 | /**
476 | * @param is
477 | * Image data as input stream. This method will read from the
478 | * input stream's current position. It will not reset the
479 | * position before reading and won't reset or close the stream
480 | * afterwards. Call these methods before and after calling this
481 | * method as needed.
482 | * @return A GifImage object exposing the properties of the GIF image.
483 | * @throws IOException
484 | * If an I/O error occurs, the image violates the GIF
485 | * specification or the GIF is truncated.
486 | */
487 | public static final GifImage read(final InputStream is) throws IOException {
488 | final byte[] data = new byte[is.available()];
489 | is.read(data, 0, data.length);
490 | return read(data);
491 | }
492 |
493 | /**
494 | * @param img
495 | * Empty application extension object
496 | * @param in
497 | * Raw data
498 | * @param i
499 | * Index of the first byte of the application extension
500 | * @return Index of the first byte after this extension
501 | */
502 | static final int readAppExt(final GifImage img, final byte[] in, int i) {
503 | img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
504 | img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
505 | i += 14; // Go to sub-block size, it's value should be 3
506 | final int subBlockSize = in[i] & 0xFF;
507 | // The only app extension widely used is NETSCAPE, it's got 3 data bytes
508 | if (subBlockSize == 3) {
509 | // in[i+1] should have value 01, in[i+5] should be block terminator
510 | img.repetitions = in[i + 2] & 0xFF | in[i + 3] & 0xFF << 8; // Short
511 | return i + 5;
512 | } // Skip unknown application extensions
513 | while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
514 | i += (in[i] & 0xFF) + 1; // Skip to next sub-block
515 | }
516 | return i + 1;
517 | }
518 |
519 | /**
520 | * @param in
521 | * Raw data
522 | * @param colors
523 | * Pre-initialized target array to store ARGB colors
524 | * @param i
525 | * Index of the color table's first byte
526 | * @return Index of the first byte after the color table
527 | */
528 | static final int readColTbl(final byte[] in, final int[] colors, int i) {
529 | final int numColors = colors.length;
530 | for (int c = 0; c < numColors; c++) {
531 | final int a = 0xFF; // Alpha 255 (opaque)
532 | final int r = in[i++] & 0xFF; // 1st byte is red
533 | final int g = in[i++] & 0xFF; // 2nd byte is green
534 | final int b = in[i++] & 0xFF; // 3rd byte is blue
535 | colors[c] = ((a << 8 | r) << 8 | g) << 8 | b;
536 | }
537 | return i;
538 | }
539 |
540 | /**
541 | * @param fr
542 | * Graphic control extension object
543 | * @param in
544 | * Raw data
545 | * @param i
546 | * Index of the extension introducer
547 | * @return Index of the first byte after this block
548 | */
549 | static final int readGraphicControlExt(final GifFrame fr, final byte[] in, final int i) {
550 | fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2
551 | fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0
552 | fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB
553 | fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
554 | return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
555 | }
556 |
557 | /**
558 | * @param in
559 | * Raw data
560 | * @param img
561 | * The GifImage object that is currently read
562 | * @return Index of the first byte after this block
563 | * @throws IOException
564 | * If the GIF header/trailer is missing, incomplete or unknown
565 | */
566 | static final int readHeader(final byte[] in, final GifImage img) throws IOException {
567 | if (in.length < 6) { // Check first 6 bytes
568 | throw new IOException("Image is truncated.");
569 | }
570 | img.header = new String(in, 0, 6);
571 | if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
572 | throw new IOException("Invalid GIF header.");
573 | }
574 | return 6;
575 | }
576 |
577 | /**
578 | * @param fr
579 | * The GIF frame to whom this image descriptor belongs
580 | * @param in
581 | * Raw data
582 | * @param i
583 | * Index of the first byte of this block, i.e. the minCodeSize
584 | * @return
585 | */
586 | static final int readImgData(final GifFrame fr, final byte[] in, int i) {
587 | final int fileSize = in.length;
588 | final int minCodeSize = in[i++] & 0xFF; // Read code size, go to block
589 | final int clearCode = 1 << minCodeSize; // CLEAR = 2^minCodeSize
590 | fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
591 | fr.clearCode = clearCode;
592 | fr.endOfInfoCode = clearCode + 1;
593 | final int imgDataSize = readImgDataSize(in, i);
594 | final byte[] imgData = new byte[imgDataSize + 2];
595 | int imgDataPos = 0;
596 | int subBlockSize = in[i] & 0xFF;
597 | while (subBlockSize > 0) { // While block has data
598 | try { // Next line may throw exception if sub-block size is fake
599 | final int nextSubBlockSizePos = i + subBlockSize + 1;
600 | final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
601 | arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
602 | imgDataPos += subBlockSize; // Move output data position
603 | i = nextSubBlockSizePos; // Move to next sub-block size
604 | subBlockSize = nextSubBlockSize;
605 | } catch (final Exception e) {
606 | // Sub-block exceeds file end, only use remaining bytes
607 | subBlockSize = fileSize - i - 1; // Remaining bytes
608 | arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
609 | imgDataPos += subBlockSize; // Move output data position
610 | i += subBlockSize + 1; // Move to next sub-block size
611 | break;
612 | }
613 | }
614 | fr.data = imgData; // Holds LZW encoded data
615 | i++; // Skip last sub-block size, should be 0
616 | return i;
617 | }
618 |
619 | static final int readImgDataSize(final byte[] in, int i) {
620 | final int fileSize = in.length;
621 | int imgDataPos = 0;
622 | int subBlockSize = in[i] & 0xFF;
623 | while (subBlockSize > 0) { // While block has data
624 | try { // Next line may throw exception if sub-block size is fake
625 | final int nextSubBlockSizePos = i + subBlockSize + 1;
626 | final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
627 | imgDataPos += subBlockSize; // Move output data position
628 | i = nextSubBlockSizePos; // Move to next sub-block size
629 | subBlockSize = nextSubBlockSize;
630 | } catch (final Exception e) {
631 | // Sub-block exceeds file end, only use remaining bytes
632 | subBlockSize = fileSize - i - 1; // Remaining bytes
633 | imgDataPos += subBlockSize; // Move output data position
634 | break;
635 | }
636 | }
637 | return imgDataPos;
638 | }
639 |
640 | /**
641 | * @param fr
642 | * The GIF frame to whom this image descriptor belongs
643 | * @param in
644 | * Raw data
645 | * @param i
646 | * Index of the image separator, i.e. the first block byte
647 | * @return Index of the first byte after this block
648 | */
649 | static final int readImgDescr(final GifFrame fr, final byte[] in, int i) {
650 | fr.x = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 1-2: left
651 | fr.y = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 3-4: top
652 | fr.w = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 5-6: width
653 | fr.h = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 7-8: height
654 | fr.wh = fr.w * fr.h;
655 | final byte b = in[++i]; // Byte 9 is a packed byte
656 | fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
657 | fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6
658 | fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5
659 | final int colTblSizePower = (b & 7) + 1; // Bits 2-0
660 | fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
661 | return ++i;
662 | }
663 |
664 | /**
665 | * @param img
666 | * @param i
667 | * Start index of this block.
668 | * @return Index of the first byte after this block.
669 | */
670 | static final int readLogicalScreenDescriptor(final GifImage img, final byte[] in, final int i) {
671 | img.w = in[i] & 0xFF | (in[i + 1] & 0xFF) << 8; // 16 bit, LSB 1st
672 | img.h = in[i + 2] & 0xFF | (in[i + 3] & 0xFF) << 8; // 16 bit
673 | img.wh = img.w * img.h;
674 | final byte b = in[i + 4]; // Byte 4 is a packed byte
675 | img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
676 | final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4
677 | img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
678 | img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3
679 | final int globColTblSizePower = (b & 7) + 1; // Bits 0-2
680 | img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
681 | img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
682 | img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
683 | return i + 7;
684 | }
685 |
686 | /**
687 | * @param in
688 | * Raw data
689 | * @param pos
690 | * Index of the extension introducer
691 | * @return Index of the first byte after this block
692 | */
693 | static final int readTextExtension(final byte[] in, final int pos) {
694 | int i = pos + 2; // Skip extension introducer and label
695 | int subBlockSize = in[i++] & 0xFF;
696 | while (subBlockSize != 0 && i < in.length) {
697 | i += subBlockSize;
698 | subBlockSize = in[i++] & 0xFF;
699 | }
700 | return i;
701 | }
702 | }
--------------------------------------------------------------------------------
/src/main/java/com/allenday/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/main/java/com/allenday/.DS_Store
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/Distance.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import java.util.List;
4 | import java.util.Vector;
5 |
6 | public interface Distance {
7 | Double getScalarDistance(Integer d, Vector a, Vector b);
8 |
9 | Vector getVectorDistance(Integer d, Vector a, Vector b);
10 |
11 | Double getVectorNorm(Integer d, Vector v);
12 |
13 | Double getScalarDistance(List> a, List> b);
14 | //public abstract Double distance(Integer dimension, Vector vec);
15 | //public abstract Vector distance(Integer dimension, Vector a, Vector b);
16 | //public List reorder(ImageFeatures query, List inputItems);
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/ImageFeatures.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import org.apache.commons.codec.DecoderException;
4 | import org.apache.commons.lang3.StringUtils;
5 |
6 | import java.io.ByteArrayInputStream;
7 | import java.io.Serializable;
8 | import java.nio.ByteBuffer;
9 | import java.util.ArrayList;
10 | import java.util.List;
11 | import java.util.Vector;
12 | import java.util.stream.IntStream;
13 |
14 | import org.apache.commons.codec.binary.Hex;
15 | import org.apache.commons.codec.binary.Base64;
16 |
17 | public class ImageFeatures implements Serializable {
18 | //TODO enum this
19 | public static final Integer DIMENSIONS = 5;
20 |
21 | public static final Integer R = 0;
22 | public static final Integer G = 1;
23 | public static final Integer B = 2;
24 | public static final Integer T = 3;
25 | public static final Integer C = 4;
26 | public final String id;
27 | public Double score = null;
28 | private final List> vectors = new ArrayList<>();
29 |
30 | public ImageFeatures(String id, int bins, int blocksPerSide) {
31 | this.id = id;
32 | for (Integer d = 0; d < DIMENSIONS; d++) {
33 | Vector v = new Vector<>();
34 | v.setSize(bins);
35 | vectors.add(v);
36 | }
37 | }
38 |
39 | public ImageFeatures(String id, int bins, String encoded) {
40 | this.id = id;
41 | String[] encodedDimension = encoded.split("-");
42 | for (Integer d = 0; d < DIMENSIONS; d++) {
43 | Vector v = decodeFeatures(encodedDimension[d]);
44 | vectors.add(v);
45 | }
46 | }
47 |
48 | public Double getScore() {
49 | return score;
50 | }
51 |
52 | private void boundsCheck(int a, int b) {
53 | if (a != b)
54 | throw new IndexOutOfBoundsException("vector size mismatch: " + a + " != " + b);
55 | }
56 |
57 | public Vector getDimension(Integer d) {
58 | return vectors.get(d);
59 | }
60 |
61 | public List> getDimensions() {
62 | List> dims = new ArrayList<>();
63 | for (int i = 0; i < DIMENSIONS; i++) {
64 | dims.add(this.getDimension(i));
65 | }
66 | return dims;
67 | }
68 |
69 | private Vector d2i(Vector d) {
70 | Vector res = new Vector<>();
71 | for (Double aDouble : d) res.add(aDouble.intValue());
72 | return res;
73 | }
74 |
75 | private String getTokens(Vector x, String prefix, String sep) {
76 | StringBuilder res = new StringBuilder();
77 | Vector v = d2i(x);
78 | for (int i = 0; i < v.size(); i++) {
79 | for (int j = 0; j < v.get(i); j++) {
80 | res.append(String.format("%s%X%s", prefix, i, sep));
81 | }
82 | }
83 | return res.toString();
84 | }
85 |
86 | private String getLabeledHex(Vector x, String prefix, String sep) {
87 | StringBuilder res = new StringBuilder();
88 | Vector v = d2i(x);
89 | for (int i = 0; i < v.size(); i++) {
90 | res.append(String.format("%s%X%X%s", prefix, i, v.get(i) & 0xFFFFF, sep));
91 | }
92 | return res.toString();
93 | }
94 |
95 | public Vector decodeFeatures(String encoded) {
96 | String[] chars = encoded.split("");
97 | Double m = 0d;
98 | Double n = 0d;
99 | Vector v = new Vector<>();
100 | int k = 0;
101 | for (int i = 0; i < chars.length; i++) {
102 | v.setSize(k+2);
103 |
104 | if (chars[i].compareTo("A") == 0) { m=0d; n=0d; }
105 | else if (chars[i].compareTo("B") == 0) { m=0d; n=1d; }
106 | else if (chars[i].compareTo("C") == 0) { m=0d; n=2d; }
107 | else if (chars[i].compareTo("D") == 0) { m=0d; n=3d; }
108 | else if (chars[i].compareTo("E") == 0) { m=0d; n=4d; }
109 | else if (chars[i].compareTo("F") == 0) { m=0d; n=5d; }
110 | else if (chars[i].compareTo("G") == 0) { m=0d; n=6d; }
111 | else if (chars[i].compareTo("H") == 0) { m=0d; n=7d; }
112 | else if (chars[i].compareTo("I") == 0) { m=1d; n=0d; }
113 | else if (chars[i].compareTo("J") == 0) { m=1d; n=1d; }
114 | else if (chars[i].compareTo("K") == 0) { m=1d; n=2d; }
115 | else if (chars[i].compareTo("L") == 0) { m=1d; n=3d; }
116 | else if (chars[i].compareTo("M") == 0) { m=1d; n=4d; }
117 | else if (chars[i].compareTo("N") == 0) { m=1d; n=5d; }
118 | else if (chars[i].compareTo("O") == 0) { m=1d; n=6d; }
119 | else if (chars[i].compareTo("P") == 0) { m=1d; n=7d; }
120 | else if (chars[i].compareTo("Q") == 0) { m=2d; n=0d; }
121 | else if (chars[i].compareTo("R") == 0) { m=2d; n=1d; }
122 | else if (chars[i].compareTo("S") == 0) { m=2d; n=2d; }
123 | else if (chars[i].compareTo("T") == 0) { m=2d; n=3d; }
124 | else if (chars[i].compareTo("U") == 0) { m=2d; n=4d; }
125 | else if (chars[i].compareTo("V") == 0) { m=2d; n=5d; }
126 | else if (chars[i].compareTo("W") == 0) { m=2d; n=6d; }
127 | else if (chars[i].compareTo("X") == 0) { m=2d; n=7d; }
128 | else if (chars[i].compareTo("Y") == 0) { m=3d; n=0d; }
129 | else if (chars[i].compareTo("Z") == 0) { m=3d; n=1d; }
130 | else if (chars[i].compareTo("a") == 0) { m=3d; n=2d; }
131 | else if (chars[i].compareTo("b") == 0) { m=3d; n=3d; }
132 | else if (chars[i].compareTo("c") == 0) { m=3d; n=4d; }
133 | else if (chars[i].compareTo("d") == 0) { m=3d; n=5d; }
134 | else if (chars[i].compareTo("e") == 0) { m=3d; n=6d; }
135 | else if (chars[i].compareTo("f") == 0) { m=3d; n=7d; }
136 | else if (chars[i].compareTo("g") == 0) { m=4d; n=0d; }
137 | else if (chars[i].compareTo("h") == 0) { m=4d; n=1d; }
138 | else if (chars[i].compareTo("i") == 0) { m=4d; n=2d; }
139 | else if (chars[i].compareTo("j") == 0) { m=4d; n=3d; }
140 | else if (chars[i].compareTo("k") == 0) { m=4d; n=4d; }
141 | else if (chars[i].compareTo("l") == 0) { m=4d; n=5d; }
142 | else if (chars[i].compareTo("m") == 0) { m=4d; n=6d; }
143 | else if (chars[i].compareTo("n") == 0) { m=4d; n=7d; }
144 | else if (chars[i].compareTo("o") == 0) { m=5d; n=0d; }
145 | else if (chars[i].compareTo("p") == 0) { m=5d; n=1d; }
146 | else if (chars[i].compareTo("q") == 0) { m=5d; n=2d; }
147 | else if (chars[i].compareTo("r") == 0) { m=5d; n=3d; }
148 | else if (chars[i].compareTo("s") == 0) { m=5d; n=4d; }
149 | else if (chars[i].compareTo("t") == 0) { m=5d; n=5d; }
150 | else if (chars[i].compareTo("u") == 0) { m=5d; n=6d; }
151 | else if (chars[i].compareTo("v") == 0) { m=5d; n=7d; }
152 | else if (chars[i].compareTo("w") == 0) { m=6d; n=0d; }
153 | else if (chars[i].compareTo("x") == 0) { m=6d; n=1d; }
154 | else if (chars[i].compareTo("y") == 0) { m=6d; n=2d; }
155 | else if (chars[i].compareTo("z") == 0) { m=6d; n=3d; }
156 | else if (chars[i].compareTo("0") == 0) { m=6d; n=4d; }
157 | else if (chars[i].compareTo("1") == 0) { m=6d; n=5d; }
158 | else if (chars[i].compareTo("2") == 0) { m=6d; n=6d; }
159 | else if (chars[i].compareTo("3") == 0) { m=6d; n=7d; }
160 | else if (chars[i].compareTo("4") == 0) { m=7d; n=0d; }
161 | else if (chars[i].compareTo("5") == 0) { m=7d; n=1d; }
162 | else if (chars[i].compareTo("6") == 0) { m=7d; n=2d; }
163 | else if (chars[i].compareTo("7") == 0) { m=7d; n=3d; }
164 | else if (chars[i].compareTo("8") == 0) { m=7d; n=4d; }
165 | else if (chars[i].compareTo("9") == 0) { m=7d; n=5d; }
166 | else if (chars[i].compareTo("+") == 0) { m=7d; n=6d; }
167 | else if (chars[i].compareTo("/") == 0) { m=7d; n=7d; }
168 |
169 | //System.err.println("k="+k+","+m);
170 | v.set(k,m);
171 | k++;
172 | //System.err.println("k="+k+","+n);
173 | v.set(k,n);
174 | k++;
175 | }
176 | return v;
177 | }
178 |
179 | private String getLabeledB64(Vector x, String prefix, String sep) {
180 | Vector v = d2i(x);
181 | StringBuilder res = new StringBuilder();
182 |
183 | //TODO this assumes bits=3, but bits is never passed to ImageFeatures constructor
184 | for (int i = 0; i < v.size(); i+= 2) {
185 | String e = null;
186 | int j = i+1;
187 |
188 | if (v.get(i) == 0 && v.get(j) == 0) { e = "A"; }
189 | else if (v.get(i) == 0 && v.get(j) == 1) { e = "B"; }
190 | else if (v.get(i) == 0 && v.get(j) == 2) { e = "C"; }
191 | else if (v.get(i) == 0 && v.get(j) == 3) { e = "D"; }
192 | else if (v.get(i) == 0 && v.get(j) == 4) { e = "E"; }
193 | else if (v.get(i) == 0 && v.get(j) == 5) { e = "F"; }
194 | else if (v.get(i) == 0 && v.get(j) == 6) { e = "G"; }
195 | else if (v.get(i) == 0 && v.get(j) == 7) { e = "H"; }
196 |
197 | else if (v.get(i) == 1 && v.get(j) == 0) { e = "I"; }
198 | else if (v.get(i) == 1 && v.get(j) == 1) { e = "J"; }
199 | else if (v.get(i) == 1 && v.get(j) == 2) { e = "K"; }
200 | else if (v.get(i) == 1 && v.get(j) == 3) { e = "L"; }
201 | else if (v.get(i) == 1 && v.get(j) == 4) { e = "M"; }
202 | else if (v.get(i) == 1 && v.get(j) == 5) { e = "N"; }
203 | else if (v.get(i) == 1 && v.get(j) == 6) { e = "O"; }
204 | else if (v.get(i) == 1 && v.get(j) == 7) { e = "P"; }
205 |
206 | else if (v.get(i) == 2 && v.get(j) == 0) { e = "Q"; }
207 | else if (v.get(i) == 2 && v.get(j) == 1) { e = "R"; }
208 | else if (v.get(i) == 2 && v.get(j) == 2) { e = "S"; }
209 | else if (v.get(i) == 2 && v.get(j) == 3) { e = "T"; }
210 | else if (v.get(i) == 2 && v.get(j) == 4) { e = "U"; }
211 | else if (v.get(i) == 2 && v.get(j) == 5) { e = "V"; }
212 | else if (v.get(i) == 2 && v.get(j) == 6) { e = "W"; }
213 | else if (v.get(i) == 2 && v.get(j) == 7) { e = "X"; }
214 |
215 | else if (v.get(i) == 3 && v.get(j) == 0) { e = "Y"; }
216 | else if (v.get(i) == 3 && v.get(j) == 1) { e = "Z"; }
217 | else if (v.get(i) == 3 && v.get(j) == 2) { e = "a"; }
218 | else if (v.get(i) == 3 && v.get(j) == 3) { e = "b"; }
219 | else if (v.get(i) == 3 && v.get(j) == 4) { e = "c"; }
220 | else if (v.get(i) == 3 && v.get(j) == 5) { e = "d"; }
221 | else if (v.get(i) == 3 && v.get(j) == 6) { e = "e"; }
222 | else if (v.get(i) == 3 && v.get(j) == 7) { e = "f"; }
223 |
224 | else if (v.get(i) == 4 && v.get(j) == 0) { e = "g"; }
225 | else if (v.get(i) == 4 && v.get(j) == 1) { e = "h"; }
226 | else if (v.get(i) == 4 && v.get(j) == 2) { e = "i"; }
227 | else if (v.get(i) == 4 && v.get(j) == 3) { e = "j"; }
228 | else if (v.get(i) == 4 && v.get(j) == 4) { e = "k"; }
229 | else if (v.get(i) == 4 && v.get(j) == 5) { e = "l"; }
230 | else if (v.get(i) == 4 && v.get(j) == 6) { e = "m"; }
231 | else if (v.get(i) == 4 && v.get(j) == 7) { e = "n"; }
232 |
233 | else if (v.get(i) == 5 && v.get(j) == 0) { e = "o"; }
234 | else if (v.get(i) == 5 && v.get(j) == 1) { e = "p"; }
235 | else if (v.get(i) == 5 && v.get(j) == 2) { e = "q"; }
236 | else if (v.get(i) == 5 && v.get(j) == 3) { e = "r"; }
237 | else if (v.get(i) == 5 && v.get(j) == 4) { e = "s"; }
238 | else if (v.get(i) == 5 && v.get(j) == 5) { e = "t"; }
239 | else if (v.get(i) == 5 && v.get(j) == 6) { e = "u"; }
240 | else if (v.get(i) == 5 && v.get(j) == 7) { e = "v"; }
241 |
242 | else if (v.get(i) == 6 && v.get(j) == 0) { e = "w"; }
243 | else if (v.get(i) == 6 && v.get(j) == 1) { e = "x"; }
244 | else if (v.get(i) == 6 && v.get(j) == 2) { e = "y"; }
245 | else if (v.get(i) == 6 && v.get(j) == 3) { e = "z"; }
246 | else if (v.get(i) == 6 && v.get(j) == 4) { e = "0"; }
247 | else if (v.get(i) == 6 && v.get(j) == 5) { e = "1"; }
248 | else if (v.get(i) == 6 && v.get(j) == 6) { e = "2"; }
249 | else if (v.get(i) == 6 && v.get(j) == 7) { e = "3"; }
250 |
251 | else if (v.get(i) == 7 && v.get(j) == 0) { e = "4"; }
252 | else if (v.get(i) == 7 && v.get(j) == 1) { e = "5"; }
253 | else if (v.get(i) == 7 && v.get(j) == 2) { e = "6"; }
254 | else if (v.get(i) == 7 && v.get(j) == 3) { e = "7"; }
255 | else if (v.get(i) == 7 && v.get(j) == 4) { e = "8"; }
256 | else if (v.get(i) == 7 && v.get(j) == 5) { e = "9"; }
257 | else if (v.get(i) == 7 && v.get(j) == 6) { e = "+"; }
258 | else if (v.get(i) == 7 && v.get(j) == 7) { e = "/"; }
259 | res.append(e);
260 | }
261 |
262 | return res.toString();
263 | }
264 |
265 |
266 | public String getJsonAll() {
267 | return "{" +
268 | "\"red\":[" + getR() + "]," +
269 | "\"green\":[" + getG() + "]," +
270 | "\"blue\":[" + getB() + "]," +
271 | "\"texture\":[" + getT() + "]," +
272 | "\"curvature\":[" + getC() + "]" +
273 | "}";
274 | }
275 |
276 | //This is for TF.IDF style search
277 | public String getTokensAll() {
278 | return getTokensR() + getTokensG() + getTokensB() + getTokensT() + getTokensC();
279 |
280 | }
281 | public String getRawHexAll() {
282 | return getRawHexR() + getRawHexG() + getRawHexB() + getRawHexT() + getRawHexC();
283 | }
284 | public String getLabeledHexAll() {
285 | return getLabeledHexR() + getLabeledHexG() + getLabeledHexB() + getLabeledHexT() + getLabeledHexC();
286 | }
287 | public String getRawB64All() {
288 | return getRawB64R() + "-" + getRawB64G() + "-" + getRawB64B() + "-" + getRawB64T() + "-" + getRawB64C();
289 | }
290 | // public String getLabeledBase64All() {
291 | // return getLabeledBase64R() + getLabeledBase64G() + getLabeledBase64B() + getLabeledBase64T() + getLabeledBase64C();
292 | // }
293 |
294 | public String getTokensR() {
295 | return getTokens(vectors.get(R), "r", " ");
296 | }
297 | private String getLabeledHexR() {
298 | return getLabeledHex(vectors.get(R), "r", " ");
299 | }
300 | private String getRawHexR() {
301 | return getLabeledHex(vectors.get(R), "", " ");
302 | }
303 | private String getRawB64R() {
304 | return getLabeledB64(vectors.get(R), "", " ");
305 | }
306 |
307 | public String getTokensG() {
308 | return getTokens(vectors.get(G), "g", " ");
309 | }
310 | private String getLabeledHexG() {
311 | return getLabeledHex(vectors.get(G), "g", " ");
312 | }
313 | private String getRawHexG() {
314 | return getLabeledHex(vectors.get(G), "", " ");
315 | }
316 | private String getRawB64G() {
317 | return getLabeledB64(vectors.get(G), "", " ");
318 | }
319 |
320 | public String getTokensB() {
321 | return getTokens(vectors.get(B), "b", " ");
322 | }
323 | private String getLabeledHexB() {
324 | return getLabeledHex(vectors.get(B), "b", " ");
325 | }
326 | private String getRawHexB() {
327 | return getLabeledHex(vectors.get(B), "", " ");
328 | }
329 | private String getRawB64B() {
330 | return getLabeledB64(vectors.get(B), "", " ");
331 | }
332 |
333 | public String getTokensT() {
334 | return getTokens(vectors.get(T), "t", " ");
335 | }
336 | private String getLabeledHexT() {
337 | return getLabeledHex(vectors.get(T), "t", " ");
338 | }
339 | private String getRawHexT() {
340 | return getLabeledHex(vectors.get(T), "", " ");
341 | }
342 | private String getRawB64T() {
343 | return getLabeledB64(vectors.get(T), "", " ");
344 | }
345 |
346 | public String getTokensC() {
347 | return getTokens(vectors.get(C), "c", " ");
348 | }
349 | private String getLabeledHexC() {
350 | return getLabeledHex(vectors.get(C), "c", " ");
351 | }
352 | private String getRawHexC() {
353 | return getLabeledHex(vectors.get(C), "", " ");
354 | }
355 | private String getRawB64C() {
356 | return getLabeledB64(vectors.get(C), "", " ");
357 | }
358 |
359 | // Red. double[8], 0..255
360 | private String getR() {
361 | return StringUtils.join(d2i(vectors.get(R)), ",");
362 | }
363 |
364 | public void setR(double[] n) {
365 | boundsCheck(n.length, vectors.get(R).size());
366 | for (int i = 0; i < n.length; i++) {
367 | vectors.get(R).set(i, n[i]);
368 | }
369 | }
370 |
371 | // Green. double[8], 0..255
372 | private String getG() {
373 | return StringUtils.join(d2i(vectors.get(G)), ",");
374 | }
375 |
376 | public void setG(double[] n) {
377 | boundsCheck(n.length, vectors.get(G).size());
378 | for (int i = 0; i < n.length; i++) {
379 | vectors.get(G).set(i, n[i]);
380 | }
381 | }
382 |
383 | // Blue. double[8], 0..255
384 | private String getB() {
385 | return StringUtils.join(d2i(vectors.get(B)), ",");
386 | }
387 |
388 | public void setB(double[] n) {
389 | boundsCheck(n.length, vectors.get(B).size());
390 | for (int i = 0; i < n.length; i++) {
391 | vectors.get(B).set(i, n[i]);
392 | }
393 | }
394 |
395 | // Texture. double[8], 0..255
396 | private String getT() {
397 | return StringUtils.join(d2i(vectors.get(T)), ",");
398 | }
399 |
400 | public void setT(double[] n) {
401 | boundsCheck(n.length, vectors.get(T).size());
402 | for (int i = 0; i < n.length; i++) {
403 | vectors.get(T).set(i, n[i]);
404 | }
405 | }
406 |
407 | // Curvature. double[8], 0..255
408 | private String getC() {
409 | return StringUtils.join(d2i(vectors.get(C)), ",");
410 | }
411 |
412 | public void setC(double[] n) {
413 | boundsCheck(n.length, vectors.get(C).size());
414 | for (int i = 0; i < n.length; i++) {
415 | vectors.get(C).set(i, n[i]);
416 | }
417 | }
418 | }
419 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/ImageIndex.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import edu.wlu.cs.levy.CG.KDTree;
4 | import edu.wlu.cs.levy.CG.KeyDuplicateException;
5 | import edu.wlu.cs.levy.CG.KeySizeException;
6 | import org.apache.commons.lang3.ArrayUtils;
7 | import org.slf4j.Logger;
8 | import org.slf4j.LoggerFactory;
9 |
10 | import java.io.Serializable;
11 | import java.util.*;
12 |
13 | public class ImageIndex implements Serializable {
14 | private static final Logger logger = LoggerFactory.getLogger(ImageIndex.class);
15 |
16 | public static final Integer R = 0;
17 | public static final Integer G = 1;
18 | public static final Integer B = 2;
19 | public static final Integer T = 3;
20 | public static final Integer C = 4;
21 |
22 | private final Integer keySize;
23 | private final Map files = new HashMap<>();
24 | private final ArrayList> histogram = new ArrayList<>();
25 | private final ArrayList> tHistogram = new ArrayList<>();
26 | private List> kdTrees = new ArrayList<>();
27 | private final ArrayList> toptree = new ArrayList<>();
28 |
29 | public ImageIndex(Integer keySize) {
30 | this.keySize = keySize;
31 | clearIndex();
32 | }
33 |
34 | private ImageIndex(Integer bins, List> trees) {
35 | keySize = bins;
36 | kdTrees = trees;
37 | }
38 |
39 | public static Double getScalarDistance(ImageFeatures a, ImageFeatures b, Distance m) {
40 | List> av = a.getDimensions();
41 | List> bv = b.getDimensions();
42 | return m.getScalarDistance(av, bv);
43 | }
44 |
45 | public List> getKDTrees() {
46 | return kdTrees;
47 | }
48 |
49 | private void clearIndex() {
50 | kdTrees.clear();
51 | histogram.clear();
52 | for (int i = 0; i < ImageFeatures.DIMENSIONS; i++) {
53 | histogram.add(new HashMap<>());
54 | kdTrees.add(new KDTree<>(keySize));
55 | }
56 | toptree.clear();
57 | toptree.add(new KDTree<>(keySize));
58 | toptree.add(new KDTree<>(keySize));
59 | toptree.add(new KDTree<>(keySize));
60 |
61 | tHistogram.clear();
62 | tHistogram.add(new HashMap<>());
63 | tHistogram.add(new HashMap<>());
64 | tHistogram.add(new HashMap<>());
65 | }
66 |
67 | void putFile(String fileName, ImageFeatures features) {
68 | files.put(fileName, features);
69 | }
70 |
71 | void putPoint(Integer feature, String key, Vector vector) throws KeySizeException, KeyDuplicateException {
72 | logger.debug("putPoint " + feature + "," + key + "," + vector);
73 | double[] x = new double[vector.size()];
74 | for (int i = 0; i < vector.size(); i++)
75 | x[i] = vector.get(i);
76 | if (kdTrees.get(feature).search(x) == null)
77 | kdTrees.get(feature).insert(x, key);
78 | histogram.get(feature).put(key, x);
79 | }
80 |
81 | private double[] getPoint(Integer feature, String key) {
82 | //logger.debug("feature="+feature);
83 | //logger.debug("key="+key);
84 | logger.debug(histogram.get(feature) + "");
85 | return histogram.get(feature).get(key);
86 | }
87 |
88 | public Set getHits(ImageFeatures query, Integer howMany) throws KeySizeException, IllegalArgumentException, CloneNotSupportedException {
89 | Set results = new HashSet<>();
90 | Set unionHits = new HashSet<>();
91 | for (int d = 0; d < ImageFeatures.DIMENSIONS; d++) {
92 | //logger.debug("1: "+query.id);
93 | //logger.debug("2: "+kdTrees.get(d));
94 | Double[] a0 = new Double[query.getDimension(d).size()];
95 | //logger.debug("3: "+query.getDimension(d).size());
96 | double[] a1 = ArrayUtils.toPrimitive(query.getDimension(d).toArray(a0));
97 | //logger.debug("4: "+a0[0]);
98 | //logger.debug("5: "+a1[0]);
99 | //logger.debug("3: "+kdTrees.get(d).nearest(a1,1));
100 | //logger.debug("4: ");
101 |
102 | for (String hit : kdTrees.get(d).nearest(a1, howMany)) {
103 | unionHits.add(files.get(hit));
104 | }
105 | }
106 | return unionHits;
107 | }
108 |
109 | public ArrayList rankHits(ImageFeatures query, Set hits, Distance ranker) {
110 | Set results = new HashSet<>();
111 |
112 | for (ImageFeatures hit : hits) {
113 | Double d = getScalarDistance(query, hit, ranker);
114 | hit.score = Math.log10(d); //TODO do log transform on L1 and L2
115 | }
116 |
117 | ArrayList hitsArray = new ArrayList(hits);
118 | hitsArray.sort(new ImageFeaturesComparator());
119 | return hitsArray;
120 | }
121 |
122 | class ImageFeaturesComparator implements Comparator {
123 | public int compare(ImageFeatures s1, ImageFeatures s2) {
124 | return Double.compare(s1.score, s2.score); //1,2=sort asc, 2,1=sort desc TODO move this to distance class, L1 and L2 should be asc, corr should be desc
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/ImageIndexFactory.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import edu.wlu.cs.levy.CG.KeyDuplicateException;
4 | import edu.wlu.cs.levy.CG.KeySizeException;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.util.*;
11 |
12 | public class ImageIndexFactory {
13 | private static final Logger logger = LoggerFactory.getLogger(ImageIndexFactory.class);
14 | public final Map files = new HashMap<>();
15 | private final ImageProcessor processor;
16 | private final ImageIndex index;
17 |
18 | public ImageIndexFactory(Integer bins, Integer bits, Boolean normalize) {
19 | processor = new ImageProcessor(bins, bits, normalize);
20 | index = new ImageIndex(bins);
21 | }
22 |
23 | public ImageIndex createIndex() throws KeySizeException, KeyDuplicateException, IOException {
24 | return createIndex(0, 1);
25 | }
26 |
27 | public ImageIndex createIndex(Integer howMany, Integer stepSize) throws KeySizeException, KeyDuplicateException, IOException {
28 | List forRemoval = new ArrayList<>();
29 | Integer added = 0;
30 | Integer substep = 0;
31 | File[] sFiles = files.keySet().toArray(new File[0]);
32 | Arrays.sort(sFiles);
33 | for (File file : sFiles) {
34 | substep++;
35 | if (substep < stepSize) {
36 | //logger.debug("substep "+substep+" remove "+file);
37 | forRemoval.add(file);
38 | continue;
39 | }
40 | substep = 0;
41 |
42 | ImageFeatures features = processor.extractFeatures(file);
43 | if (features != null) {
44 | index.putFile(file.toString(), features);
45 | index.putPoint(ImageIndex.R, features.id, features.getDimension(ImageFeatures.R));
46 | index.putPoint(ImageIndex.G, features.id, features.getDimension(ImageFeatures.G));
47 | index.putPoint(ImageIndex.B, features.id, features.getDimension(ImageFeatures.B));
48 | index.putPoint(ImageIndex.T, features.id, features.getDimension(ImageFeatures.T));
49 | index.putPoint(ImageIndex.C, features.id, features.getDimension(ImageFeatures.C));
50 | } else {
51 | forRemoval.add(file);
52 | logger.debug("removing file from consideration: " + file);
53 | }
54 | added++;
55 | if (howMany > 0 && added >= howMany)
56 | break;
57 | }
58 | for (File f : forRemoval) {
59 | files.remove(f);
60 | //logger.debug("removed "+f);
61 | }
62 | return index;
63 | }
64 |
65 | public ImageProcessor getProcessor() {
66 | return processor;
67 | }
68 |
69 | private void clearFiles() {
70 | files.clear();
71 | }
72 |
73 | private void setFiles(List files) {
74 | clearFiles();
75 | addFiles(files);
76 | }
77 |
78 | private void addFiles(List files) {
79 | for (File file : files) {
80 | addFile(file);
81 | }
82 | }
83 |
84 | public void addFile(File file) {
85 | if (file.isDirectory()) {
86 | logger.debug("processing directory: " + file);
87 | recurse(file);
88 | } else if (!files.containsKey(file)) {
89 | files.put(file, null);
90 | }
91 | }
92 |
93 | private void recurse(File directory) {
94 | if (directory.isDirectory()) {
95 | String[] ents = directory.list();
96 | Arrays.sort(Objects.requireNonNull(ents));
97 | int i;
98 | for (String ent : ents) {
99 | File f = new File(directory + "/" + ent);
100 | if (f.isDirectory()) {
101 | recurse(f);
102 | } else {
103 | //logger.debug("processing file: " + f);
104 | if (!files.containsKey(f))
105 | files.put(f, null);
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/ImageProcessor.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import com.allenday.image.backend.Processor;
4 | import edu.wlu.cs.levy.CG.KeyDuplicateException;
5 | import edu.wlu.cs.levy.CG.KeySizeException;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | import java.io.File;
10 | import java.io.IOException;
11 |
12 | public class ImageProcessor {
13 | public static final int R = 0;
14 | public static final int G = 1;
15 | public static final int B = 2;
16 | public static final int T = 3;
17 | public static final int C = 4;
18 | public static final int M = 5;
19 | private static final Logger logger = LoggerFactory.getLogger(Processor.class);
20 | private final int bins;
21 | private final int bits;
22 | private final boolean normalize;
23 |
24 |
25 | private Processor processor;
26 |
27 | //static KDTree[] kdtree = {new KDTree(8), new KDTree(8), new KDTree(8)};
28 |
29 | ImageProcessor() {
30 | this(8, 8, false);
31 | }
32 |
33 | public ImageProcessor(int bins, int bits, boolean normalize) {
34 | this.bins = bins;
35 | this.bits = bits;
36 | this.normalize = normalize;
37 | }
38 |
39 |
40 | public ImageFeatures extractFeatures(File file) throws IOException {
41 | //disallow non jpg
42 | //TODO check for extension, not only string occurrence
43 | if (!file.toString().contains("jpg") && !file.toString().contains("jpeg") && !file.toString().contains("gif")) {
44 | //readLuminance() failed
45 | //&& file.toString().indexOf("png") == -1
46 | throw new IOException("skipping file not matching (jpg, jpeg, gif): " + file);
47 | }
48 |
49 | try {
50 | processor = new Processor(file, bins, bits, normalize);
51 | } catch (IOException e) {
52 | // TODO Auto-generated catch block
53 | e.printStackTrace();
54 | } catch (NullPointerException e) {
55 | logger.debug("failed to process file: " + file);
56 | e.printStackTrace();
57 | }
58 |
59 | double[] r = processor.getRedHistogram();
60 | double[] g = processor.getGreenHistogram();
61 | double[] b = processor.getBlueHistogram();
62 | double[] t = processor.getTextureHistogram();
63 | double[] c = processor.getCurvatureHistogram();
64 | double[] m = processor.getTopologyValues();
65 | char[] ml = processor.getTopologyLabels();
66 |
67 | // TODO parameterize blocksPerSide
68 | ImageFeatures features = new ImageFeatures(file.toString(), bins, 16);
69 | features.setR(r);
70 | features.setG(g);
71 | features.setB(b);
72 | features.setT(t);
73 | features.setC(c);
74 |
75 | return features;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/Ranker.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import edu.wlu.cs.levy.CG.KDTree;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 |
7 | import java.util.ArrayList;
8 | import java.util.HashMap;
9 |
10 | class Ranker {
11 | private static final Logger logger = LoggerFactory.getLogger(Ranker.class);
12 |
13 | public static String[] LABEL = {"RED", "GREEN", "BLUE", "TEXTURE", "CURVATURE" };
14 | private static Double[] COR_WEIGHT = {0.6d, 0.8d, 0.8d, 1.2d, 1.6d};
15 | private static Double[] COL_WEIGHT = {0.6d, 0.8d, 0.8d, 1.2d, 1.6d};
16 |
17 | private static double[][] MAT_WEIGHT = {
18 | {0.2d, 0.3d, 0.3d, 0.4d, 0.5d, 0.7d, 0.8d, 0.3d}, //R
19 | {0.2d, 0.5d, 0.7d, 0.9d, 1.0d, 1.0d, 0.9d, 0.4d}, //G
20 | {0.2d, 0.5d, 0.7d, 0.9d, 1.0d, 0.9d, 0.9d, 0.4d}, //B
21 | {1.0d, 1.0d, 1.0d, 1.0d, 1.0d, 1.0d, 1.0d, 1.0d}, //T
22 | {1.0d, 1.0d, 1.0d, 1.0d, 1.0d, 1.0d, 1.0d, 1.0d}, //C
23 | };
24 |
25 | private ArrayList> histogram = new ArrayList<>();
26 | private ArrayList> tHistogram = new ArrayList<>();
27 | private ArrayList> kdTrees = new ArrayList<>();
28 | private ArrayList> toptree = new ArrayList<>();
29 |
30 | private double getWeightedPearsonCorrelationSimilarity(double[] weight, double[] vector1, double[] vector2) {
31 | double sumXxY = 0;
32 | double sumX = 0;
33 | double sumY = 0;
34 | double sumXxX = 0;
35 | double sumYxY = 0;
36 |
37 | for (int i = 0; i < vector1.length; i++) {
38 | double value1 = vector1[i] * weight[i];
39 | double value2 = vector2[i] * weight[i];
40 | sumXxY += value1 * value2;
41 | sumX += value1;
42 | sumY += value2;
43 | sumXxX += value1 * value1;
44 | sumYxY += value2 * value2;
45 | }
46 | return (vector1.length * sumXxY - sumX * sumY)
47 | / Math.sqrt((vector1.length * sumXxX - sumX * sumX)
48 | * (vector1.length * sumYxY - sumY * sumY));
49 | }
50 |
51 | /*
52 | private Map getHits(Integer feature, String query, Integer howMany, Boolean boost) throws KeySizeException, IllegalArgumentException {
53 | List hits = toptree.get(feature).nearest(topPoint(feature, query), howMany);
54 | for (String hit : hits) {
55 | double[] q = topPoint(feature, query);
56 | double[] h = topPoint(feature, hit);
57 | Integer distance;
58 | if (boost) {
59 | distance = getWeightedEuclideanDistanceSimilarity(MAT_WEIGHT[feature], q, h);
60 | } else {
61 | distance = getEuclideanDistanceSimilarity(q, h);
62 | }
63 | results.put(hit, distance);
64 | }
65 | return results;
66 | }
67 | */
68 | private int getEuclideanDistanceSimilarity(double[] a, double[] b) {
69 | int distance = 0;
70 | for (int i = 0; i < a.length; i++)
71 | distance += Math.abs(a[i] - b[i]);
72 | return distance;
73 | }
74 |
75 | private int getWeightedEuclideanDistanceSimilarity(double[] w, double[] a, double[] b) {
76 | int distance = 0;
77 | for (int i = 0; i < a.length; i++)
78 | distance += Math.abs(w[i] * (a[i] - b[i]));
79 | return distance;
80 | }
81 |
82 |
83 | /*
84 | private double[] getPoint(Integer feature, String key) {
85 | return histogram.get(feature).get(key);
86 | }
87 | */
88 |
89 | /*
90 | @SuppressWarnings("unused")
91 | private String getPointString(Integer feature, String key) {
92 | StringBuilder result = new StringBuilder();
93 | double[] point = getPoint(feature, key);
94 | for (double aPoint : point) result.append(aPoint).append(",");
95 | return result.toString();
96 | }
97 | */
98 |
99 | /*
100 | @SuppressWarnings("unused")
101 | List rank(ImageFeatures query, Boolean boost) throws KeySizeException, IllegalArgumentException {
102 | List results = new ArrayList();
103 |
104 | Map combine = new HashMap();
105 | Map merged = new TreeMap();//Collections.reverseOrder());
106 | Map euclidean = new HashMap();
107 | Map pearson = new HashMap();
108 |
109 | for (int i = 0; i < 5; i++) {
110 | Map hits = getHits(i, query.id, 500, boost);
111 | for (String j : hits.keySet()) {
112 | Integer d = hits.get(j);
113 |
114 | if (!combine.containsKey(j)) {
115 | double value = 0;
116 | double euclid = 0;
117 | for (int k = 0; k < 5; k++) {
118 | euclid += 1.0 * COL_WEIGHT[k] * getWeightedEuclideanDistanceSimilarity(MAT_WEIGHT[k], getPoint(k, query.id), getPoint(k, j)) / 255;
119 | value += 0.2 * COR_WEIGHT[k] * getWeightedPearsonCorrelationSimilarity(MAT_WEIGHT[k], getPoint(k, query.id), getPoint(k, j));
120 |
121 | }
122 | combine.put(j, (1.0 * value) * (0.5 * euclid));
123 | pearson.put(j, value);
124 | euclidean.put(j, euclid);
125 | }
126 | }
127 | }
128 |
129 | for (String k : combine.keySet()) {
130 | Double v = combine.get(k);
131 | while (merged.containsKey(v))
132 | v += 0.1d;
133 | merged.put(v, k);
134 | }
135 |
136 | int m = 0;
137 | for (Double d : merged.keySet()) {
138 | if (pearson.get(merged.get(d)) > 0.85 && euclidean.get(merged.get(d)) < 5) {//&& topology.get(merged.get(d)) != null && topology.get(merged.get(d)) < 30)
139 | SearchResult sr = new SearchResult();
140 | sr.id = merged.get(d);
141 | sr.score = d;
142 | results.add(sr);
143 | //results.add(ff);
144 | //System.out.println((m++)
145 | // + "\t" + d
146 | // + "\t" + merged.get(d)
147 | // + "\t" + pearson.get(merged.get(d))
148 | // + "\t" + euclidean.get(merged.get(d))
149 | // + "\t" + topology.get(merged.get(d))
150 | // + "\t" + ""
151 | //);
152 | }
153 | }
154 |
155 | return results;
156 | }
157 | */
158 |
159 | /*
160 | public static void main(String[] args) throws IOException, KeySizeException, KeyDuplicateException {
161 |
162 | String query = args[0];
163 |
164 | // String query = "/Users/allenday/Sites/tmp/21K/ce85aabe68b9449bc0b799a6505c3031.300x.jpg";
165 | Ranker ranker = new Ranker();
166 |
167 | Map combine = new HashMap();
168 | Map merged = new TreeMap();//Collections.reverseOrder());
169 | Map euclidean = new HashMap();
170 | Map pearson = new HashMap();
171 | Map topology = new HashMap();
172 |
173 | for (int i = 0; i < 5; i++) {
174 | List> hits = ranker.getHits(i, query, 500);
175 | for (List hit : hits) {
176 | String j = hit.get(0);
177 | String d = hit.get(1);
178 |
179 | if (!combine.containsKey(j)) {
180 | double value = 0;
181 | double euclid = 0;
182 | for (int k = 0; k < 5; k++) {
183 | euclid += 1.0 * COL_WEIGHT[k] * ranker.getWeightedEuclideanDistanceSimilarity(MAT_WEIGHT[k], ranker.getPoint(k, query), ranker.getPoint(k, j)) / 255;
184 | value += 0.2 * COR_WEIGHT[k] * ranker.getWeightedPearsonCorrelationSimilarity(MAT_WEIGHT[k], ranker.getPoint(k, query), ranker.getPoint(k, j));
185 | }
186 | combine.put(j,(1.0*value)*(0.5*euclid));
187 | pearson.put(j, value);
188 | euclidean.put(j,euclid);
189 | }
190 | }
191 | }
192 |
193 | for (int i = 0; i < 3; i++) {
194 | List> hits = ranker.topHits(i, query, 10000);
195 | for (List hit : hits) {
196 | String j = hit.get(0);
197 | String d = hit.get(1);
198 | System.out.println(j+"\t"+d);
199 | double sum = 0;
200 | for (int k = 0; k < 3; k++) {
201 | sum += ranker.getEuclideanDistanceSimilarity(ranker.topPoint(k, query), ranker.topPoint(k, j));
202 | }
203 | topology.put(j, sum);
204 | }
205 | }
206 |
207 | for (String k : combine.keySet()) {
208 | Double v = combine.get(k);
209 | while (merged.containsKey(v))
210 | v += 0.1d;
211 | merged.put(v, k);
212 | }
213 | int m = 0;
214 | for (Double d : merged.keySet()) {
215 | if (pearson.get(merged.get(d)) > 0.85 && euclidean.get(merged.get(d)) < 5 )//&& topology.get(merged.get(d)) != null && topology.get(merged.get(d)) < 30)
216 | System.out.println((m++) + "\t" + d + "\t" + merged.get(d) + "\t" + pearson.get(merged.get(d)) + "\t" + euclidean.get(merged.get(d)) + "\t" + topology.get(merged.get(d)) + "\t" +
217 | // ranker.getPointString(0, merged.get(d)).toString() + "\t" +
218 | // ranker.getPointString(1, merged.get(d)).toString() + "\t" +
219 | // ranker.getPointString(2, merged.get(d)).toString() + "\t" +
220 | // ranker.getPointString(3, merged.get(d)).toString() + "\t" +
221 | // ranker.getPointString(4, merged.get(d)).toString() + "\t" +
222 | ""
223 | );
224 | }
225 | }
226 | */
227 | }
228 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/SearchResult.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | class SearchResult {
4 | private final String id;
5 | private final Double score;
6 |
7 | public SearchResult(String id, Double score) {
8 | this.id = id;
9 | this.score = score;
10 | }
11 |
12 | public Double getScore() {
13 | return score;
14 | }
15 |
16 | public String getId() {
17 | return id;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/backend/CannyEdgeDetector.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.backend;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 |
6 | import java.awt.image.BufferedImage;
7 | import java.util.Arrays;
8 |
9 | /**
10 | *
This software has been released into the public domain.
11 | * Please read the notes in this source file for additional information.
12 | *
13 | *
14 | *
This class provides a configurable implementation of the Canny edge
15 | * detection algorithm. This classic algorithm has a number of shortcomings,
16 | * but remains an effective tool in many scenarios. This class is designed
17 | * for single threaded use only.
18 | *
19 | *
Sample usage:
20 | *
21 | *
22 | * //create the detector
23 | * CannyEdgeDetector detector = new CannyEdgeDetector();
24 | * //adjust its parameters as desired
25 | * detector.setLowThreshold(0.5f);
26 | * detector.setHighThreshold(1f);
27 | * //apply it to an image
28 | * detector.setSourceImage(frame);
29 | * detector.process();
30 | * BufferedImage edges = detector.getEdgesImage();
31 | *
32 | *
33 | *
For a more complete understanding of this edge detector's parameters
34 | * consult an explanation of the algorithm.
35 | *
36 | * @author Tom Gibara
37 | */
38 |
39 | class CannyEdgeDetector {
40 |
41 | // statics
42 | private static final Logger logger = LoggerFactory.getLogger(CannyEdgeDetector.class);
43 |
44 | private final static float GAUSSIAN_CUT_OFF = 0.005f;
45 | private final static float MAGNITUDE_SCALE = 100F;
46 | private final static float MAGNITUDE_LIMIT = 1000F;
47 | private final static int MAGNITUDE_MAX = (int) (MAGNITUDE_SCALE * MAGNITUDE_LIMIT);
48 |
49 | // fields
50 | private int height;
51 | private int width;
52 | private int picsize;
53 | private int[] data;
54 | private int[] magnitude;
55 | private int[] orientation;
56 | private BufferedImage sourceImage;
57 | private BufferedImage edgesImage;
58 |
59 | private float gaussianKernelRadius;
60 | private float lowThreshold;
61 | private float highThreshold;
62 | private int gaussianKernelWidth;
63 | private boolean contrastNormalized;
64 |
65 | private float[] xConv;
66 | private float[] yConv;
67 | private float[] xGradient;
68 | private float[] yGradient;
69 |
70 | // constructors
71 |
72 | /**
73 | * Constructs a new detector with default parameters.
74 | */
75 | CannyEdgeDetector() {
76 | lowThreshold = 2.5f;
77 | highThreshold = 7.5f;
78 | gaussianKernelRadius = 2f;
79 | gaussianKernelWidth = 16;
80 | contrastNormalized = false;
81 | }
82 |
83 | // accessors
84 |
85 | /**
86 | * The image that provides the luminance data used by this detector to
87 | * generate edges.
88 | *
89 | * @return the source image, or null
90 | */
91 | public BufferedImage getSourceImage() {
92 | return sourceImage;
93 | }
94 |
95 | /**
96 | * Specifies the image that will provide the luminance data in which edges
97 | * will be detected. A source image must be set before the process method
98 | * is called.
99 | *
100 | * @param image a source of luminance data
101 | */
102 | void setSourceImage(BufferedImage image) {
103 | sourceImage = image;
104 | }
105 |
106 | /**
107 | * Obtains an image containing the edges detected during the last call to
108 | * the process method. The buffered image is an opaque image of type
109 | * BufferedImage.TYPE_INT_ARGB in which edge pixels are white and all other
110 | * pixels are black.
111 | *
112 | * @return an image containing the detected edges, or null if the process
113 | * method has not yet been called.
114 | */
115 | BufferedImage getEdgesImage() {
116 | return edgesImage;
117 | }
118 |
119 | /**
120 | * Sets the edges image. Calling this method will not change the operation
121 | * of the edge detector in any way. It is intended to provide a means by
122 | * which the memory referenced by the detector object may be reduced.
123 | *
124 | * @param edgesImage expected (though not required) to be null
125 | */
126 | public void setEdgesImage(BufferedImage edgesImage) {
127 | this.edgesImage = edgesImage;
128 | }
129 |
130 | /**
131 | * The low threshold for hysteresis. The default value is 2.5.
132 | *
133 | * @return the low hysteresis threshold
134 | */
135 | public float getLowThreshold() {
136 | return lowThreshold;
137 | }
138 |
139 | /**
140 | * Sets the low threshold for hysteresis. Suitable values for this parameter
141 | * must be determined experimentally for each application. It is nonsensical
142 | * (though not prohibited) for this value to exceed the high threshold value.
143 | *
144 | * @param threshold a low hysteresis threshold
145 | */
146 | void setLowThreshold(float threshold) {
147 | if (threshold < 0) throw new IllegalArgumentException();
148 | lowThreshold = threshold;
149 | }
150 |
151 | /**
152 | * The high threshold for hysteresis. The default value is 7.5.
153 | *
154 | * @return the high hysteresis threshold
155 | */
156 | public float getHighThreshold() {
157 | return highThreshold;
158 | }
159 |
160 | /**
161 | * Sets the high threshold for hysteresis. Suitable values for this
162 | * parameter must be determined experimentally for each application. It is
163 | * nonsensical (though not prohibited) for this value to be less than the
164 | * low threshold value.
165 | *
166 | * @param threshold a high hysteresis threshold
167 | */
168 | void setHighThreshold(float threshold) {
169 | if (threshold < 0) throw new IllegalArgumentException();
170 | highThreshold = threshold;
171 | }
172 |
173 | /**
174 | * The number of pixels across which the Gaussian kernel is applied.
175 | * The default value is 16.
176 | *
177 | * @return the radius of the convolution operation in pixels
178 | */
179 | public int getGaussianKernelWidth() {
180 | return gaussianKernelWidth;
181 | }
182 |
183 | /**
184 | * The number of pixels across which the Gaussian kernel is applied.
185 | * This implementation will reduce the radius if the contribution of pixel
186 | * values is deemed negligible, so this is actually a maximum radius.
187 | *
188 | * @param gaussianKernelWidth a radius for the convolution operation in
189 | * pixels, at least 2.
190 | */
191 | public void setGaussianKernelWidth(int gaussianKernelWidth) {
192 | if (gaussianKernelWidth < 2) throw new IllegalArgumentException();
193 | this.gaussianKernelWidth = gaussianKernelWidth;
194 | }
195 |
196 | /**
197 | * The radius of the Gaussian convolution kernel used to smooth the source
198 | * image prior to gradient calculation. The default value is 16.
199 | *
200 | * @return the Gaussian kernel radius in pixels
201 | */
202 | public float getGaussianKernelRadius() {
203 | return gaussianKernelRadius;
204 | }
205 |
206 | /**
207 | * Sets the radius of the Gaussian convolution kernel used to smooth the
208 | * source image prior to gradient calculation.
209 | *
210 | * returns a Gaussian kernel radius in pixels, must exceed 0.1f.
211 | */
212 | public void setGaussianKernelRadius(float gaussianKernelRadius) {
213 | if (gaussianKernelRadius < 0.1f) throw new IllegalArgumentException();
214 | this.gaussianKernelRadius = gaussianKernelRadius;
215 | }
216 |
217 | /**
218 | * Whether the luminance data extracted from the source image is normalized
219 | * by linearizing its histogram prior to edge extraction. The default value
220 | * is false.
221 | *
222 | * @return whether the contrast is normalized
223 | */
224 | public boolean isContrastNormalized() {
225 | return contrastNormalized;
226 | }
227 |
228 | /**
229 | * Sets whether the contrast is normalized
230 | *
231 | * @param contrastNormalized true if the contrast should be normalized,
232 | * false otherwise
233 | */
234 | public void setContrastNormalized(boolean contrastNormalized) {
235 | this.contrastNormalized = contrastNormalized;
236 | }
237 |
238 | // methods
239 | void process() {
240 | width = sourceImage.getWidth();
241 | height = sourceImage.getHeight();
242 | picsize = width * height;
243 | initArrays();
244 | try {
245 | readLuminance();
246 | } catch (IllegalArgumentException e) {
247 | logger.error("readLuminance() failed");
248 | e.printStackTrace();
249 | return;
250 | }
251 | if (contrastNormalized) normalizeContrast();
252 | computeGradients(gaussianKernelRadius, gaussianKernelWidth);
253 | int low = Math.round(lowThreshold * MAGNITUDE_SCALE);
254 | int high = Math.round(highThreshold * MAGNITUDE_SCALE);
255 | performHysteresis(low, high);
256 | thresholdEdges();
257 | writeEdges(data);
258 | }
259 |
260 | // private utility methods
261 | private void initArrays() {
262 | if (data == null || picsize != data.length) {
263 | data = new int[picsize];
264 | magnitude = new int[picsize];
265 | orientation = new int[picsize];
266 |
267 | xConv = new float[picsize];
268 | yConv = new float[picsize];
269 | xGradient = new float[picsize];
270 | yGradient = new float[picsize];
271 | }
272 | }
273 |
274 | //NOTE: The elements of the method below (specifically the technique for
275 | //non-maximal suppression and the technique for gradient computation)
276 | //are derived from an implementation posted in the following forum (with the
277 | //clear intent of others using the code):
278 | // http://forum.java.sun.com/thread.jspa?threadID=546211&start=45&tstart=0
279 | //My code effectively mimics the algorithm exhibited above.
280 | //Since I don't know the providence of the code that was posted it is a
281 | //possibility (though I think a very remote one) that this code violates
282 | //someone's intellectual property rights. If this concerns you feel free to
283 | //contact me for an alternative, though less efficient, implementation.
284 | private void computeGradients(float kernelRadius, int kernelWidth) {
285 | //generate the gaussian convolution masks
286 | float[] kernel = new float[kernelWidth];
287 | float[] diffKernel = new float[kernelWidth];
288 | int kwidth;
289 | for (kwidth = 0; kwidth < kernelWidth; kwidth++) {
290 | float g1 = gaussian(kwidth, kernelRadius);
291 | if (g1 <= GAUSSIAN_CUT_OFF && kwidth >= 2) break;
292 | float g2 = gaussian(kwidth - 0.5f, kernelRadius);
293 | float g3 = gaussian(kwidth + 0.5f, kernelRadius);
294 | kernel[kwidth] = (g1 + g2 + g3) / 3f / (2f * (float) Math.PI * kernelRadius * kernelRadius);
295 | diffKernel[kwidth] = g3 - g2;
296 | }
297 |
298 | int initX = kwidth - 1;
299 | int maxX = width - (kwidth - 1);
300 | int initY = width * (kwidth - 1);
301 | int maxY = width * (height - (kwidth - 1));
302 |
303 | //perform convolution in x and y directions
304 | for (int x = initX; x < maxX; x++) {
305 | for (int y = initY; y < maxY; y += width) {
306 | int index = x + y;
307 | float sumX = data[index] * kernel[0];
308 | float sumY = sumX;
309 | int xOffset = 1;
310 | int yOffset = width;
311 | for (; xOffset < kwidth; ) {
312 | sumY += kernel[xOffset] * (data[index - yOffset] + data[index + yOffset]);
313 | sumX += kernel[xOffset] * (data[index - xOffset] + data[index + xOffset]);
314 | yOffset += width;
315 | xOffset++;
316 | }
317 |
318 | yConv[index] = sumY;
319 | xConv[index] = sumX;
320 | }
321 | }
322 |
323 | for (int x = initX; x < maxX; x++) {
324 | for (int y = initY; y < maxY; y += width) {
325 | float sum = 0f;
326 | int index = x + y;
327 | for (int i = 1; i < kwidth; i++)
328 | sum += diffKernel[i] * (yConv[index - i] - yConv[index + i]);
329 |
330 | xGradient[index] = sum;
331 | }
332 | }
333 |
334 | for (int x = kwidth; x < width - kwidth; x++) {
335 | for (int y = initY; y < maxY; y += width) {
336 | float sum = 0.0f;
337 | int index = x + y;
338 | int yOffset = width;
339 | for (int i = 1; i < kwidth; i++) {
340 | sum += diffKernel[i] * (xConv[index - yOffset] - xConv[index + yOffset]);
341 | yOffset += width;
342 | }
343 |
344 | yGradient[index] = sum;
345 | }
346 | }
347 |
348 | initX = kwidth;
349 | maxX = width - kwidth;
350 | initY = width * kwidth;
351 | maxY = width * (height - kwidth);
352 | for (int x = initX; x < maxX; x++) {
353 | for (int y = initY; y < maxY; y += width) {
354 | int index = x + y;
355 | int indexN = index - width;
356 | int indexS = index + width;
357 | int indexW = index - 1;
358 | int indexE = index + 1;
359 | int indexNW = indexN - 1;
360 | int indexNE = indexN + 1;
361 | int indexSW = indexS - 1;
362 | int indexSE = indexS + 1;
363 |
364 | float xGrad = xGradient[index];
365 | float yGrad = yGradient[index];
366 | float gradMag = hypot(xGrad, yGrad);
367 |
368 | //perform non-maximal supression
369 | float nMag = hypot(xGradient[indexN], yGradient[indexN]);
370 | float sMag = hypot(xGradient[indexS], yGradient[indexS]);
371 | float wMag = hypot(xGradient[indexW], yGradient[indexW]);
372 | float eMag = hypot(xGradient[indexE], yGradient[indexE]);
373 | float neMag = hypot(xGradient[indexNE], yGradient[indexNE]);
374 | float seMag = hypot(xGradient[indexSE], yGradient[indexSE]);
375 | float swMag = hypot(xGradient[indexSW], yGradient[indexSW]);
376 | float nwMag = hypot(xGradient[indexNW], yGradient[indexNW]);
377 | float tmp;
378 | double orient;
379 | double min = 360;
380 | int[] direction = {0, 23, 45, 68, 90, 113, 135, 158, 180, -180, -158, -135, -113, -90, -68, -45, -23};
381 | /*
382 | * An explanation of what's happening here, for those who want
383 | * to understand the source: This performs the "non-maximal
384 | * supression" phase of the Canny edge detection in which we
385 | * need to compare the gradient magnitude to that in the
386 | * direction of the gradient; only if the value is a local
387 | * maximum do we consider the point as an edge candidate.
388 | *
389 | * We need to break the comparison into a number of different
390 | * cases depending on the gradient direction so that the
391 | * appropriate values can be used. To avoid computing the
392 | * gradient direction, we use two simple comparisons: first we
393 | * check that the partial derivatives have the same sign (1)
394 | * and then we check which is larger (2). As a consequence, we
395 | * have reduced the problem to one of four identical cases that
396 | * each test the central gradient magnitude against the values at
397 | * two points with 'identical support'; what this means is that
398 | * the geometry required to accurately interpolate the magnitude
399 | * of gradient function at those points has an identical
400 | * geometry (upto right-angled-rotation/reflection).
401 | *
402 | * When comparing the central gradient to the two interpolated
403 | * values, we avoid performing any divisions by multiplying both
404 | * sides of each inequality by the greater of the two partial
405 | * derivatives. The common comparand is stored in a temporary
406 | * variable (3) and reused in the mirror case (4).
407 | *
408 | */
409 | if (xGrad * yGrad <= (float) 0 /*(1)*/
410 | ? Math.abs(xGrad) >= Math.abs(yGrad) /*(2)*/
411 | ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad * neMag - (xGrad + yGrad) * eMag) /*(3)*/
412 | && tmp > Math.abs(yGrad * swMag - (xGrad + yGrad) * wMag) /*(4)*/
413 | : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad * neMag - (yGrad + xGrad) * nMag) /*(3)*/
414 | && tmp > Math.abs(xGrad * swMag - (yGrad + xGrad) * sMag) /*(4)*/
415 | : Math.abs(xGrad) >= Math.abs(yGrad) /*(2)*/
416 | ? (tmp = Math.abs(xGrad * gradMag)) >= Math.abs(yGrad * seMag + (xGrad - yGrad) * eMag) /*(3)*/
417 | && tmp > Math.abs(yGrad * nwMag + (xGrad - yGrad) * wMag) /*(4)*/
418 | : (tmp = Math.abs(yGrad * gradMag)) >= Math.abs(xGrad * seMag + (yGrad - xGrad) * sMag) /*(3)*/
419 | && tmp > Math.abs(xGrad * nwMag + (yGrad - xGrad) * nMag) /*(4)*/
420 | ) {
421 |
422 | //magnitude[index] = gradMag >= MAGNITUDE_LIMIT ? MAGNITUDE_MAX : (int) (MAGNITUDE_SCALE * gradMag);
423 | magnitude[index] = gradMag >= MAGNITUDE_LIMIT ? MAGNITUDE_MAX : (int) (MAGNITUDE_SCALE * gradMag);
424 | orient = 57.2957795 * Math.atan2(yGrad, xGrad);
425 | //System.err.println(orient);
426 |
427 | orientation[index] = 0;
428 | for (int i : direction) {
429 | double delta = Math.abs(orient - i);
430 | if (delta < min) {
431 | orientation[index] = i;
432 | min = delta;
433 | }
434 | }
435 | if (orientation[index] >= 180)
436 | orientation[index] -= 180;
437 |
438 | //NOTE: The orientation of the edge is not employed by this
439 | //implementation. It is a simple matter to compute it at
440 | //this point as: Math.atan2(yGrad, xGrad);
441 | } else {
442 | magnitude[index] = 0;
443 | orientation[index] = -1;
444 | }
445 | }
446 | }
447 | }
448 |
449 | //NOTE: It is quite feasible to replace the implementation of this method
450 | //with one which only loosely approximates the hypot function. I've tested
451 | //simple approximations such as Math.abs(x) + Math.abs(y) and they work fine.
452 | private float hypot(float x, float y) {
453 | return (float) Math.hypot(x, y);
454 | }
455 |
456 | private float gaussian(float x, float sigma) {
457 | return (float) Math.exp(-(x * x) / (2f * sigma * sigma));
458 | }
459 |
460 | private void performHysteresis(int low, int high) {
461 | //NOTE: this implementation reuses the data array to store both
462 | //luminance data from the image, and edge intensity from the processing.
463 | //This is done for memory efficiency, other implementations may wish
464 | //to separate these functions.
465 | Arrays.fill(data, 0);
466 |
467 | int offset = 0;
468 | for (int y = 0; y < height; y++) {
469 | for (int x = 0; x < width; x++) {
470 | if (data[offset] == 0 && magnitude[offset] >= high) {
471 | follow(x, y, offset, low);
472 | }
473 | offset++;
474 | }
475 | }
476 | }
477 |
478 | private void follow(int x1, int y1, int i1, int threshold) {
479 | int x0 = x1 == 0 ? x1 : x1 - 1;
480 | int x2 = x1 == width - 1 ? x1 : x1 + 1;
481 | int y0 = y1 == 0 ? y1 : y1 - 1;
482 | int y2 = y1 == height - 1 ? y1 : y1 + 1;
483 |
484 | data[i1] = magnitude[i1];
485 | for (int x = x0; x <= x2; x++) {
486 | for (int y = y0; y <= y2; y++) {
487 | int i2 = x + y * width;
488 | if ((y != y1 || x != x1)
489 | && data[i2] == 0
490 | && magnitude[i2] >= threshold) {
491 | follow(x, y, i2, threshold);
492 | return;
493 | }
494 | }
495 | }
496 | }
497 |
498 | private void thresholdEdges() {
499 | for (int i = 0; i < picsize; i++) {
500 | if (data[i] > 0) {
501 | // System.err.println("a\t" + data[i] + "\t" + orientation[i]);
502 |
503 | //red
504 | //orange
505 | //yellow
506 | //green
507 | //cyan
508 | //blue
509 | //violet
510 | //magenta
511 |
512 | if (orientation[i] == 0) {
513 | data[i] = 0xffff0000; //red
514 | } else if (orientation[i] == 23) {
515 | data[i] = 0xffff8800; //orange
516 | } else if (orientation[i] == 45) {
517 | data[i] = 0xffffff00; //yellow
518 | } else if (orientation[i] == 68) {
519 | data[i] = 0xff00ff00; //green
520 | } else if (orientation[i] == 90) {
521 | data[i] = 0xff00ffff; //cyan
522 | } else if (orientation[i] == 113) {
523 | data[i] = 0xff0000ff; //blue
524 | } else if (orientation[i] == 135) {
525 | data[i] = 0xff0088ff; //violet
526 | } else if (orientation[i] == 158) {
527 | data[i] = 0xffff00ff; //magenta
528 | } else {
529 | data[i] = 0xff000000; //black
530 | }
531 | // data[i] = -1;
532 | } else {
533 | data[i] = 0xff000000; //black
534 | // System.err.println("b " + data[i]);
535 | }
536 |
537 | // data[i] = data[i] > 0 ? -1 : 0xff000000;
538 |
539 | // data[i] = data[i] > 0 ? 0x00000000 : 0xffffffff; //black/white alpha=1
540 | }
541 | }
542 |
543 | private int luminance(float r, float g, float b) {
544 | return Math.round(0.299f * r + 0.587f * g + 0.114f * b);
545 | }
546 |
547 | private void readLuminance() {
548 | int type = sourceImage.getType();
549 | //logger.debug("imagetype="+sourceImage.getType());
550 |
551 | if (type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB) {
552 | logger.debug("RGB");
553 | int[] pixels = (int[]) sourceImage.getData().getDataElements(0, 0, width, height, null);
554 | for (int i = 0; i < picsize; i++) {
555 | int p = pixels[i];
556 | int r = (p & 0xff0000) >> 16;
557 | int g = (p & 0xff00) >> 8;
558 | int b = p & 0xff;
559 | data[i] = luminance(r, g, b);
560 | }
561 | } else if (type == BufferedImage.TYPE_BYTE_GRAY) {
562 | byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, 0, width, height, null);
563 | for (int i = 0; i < picsize; i++) {
564 | data[i] = (pixels[i] & 0xff);
565 | }
566 | } else if (type == BufferedImage.TYPE_USHORT_GRAY) {
567 | short[] pixels = (short[]) sourceImage.getData().getDataElements(0, 0, width, height, null);
568 | for (int i = 0; i < picsize; i++) {
569 | data[i] = (pixels[i] & 0xffff) / 256;
570 | }
571 | } else if (type == BufferedImage.TYPE_3BYTE_BGR) {
572 | byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, 0, width, height, null);
573 | int offset = 0;
574 | for (int i = 0; i < picsize; i++) {
575 | int b = pixels[offset++] & 0xff;
576 | int g = pixels[offset++] & 0xff;
577 | int r = pixels[offset++] & 0xff;
578 | data[i] = luminance(r, g, b);
579 | }
580 | // } else if (type == BufferedImage.TYPE_BYTE_INDEXED) {
581 | // byte[] pixels = (byte[]) sourceImage.getData().getDataElements(0, 0, width, height, null);
582 | // int offset = 0;
583 | // for (int i = 0; i < picsize; i++) {
584 | // logger.debug("offset,picsize="+offset+","+picsize);
585 | // if (offset+2 >= picsize) break;
586 | // int r = pixels[offset++] & 0xff;
587 | // int g = pixels[offset++] & 0xff;
588 | // int b = pixels[offset++] & 0xff;
589 | // data[i] = luminance(r, g, b);
590 | // }
591 | } else {
592 | throw new IllegalArgumentException("Unsupported image type: " + type);
593 | }
594 | }
595 |
596 | private void normalizeContrast() {
597 | int[] histogram = new int[256];
598 | for (int datum : data) {
599 | histogram[datum]++;
600 | }
601 | int[] remap = new int[256];
602 | int sum = 0;
603 | int j = 0;
604 | for (int i = 0; i < histogram.length; i++) {
605 | sum += histogram[i];
606 | int target = sum * 255 / picsize;
607 | for (int k = j + 1; k <= target; k++) {
608 | remap[k] = i;
609 | }
610 | j = target;
611 | }
612 | for (int i = 0; i < data.length; i++) {
613 | data[i] = remap[data[i]];
614 | }
615 | }
616 |
617 | private void writeEdges(int[] pixels) {
618 | //NOTE: There is currently no mechanism for obtaining the edge data
619 | //in any other format other than an INT_ARGB type BufferedImage.
620 | //This may be easily remedied by providing alternative accessors.
621 | if (edgesImage == null) {
622 | // edgesImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
623 | edgesImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
624 | }
625 | edgesImage.getWritableTile(0, 0).setDataElements(0, 0, width, height, pixels);
626 | }
627 | }
628 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/backend/Processor.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.backend;
2 |
3 | import at.dhyan.open_imaging.GifDecoder;
4 | import com.allenday.image.ImageFeatures;
5 | import org.imgscalr.Scalr;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 |
9 | import javax.imageio.ImageIO;
10 | import javax.media.jai.Histogram;
11 | import javax.media.jai.PlanarImage;
12 | import javax.media.jai.RenderedOp;
13 | import javax.media.jai.operator.HistogramDescriptor;
14 | import java.awt.*;
15 | import java.awt.image.BufferedImage;
16 | import java.awt.image.DataBuffer;
17 | import java.awt.image.Raster;
18 | import java.awt.image.SampleModel;
19 | import java.io.File;
20 | import java.io.IOException;
21 | import java.io.InputStream;
22 | import java.nio.file.Files;
23 |
24 | //import org.imgscalr.Scalr;
25 | //import org.imgscalr.Scalr.Method;
26 |
27 | public class Processor {
28 | private static final int R = 0;
29 |
30 | // private KDTree[] kdtree = {new KDTree(8), new KDTree(8), new KDTree(8), new KDTree(8), new KDTree(8)};
31 |
32 | // public static final int Y = 0;
33 | // public static final int Cb = 1;
34 | // public static final int Cr = 2;
35 | // public static final int T = 3;
36 | // public static final int C = 4;
37 | // public static final int O = 5;
38 | private static final int G = 1;
39 | private static final int B = 2;
40 | public static final int T = 3;
41 | public static final int C = 4;
42 | private static final Logger logger = LoggerFactory.getLogger(Processor.class);
43 | //always 4x4
44 | private final int blocksPerSide = 4;
45 | private double[] texture;
46 | private double[] curviness;
47 | private char[] topologyLabel;
48 | private double[] topologyValue;
49 | private boolean hasEdgeHistograms = false;
50 | private BufferedImage bufferedImage;
51 | private SampleModel sampleModel;
52 | private Histogram histogram;
53 | private PlanarImage image;
54 | private int bandGlobalMax = 1;
55 | private final boolean normalize;
56 | private final int bins;
57 | private final int bitsPerBin;
58 | private File file;
59 |
60 | public Processor(InputStream inputStream, int bins, int bitsPerBin, boolean normalize) throws IOException {
61 | this.bins = bins;
62 | this.bitsPerBin = bitsPerBin;
63 | this.normalize = normalize;
64 |
65 | bufferedImage = ImageIO.read(inputStream);
66 |
67 | processImage(bufferedImage);
68 | }
69 |
70 | public Processor(File file, int bins, int bitsPerBin, boolean normalize) throws IOException {
71 | this.file = file;
72 | this.bins = bins;
73 | this.bitsPerBin = bitsPerBin;
74 | this.normalize = normalize;
75 | //logger.debug("file="+file);
76 | try {
77 | GifDecoder.GifImage gifImage = GifDecoder.read(Files.readAllBytes(file.toPath()));
78 | logger.debug("got animated gif!");
79 | logger.debug("frames="+gifImage.getFrameCount());
80 | logger.debug("delay="+gifImage.getDelay(0));
81 | logger.debug("bg="+gifImage.getBackgroundColor());
82 | logger.debug("width="+gifImage.getWidth());
83 | logger.debug("height="+gifImage.getHeight());
84 |
85 | Integer midPoint = new Double(Math.floor(gifImage.getFrameCount()/2)).intValue();
86 | logger.debug("midPoint="+midPoint);
87 | bufferedImage = gifImage.getFrame(midPoint);
88 | } catch (IOException e) {
89 | bufferedImage = ImageIO.read(file);
90 | }
91 | processImage(bufferedImage);
92 |
93 | }
94 |
95 | public Processor(InputStream inputStream, boolean normalize) throws IOException {
96 | this(inputStream, 8, 8, normalize);
97 | }
98 |
99 | public Processor(File file, boolean normalize) throws IOException {
100 | this(file, 8, 8, normalize);
101 | }
102 |
103 | public ImageFeatures getImageFeatures() {
104 | ImageFeatures f = new ImageFeatures(".", bins, blocksPerSide);
105 | f.setR(getRedHistogram());
106 | f.setG(getBlueHistogram());
107 | f.setB(getBlueHistogram());
108 | f.setT(getTextureHistogram());
109 | f.setC(getCurvatureHistogram());
110 | return f;
111 | }
112 |
113 | private void processImage(BufferedImage b) throws IOException {
114 | texture = new double[bins];
115 | curviness = new double[bins];
116 | topologyLabel = new char[blocksPerSide * blocksPerSide];
117 | topologyValue = new double[blocksPerSide * blocksPerSide];
118 |
119 | if (bufferedImage == null)
120 | throw new IOException("cannot read file");
121 |
122 | // BufferedImage thumbnail = Scalr.resize(bufferedImage, Method.ULTRA_QUALITY, 480, 480);
123 | BufferedImage thumbnail = bufferedImage;
124 | if (thumbnail.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
125 | // thumbnail = Scalr.resize(thumbnail, Scalr.Method.ULTRA_QUALITY, 480, 480);
126 |
127 | //see: https://github.com/DhyanB/Open-Imaging/issues/3
128 | //https://github.com/DhyanB/Open-Imaging
129 | //https://stackoverflow.com/questions/8933893/convert-each-animated-gif-frame-to-a-separate-bufferedimage
130 | }
131 |
132 | image = PlanarImage.wrapRenderedImage(thumbnail);
133 | sampleModel = image.getSampleModel();
134 | int bandCount = sampleModel.getNumBands();
135 | int bits = DataBuffer.getDataTypeSize(sampleModel.getDataType());
136 | int[] binz = new int[bandCount];
137 | double[] min = new double[bandCount];
138 | double[] max = new double[bandCount];
139 | int maxxx = 1 << bits;
140 |
141 | for (int i = 0; i < bandCount; i++) {
142 | //bins[i] = maxxx;
143 | binz[i] = bins;
144 | min[i] = 0;
145 | max[i] = maxxx;
146 | }
147 | RenderedOp op = HistogramDescriptor.create(image, null, 1, 1, binz, min, max, null);
148 | histogram = (Histogram) op.getProperty("histogram");
149 |
150 | if (sampleModel.getNumBands() > 0)
151 | getBandHistogram(histogram, 0, bins, normalize);
152 | makeTopologies();
153 | makeEdgeHistograms();
154 | }
155 |
156 | private double[] getBandHistogram(Histogram h, int band, int bins, boolean normalize) {
157 | if (band >= getNumBands()) {
158 | logger.info("no band " + band + ", using band 0");
159 | band = 0;
160 | }
161 |
162 | if (bandGlobalMax == 1) {
163 | for (int i = 0; i < getNumBands(); i++) {
164 | int[] frequencies = h.getBins(band);
165 | for (int frequency : frequencies) {
166 | bandGlobalMax = bandGlobalMax > frequency ? bandGlobalMax : frequency;
167 | }
168 |
169 | }
170 | }
171 |
172 | int[] frequencies = h.getBins(band);
173 |
174 | int bandMax = 1;
175 | for (int frequency : frequencies) {
176 | bandMax = bandMax > frequency ? bandMax : frequency;
177 | //logger.debug("band="+band+",freq="+frequencies[f]);
178 | }
179 | for (int f = 0; f < frequencies.length; f++) {
180 | if (normalize) {
181 | frequencies[f] = (int) ((Math.pow(2, bitsPerBin) - 1) * (float) frequencies[f] / bandGlobalMax);
182 | } else {
183 | frequencies[f] = (int) ((Math.pow(2, bitsPerBin) - 1) * (float) frequencies[f] / bandMax);
184 | }
185 | }
186 |
187 | double[] result = new double[frequencies.length];
188 | for (int f = 0; f < frequencies.length; f++) {
189 | result[f] = (double) frequencies[f];
190 | //logger.debug("freq="+result[f]);
191 | }
192 | return result;
193 | }
194 |
195 |
196 | private Float getLowBand() {
197 | return 1.0f;
198 | }
199 |
200 | private Float getHighBand() {
201 | return 3.0f;
202 | }
203 |
204 | private Integer getNumBands() {
205 | return sampleModel.getNumBands();
206 | }
207 |
208 | public double[] getTextureHistogram() {
209 | return texture;
210 | }
211 |
212 | public double[] getCurvatureHistogram() {
213 | return curviness;
214 | }
215 |
216 | public double[] getRedHistogram() {
217 | return getBandHistogram(histogram, R, bins, normalize);
218 | }
219 |
220 | public double[] getGreenHistogram() {
221 | return getBandHistogram(histogram, G, bins, normalize);
222 | }
223 |
224 | public double[] getBlueHistogram() {
225 | return getBandHistogram(histogram, B, bins, normalize);
226 | }
227 |
228 | private BufferedImage getEdgesImage() {
229 | CannyEdgeDetector detector = new CannyEdgeDetector();
230 | detector.setSourceImage(bufferedImage);
231 | detector.setLowThreshold(getLowBand()); //1.0f
232 | detector.setHighThreshold(getHighBand()); //3.0f
233 | detector.process();
234 | return detector.getEdgesImage();
235 | }
236 |
237 | /**
238 | * topology breaks the image into an n*n grid
239 | * calculates the max color per grid in RGB space
240 | * and assigns a letter (RGB) and value (normalized by bins)
241 | * to each cell
242 | */
243 |
244 | private void makeTopologies() {
245 | for (int i = 0; i < topologyValue.length; i++) {
246 | topologyLabel[i] = '.';
247 | topologyValue[i] = 0;
248 | }
249 |
250 | int maxH = image.getHeight();
251 | int maxW = image.getWidth();
252 |
253 | int stepH = (int) Math.floor((double) maxH / blocksPerSide);
254 | int stepW = (int) Math.floor((double) maxW / blocksPerSide);
255 |
256 | // BufferedImage img = image.getAsBufferedImage().getScaledInstance(width, height, 0);
257 |
258 | String hash = "";
259 |
260 | int tileNum = 0;
261 | for (int y = 0; y < blocksPerSide; y++) {
262 | for (int x = 0; x < blocksPerSide; x++) {
263 | tileNum++;
264 | Rectangle rect = new Rectangle();
265 | rect.width = stepW;
266 | rect.height = stepH;
267 | rect.x = x * stepW;
268 | rect.y = y * stepH;
269 | Raster raster = image.getData(rect);
270 | // BufferedImage img = new BufferedImage(image.getColorModel(), raster.createCompatibleWritableRaster(), true, null);
271 | // PlanarImage pimg = PlanarImage.wrapRenderedImage(img);
272 |
273 | int sumR = 0;
274 | int sumG = 0;
275 | int sumB = 0;
276 | int p = 0;
277 | double[] pixel = new double[4];
278 | for (int j = x * stepW; j < (x + 1) * stepW; j++) {
279 | for (int k = y * stepH; k < (y + 1) * stepH; k++) {
280 | raster.getPixel(j, k, pixel);
281 | sumR += pixel[R];
282 | sumG += pixel[G];
283 | sumB += pixel[B];
284 | p++;
285 | }
286 | }
287 |
288 | Integer cell;
289 | Character label;
290 |
291 | if (sumR >= sumG && sumR >= sumB) {
292 | label = 'r';
293 | cell = (int) ((sumR / ((float) 256 * p)) * (int) (Math.pow(2, bitsPerBin) - 1));
294 | } else if (sumG < sumR || sumG < sumB) {
295 | label = 'b';
296 | cell = (int) ((sumB / ((float) 256 * p)) * (int) (Math.pow(2, bitsPerBin) - 1));
297 | } else {
298 | label = 'g';
299 | cell = (int) ((sumG / ((float) 256 * p)) * (int) (Math.pow(2, bitsPerBin) - 1));
300 | }
301 |
302 | topologyValue[y * blocksPerSide + x] = cell;
303 | topologyLabel[y * blocksPerSide + x] = label;
304 | }
305 | }
306 | }
307 |
308 |
309 | private void makeEdgeHistograms() {
310 | if (hasEdgeHistograms)
311 | return;
312 | /*
313 | if (getEdgesImage() == null) {
314 | hasEdgeHistograms = true;
315 | for (int i = 0; i < 8; i++) {
316 | texture[i] = 0;
317 | curviness[i] = 0;
318 | }
319 | return;
320 | }
321 | */
322 | Raster r = getEdgesImage().getData();
323 | int[] pixels = null;
324 | int[] SSa = null;
325 | int[] SEa = null;
326 | int[] EEa = null;
327 | int[] SWa = null;
328 | int[] WWa = null;
329 | int[] NWa = null;
330 | int[] NNa = null;
331 | int[] NEa = null;
332 |
333 | int width = r.getWidth();
334 | int height = r.getHeight();
335 | for (int x = 1; x < width - 1; x++) {
336 | for (int y = 1; y < height - 1; y++) {
337 | pixels = r.getPixel(x, y, pixels);
338 |
339 |
340 | NNa = r.getPixel(x, y - 1, NNa);
341 | SSa = r.getPixel(x, y + 1, SSa);
342 | EEa = r.getPixel(x + 1, y, EEa);
343 | WWa = r.getPixel(x - 1, y, WWa);
344 | NEa = r.getPixel(x + 1, y - 1, NEa);
345 | SEa = r.getPixel(x + 1, y + 1, SEa);
346 | NWa = r.getPixel(x - 1, y - 1, NWa);
347 | SWa = r.getPixel(x - 1, y + 1, SWa);
348 |
349 | int NN = NNa[0] + NNa[1] + NNa[2];
350 | int SS = SSa[0] + SSa[1] + SSa[2];
351 | int EE = EEa[0] + EEa[1] + EEa[2];
352 | int WW = WWa[0] + WWa[1] + WWa[2];
353 | int NE = NEa[0] + NEa[1] + NEa[2];
354 | int SE = SEa[0] + SEa[1] + SEa[2];
355 | int NW = NWa[0] + NWa[1] + NWa[2];
356 | int SW = SWa[0] + SWa[1] + SWa[2];
357 |
358 | //System.err.println(pixels[0]+":"+pixels[1]+":"+pixels[2]);
359 | if (pixels[0] == 0xff && pixels[1] == 0x00 && pixels[2] == 0x00) {
360 | texture[0]++;
361 | if (WW > 0 && EE > 0)
362 | curviness[0]++;
363 | } else if (pixels[0] == 0xff && pixels[1] == 0x88 && pixels[2] == 0x00) { //0xff ff8800
364 | texture[1]++;
365 | if ((WW > 0 || SW > 0) && (EE > 0 || NE > 0))
366 | curviness[1]++;
367 | } else if (pixels[0] == 0xff && pixels[1] == 0xff && pixels[2] == 0x00) { //0xff ffff00
368 | texture[2]++;
369 | if (NE > 0 && SW > 0)
370 | curviness[2]++;
371 | } else if (pixels[0] == 0x00 && pixels[1] == 0xff && pixels[2] == 0x00) { //0xff 00ff00
372 | texture[3]++;
373 | if ((NN > 0 || NE > 0) && (SS > 0 || SW > 0))
374 | curviness[3]++;
375 | } else if (pixels[0] == 0x00 && pixels[1] == 0xff && pixels[2] == 0xff) { //0xff 00ffff
376 | texture[4]++;
377 | if (NN > 0 && SS > 0)
378 | curviness[4]++;
379 | } else if (pixels[0] == 0x00 && pixels[1] == 0x00 && pixels[2] == 0xff) { //0xff 0000ff
380 | texture[5]++;
381 | if ((NN > 0 || NW > 0) && (SS > 0 || SE > 0))
382 | curviness[5]++;
383 | } else if (pixels[0] == 0x00 && pixels[1] == 0x88 && pixels[2] == 0xff) { //0xff 0088ff
384 | texture[6]++;
385 | if (NW > 0 && SE > 0)
386 | curviness[6]++;
387 | } else if (pixels[0] == 0xff && pixels[1] == 0x00 && pixels[2] == 0xff) { //0xff ff00ff
388 | texture[7]++;
389 | if ((WW > 0 || NW > 0) && (EE > 0 || SE > 0))
390 | curviness[7]++;
391 | }
392 | }
393 | }
394 | transformVector(texture);
395 | transformVector(curviness);
396 | hasEdgeHistograms = true;
397 | }
398 |
399 | private void transformVector(double[] curviness) {
400 | double curvMax = 1;
401 | for (double v : curviness) {
402 | curvMax = curvMax > v ? curvMax : v;
403 | }
404 | for (int c = 0; c < curviness.length; c++) {
405 | curviness[c] = (int) ((Math.pow(2, bitsPerBin) - 1) * (float) curviness[c] / curvMax);
406 | }
407 | }
408 |
409 | public double[] getTopologyValues() {
410 | return topologyValue;
411 | }
412 |
413 | public char[] getTopologyLabels() {
414 | return topologyLabel;
415 | }
416 | }
417 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/AbstractDistance.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | import java.util.Vector;
4 |
5 | abstract class AbstractDistance {
6 | protected abstract Double getDimensionWeight(Integer dimension);
7 |
8 | protected abstract Double getVectorWeight(Integer dimension, Integer position);
9 |
10 | Double getVectorNorm(Integer d, Vector vec) {
11 | Double result = 0d;
12 | for (int i = 0; i < vec.size(); i++)
13 | result += getVectorWeight(d, i) * vec.get(i);
14 | return getDimensionWeight(d) * result;
15 | }
16 |
17 | public Vector getVectorDistance(Integer d, Vector a, Vector b) {
18 | Vector result = new Vector<>();
19 | result.setSize(a.size());
20 | for (int i = 0; i < a.size(); i++) {
21 | result.set(i, getVectorWeight(d, i) * (a.get(i) - b.get(i)));
22 | }
23 | return result;
24 | }
25 |
26 | /*
27 | public List reorder(ImageFeatures query, List inputItems) {
28 | TreeMap> tree = new TreeMap>();
29 |
30 | for (ImageFeatures item : inputItems) {
31 | Double distance = 0d;
32 | for (Integer d = 0 ; d < ImageFeatures.DIMENSIONS ; d++) {
33 | distance += distance(d, distance(d, query.getDimension(d), item.getDimension(d))) / 255;
34 | }
35 | if (!tree.containsKey(distance))
36 | tree.put(distance,new ArrayList());
37 | tree.get(distance).add(item);
38 | }
39 | List results = new ArrayList();
40 | for (Map.Entry> e : tree.entrySet())
41 | for (ImageFeatures f : e.getValue())
42 | results.add(new SearchResult(f.id, e.getKey()));
43 | return results;
44 | }
45 | */
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/AbstractDistance_UDUV.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | public abstract class AbstractDistance_UDUV extends AbstractDistance {
4 | @Override
5 | protected Double getDimensionWeight(Integer d) {
6 | return 1d;
7 | }
8 |
9 | @Override
10 | protected Double getVectorWeight(Integer d, Integer Position) {
11 | return 1d;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/AbstractDistance_UDWV.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | public abstract class AbstractDistance_UDWV extends AbstractDistance {
4 | @Override
5 | protected Double getDimensionWeight(Integer dimension) {
6 | return 1d;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/AbstractDistance_WDUV.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 |
4 | public abstract class AbstractDistance_WDUV extends AbstractDistance {
5 | @Override
6 | protected Double getVectorWeight(Integer dimension, Integer Position) {
7 | return 1d;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/AbstractDistance_WDWV.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | abstract class AbstractDistance_WDWV extends AbstractDistance {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/UDUV_L1Norm.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | import com.allenday.image.Distance;
4 | import com.allenday.image.ImageFeatures;
5 |
6 | import java.util.List;
7 | import java.util.Vector;
8 |
9 | public class UDUV_L1Norm extends AbstractDistance_UDUV implements Distance {
10 | public Double getScalarDistance(Integer d, Vector a, Vector b) {
11 | Double r = 0d;
12 | for (int i = 0; i < a.size(); i++) {
13 | r += super.getVectorWeight(d, i) * (a.get(i) - b.get(i));
14 | }
15 | return super.getDimensionWeight(d) * r;
16 | }
17 |
18 | @Override
19 | public Double getVectorNorm(Integer d, Vector v) {
20 | //FIXME
21 | return null;
22 | }
23 |
24 | @Override
25 | public Double getScalarDistance(List> a, List> b) {
26 | double dist = 0d;
27 | for (Integer d = 0; d < ImageFeatures.DIMENSIONS; d++) {
28 | double vDist = 0d;
29 | for (int i = 0; i < a.size(); i++) {
30 | vDist += getVectorWeight(d, i) * (a.get(d).get(i) - b.get(d).get(i));
31 | }
32 | dist += getDimensionWeight(d) * vDist;
33 | }
34 | return dist;
35 | }
36 |
37 | @Override
38 | public Double getDimensionWeight(Integer dimension) {
39 | return 1d;
40 | }
41 |
42 | @Override
43 | public Double getVectorWeight(Integer dimension, Integer position) {
44 | return 1d;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/UDUV_L2Norm.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | import com.allenday.image.Distance;
4 |
5 | import java.util.Vector;
6 |
7 | class UDUV_L2Norm extends UDUV_L1Norm implements Distance {
8 | public Double distance(Integer dimension, Vector vec) {
9 | Double result = 0d;
10 | for (Double aDouble : vec) result += aDouble;
11 | return Math.sqrt(result);
12 | }
13 |
14 | @Override
15 | public Double getVectorNorm(Integer d, Vector v) {
16 | return null;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/UDWV_L1Norm.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | import com.allenday.image.Distance;
4 | import com.allenday.image.ImageFeatures;
5 |
6 | import java.util.List;
7 | import java.util.Vector;
8 |
9 | public class UDWV_L1Norm extends AbstractDistance_UDWV implements Distance {
10 | private static final double[][] MAT_WEIGHT = {
11 | {0.2d, 0.25d, 0.3d, 0.3d, 0.3d, 0.35d, 0.4d, 0.45d, 0.5d, 0.60d, 0.7d, 0.75d, 0.8d, 0.55d, 0.3d, 0.4d}, //R
12 | {0.2d, 0.35d, 0.5d, 0.6d, 0.7d, 0.80d, 0.9d, 0.95d, 1.0d, 1.00d, 1.0d, 0.95d, 0.9d, 0.65d, 0.4d, 0.4d}, //G
13 | {0.2d, 0.35d, 0.5d, 0.6d, 0.7d, 0.80d, 0.9d, 0.95d, 1.0d, 0.95d, 0.9d, 0.90d, 0.9d, 0.65d, 0.4d, 0.4d}, //B
14 | {1.0d, 1.00d, 1.0d, 1.0d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.0d}, //T
15 | {1.0d, 1.00d, 1.0d, 1.0d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.0d}, //C
16 | };
17 |
18 | public Double getScalarDistance(Integer d, Vector a, Vector b) {
19 | Double r = 0d;
20 | for (int i = 0; i < a.size(); i++) {
21 | r += getVectorWeight(d, i) * (a.get(i) - b.get(i));
22 | }
23 | return super.getDimensionWeight(d) * r;
24 | }
25 |
26 | @Override
27 | public Double getVectorNorm(Integer d, Vector v) {
28 | //FIXME
29 | return null;
30 | }
31 |
32 | @Override
33 | public Double getScalarDistance(List> a, List> b) {
34 | Double totalDist = 0d;
35 | for (Integer d = 0; d < ImageFeatures.DIMENSIONS; d++) {
36 | totalDist += getScalarDistance(d, a.get(d), b.get(d));
37 | }
38 | return totalDist;
39 | }
40 |
41 | @Override
42 | protected Double getVectorWeight(Integer d, Integer p) {
43 | return MAT_WEIGHT[d][p];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/UDWV_L2Norm.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | import com.allenday.image.Distance;
4 |
5 | import java.util.Vector;
6 |
7 | public class UDWV_L2Norm extends UDWV_L1Norm implements Distance {
8 | private static double[][] MAT_WEIGHT = {
9 | {0.2d, 0.25d, 0.3d, 0.3d, 0.3d, 0.35d, 0.4d, 0.45d, 0.5d, 0.60d, 0.7d, 0.75d, 0.8d, 0.55d, 0.3d, 0.4d}, //R
10 | {0.2d, 0.35d, 0.5d, 0.6d, 0.7d, 0.80d, 0.9d, 0.95d, 1.0d, 1.00d, 1.0d, 0.95d, 0.9d, 0.65d, 0.4d, 0.4d}, //G
11 | {0.2d, 0.35d, 0.5d, 0.6d, 0.7d, 0.80d, 0.9d, 0.95d, 1.0d, 0.95d, 0.9d, 0.90d, 0.9d, 0.65d, 0.4d, 0.4d}, //B
12 | {1.0d, 1.00d, 1.0d, 1.0d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.0d}, //T
13 | {1.0d, 1.00d, 1.0d, 1.0d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.00d, 1.0d, 1.0d}, //C
14 | };
15 |
16 | @Override
17 | public Double getVectorNorm(Integer d, Vector v) {
18 | Double L1 = super.getVectorNorm(d, v);
19 | return Math.sqrt(L1);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/image/distance/WDUV_PearsonDistance.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image.distance;
2 |
3 | import com.allenday.image.Distance;
4 | import com.allenday.image.ImageFeatures;
5 |
6 | import java.util.List;
7 | import java.util.Vector;
8 |
9 | public class WDUV_PearsonDistance extends AbstractDistance_WDUV implements Distance {
10 | private static final Double[] COR_WEIGHT = {0.6d, 0.8d, 0.8d, 1.2d, 1.6d, 1.0d}; //NB 6th dimension is a hack for the full cor
11 |
12 | @Override
13 | public Double getScalarDistance(List> a, List> b) {
14 | Vector aConcat = new Vector<>();
15 | Vector bConcat = new Vector<>();
16 | aConcat.setSize(ImageFeatures.DIMENSIONS * a.get(0).size());
17 | bConcat.setSize(ImageFeatures.DIMENSIONS * b.get(0).size());
18 |
19 | // do cor across all dimensions
20 | for (int d = 0; d < ImageFeatures.DIMENSIONS; d++) {
21 | for (int i = 0; i < a.get(d).size(); i++) {
22 | aConcat.set(d * a.get(0).size() + i, a.get(d).get(i));
23 | bConcat.set(d * b.get(0).size() + i, b.get(d).get(i));
24 | }
25 | }
26 | return getScalarDistance(5, aConcat, bConcat); //uses fake 6th dimension
27 | }
28 |
29 | @Override
30 | protected Double getDimensionWeight(Integer d) {
31 | return COR_WEIGHT[d];
32 | }
33 |
34 |
35 | @Override
36 | public Double getScalarDistance(Integer dimension, Vector a, Vector b) {
37 | double sumXxY = 0;
38 | double sumX = 0;
39 | double sumY = 0;
40 | double sumXxX = 0;
41 | double sumYxY = 0;
42 |
43 | for (int i = 0; i < a.size(); i++) {
44 | double value1 = a.get(i) * getVectorWeight(dimension, i);
45 | double value2 = b.get(i) * getVectorWeight(dimension, i);
46 | sumXxY += value1 * value2;
47 | sumX += value1;
48 | sumY += value2;
49 | sumXxX += value1 * value1;
50 | sumYxY += value2 * value2;
51 | }
52 |
53 | return getDimensionWeight(dimension) * (a.size() * sumXxY - sumX * sumY) /
54 | Math.sqrt((a.size() * sumXxX - sumX * sumX) * (a.size() * sumYxY - sumY * sumY));
55 | }
56 |
57 | @Override
58 | public Double getVectorNorm(Integer d, Vector v) {
59 | //FIXME
60 | return null;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/FrameshiftBulkLoad.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | import com.allenday.image.ImageFeatures;
4 | import org.apache.solr.client.solrj.SolrClient;
5 | import org.apache.solr.client.solrj.SolrServerException;
6 | import org.apache.solr.client.solrj.impl.HttpSolrClient;
7 | import org.apache.solr.client.solrj.response.UpdateResponse;
8 | import org.apache.solr.common.SolrInputDocument;
9 |
10 | import java.io.BufferedReader;
11 | import java.io.IOException;
12 | import java.io.InputStreamReader;
13 |
14 | public class FrameshiftBulkLoad {
15 | public static void main(String[] args) throws SolrServerException, IOException {
16 | int bins = 8;
17 | Integer batchSize = 100;
18 | SolrClient solr = new HttpSolrClient.Builder(args[0]).build();
19 | InputStreamReader isr = new InputStreamReader(System.in);
20 | BufferedReader br = new BufferedReader(isr);
21 | String record;
22 |
23 | Integer batchIndex = 0;
24 | Integer totalRecords = 0;
25 |
26 | while ((record = br.readLine()) != null) {
27 | System.err.println(totalRecords + " " + record);
28 | String[] fields = record.split("\t");
29 | ImageFeatures x = new ImageFeatures(null,8,fields[1]);
30 |
31 | SolrInputDocument document = new SolrInputDocument();
32 | document.addField("id", fields[0]);
33 | document.addField("file_id", fields[0]);
34 | document.addField("time_offset", 0);
35 | document.addField("rgbtc", x.getLabeledHexAll());
36 | document.addField("xception", fields[2]);
37 | UpdateResponse response = solr.add(document);
38 | batchIndex++;
39 | totalRecords++;
40 | if (batchIndex >= batchSize) {
41 | solr.commit();
42 | }
43 | }
44 | solr.commit();
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/FrameshiftInsert.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | import org.apache.solr.client.solrj.SolrClient;
4 | import org.apache.solr.client.solrj.SolrServerException;
5 | import org.apache.solr.client.solrj.impl.HttpSolrClient;
6 | import org.apache.solr.client.solrj.response.UpdateResponse;
7 | import org.apache.solr.common.SolrInputDocument;
8 |
9 | import java.io.IOException;
10 |
11 | class FrameshiftInsert {
12 | public static void main(String[] args) throws SolrServerException, IOException {
13 | SolrClient solr = new HttpSolrClient.Builder("http://localhost:8983/solr/frameshift").build();
14 | SolrInputDocument document = new SolrInputDocument();
15 | document.addField("id", "");
16 | document.addField("file_id", "");
17 | document.addField("time_offset", "");
18 | document.addField("rgbtc", "");
19 | UpdateResponse response = solr.add(document);
20 |
21 |
22 | solr.commit();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/FrameshiftSearch.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | class FrameshiftSearch {
4 | /*
5 | public static void main( String[] args ) throws SolrServerException, IOException {
6 | SolrClient solr = new HttpSolrClient.Builder("http://localhost:8983/solr/frameshift").build();
7 |
8 | ImageProcessor processor = new ImageProcessor(16,4,false);
9 | processor.addFile(new File(args[0]));
10 | processor.processImages();
11 | for (Entry e : processor.getResults().entrySet()) {
12 | File image = e.getKey();
13 | ImageFeatures f = e.getValue();
14 |
15 | SolrQuery query = new SolrQuery();
16 | String qq =
17 | f.getRcompact() + " " +
18 | f.getGcompact() + " " +
19 | f.getBcompact() + " " +
20 | f.getTcompact() + " " +
21 | f.getCcompact() + " " +
22 | "";
23 |
24 | System.err.println("query: "+qq);
25 | query.setQuery(qq);
26 | query.set("fl", "id,file_id,score");
27 |
28 | QueryResponse response = solr.query(query);
29 |
30 | SolrDocumentList list = response.getResults();
31 | ListIterator resultsIterator = list.listIterator();
32 | while (resultsIterator.hasNext()) {
33 | SolrDocument result = resultsIterator.next();
34 | for (String fieldName : result.getFieldNames()) {
35 | System.err.println(fieldName + "\t" + result.getFieldValue(fieldName));
36 | }
37 | }
38 | }
39 | }
40 | */
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/ImageVectors.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | import com.allenday.image.ImageFeatures;
4 | import com.allenday.image.ImageProcessor;
5 | import org.apache.commons.io.DirectoryWalker;
6 | import org.apache.solr.client.solrj.SolrClient;
7 | import org.apache.solr.client.solrj.SolrServerException;
8 | import org.apache.solr.client.solrj.impl.HttpSolrClient;
9 | import org.apache.solr.client.solrj.response.UpdateResponse;
10 | import org.apache.solr.common.SolrInputDocument;
11 |
12 | import java.awt.color.CMMException;
13 | import java.io.File;
14 | import java.io.IOException;
15 | import java.util.ArrayList;
16 | import java.util.List;
17 | import java.util.TreeMap;
18 | import java.util.TreeSet;
19 |
20 | public class ImageVectors {
21 | public static void main(String[] argv) {
22 | SolrClient solr = null;
23 | int batchSize = 10;
24 | int bins = 8;
25 | int bits = 3; //specifically set to 3 to enable base64 packing
26 | boolean normalize = false;
27 | ImageProcessor processor = new ImageProcessor(bins, bits, false);
28 |
29 | //String input = "/Users/allenday/Downloads/01";//"src/test/resources/image/";//pictures-of-nasa-s-atlantis-shuttle-launch-photos-video.jpeg";//bad.gif";
30 | String input = argv[0];
31 | String frameshiftUrl = "";
32 | if (argv.length > 1)
33 | frameshiftUrl = argv[1];
34 | Boolean loadFrameshift = frameshiftUrl.compareTo("") == 1 ? true : false;
35 |
36 | List files = new ArrayList<>();
37 | File f0 = new File(input);
38 | if (f0.isDirectory()) {
39 | for (String f1 : f0.list()){
40 | files.add(new File(f0.getAbsolutePath()+"/"+f1));
41 | }
42 | }
43 | else {
44 | files.add(f0);
45 | }
46 |
47 |
48 | TreeSet sortedFiles = new TreeSet<>();
49 | for (File file : files) {
50 | sortedFiles.add(file);
51 | }
52 |
53 | Integer batchIndex = 0;
54 | for (File file : sortedFiles) {
55 | System.err.println(file.getAbsolutePath());
56 | ImageFeatures features = null;
57 | try {
58 | features = processor.extractFeatures(file);
59 | System.out.println(file.getAbsolutePath() + " " + features.getRawB64All() + " " + features.getLabeledHexAll());
60 | //ImageFeatures x = new ImageFeatures("asdf",bins,features.getRawB64All());
61 | //System.out.println(file.getAbsolutePath() + " " + features.getRawB64All() + " " + x.getLabeledHexAll());
62 |
63 | if (loadFrameshift) {
64 | if (solr == null) {
65 | solr = new HttpSolrClient.Builder(frameshiftUrl).build();
66 | }
67 | SolrInputDocument document = new SolrInputDocument();
68 | document.addField("id", features.id);
69 | document.addField("file_id", features.id);
70 | document.addField("time_offset", 0);
71 | document.addField("rgbtc", features.getLabeledHexAll());
72 | UpdateResponse response = solr.add(document);
73 | //System.err.println(response);
74 | //solr.commit();
75 | batchIndex++;
76 | }
77 | } catch (CMMException e) {
78 | System.out.println("CMMException failed to process: " + file.getAbsolutePath());
79 | } catch (IllegalArgumentException e) {
80 | System.out.println("IllegalArgumentException failed to process: " + file.getAbsolutePath());
81 | } catch (IOException e) {
82 | System.out.println("IOException failed to process: " + file.getAbsolutePath());
83 | } catch (SolrServerException e) {
84 | System.out.println("SolrServerException [1] failed to process: " + file.getAbsolutePath());
85 | e.printStackTrace();
86 | }
87 | if (loadFrameshift && batchIndex >= batchSize) {
88 | try {
89 | solr.commit();
90 | } catch (SolrServerException e) {
91 | System.out.println("SolrServerException [2] failed to process: " + file.getAbsolutePath());
92 | e.printStackTrace();
93 | } catch (IOException e) {
94 | e.printStackTrace();
95 | }
96 | batchIndex = 0;
97 | }
98 | }
99 | if (loadFrameshift) {
100 | try {
101 | solr.commit();
102 | } catch (SolrServerException e) {
103 | System.out.println("SolrServerException [3] failed to process");
104 | e.printStackTrace();
105 | } catch (IOException e) {
106 | System.out.println("IOException [2] failed to process");
107 | e.printStackTrace();
108 | }
109 | }
110 |
111 | //String imageFile = argv[0];
112 |
113 | //System.out.println(features.getJsonAll());
114 | //System.out.println(features.getTokensAll());
115 | //System.out.println(features.getLabeledHexAll());
116 | //System.out.println(features.getRawHexAll());
117 |
118 | // for (Entry e : processor.getResults().entrySet()) {
119 | // File image = e.getKey();
120 | // ImageFeatures features = e.getValue();
121 | // System.err.println( imageFile + "\t" + features.getRcompact() );
122 | // System.err.println( imageFile + "\t" + features.getGcompact() );
123 | // System.err.println( imageFile + "\t" + features.getBcompact() );
124 | // System.err.println( imageFile + "\t" + features.getTcompact() );
125 | // System.err.println( imageFile + "\t" + features.getCcompact() );
126 | // System.err.println();
127 | // }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/IndexDirectory.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | import com.allenday.image.ImageFeatures;
4 | import com.allenday.image.ImageIndex;
5 | import com.allenday.image.ImageIndexFactory;
6 | import com.allenday.image.ImageProcessor;
7 | import com.allenday.image.distance.UDWV_L1Norm;
8 | import com.allenday.image.distance.WDUV_PearsonDistance;
9 | import edu.wlu.cs.levy.CG.KeyDuplicateException;
10 | import edu.wlu.cs.levy.CG.KeySizeException;
11 | import org.slf4j.Logger;
12 | import org.slf4j.LoggerFactory;
13 |
14 | import java.io.*;
15 | import java.util.Arrays;
16 | import java.util.List;
17 | import java.util.Objects;
18 | import java.util.Set;
19 |
20 | class IndexDirectory {
21 | private static final Logger logger = LoggerFactory.getLogger(IndexDirectory.class);
22 | private static final Integer bins = 16;
23 | private static final Integer bits = 4;
24 | private static final Boolean normalize = false;
25 |
26 | public static void main(String[] argv) throws KeySizeException, IOException, ClassNotFoundException, CloneNotSupportedException {
27 | //createIndex();
28 | searchIndex();
29 | }
30 |
31 | private static void searchIndex() throws IOException, ClassNotFoundException, KeySizeException, CloneNotSupportedException {
32 | FileInputStream fis = new FileInputStream(new File("/Volumes/..."));
33 | ObjectInputStream ois = new ObjectInputStream(fis);
34 | ImageIndex index = (ImageIndex) ois.readObject();
35 |
36 | ImageProcessor processor = new ImageProcessor(16, 4, false);
37 | String pathname = "/Volumes/...";
38 | File path = new File(pathname);
39 | String[] filenames = path.list();
40 | Arrays.sort(Objects.requireNonNull(filenames));
41 |
42 | for (String filename : filenames) {
43 | ImageFeatures query = processor.extractFeatures(new File(pathname + filename));
44 | Set hits = index.getHits(query, 1);
45 | List rankedHits = index.rankHits(query, hits, new UDWV_L1Norm());//WDUV_PearsonDistance());
46 | ImageFeatures topHit = rankedHits.get(0);
47 | //for (ImageFeatures hit : rankedHits) {
48 | logger.debug(pathname + filename + " HIT " + topHit.id + ", score=" + topHit.score);
49 | //}
50 | }
51 | }
52 |
53 | public static void createIndex() throws KeySizeException, KeyDuplicateException, IOException, CloneNotSupportedException {
54 | File path = new File("/Volumes/...");
55 |
56 | ImageIndexFactory indexFactory = new ImageIndexFactory(bins, bits, normalize);
57 | indexFactory.addFile(path);
58 | ImageIndex index = indexFactory.createIndex(0, 30);
59 |
60 | File[] sFiles = indexFactory.files.keySet().toArray(new File[0]);
61 | Arrays.sort(sFiles);
62 | File queryFile = sFiles[0];
63 | ImageFeatures query = indexFactory.getProcessor().extractFeatures(queryFile);
64 | logger.debug("query=" + query.id);
65 |
66 | Set hits = index.getHits(query, 1);
67 | List rankedHits = index.rankHits(query, hits, new WDUV_PearsonDistance());
68 | for (ImageFeatures hit : rankedHits) {
69 | logger.debug("HIT: " + hit.id + ", score=" + hit.score);
70 | }
71 |
72 | FileOutputStream fos = new FileOutputStream(new File("/Volumes/..."));
73 | ObjectOutputStream oos = new ObjectOutputStream(fos);
74 | oos.writeObject(index);
75 | oos.close();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/LoadSimhash.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | public class LoadSimhash {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/runnable/SceneChangeDirectory.java:
--------------------------------------------------------------------------------
1 | package com.allenday.runnable;
2 |
3 | import com.allenday.image.Distance;
4 | import com.allenday.image.ImageFeatures;
5 | import com.allenday.image.ImageIndex;
6 | import com.allenday.image.ImageProcessor;
7 | import com.allenday.image.distance.UDUV_L1Norm;
8 | import com.allenday.image.distance.UDWV_L1Norm;
9 | import com.allenday.image.distance.WDUV_PearsonDistance;
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 |
13 | import java.io.File;
14 | import java.io.IOException;
15 | import java.util.Arrays;
16 | import java.util.Objects;
17 |
18 | class SceneChangeDirectory {
19 | private static final Logger logger = LoggerFactory.getLogger(IndexDirectory.class);
20 | static int bins = 16;
21 | static int bits = 4;
22 | static boolean normalize = false;
23 |
24 | public static void main(String[] argv) throws IOException {
25 | ImageProcessor processor = new ImageProcessor(16, 4, false);
26 | String pathname = "/Volumes/.../";
27 | File path = new File(pathname);
28 | String[] filenames = path.list();
29 | Arrays.sort(Objects.requireNonNull(filenames));
30 |
31 | int i = 2430;
32 |
33 | Distance m0 = new UDUV_L1Norm();
34 | Distance m1 = new UDWV_L1Norm();
35 | Distance m2 = new WDUV_PearsonDistance();
36 | ImageFeatures prevFrame = processor.extractFeatures(new File(pathname + filenames[i]));
37 | while (i < filenames.length) {
38 | ImageFeatures thisFrame = processor.extractFeatures(new File(pathname + filenames[i]));
39 | Double d0 = ImageIndex.getScalarDistance(prevFrame, thisFrame, m0);
40 | Double d1 = ImageIndex.getScalarDistance(prevFrame, thisFrame, m1);
41 | Double d2 = ImageIndex.getScalarDistance(prevFrame, thisFrame, m2);
42 | logger.debug(pathname + filenames[i] + "\t" + d0 + "\t" + d1 + "\t" + d2);//+ "\t" + thisFrame.getAllcompact());
43 | //logger.debug(pathname+filenames[i] + "\t" + distance);
44 | prevFrame = thisFrame;
45 | i++;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/util/BitBuffer.java:
--------------------------------------------------------------------------------
1 | package com.allenday.util;
2 |
3 | /**
4 | * A class for reading arbitrary numbers of bits from a byte array.
5 | *
6 | * @author Eric Kjellman egkjellman at wisc.edu
7 | */
8 | class BitBuffer {
9 |
10 | private int currentByte;
11 | private int currentBit;
12 | private final byte[] byteBuffer;
13 | private final int eofByte;
14 | private final int[] backMask;
15 | private final int[] frontMask;
16 | private boolean eofFlag;
17 |
18 | public BitBuffer(byte[] byteBuffer) {
19 | this.byteBuffer = byteBuffer;
20 | currentByte = 0;
21 | currentBit = 0;
22 | eofByte = byteBuffer.length;
23 | backMask = new int[]{0x0000, 0x0001, 0x0003, 0x0007,
24 | 0x000F, 0x001F, 0x003F, 0x007F};
25 | frontMask = new int[]{0x0000, 0x0080, 0x00C0, 0x00E0,
26 | 0x00F0, 0x00F8, 0x00FC, 0x00FE};
27 | }
28 |
29 | public int getBits(int bitsToRead) {
30 | if (bitsToRead == 0)
31 | return 0;
32 | if (eofFlag)
33 | return -1; // Already at end of file
34 | int toStore = 0;
35 | while (bitsToRead != 0) {
36 | if (bitsToRead >= 8 - currentBit) {
37 | if (currentBit == 0) { // special
38 | toStore = toStore << 8;
39 | int cb = ((int) byteBuffer[currentByte]);
40 | toStore += (cb < 0 ? 256 + cb : cb);
41 | bitsToRead -= 8;
42 | currentByte++;
43 | } else {
44 | toStore = toStore << (8 - currentBit);
45 | toStore += ((int) byteBuffer[currentByte]) & backMask[8 - currentBit];
46 | bitsToRead -= (8 - currentBit);
47 | currentBit = 0;
48 | currentByte++;
49 | }
50 | } else {
51 | toStore = toStore << bitsToRead;
52 | int cb = ((int) byteBuffer[currentByte]);
53 | cb = (cb < 0 ? 256 + cb : cb);
54 | toStore += ((cb) & (0x00FF - frontMask[currentBit])) >> (8 - (currentBit + bitsToRead));
55 | currentBit += bitsToRead;
56 | bitsToRead = 0;
57 | }
58 | if (currentByte == eofByte) {
59 | eofFlag = true;
60 | return toStore;
61 | }
62 | }
63 | return toStore;
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/allenday/util/Pack.java:
--------------------------------------------------------------------------------
1 | package com.allenday.util;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.Base64;
5 | import java.util.BitSet;
6 |
7 | class Pack {
8 | public static void main(String[] args) {
9 | // 5 bins, 5 bits, 7 dimensions = 175
10 | // 8 bins, 4 bits, 7 dimensions (32 reserved bits) =
11 | int[] data = {
12 | 1, 2, 3, 4, 5, 6, 7, 0, //R
13 | 1, 2, 3, 4, 5, 6, 7, 0, //G
14 | 1, 2, 3, 4, 5, 6, 7, 0, //B
15 | 1, 2, 3, 4, 5, 6, 7, 0, //T
16 | 1, 2, 3, 4, 5, 6, 7, 0, //C
17 | 1, 2, 3, 4, 5, 6, 7, 0, //M1
18 | 1, 2, 3, 4, 5, 6, 7, 0, //M2
19 | };
20 | ByteBuffer byteBuffer = ByteBuffer.allocate(data.length);
21 | BitSet bs = new BitSet(256);
22 |
23 | StringBuilder tt = new StringBuilder();
24 | for (int i = 0; i < 56; i++) {
25 | int low = data[i] >> 32;
26 | for (int j = 0; j < 4; j++) {
27 | if ((low >> j & 0x1) != 0x0) {
28 | bs.set(i * 8 + j);
29 | tt.append("1");
30 | } else {
31 | tt.append("0");
32 | }
33 | // tt += " ";
34 | }
35 | // tt += "\n";
36 | }
37 | byte[] array = new byte[56];
38 |
39 |
40 | int m = 0;
41 | int n = 0;
42 | while (m + 8 <= tt.length()) {
43 | array[n] = Byte.valueOf(tt.substring(m, m + 8), 2);
44 | m += 8;
45 | n++;
46 | }
47 | // System.err.println(bs.toString());
48 | // System.err.println(tt);
49 |
50 | for (int datum : data) {
51 | int low = datum >> 32;
52 | byteBuffer.put((byte) low);
53 | }
54 |
55 | // byte[] array = byteBuffer.array();
56 | byte[] encoded = Base64.getEncoder().encode(array);
57 |
58 | for (int i = 0; i < array.length; i++) {
59 |
60 | // System.out.println(i + ": " + array[i]);
61 | }
62 | System.out.println(new String(encoded));
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/Checker.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 | interface Checker {
4 | boolean usable(T v);
5 | }
6 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/DistanceMetric.java:
--------------------------------------------------------------------------------
1 | // Abstract distance metric class
2 |
3 | package edu.wlu.cs.levy.CG;
4 |
5 | abstract class DistanceMetric {
6 |
7 | protected abstract double distance(double[] a, double[] b);
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/Editor.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 |
4 | public interface Editor {
5 | T edit(T current) throws KeyDuplicateException;
6 |
7 | abstract class BaseEditor implements Editor {
8 | final T val;
9 |
10 | BaseEditor(T val) {
11 | this.val = val;
12 | }
13 |
14 | public abstract T edit(T current) throws KeyDuplicateException;
15 | }
16 |
17 | class Inserter extends BaseEditor {
18 | Inserter(T val) {
19 | super(val);
20 | }
21 |
22 | public T edit(T current) throws KeyDuplicateException {
23 | if (current == null) {
24 | return this.val;
25 | }
26 | throw new KeyDuplicateException();
27 | }
28 | }
29 |
30 | class OptionalInserter extends BaseEditor {
31 | OptionalInserter(T val) {
32 | super(val);
33 | }
34 |
35 | public T edit(T current) {
36 | return (current == null) ? this.val : current;
37 | }
38 | }
39 |
40 | class Replacer extends BaseEditor {
41 | Replacer(T val) {
42 | super(val);
43 | }
44 |
45 | public T edit(T current) {
46 | return this.val;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/EuclideanDistance.java:
--------------------------------------------------------------------------------
1 | // Hamming distance metric class
2 |
3 | package edu.wlu.cs.levy.CG;
4 |
5 | class EuclideanDistance extends DistanceMetric {
6 |
7 | static double sqrdist(double[] a, double[] b) {
8 |
9 | double dist = 0;
10 |
11 | for (int i = 0; i < a.length; ++i) {
12 | double diff = (a[i] - b[i]);
13 | dist += diff * diff;
14 | }
15 |
16 | return dist;
17 | }
18 |
19 | protected double distance(double[] a, double[] b) {
20 |
21 | return Math.sqrt(sqrdist(a, b));
22 |
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/HPoint.java:
--------------------------------------------------------------------------------
1 | // Hyper-Point class supporting KDTree class
2 |
3 | package edu.wlu.cs.levy.CG;
4 |
5 | import java.io.Serializable;
6 |
7 | class HPoint implements Serializable {
8 |
9 | final double[] coord;
10 |
11 | HPoint(int n) {
12 | coord = new double[n];
13 | }
14 |
15 | HPoint(double[] x) {
16 |
17 | coord = new double[x.length];
18 | System.arraycopy(x, 0, coord, 0, x.length);
19 | }
20 |
21 | static double sqrdist(HPoint x, HPoint y) {
22 |
23 | return EuclideanDistance.sqrdist(x.coord, y.coord);
24 | }
25 |
26 | protected Object clone() throws CloneNotSupportedException {
27 | Object o = super.clone();
28 |
29 | return new HPoint(coord);
30 | }
31 |
32 | boolean equals(HPoint p) {
33 |
34 | // seems faster than java.util.Arrays.equals(), which is not
35 | // currently supported by Matlab anyway
36 | for (int i = 0; i < coord.length; ++i)
37 | if (coord[i] != p.coord[i])
38 | return false;
39 |
40 | return true;
41 | }
42 |
43 | public String toString() {
44 | StringBuilder s = new StringBuilder();
45 | for (double v : coord) {
46 | s.append(v).append(" ");
47 | }
48 | return s.toString();
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/HRect.java:
--------------------------------------------------------------------------------
1 | // Hyper-Rectangle class supporting KDTree class
2 |
3 | package edu.wlu.cs.levy.CG;
4 |
5 | class HRect {
6 |
7 | HPoint min;
8 | HPoint max;
9 |
10 | protected HRect(int ndims) {
11 | min = new HPoint(ndims);
12 | max = new HPoint(ndims);
13 | }
14 |
15 | private HRect(HPoint vmin, HPoint vmax) throws CloneNotSupportedException {
16 |
17 | min = (HPoint) vmin.clone();
18 | max = (HPoint) vmax.clone();
19 | }
20 |
21 | // used in initial conditions of KDTree.nearest()
22 | static HRect infiniteHRect(int d) throws CloneNotSupportedException {
23 |
24 | HPoint vmin = new HPoint(d);
25 | HPoint vmax = new HPoint(d);
26 |
27 | for (int i = 0; i < d; ++i) {
28 | vmin.coord[i] = Double.NEGATIVE_INFINITY;
29 | vmax.coord[i] = Double.POSITIVE_INFINITY;
30 | }
31 |
32 | return new HRect(vmin, vmax);
33 | }
34 |
35 | protected Object clone() throws CloneNotSupportedException {
36 |
37 | return new HRect(min, max);
38 | }
39 |
40 | // from Moore's eqn. 6.6
41 | HPoint closest(HPoint t) {
42 |
43 | HPoint p = new HPoint(t.coord.length);
44 |
45 | for (int i = 0; i < t.coord.length; ++i) {
46 | if (t.coord[i] <= min.coord[i]) {
47 | p.coord[i] = min.coord[i];
48 | } else if (t.coord[i] >= max.coord[i]) {
49 | p.coord[i] = max.coord[i];
50 | } else {
51 | p.coord[i] = t.coord[i];
52 | }
53 | }
54 |
55 | return p;
56 | }
57 |
58 | // currently unused
59 | protected HRect intersection(HRect r) throws CloneNotSupportedException {
60 |
61 | HPoint newmin = new HPoint(min.coord.length);
62 | HPoint newmax = new HPoint(min.coord.length);
63 |
64 | for (int i = 0; i < min.coord.length; ++i) {
65 | newmin.coord[i] = Math.max(min.coord[i], r.min.coord[i]);
66 | newmax.coord[i] = Math.min(max.coord[i], r.max.coord[i]);
67 | if (newmin.coord[i] >= newmax.coord[i]) return null;
68 | }
69 |
70 | return new HRect(newmin, newmax);
71 | }
72 |
73 | // currently unused
74 | protected double area() {
75 |
76 | double a = 1;
77 |
78 | for (int i = 0; i < min.coord.length; ++i) {
79 | a *= (max.coord[i] - min.coord[i]);
80 | }
81 |
82 | return a;
83 | }
84 |
85 | public String toString() {
86 | return min + "\n" + max + "\n";
87 | }
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/HammingDistance.java:
--------------------------------------------------------------------------------
1 | // Hamming distance metric class
2 |
3 | package edu.wlu.cs.levy.CG;
4 |
5 | class HammingDistance extends DistanceMetric {
6 |
7 | protected double distance(double[] a, double[] b) {
8 |
9 | double dist = 0;
10 |
11 | for (int i = 0; i < a.length; ++i) {
12 | double diff = (a[i] - b[i]);
13 | dist += Math.abs(diff);
14 | }
15 |
16 | return dist;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/KDException.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 | class KDException extends Exception {
4 | public static final long serialVersionUID = 1L;
5 |
6 | KDException(String s) {
7 | super(s);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/KDNode.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 | import java.io.Serializable;
4 | import java.util.List;
5 |
6 | // K-D Tree node class
7 |
8 | class KDNode implements Serializable {
9 |
10 | // these are seen by KDTree
11 | final HPoint k;
12 | T v;
13 | private KDNode left;
14 | private KDNode right;
15 | boolean deleted;
16 |
17 | // Method ins translated from 352.ins.c of Gonnet & Baeza-Yates
18 | static int edit(HPoint key, Editor editor, KDNode t, int lev, int K)
19 | throws KeyDuplicateException {
20 | KDNode next_node;
21 | int next_lev = (lev+1) % K;
22 | synchronized (t) {
23 | if (key.equals(t.k)) {
24 | boolean was_deleted = t.deleted;
25 | t.v = editor.edit(t.deleted ? null : t.v );
26 | t.deleted = (t.v == null);
27 |
28 | if (t.deleted == was_deleted) {
29 | return 0;
30 | } else if (was_deleted) {
31 | return -1;
32 | }
33 | return 1;
34 | } else if (key.coord[lev] > t.k.coord[lev]) {
35 | next_node = t.right;
36 | if (next_node == null) {
37 | t.right = create(key, editor);
38 | return t.right.deleted ? 0 : 1;
39 | }
40 | }
41 | else {
42 | next_node = t.left;
43 | if (next_node == null) {
44 | t.left = create(key, editor);
45 | return t.left.deleted ? 0 : 1;
46 | }
47 | }
48 | }
49 |
50 | return edit(key, editor, next_node, next_lev, K);
51 | }
52 |
53 | static KDNode create(HPoint key, Editor editor)
54 | throws KeyDuplicateException {
55 | KDNode t = new KDNode<>(key, editor.edit(null));
56 | if (t.v == null) {
57 | t.deleted = true;
58 | }
59 | return t;
60 | }
61 |
62 | static boolean del(KDNode t) {
63 | synchronized (t) {
64 | if (!t.deleted) {
65 | t.deleted = true;
66 | return true;
67 | }
68 | }
69 | return false;
70 | }
71 |
72 | // Method srch translated from 352.srch.c of Gonnet & Baeza-Yates
73 | static KDNode srch(HPoint key, KDNode t, int K) {
74 |
75 | for (int lev=0; t!=null; lev=(lev+1)%K) {
76 |
77 | if (!t.deleted && key.equals(t.k)) {
78 | return t;
79 | }
80 | else if (key.coord[lev] > t.k.coord[lev]) {
81 | t = t.right;
82 | }
83 | else {
84 | t = t.left;
85 | }
86 | }
87 |
88 | return null;
89 | }
90 |
91 | // Method rsearch translated from 352.range.c of Gonnet & Baeza-Yates
92 | static void rsearch(HPoint lowk, HPoint uppk, KDNode t, int lev,
93 | int K, List> v) {
94 |
95 | if (t == null) return;
96 | if (lowk.coord[lev] <= t.k.coord[lev]) {
97 | rsearch(lowk, uppk, t.left, (lev+1)%K, K, v);
98 | }
99 | if (!t.deleted) {
100 | int j = 0;
101 | while (j=t.k.coord[j]) {
103 | j++;
104 | }
105 | if (j==K) v.add(t);
106 | }
107 | if (uppk.coord[lev] > t.k.coord[lev]) {
108 | rsearch(lowk, uppk, t.right, (lev+1)%K, K, v);
109 | }
110 | }
111 |
112 | // Method Nearest Neighbor from Andrew Moore's thesis. Numbered
113 | // comments are direct quotes from there. NearestNeighborList solution
114 | // courtesy of Bjoern Heckel.
115 | static void nnbr(KDNode kd, HPoint target, HRect hr,
116 | double max_dist_sqd, int lev, int K,
117 | NearestNeighborList> nnl,
118 | Checker checker,
119 | long timeout) throws CloneNotSupportedException {
120 |
121 | // 1. if kd is empty then set dist-sqd to infinity and exit.
122 | if (kd == null) {
123 | return;
124 | }
125 |
126 | if ((timeout > 0) && (timeout < System.currentTimeMillis())) {
127 | return;
128 | }
129 | // 2. s := split field of kd
130 | int s = lev % K;
131 |
132 | // 3. pivot := dom-elt field of kd
133 | HPoint pivot = kd.k;
134 | double pivot_to_target = HPoint.sqrdist(pivot, target);
135 |
136 | // 4. Cut hr into to sub-hyperrectangles left-hr and right-hr.
137 | // The cut plane is through pivot and perpendicular to the s
138 | // dimension.
139 | HRect right_hr = (HRect) hr.clone();
140 | hr.max.coord[s] = pivot.coord[s];
141 | right_hr.min.coord[s] = pivot.coord[s];
142 |
143 | // 5. target-in-left := target_s <= pivot_s
144 | boolean target_in_left = target.coord[s] < pivot.coord[s];
145 |
146 | KDNode nearer_kd;
147 | HRect nearer_hr;
148 | KDNode further_kd;
149 | HRect further_hr;
150 |
151 | // 6. if target-in-left then
152 | // 6.1. nearer-kd := left field of kd and nearer-hr := left-hr
153 | // 6.2. further-kd := right field of kd and further-hr := right-hr
154 | if (target_in_left) {
155 | nearer_kd = kd.left;
156 | nearer_hr = hr;
157 | further_kd = kd.right;
158 | further_hr = right_hr;
159 | }
160 | //
161 | // 7. if not target-in-left then
162 | // 7.1. nearer-kd := right field of kd and nearer-hr := right-hr
163 | // 7.2. further-kd := left field of kd and further-hr := left-hr
164 | else {
165 | nearer_kd = kd.right;
166 | nearer_hr = right_hr;
167 | further_kd = kd.left;
168 | further_hr = hr;
169 | }
170 |
171 | // 8. Recursively call Nearest Neighbor with paramters
172 | // (nearer-kd, target, nearer-hr, max-dist-sqd), storing the
173 | // results in nearest and dist-sqd
174 | nnbr(nearer_kd, target, nearer_hr, max_dist_sqd, lev + 1, K, nnl, checker, timeout);
175 |
176 | @SuppressWarnings("unused")
177 | KDNode nearest = nnl.getHighest();
178 | double dist_sqd;
179 |
180 | if (!nnl.isCapacityReached()) {
181 | dist_sqd = Double.MAX_VALUE;
182 | }
183 | else {
184 | dist_sqd = nnl.getMaxPriority();
185 | }
186 |
187 | // 9. max-dist-sqd := minimum of max-dist-sqd and dist-sqd
188 | max_dist_sqd = Math.min(max_dist_sqd, dist_sqd);
189 |
190 | // 10. A nearer point could only lie in further-kd if there were some
191 | // part of further-hr within distance max-dist-sqd of
192 | // target.
193 | HPoint closest = further_hr.closest(target);
194 | if (HPoint.sqrdist(closest, target) < max_dist_sqd) {
195 |
196 | // 10.1 if (pivot-target)^2 < dist-sqd then
197 | if (pivot_to_target < dist_sqd) {
198 |
199 | // 10.1.1 nearest := (pivot, range-elt field of kd)
200 |
201 | // 10.1.2 dist-sqd = (pivot-target)^2
202 | dist_sqd = pivot_to_target;
203 |
204 | // add to nnl
205 | if (!kd.deleted && ((checker == null) || checker.usable(kd.v))) {
206 | nnl.insert(kd, dist_sqd);
207 | }
208 |
209 | // 10.1.3 max-dist-sqd = dist-sqd
210 | // max_dist_sqd = dist_sqd;
211 | if (nnl.isCapacityReached()) {
212 | max_dist_sqd = nnl.getMaxPriority();
213 | }
214 | else {
215 | max_dist_sqd = Double.MAX_VALUE;
216 | }
217 | }
218 |
219 | // 10.2 Recursively call Nearest Neighbor with parameters
220 | // (further-kd, target, further-hr, max-dist_sqd),
221 | // storing results in temp-nearest and temp-dist-sqd
222 | nnbr(further_kd, target, further_hr, max_dist_sqd, lev + 1, K, nnl, checker, timeout);
223 | }
224 | }
225 |
226 |
227 | // constructor is used only by class; other methods are static
228 | private KDNode(HPoint key, T val) {
229 |
230 | k = key;
231 | v = val;
232 | left = null;
233 | right = null;
234 | deleted = false;
235 | }
236 |
237 | String toString(int depth) {
238 | String s = k + " " + v + (deleted ? "*" : "");
239 | if (left != null) {
240 | s = s + "\n" + pad(depth) + "L " + left.toString(depth+1);
241 | }
242 | if (right != null) {
243 | s = s + "\n" + pad(depth) + "R " + right.toString(depth+1);
244 | }
245 | return s;
246 | }
247 |
248 | private static String pad(int n) {
249 | StringBuilder s = new StringBuilder();
250 | for (int i=0; i
14 | *
Two different keys containing identical numbers should retrieve the
15 | * same value from a given KD-tree. Therefore keys are cloned when a
16 | * node is inserted.
17 | *
18 | *
As with Hashtables, values inserted into a KD-tree are not
19 | * cloned. Modifying a value between insertion and retrieval will
20 | * therefore modify the value stored in the tree.
21 | *
22 | *
23 | * Implements the Nearest Neighbor algorithm (Table 6.4) of
24 | *
25 | *
26 | * @techreport{AndrewMooreNearestNeighbor,
27 | * author = {Andrew Moore},
28 | * title = {An introductory tutorial on kd-trees},
29 | * institution = {Robotics Institute, Carnegie Mellon University},
30 | * year = {1991},
31 | * number = {Technical Report No. 209, Computer Laboratory,
32 | * University of Cambridge},
33 | * address = {Pittsburgh, PA}
34 | * }
35 | *
36 | *
37 | *
38 | * @author Simon Levy, Bjoern Heckel
39 | * @version %I%, %G%
40 | * @since JDK1.2
41 | */
42 | public class KDTree {
43 | // number of milliseconds
44 | final long m_timeout;
45 |
46 | // K = number of dimensions
47 | final private int m_K;
48 |
49 | // root of KD-tree
50 | private KDNode m_root;
51 |
52 | // count of nodes
53 | private int m_count;
54 |
55 | /**
56 | * Creates a KD-tree with specified number of dimensions.
57 | *
58 | * @param k number of dimensions
59 | */
60 | public KDTree(int k) {
61 | this(k, 0);
62 | }
63 | public KDTree(int k, long timeout) {
64 | this.m_timeout = timeout;
65 | m_K = k;
66 | m_root = null;
67 | }
68 |
69 |
70 | /**
71 | * Insert a node in a KD-tree. Uses algorithm translated from 352.ins.c of
72 | *
73 | *
74 | * @Book{GonnetBaezaYates1991,
75 | * author = {G.H. Gonnet and R. Baeza-Yates},
76 | * title = {Handbook of Algorithms and Data Structures},
77 | * publisher = {Addison-Wesley},
78 | * year = {1991}
79 | * }
80 | *
81 | *
82 | * @param key key for KD-tree node
83 | * @param value value at that key
84 | *
85 | * @throws KeySizeException if key.length mismatches K
86 | * @throws KeyDuplicateException if key already in tree
87 | */
88 | public void insert(double [] key, T value)
89 | throws KeySizeException, KeyDuplicateException {
90 | this.edit(key, new Editor.Inserter(value));
91 | }
92 |
93 | /**
94 | * Edit a node in a KD-tree
95 | *
96 | * @param key key for KD-tree node
97 | * @param editor object to edit the value at that key
98 | *
99 | * @throws KeySizeException if key.length mismatches K
100 | * @throws KeyDuplicateException if key already in tree
101 | */
102 |
103 | public void edit(double [] key, Editor editor)
104 | throws KeySizeException, KeyDuplicateException {
105 |
106 | if (key.length != m_K) {
107 | throw new KeySizeException();
108 | }
109 |
110 | synchronized (this) {
111 | // the first insert has to be synchronized
112 | if (null == m_root) {
113 | m_root = KDNode.create(new HPoint(key), editor);
114 | m_count = m_root.deleted ? 0 : 1;
115 | return;
116 | }
117 | }
118 |
119 | m_count += KDNode.edit(new HPoint(key), editor, m_root, 0, m_K);
120 | }
121 |
122 | /**
123 | * Find KD-tree node whose key is identical to key. Uses algorithm
124 | * translated from 352.srch.c of Gonnet & Baeza-Yates.
125 | *
126 | * @param key key for KD-tree node
127 | *
128 | * @return object at key, or null if not found
129 | *
130 | * @throws KeySizeException if key.length mismatches K
131 | */
132 | public T search(double [] key) throws KeySizeException {
133 |
134 | if (key.length != m_K) {
135 | throw new KeySizeException();
136 | }
137 |
138 | KDNode kd = KDNode.srch(new HPoint(key), m_root, m_K);
139 |
140 | return (kd == null ? null : kd.v);
141 | }
142 |
143 |
144 | public void delete(double [] key)
145 | throws KeySizeException, KeyMissingException {
146 | delete(key, false);
147 | }
148 | /**
149 | * Delete a node from a KD-tree. Instead of actually deleting node and
150 | * rebuilding tree, marks node as deleted. Hence, it is up to the caller
151 | * to rebuild the tree as needed for efficiency.
152 | *
153 | * @param key key for KD-tree node
154 | * @param optional if false and node not found, throw an exception
155 | *
156 | * @throws KeySizeException if key.length mismatches K
157 | * @throws KeyMissingException if no node in tree has key
158 | */
159 | public void delete(double [] key, boolean optional)
160 | throws KeySizeException, KeyMissingException {
161 |
162 | if (key.length != m_K) {
163 | throw new KeySizeException();
164 | }
165 | KDNode t = KDNode.srch(new HPoint(key), m_root, m_K);
166 | if (t == null) {
167 | if (optional == false) {
168 | throw new KeyMissingException();
169 | }
170 | }
171 | else {
172 | if (KDNode.del(t)) {
173 | m_count--;
174 | }
175 | }
176 | }
177 |
178 | /**
179 | * Find KD-tree node whose key is nearest neighbor to
180 | * key.
181 | *
182 | * @param key key for KD-tree node
183 | *
184 | * @return object at node nearest to key, or null on failure
185 | *
186 | * @throws KeySizeException if key.length mismatches K
187 |
188 | */
189 | public T nearest(double [] key) throws KeySizeException, CloneNotSupportedException {
190 |
191 | List nbrs = nearest(key, 1, null);
192 | return nbrs.get(0);
193 | }
194 |
195 | /**
196 | * Find KD-tree nodes whose keys are n nearest neighbors to
197 | * key.
198 | *
199 | * @param key key for KD-tree node
200 | * @param n number of nodes to return
201 | *
202 | * @return objects at nodes nearest to key, or null on failure
203 | *
204 | * @throws KeySizeException if key.length mismatches K
205 |
206 | */
207 | public List nearest(double [] key, int n)
208 | throws KeySizeException, IllegalArgumentException, CloneNotSupportedException {
209 | return nearest(key, n, null);
210 | }
211 |
212 | /**
213 | * Find KD-tree nodes whose keys are within a given Euclidean distance of
214 | * a given key.
215 | *
216 | * @param key key for KD-tree node
217 | * @param dist Euclidean distance
218 | *
219 | * @return objects at nodes with distance of key, or null on failure
220 | *
221 | * @throws KeySizeException if key.length mismatches K
222 |
223 | */
224 | public List nearestEuclidean(double [] key, double dist)
225 | throws KeySizeException, CloneNotSupportedException {
226 | return nearestDistance(key, dist, new EuclideanDistance());
227 | }
228 |
229 |
230 | /**
231 | * Find KD-tree nodes whose keys are within a given Hamming distance of
232 | * a given key.
233 | *
234 | * @param key key for KD-tree node
235 | * @param dist Hamming distance
236 | *
237 | * @return objects at nodes with distance of key, or null on failure
238 | *
239 | * @throws KeySizeException if key.length mismatches K
240 |
241 | */
242 | public List nearestHamming(double [] key, double dist)
243 | throws KeySizeException, CloneNotSupportedException {
244 |
245 | return nearestDistance(key, dist, new HammingDistance());
246 | }
247 |
248 |
249 | /**
250 | * Find KD-tree nodes whose keys are n nearest neighbors to
251 | * key. Uses algorithm above. Neighbors are returned in ascending
252 | * order of distance to key.
253 | *
254 | * @param key key for KD-tree node
255 | * @param n how many neighbors to find
256 | * @param checker an optional object to filter matches
257 | *
258 | * @return objects at node nearest to key, or null on failure
259 | *
260 | * @throws KeySizeException if key.length mismatches K
261 | * @throws IllegalArgumentException if n is negative or
262 | * exceeds tree size
263 | */
264 | public List nearest(double [] key, int n, Checker checker)
265 | throws KeySizeException, IllegalArgumentException, CloneNotSupportedException {
266 |
267 | if (n <= 0) {
268 | return new LinkedList();
269 | }
270 |
271 | NearestNeighborList> nnl = getnbrs(key, n, checker);
272 |
273 | n = nnl.getSize();
274 | Stack nbrs = new Stack();
275 |
276 | for (int i=0; i kd = nnl.removeHighest();
278 | nbrs.push(kd.v);
279 | }
280 |
281 | return nbrs;
282 | }
283 |
284 |
285 | /**
286 | * Range search in a KD-tree. Uses algorithm translated from
287 | * 352.range.c of Gonnet & Baeza-Yates.
288 | *
289 | * @param lowk lower-bounds for key
290 | * @param uppk upper-bounds for key
291 | *
292 | * @return array of Objects whose keys fall in range [lowk,uppk]
293 | *
294 | * @throws KeySizeException on mismatch among lowk.length, uppk.length, or K
295 | */
296 | public List range(double [] lowk, double [] uppk)
297 | throws KeySizeException {
298 |
299 | if (lowk.length != uppk.length) {
300 | throw new KeySizeException();
301 | }
302 |
303 | else if (lowk.length != m_K) {
304 | throw new KeySizeException();
305 | }
306 |
307 | else {
308 | List> found = new LinkedList>();
309 | KDNode.rsearch(new HPoint(lowk), new HPoint(uppk),
310 | m_root, 0, m_K, found);
311 | List o = new LinkedList();
312 | for (KDNode node : found) {
313 | o.add(node.v);
314 | }
315 | return o;
316 | }
317 | }
318 |
319 | public int size() { /* added by MSL */
320 | return m_count;
321 | }
322 |
323 | public String toString() {
324 | return m_root.toString(0);
325 | }
326 |
327 | private NearestNeighborList> getnbrs(double [] key)
328 | throws KeySizeException, CloneNotSupportedException {
329 | return getnbrs(key, m_count, null);
330 | }
331 |
332 |
333 | private NearestNeighborList> getnbrs(double [] key, int n,
334 | Checker checker) throws KeySizeException, CloneNotSupportedException {
335 |
336 | if (key.length != m_K) {
337 | throw new KeySizeException();
338 | }
339 |
340 | NearestNeighborList> nnl = new NearestNeighborList>(n);
341 |
342 | // initial call is with infinite hyper-rectangle and max distance
343 | HRect hr = HRect.infiniteHRect(key.length);
344 | double max_dist_sqd = Double.MAX_VALUE;
345 | HPoint keyp = new HPoint(key);
346 |
347 | if (m_count > 0) {
348 | long timeout = (this.m_timeout > 0) ?
349 | (System.currentTimeMillis() + this.m_timeout) :
350 | 0;
351 | KDNode.nnbr(m_root, keyp, hr, max_dist_sqd, 0, m_K, nnl, checker, timeout);
352 | }
353 |
354 | return nnl;
355 |
356 | }
357 |
358 | private List nearestDistance(double [] key, double dist,
359 | DistanceMetric metric) throws KeySizeException, CloneNotSupportedException {
360 |
361 | NearestNeighborList> nnl = getnbrs(key);
362 | int n = nnl.getSize();
363 | Stack nbrs = new Stack();
364 |
365 | for (int i=0; i kd = nnl.removeHighest();
367 | @SuppressWarnings("unused")
368 | HPoint p = kd.k;
369 | if (metric.distance(kd.k.coord, key) < dist) {
370 | nbrs.push(kd.v);
371 | }
372 | }
373 |
374 | return nbrs;
375 | }
376 |
377 |
378 | }
379 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/KDTree.java.new:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 | import java.util.Vector;
4 |
5 | /**
6 | * KDTree is a class supporting KD-tree insertion, deletion, equality
7 | * search, range search, and nearest neighbor(s) using double-precision
8 | * floating-point keys. Splitting dimension is chosen naively, by
9 | * depth modulo K. Semantics are as follows:
10 | *
11 | *
12 | *
Two different keys containing identical numbers should retrieve the
13 | * same value from a given KD-tree. Therefore keys are cloned when a
14 | * node is inserted.
15 | *
16 | *
As with Hashtables, values inserted into a KD-tree are not
17 | * cloned. Modifying a value between insertion and retrieval will
18 | * therefore modify the value stored in the tree.
19 | *
20 | *
21 | * @author Simon Levy
22 | * @version %I%, %G%
23 | * @since JDK1.2
24 | */
25 | public class KDTree {
26 |
27 | // K = number of dimensions
28 | private int m_K;
29 |
30 | // root of KD-tree
31 | private KDNode m_root;
32 |
33 | // count of nodes
34 | private int m_count;
35 |
36 | /**
37 | * Creates a KD-tree with specified number of dimensions.
38 | *
39 | * @param k number of dimensions
40 | */
41 | public KDTree(int k) {
42 |
43 | m_K = k;
44 | m_root = null;
45 | }
46 |
47 |
48 | /**
49 | * Insert a node in a KD-tree. Uses algorithm translated from 352.ins.c of
50 | *
51 | *
52 | * @Book{GonnetBaezaYates1991,
53 | * author = {G.H. Gonnet and R. Baeza-Yates},
54 | * title = {Handbook of Algorithms and Data Structures},
55 | * publisher = {Addison-Wesley},
56 | * year = {1991}
57 | * }
58 | *
59 | *
60 | * @param key key for KD-tree node
61 | * @param value value at that key
62 | *
63 | * @throws KeySizeException if key.length mismatches K
64 | * @throws KeyDuplicateException if key already in tree
65 | */
66 | public void insert(double [] key, Object value)
67 | throws KeySizeException, KeyDuplicateException {
68 |
69 | if (key.length != m_K) {
70 | throw new KeySizeException();
71 | }
72 |
73 | else try {
74 | m_root = KDNode.ins(new HPoint(key), value, m_root, 0, m_K);
75 | }
76 |
77 | catch (KeyDuplicateException e) {
78 | throw e;
79 | }
80 |
81 | m_count++;
82 | }
83 |
84 | /**
85 | * Find KD-tree node whose key is identical to key. Uses algorithm
86 | * translated from 352.srch.c of Gonnet & Baeza-Yates.
87 | *
88 | * @param key key for KD-tree node
89 | *
90 | * @return object at key, or null if not found
91 | *
92 | * @throws KeySizeException if key.length mismatches K
93 | */
94 | public Object search(double [] key) throws KeySizeException {
95 |
96 | if (key.length != m_K) {
97 | throw new KeySizeException();
98 | }
99 |
100 | KDNode kd = KDNode.srch(new HPoint(key), m_root, m_K);
101 |
102 | return (kd == null ? null : kd.v);
103 | }
104 |
105 |
106 | /**
107 | * Delete a node from a KD-tree. Instead of actually deleting node and
108 | * rebuilding tree, marks node as deleted. Hence, it is up to the caller
109 | * to rebuild the tree as needed for efficiency.
110 | *
111 | * @param key key for KD-tree node
112 | *
113 | * @throws KeySizeException if key.length mismatches K
114 | * @throws KeyMissingException if no node in tree has key
115 | */
116 | public void delete(double [] key)
117 | throws KeySizeException, KeyMissingException {
118 |
119 | if (key.length != m_K) {
120 | throw new KeySizeException();
121 | }
122 |
123 | else {
124 |
125 | KDNode t = KDNode.srch(new HPoint(key), m_root, m_K);
126 | if (t == null) {
127 | throw new KeyMissingException();
128 | }
129 | else {
130 | t.deleted = true;
131 | }
132 |
133 | m_count--;
134 | }
135 | }
136 |
137 | /**
138 | * Find KD-tree node whose key is nearest neighbor to
139 | * key. Implements the Nearest Neighbor algorithm (Table 6.4) of
140 | *
141 | *
142 | * @techreport{AndrewMooreNearestNeighbor,
143 | * author = {Andrew Moore},
144 | * title = {An introductory tutorial on kd-trees},
145 | * institution = {Robotics Institute, Carnegie Mellon University},
146 | * year = {1991},
147 | * number = {Technical Report No. 209, Computer Laboratory,
148 | * University of Cambridge},
149 | * address = {Pittsburgh, PA}
150 | * }
151 | *
152 | *
153 | * @param key key for KD-tree node
154 | *
155 | * @return object at node nearest to key, or null on failure
156 | *
157 | * @throws KeySizeException if key.length mismatches K
158 |
159 | */
160 | public Object nearest(double [] key) throws KeySizeException {
161 |
162 | KDNode nearest = nnbr(key);
163 | return nearest.v;
164 | }
165 |
166 | /**
167 | * Find KD-tree nodes whose keys are n nearest neighbors to
168 | * key. Uses algorithm above. Neighbors are returned in ascending
169 | * order of distance to key. Loops n times, finding nearest
170 | * neighbors in succession. If you know of a more efficient
171 | * algorithm, please email me a
172 | * reference so I can implement it here.
173 | *
174 | * @param key key for KD-tree node
175 | * @param n how many neighbors to find
176 | *
177 | * @return objects at node nearest to key, or null on failure
178 | *
179 | * @throws KeySizeException if key.length mismatches K
180 | * @throws IllegalArgumentException if n is negative or
181 | * exceeds tree size
182 | */
183 | public Object [] nearest(double [] key, int n)
184 | throws KeySizeException, IllegalArgumentException {
185 |
186 | if (n < 0 || n > m_count) {
187 | throw new IllegalArgumentException("Number of neighbors cannot" +
188 | " be negative or greater than number of nodes");
189 | }
190 |
191 | Object [] nbrs = new Object [n];
192 | KDNode [] removed = new KDNode [n];
193 |
194 | // run single nearest-neighbor N times, marking neighbors deleted
195 | for (int i=0; iKDTree.insert method
5 | * is invoked on a key already in the KDTree.
6 | *
7 | * @author Simon Levy
8 | * @version %I%, %G%
9 | * @since JDK1.2
10 | */
11 |
12 | public class KeyDuplicateException extends KDException {
13 |
14 | // arbitrary; every serializable class has to have one of these
15 | public static final long serialVersionUID = 1L;
16 |
17 | KeyDuplicateException() {
18 | super("Key already in tree");
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/KeyMissingException.java:
--------------------------------------------------------------------------------
1 | // Key-size mismatch exception supporting KDTree class
2 |
3 | package edu.wlu.cs.levy.CG;
4 |
5 | class KeyMissingException extends KDException { /* made public by MSL */
6 |
7 | // arbitrary; every serializable class has to have one of these
8 | public static final long serialVersionUID = 3L;
9 |
10 | public KeyMissingException() {
11 | super("Key not found");
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/KeySizeException.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 | /**
4 | * KeySizeException is thrown when a KDTree method is invoked on a
5 | * key whose size (array length) mismatches the one used in the that
6 | * KDTree's constructor.
7 | *
8 | * @author Simon Levy
9 | * @version %I%, %G%
10 | * @since JDK1.2
11 | */
12 |
13 | public class KeySizeException extends KDException {
14 |
15 | // arbitrary; every serializable class has to have one of these
16 | public static final long serialVersionUID = 2L;
17 |
18 | KeySizeException() {
19 | super("Key size mismatch");
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/NearestNeighborList.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 |
4 | // Bjoern Heckel's solution to the KD-Tree n-nearest-neighbor problem
5 |
6 | class NearestNeighborList {
7 |
8 | private final java.util.PriorityQueue> m_Queue;
9 | private final int m_Capacity;
10 | // constructor
11 | public NearestNeighborList(int capacity) {
12 | m_Capacity = capacity;
13 | m_Queue = new java.util.PriorityQueue<>(m_Capacity);
14 | }
15 |
16 | @SuppressWarnings("rawtypes")
17 | public double getMaxPriority() {
18 | NeighborEntry p = m_Queue.peek();
19 | return (p == null) ? Double.POSITIVE_INFINITY : p.value;
20 | }
21 |
22 | public void insert(T object, double priority) {
23 | if (isCapacityReached()) {
24 | if (priority > getMaxPriority()) {
25 | // do not insert - all elements in queue have lower priority
26 | return;
27 | }
28 | m_Queue.add(new NeighborEntry<>(object, priority));
29 | // remove object with highest priority
30 | m_Queue.poll();
31 | } else {
32 | m_Queue.add(new NeighborEntry<>(object, priority));
33 | }
34 | }
35 |
36 | public boolean isCapacityReached() {
37 | return m_Queue.size() >= m_Capacity;
38 | }
39 |
40 | public T getHighest() {
41 | NeighborEntry p = m_Queue.peek();
42 | return (p == null) ? null : p.data;
43 | }
44 |
45 | public boolean isEmpty() {
46 | return m_Queue.size() == 0;
47 | }
48 |
49 | public int getSize() {
50 | return m_Queue.size();
51 | }
52 |
53 | public T removeHighest() {
54 | // remove object with highest priority
55 | NeighborEntry p = m_Queue.poll();
56 | return (p == null) ? null : p.data;
57 | }
58 |
59 | static class NeighborEntry implements Comparable> {
60 | final T data;
61 | final double value;
62 |
63 | NeighborEntry(final T data,
64 | final double value) {
65 | this.data = data;
66 | this.value = value;
67 | }
68 |
69 | public int compareTo(NeighborEntry t) {
70 | // note that the positions are reversed!
71 | return Double.compare(t.value, this.value);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/edu/wlu/cs/levy/CG/changes:
--------------------------------------------------------------------------------
1 | + removed superfluous sqrt
2 | + added size()
3 | + support for Checker object
4 | + made exception public
5 | + fixed range-search to exclude deleted values
6 | + added tests
7 | + made delete optional
8 | + changed to use generic lists
9 | + removed unnecessary "SDL" step
10 | + made thread-safe
11 | + supported editing of found objects
12 | + adding timeout
13 |
--------------------------------------------------------------------------------
/src/main/resources/log4j.properties:
--------------------------------------------------------------------------------
1 | ### direct log messages to stdout ###
2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender
3 | log4j.appender.stdout.Target=System.out
4 | log4j.appender.stdout.layout=org.apache.log4j.SimpleLayout
5 | log4j.rootLogger=debug, stdout
6 |
--------------------------------------------------------------------------------
/src/test/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/.DS_Store
--------------------------------------------------------------------------------
/src/test/java/com/allenday/image/ImageProcessorTest.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import org.junit.Test;
4 |
5 | public class ImageProcessorTest {
6 | ImageProcessor processor = new ImageProcessor();
7 |
8 | @Test
9 | public void test() {
10 | // processor.addFile(new File("src/test/resources/image"));
11 | // processor.processImages();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/test/java/com/allenday/image/RankerTest.java:
--------------------------------------------------------------------------------
1 | package com.allenday.image;
2 |
3 | import org.junit.Test;
4 |
5 | public class RankerTest {
6 | ImageProcessor processor = new ImageProcessor();
7 | Ranker ranker;
8 |
9 | @Test
10 | public void test() {
11 | // processor.addFile(new File("src/test/resources/image"));
12 | // processor.processImages();
13 | // Map res = processor.getResults();
14 | // Ranker ranker = new Ranker(res.values(), 16);
15 | // List match = ranker.rank(res.values().iterator().next(), false);
16 | //
17 | // for (SearchResult m : match) {
18 | // System.err.println(m.score + "\t" + m.id);
19 | // }
20 | //
21 | // //fail("Not yet implemented");
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/test/java/edu/wlu/cs/levy/CG/KDTests.java:
--------------------------------------------------------------------------------
1 | package edu.wlu.cs.levy.CG;
2 |
3 |
4 | /*
5 | written by MSL for SpeedDate
6 | */
7 |
8 | import org.junit.Assert;
9 | import org.junit.Test;
10 |
11 | import java.util.List;
12 |
13 | public class KDTests {
14 | private static final java.util.Random rand = new java.util.Random();
15 |
16 | private static double[] makeSample(int dims) {
17 | double[] rv = new double[dims];
18 | for (int j = 0; j < dims; ++j) {
19 | rv[j] = rand.nextDouble();
20 | }
21 | return rv;
22 | }
23 |
24 | private static double distSquared(double[] p0, double[] p1) {
25 | double rv = 0;
26 | for (int i = 0; i < p0.length; i++) {
27 | double diff = p0[i] - p1[i];
28 | rv += (diff * diff);
29 | }
30 | return rv;
31 | }
32 |
33 |
34 | @Test
35 | public void testNearestNeighborList() {
36 | NearestNeighborList nnl = new NearestNeighborList<>(3);
37 | nnl.insert("A", 3.0);
38 | nnl.insert("B", 2.0);
39 | nnl.insert("D", 0.0);
40 | nnl.insert("C", 1.0);
41 |
42 | Assert.assertEquals(2.0, nnl.getMaxPriority(), 0.1);
43 | Assert.assertEquals("B", nnl.getHighest());
44 | Assert.assertEquals("B", nnl.removeHighest());
45 | Assert.assertEquals("C", nnl.removeHighest());
46 | Assert.assertEquals("D", nnl.removeHighest());
47 | }
48 |
49 | @Test
50 | public void testNearestNeighbor() throws KDException, CloneNotSupportedException {
51 | int dims = 3;
52 | int samples = 300;
53 | KDTree kt = new KDTree<>(dims);
54 | double[] targ = makeSample(dims);
55 |
56 | int min_index = 0;
57 | double min_value = Double.MAX_VALUE;
58 | for (int i = 0; i < samples; ++i) {
59 | double[] keys = makeSample(dims);
60 | kt.insert(keys, i);
61 |
62 | /*
63 | for the purposes of test, we want the nearest EVEN-NUMBERED point
64 | */
65 | if ((i % 2) == 0) {
66 | double dist = distSquared(targ, keys);
67 | if (dist < min_value) {
68 | min_value = dist;
69 | min_index = i;
70 | }
71 | }
72 | }
73 |
74 |
75 | List nbrs = kt.nearest(targ, 1, v -> (v % 2) == 0);
76 |
77 | Assert.assertEquals(1, nbrs.size());
78 | if (nbrs.size() == 1) {
79 | Assert.assertEquals(min_index, nbrs.get(0).intValue());
80 | }
81 | }
82 |
83 | @Test
84 | public void testRange() throws KDException {
85 | int dims = 2;
86 | KDTree