Skip to content

Commit e533405

Browse files
author
Mykola Mokhnach
authored
Add image comparison helpers (appium#870)
* Add image comparison helpers * Fix style * Add docstrings * Add some debug fixes
1 parent 5b8f1c7 commit e533405

17 files changed

+957
-5
lines changed

src/main/java/io/appium/java_client/AppiumDriver.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
*/
6565
@SuppressWarnings("unchecked")
6666
public class AppiumDriver<T extends WebElement>
67-
extends DefaultGenericMobileDriver<T> {
67+
extends DefaultGenericMobileDriver<T> implements ComparesImages {
6868

6969
private static final ErrorHandler errorHandler = new ErrorHandler(new ErrorCodesMobile(), true);
7070
// frequently used command parameters
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* See the NOTICE file distributed with this work for additional
5+
* information regarding copyright ownership.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.appium.java_client;
18+
19+
import static io.appium.java_client.MobileCommand.compareImagesCommand;
20+
21+
import io.appium.java_client.imagecomparison.ComparisonMode;
22+
import io.appium.java_client.imagecomparison.FeaturesMatchingOptions;
23+
import io.appium.java_client.imagecomparison.FeaturesMatchingResult;
24+
import io.appium.java_client.imagecomparison.OccurrenceMatchingOptions;
25+
import io.appium.java_client.imagecomparison.OccurrenceMatchingResult;
26+
import io.appium.java_client.imagecomparison.SimilarityMatchingOptions;
27+
import io.appium.java_client.imagecomparison.SimilarityMatchingResult;
28+
import org.apache.commons.codec.binary.Base64;
29+
import org.apache.commons.io.FileUtils;
30+
31+
import java.io.File;
32+
import java.io.IOException;
33+
import java.util.Map;
34+
import javax.annotation.Nullable;
35+
36+
public interface ComparesImages extends ExecutesMethod {
37+
38+
/**
39+
* Performs images matching by features with default options. Read
40+
* https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html
41+
* for more details on this topic.
42+
*
43+
* @param base64image1 base64-encoded representation of the first image
44+
* @param base64Image2 base64-encoded representation of the second image
45+
* @return The matching result.
46+
*/
47+
default FeaturesMatchingResult matchImagesFeatures(byte[] base64image1, byte[] base64Image2) {
48+
return matchImagesFeatures(base64image1, base64Image2, null);
49+
}
50+
51+
/**
52+
* Performs images matching by features. Read
53+
* https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html
54+
* for more details on this topic.
55+
*
56+
* @param base64image1 base64-encoded representation of the first image
57+
* @param base64Image2 base64-encoded representation of the second image
58+
* @param options comparison options
59+
* @return The matching result. The configuration of fields in the result depends on comparison options.
60+
*/
61+
default FeaturesMatchingResult matchImagesFeatures(byte[] base64image1, byte[] base64Image2,
62+
@Nullable FeaturesMatchingOptions options) {
63+
Object response = CommandExecutionHelper.execute(this,
64+
compareImagesCommand(ComparisonMode.MATCH_FEATURES, base64image1, base64Image2, options));
65+
//noinspection unchecked
66+
return new FeaturesMatchingResult((Map<String, Object>) response);
67+
}
68+
69+
/**
70+
* Performs images matching by features with default options. Read
71+
* https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html
72+
* for more details on this topic.
73+
*
74+
* @param image1 The location of the first image
75+
* @param image2 The location of the second image
76+
* @return The matching result.
77+
*/
78+
default FeaturesMatchingResult matchImagesFeatures(File image1, File image2) throws IOException {
79+
return matchImagesFeatures(image1, image2, null);
80+
}
81+
82+
/**
83+
* Performs images matching by features. Read
84+
* https://docs.opencv.org/3.0-beta/doc/py_tutorials/py_feature2d/py_matcher/py_matcher.html
85+
* for more details on this topic.
86+
*
87+
* @param image1 The location of the first image
88+
* @param image2 The location of the second image
89+
* @param options comparison options
90+
* @return The matching result. The configuration of fields in the result depends on comparison options.
91+
*/
92+
default FeaturesMatchingResult matchImagesFeatures(File image1, File image2,
93+
@Nullable FeaturesMatchingOptions options) throws IOException {
94+
return matchImagesFeatures(Base64.encodeBase64(FileUtils.readFileToByteArray(image1)),
95+
Base64.encodeBase64(FileUtils.readFileToByteArray(image2)), options);
96+
}
97+
98+
/**
99+
* Performs images matching by template to find possible occurrence of the partial image
100+
* in the full image with default options. Read
101+
* https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html
102+
* for more details on this topic.
103+
*
104+
* @param fullImage base64-encoded representation of the full image
105+
* @param partialImage base64-encoded representation of the partial image
106+
* @return The matching result.
107+
*/
108+
default OccurrenceMatchingResult findImageOccurrence(byte[] fullImage, byte[] partialImage) {
109+
return findImageOccurrence(fullImage, partialImage, null);
110+
}
111+
112+
/**
113+
* Performs images matching by template to find possible occurrence of the partial image
114+
* in the full image. Read
115+
* https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html
116+
* for more details on this topic.
117+
*
118+
* @param fullImage base64-encoded representation of the full image
119+
* @param partialImage base64-encoded representation of the partial image
120+
* @param options comparison options
121+
* @return The matching result. The configuration of fields in the result depends on comparison options.
122+
*/
123+
default OccurrenceMatchingResult findImageOccurrence(byte[] fullImage, byte[] partialImage,
124+
@Nullable OccurrenceMatchingOptions options) {
125+
Object response = CommandExecutionHelper.execute(this,
126+
compareImagesCommand(ComparisonMode.MATCH_TEMPLATE, fullImage, partialImage, options));
127+
//noinspection unchecked
128+
return new OccurrenceMatchingResult((Map<String, Object>) response);
129+
}
130+
131+
/**
132+
* Performs images matching by template to find possible occurrence of the partial image
133+
* in the full image with default options. Read
134+
* https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html
135+
* for more details on this topic.
136+
*
137+
* @param fullImage The location of the full image
138+
* @param partialImage The location of the partial image
139+
* @return The matching result. The configuration of fields in the result depends on comparison options.
140+
*/
141+
default OccurrenceMatchingResult findImageOccurrence(File fullImage, File partialImage) throws IOException {
142+
return findImageOccurrence(fullImage, partialImage, null);
143+
}
144+
145+
/**
146+
* Performs images matching by template to find possible occurrence of the partial image
147+
* in the full image. Read
148+
* https://docs.opencv.org/2.4/doc/tutorials/imgproc/histograms/template_matching/template_matching.html
149+
* for more details on this topic.
150+
*
151+
* @param fullImage The location of the full image
152+
* @param partialImage The location of the partial image
153+
* @param options comparison options
154+
* @return The matching result. The configuration of fields in the result depends on comparison options.
155+
*/
156+
default OccurrenceMatchingResult findImageOccurrence(File fullImage, File partialImage,
157+
@Nullable OccurrenceMatchingOptions options)
158+
throws IOException {
159+
return findImageOccurrence(Base64.encodeBase64(FileUtils.readFileToByteArray(fullImage)),
160+
Base64.encodeBase64(FileUtils.readFileToByteArray(partialImage)), options);
161+
}
162+
163+
/**
164+
* Performs images matching to calculate the similarity score between them
165+
* with default options. The flow there is similar to the one used in
166+
* {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)},
167+
* but it is mandatory that both images are of equal size.
168+
*
169+
* @param base64image1 base64-encoded representation of the first image
170+
* @param base64Image2 base64-encoded representation of the second image
171+
* @return Matching result. The configuration of fields in the result depends on comparison options.
172+
*/
173+
default SimilarityMatchingResult getImagesSimilarity(byte[] base64image1, byte[] base64Image2) {
174+
return getImagesSimilarity(base64image1, base64Image2, null);
175+
}
176+
177+
/**
178+
* Performs images matching to calculate the similarity score between them.
179+
* The flow there is similar to the one used in
180+
* {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)},
181+
* but it is mandatory that both images are of equal size.
182+
*
183+
* @param base64image1 base64-encoded representation of the first image
184+
* @param base64Image2 base64-encoded representation of the second image
185+
* @param options comparison options
186+
* @return Matching result. The configuration of fields in the result depends on comparison options.
187+
*/
188+
default SimilarityMatchingResult getImagesSimilarity(byte[] base64image1, byte[] base64Image2,
189+
@Nullable SimilarityMatchingOptions options) {
190+
Object response = CommandExecutionHelper.execute(this,
191+
compareImagesCommand(ComparisonMode.GET_SIMILARITY, base64image1, base64Image2, options));
192+
//noinspection unchecked
193+
return new SimilarityMatchingResult((Map<String, Object>) response);
194+
}
195+
196+
/**
197+
* Performs images matching to calculate the similarity score between them
198+
* with default options. The flow there is similar to the one used in
199+
* {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)},
200+
* but it is mandatory that both images are of equal size.
201+
*
202+
* @param image1 The location of the full image
203+
* @param image2 The location of the partial image
204+
* @return Matching result. The configuration of fields in the result depends on comparison options.
205+
*/
206+
default SimilarityMatchingResult getImagesSimilarity(File image1, File image2) throws IOException {
207+
return getImagesSimilarity(image1, image2, null);
208+
}
209+
210+
/**
211+
* Performs images matching to calculate the similarity score between them.
212+
* The flow there is similar to the one used in
213+
* {@link #findImageOccurrence(byte[], byte[], OccurrenceMatchingOptions)},
214+
* but it is mandatory that both images are of equal size.
215+
*
216+
* @param image1 The location of the full image
217+
* @param image2 The location of the partial image
218+
* @param options comparison options
219+
* @return Matching result. The configuration of fields in the result depends on comparison options.
220+
*/
221+
default SimilarityMatchingResult getImagesSimilarity(File image1, File image2,
222+
@Nullable SimilarityMatchingOptions options)
223+
throws IOException {
224+
return getImagesSimilarity(Base64.encodeBase64(FileUtils.readFileToByteArray(image1)),
225+
Base64.encodeBase64(FileUtils.readFileToByteArray(image2)), options);
226+
}
227+
}

src/main/java/io/appium/java_client/MobileCommand.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import com.google.common.collect.ImmutableMap;
2020

21+
import io.appium.java_client.imagecomparison.BaseComparisonOptions;
22+
import io.appium.java_client.imagecomparison.ComparisonMode;
2123
import io.appium.java_client.screenrecording.BaseStartScreenRecordingOptions;
2224
import io.appium.java_client.screenrecording.BaseStopScreenRecordingOptions;
2325
import org.apache.commons.lang3.StringUtils;
@@ -29,6 +31,7 @@
2931
import java.util.AbstractMap;
3032
import java.util.HashMap;
3133
import java.util.Map;
34+
import javax.annotation.Nullable;
3235

3336
/**
3437
* The repository of mobile commands defined in the Mobile JSON
@@ -106,6 +109,7 @@ public class MobileCommand {
106109
protected static final String TOGGLE_WIFI;
107110
protected static final String TOGGLE_AIRPLANE_MODE;
108111
protected static final String TOGGLE_DATA;
112+
protected static final String COMPARE_IMAGES;
109113

110114
public static final Map<String, CommandInfo> commandRepository;
111115

@@ -179,6 +183,7 @@ public class MobileCommand {
179183
TOGGLE_WIFI = "toggleWiFi";
180184
TOGGLE_AIRPLANE_MODE = "toggleFlightMode";
181185
TOGGLE_DATA = "toggleData";
186+
COMPARE_IMAGES = "compareImages";
182187

183188
commandRepository = new HashMap<>();
184189
commandRepository.put(RESET, postC("/session/:sessionId/appium/app/reset"));
@@ -262,6 +267,7 @@ public class MobileCommand {
262267
commandRepository.put(TOGGLE_WIFI, postC("/session/:sessionId/appium/device/toggle_wifi"));
263268
commandRepository.put(TOGGLE_AIRPLANE_MODE, postC("/session/:sessionId/appium/device/toggle_airplane_mode"));
264269
commandRepository.put(TOGGLE_DATA, postC("/session/:sessionId/appium/device/toggle_data"));
270+
commandRepository.put(COMPARE_IMAGES, postC("/session/:sessionId/appium/compare_images"));
265271
}
266272

267273
/**
@@ -435,7 +441,7 @@ public static ImmutableMap<String, Object> prepareArguments(String[] params,
435441
* This method forms a {@link java.util.Map} of parameters for the
436442
* device unlocking.
437443
*
438-
* @return a key-value pair. The key is the command name. The value is a
444+
* @return a key-value pair. The key is the command name. The value is a
439445
* {@link java.util.Map} command arguments.
440446
*/
441447
public static Map.Entry<String, Map<String, ?>> unlockDeviceCommand() {
@@ -446,7 +452,7 @@ public static ImmutableMap<String, Object> prepareArguments(String[] params,
446452
* This method forms a {@link java.util.Map} of parameters for the
447453
* device locked query.
448454
*
449-
* @return a key-value pair. The key is the command name. The value is a
455+
* @return a key-value pair. The key is the command name. The value is a
450456
* {@link java.util.Map} command arguments.
451457
*/
452458
public static Map.Entry<String, Map<String, ?>> getIsDeviceLockedCommand() {
@@ -472,7 +478,7 @@ public static ImmutableMap<String, Object> prepareArguments(String[] params,
472478
* {@link java.util.Map} command arguments.
473479
*/
474480
public static Map.Entry<String, Map<String, ?>> pushFileCommand(String remotePath, byte[] base64Data) {
475-
String[] parameters = new String[] {"path", "data"};
481+
String[] parameters = new String[]{"path", "data"};
476482
Object[] values = new Object[]{remotePath, new String(base64Data, StandardCharsets.UTF_8)};
477483
return new AbstractMap.SimpleEntry<>(PUSH_FILE, prepareArguments(parameters, values));
478484
}
@@ -486,4 +492,27 @@ public static ImmutableMap<String, Object> prepareArguments(String[] params,
486492
return new AbstractMap.SimpleEntry<>(STOP_RECORDING_SCREEN,
487493
prepareArguments("options", opts.build()));
488494
}
495+
496+
/**
497+
* Forms a {@link java.util.Map} of parameters for images comparison.
498+
*
499+
* @param mode one of possible comparison modes
500+
* @param img1Data base64-encoded data of the first image
501+
* @param img2Data base64-encoded data of the second image
502+
* @param options comparison options
503+
* @return key-value pairs
504+
*/
505+
public static Map.Entry<String, Map<String, ?>> compareImagesCommand(ComparisonMode mode,
506+
byte[] img1Data, byte[] img2Data,
507+
@Nullable BaseComparisonOptions options) {
508+
String[] parameters = options == null
509+
? new String[]{"mode", "firstImage", "secondImage"}
510+
: new String[]{"mode", "firstImage", "secondImage", "options"};
511+
Object[] values = options == null
512+
? new Object[]{mode.toString(), new String(img1Data, StandardCharsets.UTF_8),
513+
new String(img2Data, StandardCharsets.UTF_8)}
514+
: new Object[]{mode.toString(), new String(img1Data, StandardCharsets.UTF_8),
515+
new String(img2Data, StandardCharsets.UTF_8), options.build()};
516+
return new AbstractMap.SimpleEntry<>(COMPARE_IMAGES, prepareArguments(parameters, values));
517+
}
489518
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* See the NOTICE file distributed with this work for additional
5+
* information regarding copyright ownership.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.appium.java_client.imagecomparison;
18+
19+
import static java.util.Optional.ofNullable;
20+
21+
import com.google.common.collect.ImmutableMap;
22+
23+
import java.util.Map;
24+
25+
public abstract class BaseComparisonOptions<T extends BaseComparisonOptions<T>> {
26+
private Boolean visualize;
27+
28+
/**
29+
* Makes the endpoint to return an image,
30+
* which contains the visualized result of the corresponding
31+
* picture matching operation. This option is disabled by default.
32+
*
33+
* @return self instance for chaining
34+
*/
35+
public T withEnabledVisualization() {
36+
visualize = true;
37+
//noinspection unchecked
38+
return (T) this;
39+
}
40+
41+
/**
42+
* Builds a map, which is ready to be passed to the subordinated
43+
* Appium API.
44+
*
45+
* @return comparison options mapping.
46+
*/
47+
@SuppressWarnings("unchecked")
48+
public Map<String, Object> build() {
49+
final ImmutableMap.Builder builder = new ImmutableMap.Builder<String, Object>();
50+
ofNullable(visualize).map(x -> builder.put("visualize", x));
51+
return builder.build();
52+
}
53+
}

0 commit comments

Comments
 (0)