├── README.md ├── lib └── opencv-247.jar ├── native ├── cv2.so └── libopencv_java247.dylib ├── pom.xml ├── src └── main │ └── java │ └── com │ └── swansonb │ └── imagematching │ ├── ImageMatcherApplication.java │ ├── controller │ └── ImageController.java │ ├── datastore │ ├── AlbumStore.java │ └── FileAlbumStore.java │ ├── filter │ ├── ImageFileFilter.java │ ├── JsonFileFilter.java │ └── SecurityHeaderFilter.java │ ├── model │ ├── Album.java │ ├── Image.java │ └── Status.java │ └── utils │ ├── ImageHelper.java │ ├── JsonUtils.java │ └── NativeUtils.java └── start.sh /README.md: -------------------------------------------------------------------------------- 1 | ## Image Matcher 2 | ImageMatcher is a Spring Boot Java application that provides a RESTful interface to a connected camera and image matching algorithms. 3 | 4 | The Images are stored in memory (HashMap) and on the file system (under data/images). 5 | The album metadata is also stored in memory (separate HashMap) and on the file system (under data/albums) as JSON. 6 | 7 | ### Running image matcher code 8 | * CD into the `image_matcher` folder in the rpm project 9 | * Run `mvn package` to compile the project 10 | * Once compiled run `sh start.sh` to initialise 11 | 12 | ### RESTful Endpoints: 13 | 14 | #### /snap - *GET* 15 | 16 | Snap tells the camera to take an image and save it to the file system. 17 | 18 | **Returns:** 19 | 20 | { 21 | "id" : "1", 22 | "image" : "Base64 encoded PNG" 23 | } 24 | 25 | #### /update - *POST* 26 | 27 | Creates an album give an image id, artist and album name 28 | 29 | **Expects:** 30 | 31 | { 32 | "id" : "1", 33 | "artist" : "Ben", 34 | "albumName" : "Imma do some music" 35 | } 36 | 37 | **Returns:** 38 | 39 | Success: 40 | 41 | { 42 | "status" : "success", 43 | "message" : "album successfully created" 44 | } 45 | 46 | Error: 47 | 48 | { 49 | "status" : "error", 50 | "message" : "unable to create album" 51 | } 52 | 53 | #### /identify - *GET* 54 | 55 | Takes an image (but does not save it to the filesystem) and then loops through the albums and returns the album with the 56 | image that most closest matches the image taken. 57 | 58 | Please note: the only time this will not return an album is if there are no albums in the system. If you scan an album 59 | which is not in the system it will return the album with the image that it most closely resembles. 60 | 61 | **Returns:** 62 | 63 | Success: 64 | 65 | { 66 | "id" : "1", 67 | "artist" : "Ben", 68 | "albumName" : "Imma do some music" 69 | } 70 | 71 | Error: 72 | 73 | { 74 | "status" : "error", 75 | "message" : "No best match found" 76 | } 77 | 78 | #### /camera - *POST* 79 | 80 | Changes the attached camera by providing it's id. *This is used during initial setup to choose the correct camera on systems 81 | with multiple camera*. This will need to be done each time the application is started as this information is not persisted. 82 | 83 | **Expects:** 84 | 85 | { 86 | "id" : 0 87 | } 88 | 89 | **Returns:** 90 | 91 | { 92 | "status" : "success", 93 | "message" : "Camera changed" 94 | } -------------------------------------------------------------------------------- /lib/opencv-247.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjsswanson/ImageMatcher/c31f30eff608bd64b0ec29760598e5050ff6e5fc/lib/opencv-247.jar -------------------------------------------------------------------------------- /native/cv2.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjsswanson/ImageMatcher/c31f30eff608bd64b0ec29760598e5050ff6e5fc/native/cv2.so -------------------------------------------------------------------------------- /native/libopencv_java247.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bjsswanson/ImageMatcher/c31f30eff608bd64b0ec29760598e5050ff6e5fc/native/libopencv_java247.dylib -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.swansonb 8 | ImageMatcher 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 0.5.0.M6 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | com.fasterxml.jackson.core 25 | jackson-databind 26 | 27 | 28 | com.google.code.gson 29 | gson 30 | 2.2.2 31 | 32 | 33 | opencv 34 | opencv 35 | 247 36 | system 37 | ${project.basedir}/lib/opencv-247.jar 38 | 39 | 40 | commons-io 41 | commons-io 42 | 2.4 43 | 44 | 45 | 46 | 47 | com.swansonb.imagematching.ImageMatcherApplication 48 | 49 | 50 | 51 | 52 | 53 | ${project.basedir}/lib 54 | ${project.build.outputDirectory}/lib 55 | 56 | 57 | ${project.basedir}/native 58 | ${project.build.outputDirectory}/native 59 | 60 | 61 | 62 | 63 | 64 | maven-compiler-plugin 65 | 2.3.2 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-maven-plugin 70 | 71 | 72 | 73 | 74 | 75 | 76 | repo 77 | 78 | true 79 | ignore 80 | 81 | 82 | false 83 | 84 | file://${project.basedir}/lib 85 | 86 | 87 | spring-snapshots 88 | http://repo.spring.io/libs-snapshot 89 | true 90 | 91 | 92 | 93 | 94 | spring-snapshots 95 | http://repo.spring.io/libs-snapshot 96 | true 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/ImageMatcherApplication.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching; 2 | 3 | import com.swansonb.imagematching.utils.NativeUtils; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.context.annotation.ComponentScan; 7 | 8 | import java.io.IOException; 9 | 10 | @ComponentScan 11 | @EnableAutoConfiguration 12 | public class ImageMatcherApplication { 13 | 14 | public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException { 15 | loadLibrary(); 16 | SpringApplication.run(ImageMatcherApplication.class, args); 17 | } 18 | 19 | private static void loadLibrary() throws NoSuchFieldException, IllegalAccessException, IOException { 20 | NativeUtils.loadLibraryFromJar("/native/libopencv_java247.dylib"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/controller/ImageController.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.controller; 2 | 3 | import com.swansonb.imagematching.datastore.AlbumStore; 4 | import com.swansonb.imagematching.model.Album; 5 | import com.swansonb.imagematching.model.Image; 6 | import com.swansonb.imagematching.model.Status; 7 | import com.swansonb.imagematching.utils.ImageHelper; 8 | import com.swansonb.imagematching.utils.JsonUtils; 9 | import org.opencv.core.Mat; 10 | import org.opencv.highgui.VideoCapture; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Controller; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | import org.springframework.web.bind.annotation.RequestMethod; 15 | import org.springframework.web.bind.annotation.RequestParam; 16 | import org.springframework.web.bind.annotation.ResponseBody; 17 | 18 | import java.io.IOException; 19 | import java.util.Collection; 20 | 21 | @Controller 22 | public class ImageController { 23 | 24 | public static final String EMPTY_JSON = "{}"; 25 | private VideoCapture camera; 26 | 27 | @Autowired 28 | private AlbumStore albumStore; 29 | 30 | public ImageController() throws NoSuchFieldException, IllegalAccessException { 31 | initCamera(0); 32 | } 33 | 34 | private void initCamera(int i) { 35 | camera = new VideoCapture(i); 36 | camera.open(i); //Useless 37 | 38 | if (!camera.isOpened()) { 39 | System.out.println("Camera Error"); 40 | } else { 41 | System.out.println("Camera OK?"); 42 | } 43 | } 44 | 45 | @RequestMapping(value = "/camera", method = RequestMethod.POST, 46 | produces = "application/json; charset=utf-8") 47 | public @ResponseBody String camera( 48 | @RequestParam("id") String id) throws IOException { 49 | try { 50 | Integer cameraId = Integer.valueOf(id); 51 | initCamera(cameraId); 52 | } catch(NumberFormatException e) { 53 | return constructStatus("error", "invalid camera id"); 54 | } 55 | 56 | return constructStatus("success", "Camera changed"); 57 | } 58 | 59 | 60 | @RequestMapping(value = "/snap", method = RequestMethod.GET, 61 | produces = "application/json; charset=utf-8") 62 | public @ResponseBody String snap() throws IOException { 63 | Mat imageMat = new Mat(); 64 | if (camera.isOpened()) { 65 | camera.read(imageMat); 66 | } 67 | 68 | Image temp = albumStore.storeImage(imageMat); 69 | return JsonUtils.toJson(temp); 70 | } 71 | 72 | @RequestMapping(value = "/identify", method = RequestMethod.GET, 73 | produces = "application/json; charset=utf-8") 74 | public @ResponseBody String identify() throws IOException { 75 | Mat image = new Mat(); 76 | if (camera.isOpened()) { 77 | camera.read(image); 78 | } 79 | 80 | Album bestMatch = findBestMatch(image); 81 | 82 | if (bestMatch != null) { 83 | return JsonUtils.toJson(bestMatch); 84 | } else { 85 | return constructStatus("error", "No best match"); 86 | } 87 | } 88 | 89 | private Album findBestMatch(Mat image) { 90 | Album bestMatch = null; 91 | double bestMatchRating = 0; 92 | 93 | Collection albums = albumStore.getAlbums(); 94 | for (Album album : albums) { 95 | double matchRating = ImageHelper.matchImages(image, album.getImage()); 96 | if (bestMatch == null || matchRating > bestMatchRating) { 97 | bestMatch = album; 98 | bestMatchRating = matchRating; 99 | } 100 | } 101 | return bestMatch; 102 | } 103 | 104 | @RequestMapping(value = "/update", method = RequestMethod.POST, 105 | produces = "application/json; charset=utf-8") 106 | public @ResponseBody String update( 107 | @RequestParam("id") String id, 108 | @RequestParam("uri") String uri) throws IOException { 109 | 110 | boolean valid = isValid(id, uri); 111 | 112 | if (valid) { 113 | Album album = albumStore.storeAlbum(id, uri); 114 | if (album != null) { 115 | return constructStatus("success", "album created or updated"); 116 | } else { 117 | return constructStatus("error", "error creating or updating album"); 118 | } 119 | } else { 120 | return constructStatus("error", "POST value missing"); 121 | } 122 | } 123 | 124 | private boolean isValid(String id, String uri) { 125 | return id != null && id.length() > 0 && uri != null && uri.length() > 0; 126 | } 127 | 128 | private String constructStatus(String status, String message) { 129 | return JsonUtils.toJson(new Status(status, message)); 130 | } 131 | 132 | private String imageTag(Mat image) { 133 | return imageTag(ImageHelper.createThumbnail(image)); 134 | } 135 | 136 | private String imageTag(String thumb) { 137 | return ""; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/datastore/AlbumStore.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.datastore; 2 | 3 | import com.swansonb.imagematching.model.Album; 4 | import com.swansonb.imagematching.model.Image; 5 | import org.opencv.core.Mat; 6 | 7 | import java.util.Collection; 8 | 9 | public interface AlbumStore { 10 | Image storeImage(Mat mat); 11 | Image getImage(String id); 12 | Album storeAlbum(String id, String uri); 13 | Album getAlbum(String id); 14 | 15 | Collection getAlbums(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/datastore/FileAlbumStore.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.datastore; 2 | 3 | import com.swansonb.imagematching.filter.ImageFileFilter; 4 | import com.swansonb.imagematching.filter.JsonFileFilter; 5 | import com.swansonb.imagematching.model.Album; 6 | import com.swansonb.imagematching.model.Image; 7 | import com.swansonb.imagematching.utils.JsonUtils; 8 | import org.apache.commons.io.FileUtils; 9 | import org.apache.commons.io.FilenameUtils; 10 | import org.opencv.core.Mat; 11 | import org.opencv.highgui.Highgui; 12 | import org.springframework.stereotype.Repository; 13 | 14 | import java.io.File; 15 | import java.io.FileFilter; 16 | import java.io.IOException; 17 | import java.util.Collection; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.concurrent.atomic.AtomicLong; 21 | 22 | @Repository 23 | public class FileAlbumStore implements AlbumStore { 24 | 25 | public static final String ENCODING = "UTF-8"; 26 | private static final String IMAGE_EXT = ".png"; 27 | private static final String ALBUM_EXT = ".json"; 28 | 29 | private File dataStore; 30 | private File imageStore; 31 | private File albumStore; 32 | 33 | private AtomicLong idGen; 34 | private Map images; 35 | private Map albums; 36 | 37 | public FileAlbumStore(){ 38 | dataStore = getFolder("data"); 39 | imageStore = getFolder(dataStore, "images"); 40 | albumStore = getFolder(dataStore, "albums"); 41 | 42 | images = loadImages(); 43 | albums = loadAlbums(); 44 | 45 | idGen = new AtomicLong(images.size()); 46 | } 47 | 48 | @Override 49 | public Collection getAlbums(){ 50 | return albums.values(); 51 | } 52 | 53 | private Map loadImages(){ 54 | Map images = new HashMap(); 55 | 56 | FileFilter filter = new ImageFileFilter(); 57 | File[] files = imageStore.listFiles(filter); 58 | 59 | for(File file : files){ 60 | String id = FilenameUtils.getBaseName(file.getName()); 61 | Mat mat = Highgui.imread(file.getAbsolutePath()); 62 | images.put(id, new Image(id, mat)); 63 | } 64 | 65 | return images; 66 | } 67 | 68 | private Map loadAlbums(){ 69 | Map albums = new HashMap(); 70 | 71 | FileFilter filter = new JsonFileFilter(); 72 | File[] files = albumStore.listFiles(filter); 73 | 74 | for(File file : files){ 75 | try { 76 | String json = FileUtils.readFileToString(file); 77 | Album album = JsonUtils.fromJson(json, Album.class); 78 | Image image = images.get(album.getId()); 79 | if(image != null){ 80 | album.setImage(image.getImage()); 81 | album.setThumb(image.getThumbnail()); 82 | albums.put(album.getId(), album); 83 | } 84 | } catch (IOException e) { 85 | System.err.println("Could not load album: " + file.getPath()); 86 | } 87 | } 88 | 89 | return albums; 90 | } 91 | 92 | @Override 93 | public Image storeImage(Mat mat) { 94 | Image image = new Image(generateId(), mat); 95 | boolean stored = Highgui.imwrite(getPath(imageStore, image.getId() + IMAGE_EXT), image.getImage()); 96 | if(stored){ 97 | images.put(image.getId(), image); 98 | return image; 99 | } else { 100 | System.err.println("Unable to write image to file."); 101 | return null; 102 | } 103 | } 104 | 105 | @Override 106 | public Album storeAlbum(String id, String uri) { 107 | Image image = images.get(id); 108 | 109 | try { 110 | if(image != null){ 111 | Album album = new Album(image, uri); 112 | File file = addFile(albumStore, id + ALBUM_EXT); 113 | if(file.exists()){ 114 | FileUtils.forceDelete(file); 115 | } 116 | FileUtils.writeStringToFile(file, JsonUtils.toJson(album), ENCODING); 117 | albums.put(id, album); 118 | return album; 119 | } 120 | } catch (IOException e) { 121 | System.err.println("Unable to write album to file."); 122 | } 123 | 124 | return null; 125 | } 126 | 127 | @Override 128 | public Image getImage(String id) { 129 | return images.get(id); 130 | } 131 | 132 | @Override 133 | public Album getAlbum(String id) { 134 | return albums.get(id); 135 | } 136 | 137 | private File getFolder(String path){ 138 | File albumStore = new File(path); 139 | if(!albumStore.exists()){ 140 | try { 141 | FileUtils.forceMkdir(albumStore); 142 | } catch (IOException e) { 143 | System.err.println("Unable to make directory: " + path); 144 | } 145 | } 146 | return albumStore; 147 | } 148 | 149 | private File getFolder(File directory, String path){ 150 | return getFolder(getPath(directory, path)); 151 | } 152 | 153 | 154 | private File addFile(File directory, String fileName){ 155 | return new File(getPath(directory, fileName)); 156 | } 157 | 158 | private String getPath(File directory, String fileName){ 159 | return directory.getAbsolutePath() + File.separator + fileName; 160 | } 161 | 162 | private String generateId() { 163 | long longId = idGen.incrementAndGet(); 164 | return String.valueOf(longId); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/filter/ImageFileFilter.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.filter; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * A class that implements the Java FileFilter interface. 7 | */ 8 | public class ImageFileFilter implements FileFilter { 9 | 10 | private final String[] okFileExtensions = new String[] {"jpg", "png", "gif"}; 11 | 12 | public boolean accept(File file){ 13 | for (String extension : okFileExtensions){ 14 | if (file.getName().toLowerCase().endsWith(extension)){ 15 | return true; 16 | } 17 | } 18 | return false; 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/filter/JsonFileFilter.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.filter; 2 | 3 | import java.io.File; 4 | import java.io.FileFilter; 5 | 6 | public class JsonFileFilter implements FileFilter { 7 | private final String[] okFileExtensions = new String[] {"json"}; 8 | 9 | public boolean accept(File file) { 10 | for (String extension : okFileExtensions){ 11 | if (file.getName().toLowerCase().endsWith(extension)){ 12 | return true; 13 | } 14 | } 15 | return false; 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/filter/SecurityHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.filter; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.filter.OncePerRequestFilter; 5 | 6 | import javax.servlet.Filter; 7 | import javax.servlet.FilterChain; 8 | import javax.servlet.ServletException; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import java.io.IOException; 12 | 13 | @Component("securityHeaderFilter") 14 | public class SecurityHeaderFilter extends OncePerRequestFilter implements Filter { 15 | 16 | @Override 17 | protected void doFilterInternal(HttpServletRequest request, 18 | HttpServletResponse response, FilterChain chain) 19 | throws ServletException, IOException { 20 | 21 | response.addHeader("Access-Control-Allow-Origin", "*"); 22 | 23 | chain.doFilter(request, response); 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/model/Album.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.model; 2 | 3 | import org.opencv.core.Mat; 4 | 5 | public class Album { 6 | private String id; 7 | private String uri; 8 | private transient Mat image; 9 | private transient String thumb; 10 | 11 | public Album(Image tempImage, String uri) { 12 | this.id = tempImage.getId(); 13 | this.thumb = tempImage.getThumbnail(); 14 | this.image = tempImage.getImage(); 15 | this.uri = uri; 16 | } 17 | 18 | public String getId() { 19 | return id; 20 | } 21 | 22 | public String getUri() { 23 | return uri; 24 | } 25 | 26 | public Mat getImage() { 27 | return image; 28 | } 29 | 30 | public String getThumb() { 31 | return thumb; 32 | } 33 | 34 | public void setImage(Mat image) { 35 | this.image = image; 36 | } 37 | 38 | public void setThumb(String thumb) { 39 | this.thumb = thumb; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/model/Image.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.model; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | import com.swansonb.imagematching.utils.ImageHelper; 5 | import org.opencv.core.Mat; 6 | 7 | public class Image { 8 | private String id; 9 | private transient Mat image; 10 | 11 | @SerializedName("image") 12 | private String thumbnail; 13 | 14 | public Image(String id, Mat image) { 15 | this.id = id; 16 | this.image = image; 17 | this.thumbnail = ImageHelper.createThumbnail(image); 18 | } 19 | 20 | public String getId() { 21 | return id; 22 | } 23 | 24 | public Mat getImage() { 25 | return image; 26 | } 27 | 28 | public String getThumbnail() { 29 | return thumbnail; 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/model/Status.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.model; 2 | 3 | public class Status { 4 | private String status; 5 | private String message; 6 | 7 | public Status(String status, String message) { 8 | this.status = status; 9 | this.message = message; 10 | } 11 | 12 | public String getStatus() { 13 | return status; 14 | } 15 | 16 | public String getMessage() { 17 | return message; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/utils/ImageHelper.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.utils; 2 | 3 | import org.apache.catalina.util.Base64; 4 | import org.opencv.core.*; 5 | import org.opencv.features2d.*; 6 | import org.opencv.highgui.Highgui; 7 | import org.opencv.imgproc.Imgproc; 8 | 9 | import javax.imageio.ImageIO; 10 | import java.awt.image.BufferedImage; 11 | import java.io.ByteArrayInputStream; 12 | import java.io.ByteArrayOutputStream; 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.concurrent.atomic.AtomicInteger; 17 | 18 | public class ImageHelper { 19 | 20 | public static final int MAX_POINT_COMPARE_DIST = 200; 21 | public static final double DIST_THRESHOLD = 0.25; 22 | private static AtomicInteger counter = new AtomicInteger(); 23 | 24 | public static double matchImages(Mat img1, Mat img2) { 25 | MatOfKeyPoint keypoints1 = new MatOfKeyPoint(); 26 | MatOfKeyPoint keypoints2 = new MatOfKeyPoint(); 27 | Mat descriptors1 = new Mat(); 28 | Mat descriptors2 = new Mat(); 29 | 30 | //Definition of ORB keypoint detector and descriptor extractors 31 | FeatureDetector detector = FeatureDetector.create(FeatureDetector.DYNAMIC_FAST); 32 | DescriptorExtractor extractor = DescriptorExtractor.create(DescriptorExtractor.SIFT); 33 | 34 | //Detect keypoints 35 | detector.detect(img1, keypoints1); 36 | detector.detect(img2, keypoints2); 37 | //Extract descriptors 38 | extractor.compute(img1, keypoints1, descriptors1); 39 | extractor.compute(img2, keypoints2, descriptors2); 40 | 41 | //Definition of descriptor matcher 42 | DescriptorMatcher matcher = DescriptorMatcher.create(DescriptorMatcher.BRUTEFORCE); 43 | 44 | //Match points of two images 45 | MatOfDMatch matches = new MatOfDMatch(); 46 | matcher.match(descriptors1, descriptors2, matches); 47 | 48 | float max = 0; 49 | float min = Float.MAX_VALUE; 50 | 51 | 52 | List dMatches = matches.toList(); 53 | for (DMatch match : dMatches) { 54 | if (match.distance > max) { 55 | max = match.distance; 56 | } 57 | if (match.distance < min) { 58 | min = match.distance; 59 | } 60 | } 61 | 62 | //look whether the match is inside a defined area of the image 63 | //only 25% of maximum of possible distance 64 | double distThreshold = DIST_THRESHOLD * Math.sqrt((Math.pow(img1.size().height,2) + Math.pow(img1.size().width,2)) * 2); 65 | 66 | List goodMatches = new ArrayList(); 67 | for (DMatch match : dMatches) { 68 | Point from = keypoints1.toArray()[match.queryIdx].pt; 69 | Point to = keypoints2.toArray()[match.trainIdx].pt; 70 | if (match.distance < distThreshold && Math.abs(from.y - to.y) < MAX_POINT_COMPARE_DIST) { 71 | goodMatches.add(match); 72 | } 73 | } 74 | 75 | //createMatchDiagrams(img1, img2, keypoints1, keypoints2, goodMatches); 76 | 77 | 78 | return goodMatches.size(); 79 | } 80 | 81 | private static void createMatchDiagrams(Mat img1, Mat img2, MatOfKeyPoint keypoints1, MatOfKeyPoint keypoints2, List goodMatches) { 82 | //Draw matches 83 | MatOfDMatch goodMat = new MatOfDMatch(); 84 | goodMat.fromList(goodMatches); 85 | Mat out = new Mat(); 86 | Features2d.drawMatches(img1, keypoints1, img2, keypoints2, goodMat, out); 87 | Highgui.imwrite("matches/test" + counter.incrementAndGet() + ".png", out); 88 | System.out.println(goodMatches.size()); 89 | } 90 | 91 | public static String createThumbnail(Mat image) { 92 | return createThumbnail(image, 2); 93 | } 94 | 95 | public static String createThumbnail(Mat image, int divideBy) { 96 | Mat thumb = new Mat(); 97 | Imgproc.pyrDown(image, thumb, new Size(image.cols() / divideBy, image.rows() / divideBy)); 98 | return matBase64(thumb); 99 | } 100 | 101 | private static String matBase64(Mat mat) { 102 | BufferedImage bi = matToBufferedImage(mat); 103 | if (bi != null) { 104 | return buffImageToBase64(bi); 105 | } else { 106 | return ""; 107 | } 108 | } 109 | 110 | private static String buffImageToBase64(BufferedImage bi) { 111 | try { 112 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 113 | ImageIO.write(bi, "PNG", out); 114 | byte[] bytes = out.toByteArray(); 115 | String base64bytes = Base64.encode(bytes); 116 | return "data:image/png;base64," + base64bytes; 117 | } catch (IOException e) { 118 | e.printStackTrace(); 119 | } 120 | return ""; 121 | } 122 | 123 | private static BufferedImage matToBufferedImage(Mat matrix) { 124 | MatOfByte mb = new MatOfByte(); 125 | Highgui.imencode(".jpg", matrix, mb); 126 | try { 127 | return ImageIO.read(new ByteArrayInputStream(mb.toArray())); 128 | } catch (IOException e) { 129 | e.printStackTrace(); 130 | } 131 | return null; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/utils/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.utils; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.GsonBuilder; 5 | 6 | import java.lang.reflect.Modifier; 7 | 8 | public class JsonUtils { 9 | 10 | private static final Gson gson = new GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create(); 11 | 12 | public static String toJson(Object obj){ 13 | return gson.toJson(obj); 14 | } 15 | 16 | public static T fromJson(String json, Class clazz){ 17 | return gson.fromJson(json, clazz); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/swansonb/imagematching/utils/NativeUtils.java: -------------------------------------------------------------------------------- 1 | package com.swansonb.imagematching.utils; 2 | 3 | import java.io.*; 4 | 5 | /** 6 | * Simple library class for working with JNI (Java Native Interface) 7 | * 8 | * @see http://frommyplayground.com/how-to-load-native-jni-library-from-jar 9 | * 10 | * @author Adam Heirnich , http://www.adamh.cz 11 | */ 12 | public class NativeUtils { 13 | 14 | /** 15 | * Private constructor - this class will never be instanced 16 | */ 17 | private NativeUtils() { 18 | } 19 | 20 | /** 21 | * Loads library from current JAR archive 22 | * 23 | * The file from JAR is copied into system temporary directory and then loaded. The temporary file is deleted after exiting. 24 | * Method uses String as filename because the pathname is "abstract", not system-dependent. 25 | * 26 | * @param path The filename inside JAR as absolute path (beginning with '/'), e.g. /package/File.ext 27 | * @throws IOException If temporary file creation or read/write operation fails 28 | * @throws IllegalArgumentException If source file (param path) does not exist 29 | * @throws IllegalArgumentException If the path is not absolute or if the filename is shorter than three characters (restriction of {@see File#createTempFile(java.lang.String, java.lang.String)}). 30 | */ 31 | public static void loadLibraryFromJar(String path) throws IOException { 32 | 33 | if (!path.startsWith("/")) { 34 | throw new IllegalArgumentException("The path to be absolute (start with '/')."); 35 | } 36 | 37 | // Obtain filename from path 38 | String[] parts = path.split("/"); 39 | String filename = (parts.length > 1) ? parts[parts.length - 1] : null; 40 | 41 | // Split filename to prexif and suffix (extension) 42 | String prefix = ""; 43 | String suffix = null; 44 | if (filename != null) { 45 | parts = filename.split("\\.", 2); 46 | prefix = parts[0]; 47 | suffix = (parts.length > 1) ? "."+parts[parts.length - 1] : null; // Thanks, davs! :-) 48 | } 49 | 50 | // Check if the filename is okay 51 | if (filename == null || prefix.length() < 3) { 52 | throw new IllegalArgumentException("The filename has to be at least 3 characters long."); 53 | } 54 | 55 | // Prepare temporary file 56 | File temp = File.createTempFile(prefix, suffix); 57 | temp.deleteOnExit(); 58 | 59 | if (!temp.exists()) { 60 | throw new FileNotFoundException("File " + temp.getAbsolutePath() + " does not exist."); 61 | } 62 | 63 | // Prepare buffer for data copying 64 | byte[] buffer = new byte[1024]; 65 | int readBytes; 66 | 67 | // Open and check input stream 68 | InputStream is = NativeUtils.class.getResourceAsStream(path); 69 | if (is == null) { 70 | throw new FileNotFoundException("File " + path + " was not found inside JAR."); 71 | } 72 | 73 | // Open output stream and copy data between source file in JAR and the temporary file 74 | OutputStream os = new FileOutputStream(temp); 75 | try { 76 | while ((readBytes = is.read(buffer)) != -1) { 77 | os.write(buffer, 0, readBytes); 78 | } 79 | } finally { 80 | // If read/write fails, close streams safely before throwing an exception 81 | os.close(); 82 | is.close(); 83 | } 84 | 85 | // Finally, load the library 86 | System.load(temp.getAbsolutePath()); 87 | 88 | final String libraryPrefix = prefix; 89 | final String lockSuffix = ".lock"; 90 | 91 | // create lock file 92 | final File lock = new File( temp.getAbsolutePath() + lockSuffix); 93 | lock.createNewFile(); 94 | lock.deleteOnExit(); 95 | 96 | // file filter for library file (without .lock files) 97 | FileFilter tmpDirFilter = 98 | new FileFilter(){ 99 | public boolean accept(File pathname){ 100 | return pathname.getName().startsWith(libraryPrefix) && !pathname.getName().endsWith( lockSuffix); 101 | } 102 | }; 103 | 104 | // get all library files from temp folder 105 | String tmpDirName = System.getProperty("java.io.tmpdir"); 106 | File tmpDir = new File(tmpDirName); 107 | File[] tmpFiles = tmpDir.listFiles(tmpDirFilter); 108 | 109 | // delete all files which don't have n accompanying lock file 110 | for (int i = 0; i < tmpFiles.length; i++){ 111 | // Create a file to represent the lock and test. 112 | File lockFile = new File( tmpFiles[i].getAbsolutePath() + lockSuffix); 113 | if (!lockFile.exists()){ 114 | System.out.println( "deleting: " + tmpFiles[i].getAbsolutePath()); 115 | tmpFiles[i].delete(); 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | java -jar target/ImageMatcher-1.0-SNAPSHOT.jar --------------------------------------------------------------------------------