Skip to content

Commit 8af9d2b

Browse files
author
Mykola Mokhnach
authored
Get rid of OpenCV dependency (appium#991)
* Get rid of OpenCV dependency * Fix constructor * Fix linter
1 parent 1478647 commit 8af9d2b

File tree

3 files changed

+36
-292
lines changed

3 files changed

+36
-292
lines changed

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ dependencies {
7575
compile 'commons-io:commons-io:2.6'
7676
compile 'org.springframework:spring-context:5.0.5.RELEASE'
7777
compile 'org.aspectj:aspectjweaver:1.9.1'
78-
compile 'org.openpnp:opencv:3.2.0-1'
7978

8079
testCompile 'junit:junit:4.12'
8180
testCompile 'org.hamcrest:hamcrest-library:1.3'

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

Lines changed: 36 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -16,29 +16,26 @@
1616

1717
package io.appium.java_client;
1818

19-
import static nu.pattern.OpenCV.loadShared;
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
import static java.util.Optional.ofNullable;
2021

21-
import org.opencv.core.Core;
22-
import org.opencv.core.CvType;
23-
import org.opencv.core.Mat;
24-
import org.opencv.core.Size;
25-
import org.opencv.imgproc.Imgproc;
26-
27-
import java.awt.AlphaComposite;
28-
import java.awt.Graphics2D;
2922
import java.awt.image.BufferedImage;
30-
import java.awt.image.DataBufferByte;
23+
import java.io.ByteArrayOutputStream;
24+
import java.io.IOException;
3125
import java.time.Duration;
3226
import java.time.LocalDateTime;
33-
import java.util.Optional;
27+
import java.util.Base64;
3428
import java.util.function.Function;
3529
import java.util.function.Supplier;
3630

31+
import javax.imageio.ImageIO;
32+
3733
public class ScreenshotState {
3834
private static final Duration DEFAULT_INTERVAL_MS = Duration.ofMillis(500);
3935

40-
private Optional<BufferedImage> previousScreenshot = Optional.empty();
41-
private Supplier<BufferedImage> stateProvider;
36+
private BufferedImage previousScreenshot;
37+
private final Supplier<BufferedImage> stateProvider;
38+
private final ComparesImages comparator;
4239

4340
private Duration comparisonInterval = DEFAULT_INTERVAL_MS;
4441

@@ -72,10 +69,15 @@ public class ScreenshotState {
7269
*
7370
* @param stateProvider lambda function, which returns a screenshot for further comparison
7471
*/
75-
public ScreenshotState(Supplier<BufferedImage> stateProvider) {
72+
public ScreenshotState(ComparesImages comparator, Supplier<BufferedImage> stateProvider) {
73+
this.comparator = checkNotNull(comparator);
7674
this.stateProvider = stateProvider;
7775
}
7876

77+
public ScreenshotState(ComparesImages comparator) {
78+
this(comparator, null);
79+
}
80+
7981
/**
8082
* Gets the interval value in ms between similarity verification rounds in <em>verify*</em> methods.
8183
*
@@ -103,7 +105,7 @@ public ScreenshotState setComparisonInterval(Duration comparisonInterval) {
103105
* @return self instance for chaining
104106
*/
105107
public ScreenshotState remember() {
106-
this.previousScreenshot = Optional.of(stateProvider.get());
108+
this.previousScreenshot = stateProvider.get();
107109
return this;
108110
}
109111

@@ -116,7 +118,7 @@ public ScreenshotState remember() {
116118
* @return self instance for chaining
117119
*/
118120
public ScreenshotState remember(BufferedImage customInitialState) {
119-
this.previousScreenshot = Optional.of(customInitialState);
121+
this.previousScreenshot = checkNotNull(customInitialState);
120122
return this;
121123
}
122124

@@ -134,7 +136,7 @@ public static class ScreenshotComparisonError extends RuntimeException {
134136

135137
public static class ScreenshotComparisonTimeout extends RuntimeException {
136138
private static final long serialVersionUID = 6336247721154252476L;
137-
private double currentScore = Double.NaN;
139+
private final double currentScore;
138140

139141
ScreenshotComparisonTimeout(String message, double currentScore) {
140142
super(message);
@@ -147,17 +149,13 @@ public double getCurrentScore() {
147149
}
148150

149151
private ScreenshotState checkState(Function<Double, Boolean> checkerFunc, Duration timeout) {
150-
return checkState(checkerFunc, timeout, ResizeMode.NO_RESIZE);
151-
}
152-
153-
private ScreenshotState checkState(Function<Double, Boolean> checkerFunc, Duration timeout, ResizeMode resizeMode) {
154152
final LocalDateTime started = LocalDateTime.now();
155153
double score;
156154
do {
157155
final BufferedImage currentState = stateProvider.get();
158-
score = getOverlapScore(this.previousScreenshot
156+
score = getOverlapScore(ofNullable(this.previousScreenshot)
159157
.orElseThrow(() -> new ScreenshotComparisonError("Initial screenshot state is not set. "
160-
+ "Nothing to compare")), currentState, resizeMode);
158+
+ "Nothing to compare")), currentState);
161159
if (checkerFunc.apply(score)) {
162160
return this;
163161
}
@@ -188,25 +186,6 @@ public ScreenshotState verifyChanged(Duration timeout, double minScore) {
188186
return checkState((x) -> x < minScore, timeout);
189187
}
190188

191-
/**
192-
* Verifies whether the state of the screenshot provided by stateProvider lambda function
193-
* is changed within the given timeout.
194-
*
195-
* @param timeout timeout value
196-
* @param minScore the value in range (0.0, 1.0)
197-
* @param resizeMode one of <em>ResizeMode</em> enum values.
198-
* Set it to a value different from <em>NO_RESIZE</em>
199-
* if the actual screenshot is expected to have different
200-
* dimensions in comparison to the previously remembered one
201-
* @return self instance for chaining
202-
* @throws ScreenshotComparisonTimeout if the calculated score is still
203-
* greater or equal to the given score after timeout happens
204-
* @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet
205-
*/
206-
public ScreenshotState verifyChanged(Duration timeout, double minScore, ResizeMode resizeMode) {
207-
return checkState((x) -> x < minScore, timeout, resizeMode);
208-
}
209-
210189
/**
211190
* Verifies whether the state of the screenshot provided by stateProvider lambda function
212191
* is not changed within the given timeout.
@@ -222,112 +201,28 @@ public ScreenshotState verifyNotChanged(Duration timeout, double minScore) {
222201
return checkState((x) -> x >= minScore, timeout);
223202
}
224203

225-
/**
226-
* Verifies whether the state of the screenshot provided by stateProvider lambda function
227-
* is changed within the given timeout.
228-
*
229-
* @param timeout timeout value
230-
* @param minScore the value in range (0.0, 1.0)
231-
* @param resizeMode one of <em>ResizeMode</em> enum values.
232-
* Set it to a value different from <em>NO_RESIZE</em>
233-
* if the actual screenshot is expected to have different
234-
* dimensions in comparison to the previously remembered one
235-
* @return self instance for chaining
236-
* @throws ScreenshotComparisonTimeout if the calculated score is still
237-
* less than the given score after timeout happens
238-
* @throws ScreenshotComparisonError if {@link #remember()} method has not been invoked yet
239-
*/
240-
public ScreenshotState verifyNotChanged(Duration timeout, double minScore, ResizeMode resizeMode) {
241-
return checkState((x) -> x >= minScore, timeout, resizeMode);
242-
}
243-
244-
private static Mat prepareImageForComparison(BufferedImage srcImage) {
245-
final BufferedImage normalizedBitmap = new BufferedImage(srcImage.getWidth(), srcImage.getHeight(),
246-
BufferedImage.TYPE_3BYTE_BGR);
247-
final Graphics2D g = normalizedBitmap.createGraphics();
248-
try {
249-
g.setComposite(AlphaComposite.Src);
250-
g.drawImage(srcImage, 0, 0, null);
251-
} finally {
252-
g.dispose();
253-
}
254-
final byte[] pixels = ((DataBufferByte) normalizedBitmap.getRaster().getDataBuffer()).getData();
255-
final Mat result = new Mat(normalizedBitmap.getHeight(), normalizedBitmap.getWidth(), CvType.CV_8UC3);
256-
result.put(0, 0, pixels);
257-
return result;
258-
}
259-
260-
private static Mat resizeFirstMatrixToSecondMatrixResolution(Mat first, Mat second) {
261-
if (first.width() != second.width() || first.height() != second.height()) {
262-
final Mat result = new Mat();
263-
final Size sz = new Size(second.width(), second.height());
264-
Imgproc.resize(first, result, sz);
265-
return result;
266-
}
267-
return first;
268-
}
269-
270-
/**
271-
* A shortcut to {@link #getOverlapScore(BufferedImage, BufferedImage, ResizeMode)} method
272-
* for the case if both reference and template images are expected to have the same dimensions.
273-
*
274-
* @param refImage reference image
275-
* @param tplImage template
276-
* @return similarity score value in range (-1.0, 1.0). 1.0 is returned if the images are equal
277-
* @throws ScreenshotComparisonError if provided images are not valid or have different resolution
278-
*/
279-
public static double getOverlapScore(BufferedImage refImage, BufferedImage tplImage) {
280-
return getOverlapScore(refImage, tplImage, ResizeMode.NO_RESIZE);
281-
}
282-
283204
/**
284205
* Compares two valid java bitmaps and calculates similarity score between them.
206+
* Both images are expected to be of the same size/resolution. The method
207+
* implicitly invokes {@link ComparesImages#getImagesSimilarity(byte[], byte[])}.
285208
*
286209
* @param refImage reference image
287210
* @param tplImage template
288-
* @param resizeMode one of possible enum values. Set it either to <em>TEMPLATE_TO_REFERENCE_RESOLUTION</em> or
289-
* <em>REFERENCE_TO_TEMPLATE_RESOLUTION</em> if given bitmaps have different dimensions
290-
* @return similarity score value in range (-1.0, 1.0). 1.0 is returned if the images are equal
211+
* @return similarity score value in range (-1.0, 1.0]. 1.0 is returned if the images are equal
291212
* @throws ScreenshotComparisonError if provided images are not valid or have
292-
* different resolution, but resizeMode has been set to <em>NO_RESIZE</em>
213+
* different resolution
293214
*/
294-
public static double getOverlapScore(BufferedImage refImage, BufferedImage tplImage, ResizeMode resizeMode) {
295-
Mat ref = prepareImageForComparison(refImage);
296-
if (ref.empty()) {
297-
throw new ScreenshotComparisonError("Reference image cannot be converted for further comparison");
298-
}
299-
Mat tpl = prepareImageForComparison(tplImage);
300-
if (tpl.empty()) {
301-
throw new ScreenshotComparisonError("Template image cannot be converted for further comparison");
302-
}
303-
switch (resizeMode) {
304-
case TEMPLATE_TO_REFERENCE_RESOLUTION:
305-
tpl = resizeFirstMatrixToSecondMatrixResolution(tpl, ref);
306-
break;
307-
case REFERENCE_TO_TEMPLATE_RESOLUTION:
308-
ref = resizeFirstMatrixToSecondMatrixResolution(ref, tpl);
309-
break;
310-
default:
311-
// do nothing
312-
}
313-
314-
if (ref.width() != tpl.width() || ref.height() != tpl.height()) {
315-
throw new ScreenshotComparisonError(
316-
"Resolutions of template and reference images are expected to be equal. "
317-
+ "Try different resizeMode value."
318-
);
215+
public double getOverlapScore(BufferedImage refImage, BufferedImage tplImage) {
216+
try (ByteArrayOutputStream img1 = new ByteArrayOutputStream();
217+
ByteArrayOutputStream img2 = new ByteArrayOutputStream()) {
218+
ImageIO.write(refImage, "png", img1);
219+
ImageIO.write(tplImage, "png", img2);
220+
return comparator
221+
.getImagesSimilarity(Base64.getEncoder().encode(img1.toByteArray()),
222+
Base64.getEncoder().encode(img2.toByteArray()))
223+
.getScore();
224+
} catch (IOException e) {
225+
throw new ScreenshotComparisonError(e);
319226
}
320-
321-
Mat res = new Mat(ref.rows() - tpl.rows() + 1, ref.cols() - tpl.cols() + 1, CvType.CV_32FC1);
322-
Imgproc.matchTemplate(ref, tpl, res, Imgproc.TM_CCOEFF_NORMED);
323-
return Core.minMaxLoc(res).maxVal;
324-
}
325-
326-
public enum ResizeMode {
327-
NO_RESIZE, TEMPLATE_TO_REFERENCE_RESOLUTION, REFERENCE_TO_TEMPLATE_RESOLUTION
328-
}
329-
330-
static {
331-
loadShared();
332227
}
333228
}

0 commit comments

Comments
 (0)