├── .idea ├── image-similarity.iml └── uiDesigner.xml ├── README.md ├── pom.xml └── src ├── .DS_Store ├── main ├── java │ ├── at │ │ └── dhyan │ │ │ └── open_imaging │ │ │ └── GifDecoder.java │ ├── com │ │ └── allenday │ │ │ ├── .DS_Store │ │ │ ├── image │ │ │ ├── Distance.java │ │ │ ├── ImageFeatures.java │ │ │ ├── ImageIndex.java │ │ │ ├── ImageIndexFactory.java │ │ │ ├── ImageProcessor.java │ │ │ ├── Ranker.java │ │ │ ├── SearchResult.java │ │ │ ├── backend │ │ │ │ ├── CannyEdgeDetector.java │ │ │ │ └── Processor.java │ │ │ └── distance │ │ │ │ ├── AbstractDistance.java │ │ │ │ ├── AbstractDistance_UDUV.java │ │ │ │ ├── AbstractDistance_UDWV.java │ │ │ │ ├── AbstractDistance_WDUV.java │ │ │ │ ├── AbstractDistance_WDWV.java │ │ │ │ ├── UDUV_L1Norm.java │ │ │ │ ├── UDUV_L2Norm.java │ │ │ │ ├── UDWV_L1Norm.java │ │ │ │ ├── UDWV_L2Norm.java │ │ │ │ └── WDUV_PearsonDistance.java │ │ │ ├── runnable │ │ │ ├── FrameshiftBulkLoad.java │ │ │ ├── FrameshiftInsert.java │ │ │ ├── FrameshiftSearch.java │ │ │ ├── ImageVectors.java │ │ │ ├── IndexDirectory.java │ │ │ ├── LoadSimhash.java │ │ │ └── SceneChangeDirectory.java │ │ │ └── util │ │ │ ├── BitBuffer.java │ │ │ └── Pack.java │ ├── edu │ │ └── wlu │ │ │ └── cs │ │ │ └── levy │ │ │ └── CG │ │ │ ├── Checker.java │ │ │ ├── DistanceMetric.java │ │ │ ├── Editor.java │ │ │ ├── EuclideanDistance.java │ │ │ ├── HPoint.java │ │ │ ├── HRect.java │ │ │ ├── HammingDistance.java │ │ │ ├── KDException.java │ │ │ ├── KDNode.java │ │ │ ├── KDTree.java │ │ │ ├── KDTree.java.new │ │ │ ├── KeyDuplicateException.java │ │ │ ├── KeyMissingException.java │ │ │ ├── KeySizeException.java │ │ │ ├── NearestNeighborList.java │ │ │ └── changes │ └── org │ │ └── imgscalr │ │ └── Scalr.java └── resources │ └── log4j.properties └── test ├── .DS_Store ├── java ├── com │ └── allenday │ │ └── image │ │ ├── ImageProcessorTest.java │ │ └── RankerTest.java └── edu │ └── wlu │ └── cs │ └── levy │ └── CG │ └── KDTests.java └── resources └── image ├── .DS_Store ├── Driving_in_China_step9.jpg ├── artists_03.jpeg ├── bagatelle-1.jpg ├── bagatelle-2.jpg ├── bagatelle-3.jpg ├── bagatelle-4.jpg ├── dilke.tumblr.jpeg ├── fakesnakes.tumblr.jpeg ├── pictures-of-nasa-s-atlantis-shuttle-launch-photos-video.jpeg ├── susan.jpg ├── tumblr_m0v15f2RDt1qcecwvo1_1280.png ├── tumblr_m3lr9sFtAk1qcyp9ro1_500.png ├── tumblr_m3ve5g1MUS1qih56oo1_500.jpeg └── tumblr_m46vryx0rA1qlzn67o1_500.jpeg /.idea/image-similarity.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | image-similarity 2 | ================ 3 | 4 | Canny edges + color histograms + KD-tree indexing 5 | 6 | ``` 7 | mvn clean compile assembly:single 8 | java -jar target/image-similarity-1.0-SNAPSHOT-jar-with-dependencies.jar src/test/resources/image/artists_03.jpeg 9 | 10 | #TODO accept time offset and file_id as args 11 | #java -cp target/image-similarity-1.0-SNAPSHOT-jar-with-dependencies.jar com.allenday.runnable.FrameshiftInsert 12 | 13 | java -cp target/image-similarity-1.0-SNAPSHOT-jar-with-dependencies.jar com.allenday.runnable.FrameshiftSearch 14 | ``` 15 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.allenday 7 | image-similarity 8 | 1.0-SNAPSHOT 9 | jar 10 | 11 | image-similarity 12 | http://maven.apache.org 13 | 14 | 15 | UTF-8 16 | 1.8 17 | 1.8 18 | 19 | 20 | 21 | 22 | Apache-Releases 23 | https://repository.apache.org/content/repositories/releases/ 24 | 25 | 26 | Alfresco-Public 27 | https://artifacts.alfresco.com/nexus/content/repositories/public/ 28 | 29 | 30 | maven-central 31 | http://central.maven.org/maven2/ 32 | 33 | 34 | jboss-3rd-party-releases 35 | https://repository.jboss.org/nexus/content/repositories/thirdparty-releases/ 36 | 37 | 38 | geotoolkit 39 | Geotoolkit.org project 40 | http://maven.geotoolkit.org 41 | 42 | 43 | bintray-jai-imageio 44 | jai-imageio at bintray 45 | https://dl.bintray.com/jai-imageio/maven/ 46 | 47 | false 48 | 49 | 50 | 51 | 52 | com.springsource.repository.bundles.external 53 | SpringSource Enterprise Bundle Repository - External Bundle Releases 54 | http://repository.springsource.com/maven/bundles/external 55 | 56 | 57 | 58 | 59 | 60 | junit 61 | junit 62 | 4.8.1 63 | test 64 | 65 | 66 | org.slf4j 67 | slf4j-api 68 | 1.7.5 69 | 70 | 71 | org.slf4j 72 | slf4j-log4j12 73 | 1.7.5 74 | 75 | 76 | javax.media 77 | jai_core 78 | 1.1.3 79 | 80 | 81 | com.sun.media 82 | jai-codec 83 | 1.1.3 84 | 85 | 86 | org.apache.commons 87 | commons-lang3 88 | 3.0 89 | 90 | 91 | com.github.jai-imageio 92 | jai-imageio-core 93 | 1.3.1 94 | 95 | 96 | javax.media.jai 97 | com.springsource.javax.media.jai.core 98 | 1.1.3 99 | 100 | 101 | org.apache.solr 102 | solr-solrj 103 | 6.4.2 104 | 105 | 106 | commons-logging 107 | commons-logging 108 | 1.1.1 109 | 110 | 111 | commons-codec 112 | commons-codec 113 | 1.10 114 | 115 | 116 | commons-codec 117 | commons-codec 118 | 1.10 119 | 120 | 121 | commons-codec 122 | commons-codec 123 | 1.9 124 | 125 | 126 | 127 | 128 | 129 | src/main/resources 130 | 131 | 132 | 133 | 134 | maven-assembly-plugin 135 | 136 | 137 | 138 | com.allenday.runnable.ImageVectors 139 | 140 | 141 | 142 | jar-with-dependencies 143 | 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/.DS_Store -------------------------------------------------------------------------------- /src/main/java/at/dhyan/open_imaging/GifDecoder.java: -------------------------------------------------------------------------------- 1 | package at.dhyan.open_imaging; 2 | 3 | import static java.lang.System.arraycopy; 4 | 5 | import java.awt.Color; 6 | import java.awt.Graphics2D; 7 | import java.awt.image.BufferedImage; 8 | import java.awt.image.DataBufferInt; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | /* 15 | * Copyright 2014 Dhyan Blum 16 | * 17 | * Licensed under the Apache License, Version 2.0 (the "License"); 18 | * you may not use this file except in compliance with the License. 19 | * You may obtain a copy of the License at 20 | * 21 | * http://www.apache.org/licenses/LICENSE-2.0 22 | * 23 | * Unless required by applicable law or agreed to in writing, software 24 | * distributed under the License is distributed on an "AS IS" BASIS, 25 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 26 | * See the License for the specific language governing permissions and 27 | * limitations under the License. 28 | */ 29 | /** 30 | *

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 kt = new KDTree<>(dims); 87 | double[] p0 = {0.5, 0.5}; 88 | double[] p1 = {0.65, 0.5}; 89 | double[] p2 = {0.75, 0.5}; 90 | 91 | kt.insert(p0, new Object()); 92 | kt.insert(p1, new Object()); 93 | kt.insert(p2, new Object()); 94 | 95 | double[] lower = {0.25, 0.3}; 96 | double[] upper = {0.7, 0.6}; 97 | 98 | List rv = kt.range(lower, upper); 99 | Assert.assertEquals(2, rv.size()); 100 | 101 | kt.delete(p1); 102 | rv = kt.range(lower, upper); 103 | Assert.assertEquals(1, rv.size()); 104 | } 105 | 106 | @Test 107 | public void testSearch() throws KDException { 108 | int dims = 3; 109 | int samples = 300; 110 | KDTree kt = new KDTree<>(dims); 111 | double[] targ = makeSample(dims); 112 | Object treasure = new Object(); 113 | kt.insert(targ, treasure); 114 | 115 | for (int i = 0; i < samples; ++i) { 116 | double[] keys = makeSample(dims); 117 | kt.insert(keys, i); 118 | } 119 | 120 | Object found = kt.search(targ); 121 | Assert.assertSame(treasure, found); 122 | 123 | kt.delete(targ); 124 | found = kt.search(targ); 125 | Assert.assertNull(found); 126 | 127 | } 128 | 129 | @Test 130 | public void testDelete() throws KDException { 131 | int dims = 3; 132 | KDTree kt = new KDTree<>(dims); 133 | double[] targ = makeSample(dims); 134 | kt.insert(targ, new Object()); 135 | kt.delete(targ); 136 | try { 137 | kt.delete(targ); 138 | Assert.fail(); 139 | } catch (edu.wlu.cs.levy.CG.KeyMissingException e) { 140 | // supposed to be here 141 | } 142 | kt.delete(targ, true); 143 | Assert.assertEquals(0, kt.size()); 144 | } 145 | 146 | @Test 147 | public void testEditing() throws KDException { 148 | int dims = 3; 149 | KDTree kt = new KDTree<>(dims); 150 | double[] targ = makeSample(dims); 151 | 152 | Object p1 = "p1"; 153 | Object p2 = "p2"; 154 | kt.insert(targ, p1); 155 | try { 156 | kt.insert(targ, p2); 157 | Assert.fail(); 158 | } catch (edu.wlu.cs.levy.CG.KeyDuplicateException e) { 159 | // supposed to be here 160 | } 161 | 162 | kt.edit(targ, new Editor.OptionalInserter<>(p2)); 163 | Object found = kt.search(targ); 164 | Assert.assertSame(p1, found); 165 | kt.edit(targ, new Editor.Replacer<>(p2)); 166 | found = kt.search(targ); 167 | Assert.assertSame(p2, found); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/test/resources/image/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/.DS_Store -------------------------------------------------------------------------------- /src/test/resources/image/Driving_in_China_step9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/Driving_in_China_step9.jpg -------------------------------------------------------------------------------- /src/test/resources/image/artists_03.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/artists_03.jpeg -------------------------------------------------------------------------------- /src/test/resources/image/bagatelle-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/bagatelle-1.jpg -------------------------------------------------------------------------------- /src/test/resources/image/bagatelle-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/bagatelle-2.jpg -------------------------------------------------------------------------------- /src/test/resources/image/bagatelle-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/bagatelle-3.jpg -------------------------------------------------------------------------------- /src/test/resources/image/bagatelle-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/bagatelle-4.jpg -------------------------------------------------------------------------------- /src/test/resources/image/dilke.tumblr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/dilke.tumblr.jpeg -------------------------------------------------------------------------------- /src/test/resources/image/fakesnakes.tumblr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/fakesnakes.tumblr.jpeg -------------------------------------------------------------------------------- /src/test/resources/image/pictures-of-nasa-s-atlantis-shuttle-launch-photos-video.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/pictures-of-nasa-s-atlantis-shuttle-launch-photos-video.jpeg -------------------------------------------------------------------------------- /src/test/resources/image/susan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/susan.jpg -------------------------------------------------------------------------------- /src/test/resources/image/tumblr_m0v15f2RDt1qcecwvo1_1280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/tumblr_m0v15f2RDt1qcecwvo1_1280.png -------------------------------------------------------------------------------- /src/test/resources/image/tumblr_m3lr9sFtAk1qcyp9ro1_500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/tumblr_m3lr9sFtAk1qcyp9ro1_500.png -------------------------------------------------------------------------------- /src/test/resources/image/tumblr_m3ve5g1MUS1qih56oo1_500.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/tumblr_m3ve5g1MUS1qih56oo1_500.jpeg -------------------------------------------------------------------------------- /src/test/resources/image/tumblr_m46vryx0rA1qlzn67o1_500.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/allenday/image-similarity/142ff18b8a06cf8c33eaad50ab6b40275f24ba6f/src/test/resources/image/tumblr_m46vryx0rA1qlzn67o1_500.jpeg --------------------------------------------------------------------------------