├── 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
--------------------------------------------------------------------------------