mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-10 17:22:09 +10:00
Added regression test suite that validates each release candidate version against a barrage of different DIY files
This commit is contained in:
@@ -3731,7 +3731,7 @@
|
||||
<minor>22</minor>
|
||||
<build>0</build>
|
||||
</versionNumber>
|
||||
<releaseDate>2022-02-27 00:00:00.000 CET</releaseDate>
|
||||
<releaseDate>2022-03-02 00:00:00.000 CET</releaseDate>
|
||||
<name></name>
|
||||
<changes>
|
||||
<org.diylc.appframework.update.Change>
|
||||
@@ -3744,12 +3744,16 @@
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Separate WIRING layer (containing Hookup Wire and Twisted Wire at the moment) from TRACE layer and render it above the COMPONENT layer by default</description>
|
||||
<description>Separate WIRING layer (containing 'Hookup Wire' and 'Twisted Wire' at the moment) from TRACE layer and render it above the COMPONENT layer by default</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>IMPROVEMENT</changeType>
|
||||
<description>Upgrade OSX application package built-in Java to JRE 1.8.0_321-b07</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
<org.diylc.appframework.update.Change>
|
||||
<changeType>NEW_FEATURE</changeType>
|
||||
<description>Added regression test suite that validates each release candidate version against a barrage of different DIY files</description>
|
||||
</org.diylc.appframework.update.Change>
|
||||
</changes>
|
||||
<url>https://github.com/bancika/diy-layout-creator/releases</url>
|
||||
</org.diylc.appframework.update.Version>
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<launchConfiguration type="org.eclipse.jdt.launching.localJavaApplication">
|
||||
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_PATHS">
|
||||
<listEntry value="/diylc-swing/test/org/diylc/RegressionTestRunner.java"/>
|
||||
</listAttribute>
|
||||
<listAttribute key="org.eclipse.debug.core.MAPPED_RESOURCE_TYPES">
|
||||
<listEntry value="1"/>
|
||||
</listAttribute>
|
||||
<booleanAttribute key="org.eclipse.jdt.launching.ATTR_EXCLUDE_TEST_CODE" value="true"/>
|
||||
<stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="org.diylc.RegressionTestRunner"/>
|
||||
<stringAttribute key="org.eclipse.jdt.launching.MODULE_NAME" value="diylc-swing"/>
|
||||
<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value=""E:\Bane\Dev\DIYLC\regression" TEST"/>
|
||||
<stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="diylc-swing"/>
|
||||
</launchConfiguration>
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,708 @@
|
||||
package com.github.romankh3.image.comparison;
|
||||
|
||||
import com.github.romankh3.image.comparison.model.ExcludedAreas;
|
||||
import com.github.romankh3.image.comparison.model.ImageComparisonResult;
|
||||
import com.github.romankh3.image.comparison.model.ImageComparisonState;
|
||||
import com.github.romankh3.image.comparison.model.Rectangle;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.github.romankh3.image.comparison.ImageComparisonUtil.getDifferencePercent;
|
||||
import static java.util.Collections.emptyList;
|
||||
|
||||
/**
|
||||
* Main class for comparison images.
|
||||
*/
|
||||
public class ImageComparison {
|
||||
|
||||
/**
|
||||
* The threshold which means the max distance between non-equal pixels.
|
||||
* Could be changed according to the size and requirements of the image.
|
||||
*/
|
||||
private int threshold = 5;
|
||||
|
||||
/**
|
||||
* Expected image for comparison
|
||||
*/
|
||||
private final BufferedImage expected;
|
||||
|
||||
/**
|
||||
* Actual image for comparison
|
||||
*/
|
||||
private final BufferedImage actual;
|
||||
|
||||
/**
|
||||
* Width of the line that is drawn the rectangle
|
||||
*/
|
||||
private int rectangleLineWidth = 1;
|
||||
|
||||
/**
|
||||
* {@link File} of the result destination.
|
||||
*/
|
||||
private /* @Nullable */ File destination;
|
||||
|
||||
/**
|
||||
* The number which marks how many rectangles. Beginning from 2.
|
||||
*/
|
||||
private int counter = 2;
|
||||
|
||||
/**
|
||||
* The number of the marking specific rectangle.
|
||||
*/
|
||||
private int regionCount = counter;
|
||||
|
||||
/**
|
||||
* The number of the minimal rectangle size. Count as (width x height).
|
||||
*/
|
||||
private Integer minimalRectangleSize = 1;
|
||||
|
||||
/**
|
||||
* Maximal count of the {@link Rectangle}s.
|
||||
* It means that would get the first x biggest rectangles.
|
||||
* Default value is -1, that means that all the rectangles would be drawn.
|
||||
*/
|
||||
private Integer maximalRectangleCount = -1;
|
||||
|
||||
/**
|
||||
* Level of the pixel tolerance. By default, it's 0.1 -> 10% difference.
|
||||
* The value can be set from 0.0 to 0.99.
|
||||
*/
|
||||
private double pixelToleranceLevel = 0.1;
|
||||
|
||||
/**
|
||||
* Constant using for counting the level of the difference.
|
||||
*/
|
||||
private double differenceConstant;
|
||||
|
||||
/**
|
||||
* Matrix YxX => int[y][x].
|
||||
* E.g.:
|
||||
* | X - width ----
|
||||
* | .....................................
|
||||
* Y . (0, 0) .
|
||||
* | . .
|
||||
* | . .
|
||||
* h . .
|
||||
* e . .
|
||||
* i . .
|
||||
* g . .
|
||||
* h . .
|
||||
* t . (X, Y).
|
||||
* | .....................................
|
||||
*/
|
||||
private int[][] matrix;
|
||||
|
||||
/**
|
||||
* ExcludedAreas contains a List of {@link Rectangle}s to be ignored when comparing images
|
||||
*/
|
||||
private ExcludedAreas excludedAreas = new ExcludedAreas();
|
||||
|
||||
/**
|
||||
* Flag which says draw excluded rectangles or not.
|
||||
*/
|
||||
private boolean drawExcludedRectangles = false;
|
||||
|
||||
/**
|
||||
* The difference in percent between two images.
|
||||
*/
|
||||
private float differencePercent;
|
||||
|
||||
/**
|
||||
* Flag for filling comparison difference rectangles.
|
||||
*/
|
||||
private boolean fillDifferenceRectangles = false;
|
||||
|
||||
/**
|
||||
* Sets the opacity percentage of the fill of comparison difference rectangles. 0.0 means completely transparent and 100.0 means completely opaque.
|
||||
*/
|
||||
private double percentOpacityDifferenceRectangles = 20.0;
|
||||
|
||||
/**
|
||||
* Flag for filling excluded rectangles.
|
||||
*/
|
||||
private boolean fillExcludedRectangles = false;
|
||||
|
||||
/**
|
||||
* Sets the opacity percentage of the fill of excluded rectangles. 0.0 means completely transparent and 100.0 means completely opaque.
|
||||
*/
|
||||
private double percentOpacityExcludedRectangles = 20.0;
|
||||
|
||||
/**
|
||||
* The percent of the allowing pixels to be different to stay {@link ImageComparisonState#MATCH} for comparison.
|
||||
* E.g. percent of the pixels, which would ignore in comparison.
|
||||
*/
|
||||
private double allowingPercentOfDifferentPixels = 0.0;
|
||||
|
||||
/**
|
||||
* Sets rectangle color of image difference. By default, it's red.
|
||||
*/
|
||||
private Color differenceRectangleColor = Color.RED;
|
||||
|
||||
/**
|
||||
* Sets rectangle color of excluded part. By default, it's green.
|
||||
*/
|
||||
private Color excludedRectangleColor = Color.GREEN;
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link ImageComparison} that can compare the given images.
|
||||
*
|
||||
* @param expected expected image to be compared
|
||||
* @param actual actual image to be compared
|
||||
*/
|
||||
public ImageComparison(String expected, String actual) {
|
||||
this(ImageComparisonUtil.readImageFromResources(expected),
|
||||
ImageComparisonUtil.readImageFromResources(actual),
|
||||
null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link ImageComparison} that can compare the given images.
|
||||
*
|
||||
* @param expected expected image to be compared
|
||||
* @param actual actual image to be compared
|
||||
* @param destination destination to save the result. If null, the result is shown in the UI.
|
||||
*/
|
||||
public ImageComparison(BufferedImage expected, BufferedImage actual, File destination) {
|
||||
this.expected = expected;
|
||||
this.actual = actual;
|
||||
this.destination = destination;
|
||||
differenceConstant = calculateDifferenceConstant();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance of {@link ImageComparison} that can compare the given images.
|
||||
*
|
||||
* @param expected expected image to be compared
|
||||
* @param actual actual image to be compared
|
||||
*/
|
||||
public ImageComparison(BufferedImage expected, BufferedImage actual) {
|
||||
this(expected, actual, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rectangles which cover the regions of the difference pixels.
|
||||
*
|
||||
* @return the result of the drawing.
|
||||
*/
|
||||
public ImageComparisonResult compareImages() {
|
||||
|
||||
// check that the images have the same size
|
||||
if (isImageSizesNotEqual(expected, actual)) {
|
||||
BufferedImage actualResized = ImageComparisonUtil.resize(actual, expected.getWidth(), expected.getHeight());
|
||||
return ImageComparisonResult.defaultSizeMisMatchResult(expected, actual, getDifferencePercent(actualResized, expected));
|
||||
}
|
||||
|
||||
List<Rectangle> rectangles = populateRectangles();
|
||||
|
||||
if (rectangles.isEmpty()) {
|
||||
ImageComparisonResult matchResult = ImageComparisonResult.defaultMatchResult(expected, actual);
|
||||
if (drawExcludedRectangles) {
|
||||
matchResult.setResult(drawRectangles(rectangles));
|
||||
saveImageForDestination(matchResult.getResult());
|
||||
}
|
||||
return matchResult;
|
||||
}
|
||||
|
||||
BufferedImage resultImage = drawRectangles(rectangles);
|
||||
saveImageForDestination(resultImage);
|
||||
return ImageComparisonResult.defaultMisMatchResult(expected, actual, getDifferencePercent(actual, expected))
|
||||
.setResult(resultImage)
|
||||
.setRectangles(rectangles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check images for equals their widths and heights.
|
||||
*
|
||||
* @param expected {@link BufferedImage} object of the expected image.
|
||||
* @param actual {@link BufferedImage} object of the actual image.
|
||||
* @return true if image size are not equal, false otherwise.
|
||||
*/
|
||||
private boolean isImageSizesNotEqual(BufferedImage expected, BufferedImage actual) {
|
||||
return expected.getHeight() != actual.getHeight() || expected.getWidth() != actual.getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate binary matrix with "0" and "1". If the pixels are different set it as "1", otherwise "0".
|
||||
*
|
||||
* @return the count of different pixels
|
||||
*/
|
||||
private long populateTheMatrixOfTheDifferences() {
|
||||
long countOfDifferentPixels = 0;
|
||||
matrix = new int[expected.getHeight()][expected.getWidth()];
|
||||
for (int y = 0; y < expected.getHeight(); y++) {
|
||||
for (int x = 0; x < expected.getWidth(); x++) {
|
||||
if (!excludedAreas.contains(new Point(x, y))) {
|
||||
if (isDifferentPixels(expected.getRGB(x, y), actual.getRGB(x, y))) {
|
||||
matrix[y][x] = 1;
|
||||
countOfDifferentPixels++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return countOfDifferentPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Say if the two pixels equal or not. The rule is the difference between two pixels
|
||||
* need to be more than {@link #pixelToleranceLevel}.
|
||||
*
|
||||
* @param expectedRgb the RGB value of the Pixel of the Expected image.
|
||||
* @param actualRgb the RGB value of the Pixel of the Actual image.
|
||||
* @return {@code true} if they' are difference, {@code false} otherwise.
|
||||
*/
|
||||
private boolean isDifferentPixels(int expectedRgb, int actualRgb) {
|
||||
if (expectedRgb == actualRgb) {
|
||||
return false;
|
||||
} else if (pixelToleranceLevel == 0.0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int red1 = (expectedRgb >> 16) & 0xff;
|
||||
int green1 = (expectedRgb >> 8) & 0xff;
|
||||
int blue1 = (expectedRgb) & 0xff;
|
||||
int red2 = (actualRgb >> 16) & 0xff;
|
||||
int green2 = (actualRgb >> 8) & 0xff;
|
||||
int blue2 = (actualRgb) & 0xff;
|
||||
|
||||
return (Math.pow(red2 - red1, 2) + Math.pow(green2 - green1, 2) + Math.pow(blue2 - blue1, 2))
|
||||
> differenceConstant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate rectangles of the differences
|
||||
*
|
||||
* @return the collection of the populated {@link Rectangle} objects.
|
||||
*/
|
||||
private List<Rectangle> populateRectangles() {
|
||||
long countOfDifferentPixels = populateTheMatrixOfTheDifferences();
|
||||
|
||||
if (countOfDifferentPixels == 0) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
if (isAllowedPercentOfDifferentPixels(countOfDifferentPixels)) {
|
||||
return emptyList();
|
||||
}
|
||||
groupRegions();
|
||||
List<Rectangle> rectangles = new ArrayList<>();
|
||||
while (counter <= regionCount) {
|
||||
Rectangle rectangle = createRectangle();
|
||||
if (!rectangle.equals(Rectangle.createDefault()) && rectangle.size() >= minimalRectangleSize) {
|
||||
rectangles.add(rectangle);
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
|
||||
return mergeRectangles(mergeRectangles(rectangles));
|
||||
}
|
||||
|
||||
/**
|
||||
* Say if provided {@param countOfDifferentPixels} is allowed for {@link ImageComparisonState#MATCH} state.
|
||||
*
|
||||
* @param countOfDifferentPixels the count of the different pixels in comparison.
|
||||
* @return true, if percent of different pixels lower or equal {@link ImageComparison#allowingPercentOfDifferentPixels},
|
||||
* false - otherwise.
|
||||
*/
|
||||
private boolean isAllowedPercentOfDifferentPixels(long countOfDifferentPixels) {
|
||||
long totalPixelCount = matrix.length * matrix[0].length;
|
||||
double actualPercentOfDifferentPixels = ((double) countOfDifferentPixels / (double) totalPixelCount) * 100;
|
||||
return actualPercentOfDifferentPixels <= allowingPercentOfDifferentPixels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link Rectangle} object.
|
||||
*
|
||||
* @return the {@link Rectangle} object.
|
||||
*/
|
||||
private Rectangle createRectangle() {
|
||||
Rectangle rectangle = Rectangle.createDefault();
|
||||
for (int y = 0; y < matrix.length; y++) {
|
||||
for (int x = 0; x < matrix[0].length; x++) {
|
||||
if (matrix[y][x] == counter) {
|
||||
updateRectangleCreation(rectangle, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update {@link Point} of the rectangle based on x and y coordinates.
|
||||
*/
|
||||
private void updateRectangleCreation(Rectangle rectangle, int x, int y) {
|
||||
if (x < rectangle.getMinPoint().getX()) {
|
||||
rectangle.getMinPoint().x = x;
|
||||
}
|
||||
if (x > rectangle.getMaxPoint().getX()) {
|
||||
rectangle.getMaxPoint().x = x;
|
||||
}
|
||||
|
||||
if (y < rectangle.getMinPoint().getY()) {
|
||||
rectangle.getMinPoint().y = y;
|
||||
}
|
||||
if (y > rectangle.getMaxPoint().getY()) {
|
||||
rectangle.getMaxPoint().y = y;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find overlapping rectangles and merge them.
|
||||
*/
|
||||
private List<Rectangle> mergeRectangles(List<Rectangle> rectangles) {
|
||||
int position = 0;
|
||||
while (position < rectangles.size()) {
|
||||
if (rectangles.get(position).equals(Rectangle.createZero())) {
|
||||
position++;
|
||||
}
|
||||
for (int i = 1 + position; i < rectangles.size(); i++) {
|
||||
Rectangle r1 = rectangles.get(position);
|
||||
Rectangle r2 = rectangles.get(i);
|
||||
if (r2.equals(Rectangle.createZero())) {
|
||||
continue;
|
||||
}
|
||||
if (r1.isOverlapping(r2)) {
|
||||
rectangles.set(position, r1.merge(r2));
|
||||
r2.makeZeroRectangle();
|
||||
if (position != 0) {
|
||||
position--;
|
||||
}
|
||||
}
|
||||
}
|
||||
position++;
|
||||
}
|
||||
|
||||
return rectangles.stream().filter(it -> !it.equals(Rectangle.createZero())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the rectangles based on collection of the rectangles and result image.
|
||||
*
|
||||
* @param rectangles the collection of the {@link Rectangle} objects.
|
||||
* @return result {@link BufferedImage} with drawn rectangles.
|
||||
*/
|
||||
private BufferedImage drawRectangles(List<Rectangle> rectangles) {
|
||||
BufferedImage resultImage = ImageComparisonUtil.deepCopy(actual);
|
||||
Graphics2D graphics = preparedGraphics2D(resultImage);
|
||||
|
||||
drawExcludedRectangles(graphics);
|
||||
drawRectanglesOfDifferences(rectangles, graphics);
|
||||
|
||||
return resultImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw excluded rectangles.
|
||||
*
|
||||
* @param graphics prepared {@link Graphics2D}object.
|
||||
*/
|
||||
private void drawExcludedRectangles(Graphics2D graphics) {
|
||||
if (drawExcludedRectangles) {
|
||||
graphics.setColor(this.excludedRectangleColor);
|
||||
draw(graphics, excludedAreas.getExcluded());
|
||||
|
||||
if (fillExcludedRectangles) {
|
||||
fillRectangles(graphics, excludedAreas.getExcluded(), percentOpacityExcludedRectangles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rectangles with the differences.
|
||||
*
|
||||
* @param rectangles the collection of the {@link Rectangle} of differences.
|
||||
* @param graphics prepared {@link Graphics2D}object.
|
||||
*/
|
||||
private void drawRectanglesOfDifferences(List<Rectangle> rectangles, Graphics2D graphics) {
|
||||
List<Rectangle> rectanglesForDraw;
|
||||
graphics.setColor(this.differenceRectangleColor);
|
||||
|
||||
if (maximalRectangleCount > 0 && maximalRectangleCount < rectangles.size()) {
|
||||
rectanglesForDraw = rectangles.stream()
|
||||
.sorted(Comparator.comparing(Rectangle::size))
|
||||
.skip(rectangles.size() - maximalRectangleCount)
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
rectanglesForDraw = new ArrayList<>(rectangles);
|
||||
}
|
||||
|
||||
draw(graphics, rectanglesForDraw);
|
||||
|
||||
if (fillDifferenceRectangles) {
|
||||
fillRectangles(graphics, rectanglesForDraw, percentOpacityDifferenceRectangles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare {@link Graphics2D} based on resultImage and rectangleLineWidth
|
||||
*
|
||||
* @param image image based on created {@link Graphics2D}.
|
||||
* @return prepared {@link Graphics2D} object.
|
||||
*/
|
||||
private Graphics2D preparedGraphics2D(BufferedImage image) {
|
||||
Graphics2D graphics = image.createGraphics();
|
||||
graphics.setStroke(new BasicStroke(rectangleLineWidth));
|
||||
return graphics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image to destination object if exists.
|
||||
*
|
||||
* @param image {@link BufferedImage} to be saved.
|
||||
*/
|
||||
private void saveImageForDestination(BufferedImage image) {
|
||||
if (Objects.nonNull(destination)) {
|
||||
ImageComparisonUtil.saveImage(destination, image);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rectangles based on collection of the {@link Rectangle} and {@link Graphics2D}.
|
||||
* getWidth/getHeight return real width/height,
|
||||
* so need to draw rectangle on one px smaller because minpoint + width/height is point on excluded pixel.
|
||||
*
|
||||
* @param graphics the {@link Graphics2D} object for drawing.
|
||||
* @param rectangles the collection of the {@link Rectangle}.
|
||||
*/
|
||||
private void draw(Graphics2D graphics, List<Rectangle> rectangles) {
|
||||
rectangles.forEach(rectangle -> graphics.drawRect(
|
||||
rectangle.getMinPoint().x,
|
||||
rectangle.getMinPoint().y,
|
||||
rectangle.getWidth() - 1,
|
||||
rectangle.getHeight() - 1)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill rectangles based on collection of the {@link Rectangle} and {@link Graphics2D}.
|
||||
* getWidth/getHeight return real width/height,
|
||||
* so need to draw rectangle fill two px smaller to fit inside rectangle borders.
|
||||
*
|
||||
* @param graphics the {@link Graphics2D} object for drawing.
|
||||
* @param rectangles rectangles the collection of the {@link Rectangle}.
|
||||
* @param percentOpacity the opacity of the fill.
|
||||
*/
|
||||
private void fillRectangles(Graphics2D graphics, List<Rectangle> rectangles, double percentOpacity) {
|
||||
|
||||
graphics.setColor(new Color(graphics.getColor().getRed(),
|
||||
graphics.getColor().getGreen(),
|
||||
graphics.getColor().getBlue(),
|
||||
(int) (percentOpacity / 100 * 255)
|
||||
));
|
||||
rectangles.forEach(rectangle -> graphics.fillRect(
|
||||
rectangle.getMinPoint().x - 1,
|
||||
rectangle.getMinPoint().y - 1,
|
||||
rectangle.getWidth() - 2,
|
||||
rectangle.getHeight() - 2)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Group rectangle regions in matrix.
|
||||
*/
|
||||
private void groupRegions() {
|
||||
for (int y = 0; y < matrix.length; y++) {
|
||||
for (int x = 0; x < matrix[y].length; x++) {
|
||||
if (matrix[y][x] == 1) {
|
||||
joinToRegion(x, y);
|
||||
regionCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The recursive method which go to all directions and finds difference
|
||||
* in binary matrix using {@code threshold} for setting max distance between values which equal "1".
|
||||
* and set the {@code groupCount} to matrix.
|
||||
*
|
||||
* @param x the value of the X-coordinate.
|
||||
* @param y the value of the Y-coordinate.
|
||||
*/
|
||||
private void joinToRegion(int x, int y) {
|
||||
if (isJumpRejected(x, y)) {
|
||||
return;
|
||||
}
|
||||
|
||||
matrix[y][x] = regionCount;
|
||||
|
||||
for (int i = 0; i < threshold; i++) {
|
||||
joinToRegion(x + 1 + i, y);
|
||||
joinToRegion(x, y + 1 + i);
|
||||
|
||||
joinToRegion(x + 1 + i, y - 1 - i);
|
||||
joinToRegion(x - 1 - i, y + 1 + i);
|
||||
joinToRegion(x + 1 + i, y + 1 + i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of rectangles that would be drawn as a diff image.
|
||||
* If you submit two images that are the same barring the parts you want to excludedAreas you get a list of
|
||||
* rectangles that can be used as said excludedAreas
|
||||
*
|
||||
* @return List of {@link Rectangle}
|
||||
*/
|
||||
public List<Rectangle> createMask() {
|
||||
return populateRectangles();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check next step valid or not.
|
||||
*
|
||||
* @param x X-coordinate of the image.
|
||||
* @param y Y-coordinate of the image
|
||||
* @return true if jump rejected, otherwise false.
|
||||
*/
|
||||
private boolean isJumpRejected(int x, int y) {
|
||||
return y < 0 || y >= matrix.length || x < 0 || x >= matrix[y].length || matrix[y][x] != 1;
|
||||
}
|
||||
|
||||
public double getPixelToleranceLevel() {
|
||||
return pixelToleranceLevel;
|
||||
}
|
||||
|
||||
public ImageComparison setPixelToleranceLevel(double pixelToleranceLevel) {
|
||||
if (0.0 <= pixelToleranceLevel && pixelToleranceLevel < 1) {
|
||||
this.pixelToleranceLevel = pixelToleranceLevel;
|
||||
differenceConstant = calculateDifferenceConstant();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private double calculateDifferenceConstant() {
|
||||
return Math.pow(pixelToleranceLevel * Math.sqrt(Math.pow(255, 2) * 3), 2);
|
||||
}
|
||||
|
||||
public boolean isDrawExcludedRectangles() {
|
||||
return drawExcludedRectangles;
|
||||
}
|
||||
|
||||
public ImageComparison setDrawExcludedRectangles(boolean drawExcludedRectangles) {
|
||||
this.drawExcludedRectangles = drawExcludedRectangles;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getThreshold() {
|
||||
return threshold;
|
||||
}
|
||||
|
||||
public ImageComparison setThreshold(int threshold) {
|
||||
this.threshold = threshold;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Optional<File> getDestination() {
|
||||
return Optional.ofNullable(destination);
|
||||
}
|
||||
|
||||
public ImageComparison setDestination(File destination) {
|
||||
this.destination = destination;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BufferedImage getExpected() {
|
||||
return expected;
|
||||
}
|
||||
|
||||
public BufferedImage getActual() {
|
||||
return actual;
|
||||
}
|
||||
|
||||
public int getRectangleLineWidth() {
|
||||
return rectangleLineWidth;
|
||||
}
|
||||
|
||||
public ImageComparison setRectangleLineWidth(int rectangleLineWidth) {
|
||||
this.rectangleLineWidth = rectangleLineWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getMinimalRectangleSize() {
|
||||
return minimalRectangleSize;
|
||||
}
|
||||
|
||||
public ImageComparison setMinimalRectangleSize(Integer minimalRectangleSize) {
|
||||
this.minimalRectangleSize = minimalRectangleSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getMaximalRectangleCount() {
|
||||
return maximalRectangleCount;
|
||||
}
|
||||
|
||||
public ImageComparison setMaximalRectangleCount(Integer maximalRectangleCount) {
|
||||
this.maximalRectangleCount = maximalRectangleCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageComparison setExcludedAreas(List<Rectangle> excludedAreas) {
|
||||
this.excludedAreas = new ExcludedAreas(excludedAreas);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isFillDifferenceRectangles() {
|
||||
return this.fillDifferenceRectangles;
|
||||
}
|
||||
|
||||
public double getPercentOpacityDifferenceRectangles() {
|
||||
return this.percentOpacityDifferenceRectangles;
|
||||
}
|
||||
|
||||
public ImageComparison setDifferenceRectangleFilling(boolean fillRectangles, double percentOpacity) {
|
||||
this.fillDifferenceRectangles = fillRectangles;
|
||||
this.percentOpacityDifferenceRectangles = percentOpacity;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isFillExcludedRectangles() {
|
||||
return this.fillExcludedRectangles;
|
||||
}
|
||||
|
||||
public double getPercentOpacityExcludedRectangles() {
|
||||
return this.percentOpacityExcludedRectangles;
|
||||
}
|
||||
|
||||
public ImageComparison setExcludedRectangleFilling(boolean fillRectangles, double percentOpacity) {
|
||||
this.fillExcludedRectangles = fillRectangles;
|
||||
this.percentOpacityExcludedRectangles = percentOpacity;
|
||||
return this;
|
||||
}
|
||||
|
||||
public double getAllowingPercentOfDifferentPixels() {
|
||||
return allowingPercentOfDifferentPixels;
|
||||
}
|
||||
|
||||
public ImageComparison setAllowingPercentOfDifferentPixels(double allowingPercentOfDifferentPixels) {
|
||||
if (0.0 <= allowingPercentOfDifferentPixels && allowingPercentOfDifferentPixels <= 100) {
|
||||
this.allowingPercentOfDifferentPixels = allowingPercentOfDifferentPixels;
|
||||
} else {
|
||||
//todo add warning here
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Color getDifferenceRectangleColor() {
|
||||
return this.differenceRectangleColor;
|
||||
}
|
||||
|
||||
public ImageComparison setDifferenceRectangleColor(Color differenceRectangleColor) {
|
||||
this.differenceRectangleColor = differenceRectangleColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Color getExcludedRectangleColor() {
|
||||
return this.excludedRectangleColor;
|
||||
}
|
||||
|
||||
public ImageComparison setExcludedRectangleColor(Color excludedRectangleColor) {
|
||||
this.excludedRectangleColor = excludedRectangleColor;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.github.romankh3.image.comparison;
|
||||
|
||||
import com.github.romankh3.image.comparison.exception.ImageComparisonException;
|
||||
import com.github.romankh3.image.comparison.exception.ImageNotFoundException;
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Image;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.ConvolveOp;
|
||||
import java.awt.image.Kernel;
|
||||
import java.awt.image.WritableRaster;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
/**
|
||||
* Tools for the {@link ImageComparison} object.
|
||||
*/
|
||||
public final class ImageComparisonUtil {
|
||||
|
||||
/**
|
||||
* Make a copy of the {@link BufferedImage} object.
|
||||
*
|
||||
* @param image the provided image.
|
||||
* @return copy of the provided image.
|
||||
*/
|
||||
static BufferedImage deepCopy(BufferedImage image) {
|
||||
ColorModel cm = image.getColorModel();
|
||||
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
|
||||
WritableRaster raster = image.copyData(image.getRaster().createCompatibleWritableRaster());
|
||||
return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read image from the provided path.
|
||||
*
|
||||
* @param path the path where contains image.
|
||||
* @return the {@link BufferedImage} object of this specific image.
|
||||
* @throws ImageComparisonException due to read the image from resources.
|
||||
*/
|
||||
public static BufferedImage readImageFromResources(String path) throws ImageComparisonException {
|
||||
File imageFile = new File(path);
|
||||
if (imageFile.exists()) {
|
||||
try {
|
||||
return ImageIO.read(imageFile);
|
||||
} catch (IOException e) {
|
||||
throw new ImageComparisonException(String.format("Cannot read image from the file, path=%s", path), e);
|
||||
}
|
||||
} else {
|
||||
InputStream inputStream = ImageComparisonUtil.class.getClassLoader().getResourceAsStream(path);
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
return ImageIO.read(inputStream);
|
||||
} catch (IOException e) {
|
||||
throw new ImageComparisonException(String.format("Cannot read image from the file, path=%s", path), e);
|
||||
}
|
||||
} else {
|
||||
throw new ImageNotFoundException(String.format("Image with path = %s not found", path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save image to the provided path.
|
||||
*
|
||||
* @param pathFile the path to the saving image.
|
||||
* @param image the {@link BufferedImage} object of this specific image.
|
||||
* @throws ImageComparisonException due to save image.
|
||||
*/
|
||||
public static void saveImage(File pathFile, BufferedImage image) throws ImageComparisonException {
|
||||
File dir = pathFile.getParentFile();
|
||||
// make dir if it's not using from Gradle.
|
||||
boolean dirExists = dir == null || dir.isDirectory() || dir.mkdirs();
|
||||
if (!dirExists) {
|
||||
throw new ImageComparisonException("Unable to create directory " + dir);
|
||||
}
|
||||
try {
|
||||
ImageIO.write(image, "png", pathFile);
|
||||
} catch (IOException e) {
|
||||
throw new ImageComparisonException(
|
||||
String.format("Cannot save image to path=%s", pathFile.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize image to new dimensions and return new image.
|
||||
*
|
||||
* @param img the object of the image to be resized.
|
||||
* @param newW the new width.
|
||||
* @param newH the new height.
|
||||
* @return resized {@link BufferedImage} object.
|
||||
*/
|
||||
public static BufferedImage resize(BufferedImage img, int newW, int newH) {
|
||||
return toBufferedImage(img.getScaledInstance(newW, newH, Image.SCALE_SMOOTH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert image to buffered image.
|
||||
*
|
||||
* @param img the object of the image to be converted to buffered image.
|
||||
* @return the converted buffered image.
|
||||
*/
|
||||
public static BufferedImage toBufferedImage(Image img) {
|
||||
if (img instanceof BufferedImage) {
|
||||
return (BufferedImage) img;
|
||||
}
|
||||
|
||||
float softenFactor = 0.05f;
|
||||
final Image temp = new ImageIcon(img).getImage();
|
||||
final BufferedImage bufferedImage = new BufferedImage(
|
||||
temp.getWidth(null),
|
||||
temp.getHeight(null),
|
||||
BufferedImage.TYPE_INT_RGB);
|
||||
final Graphics g = bufferedImage.createGraphics();
|
||||
g.setColor(Color.white);
|
||||
g.fillRect(0, 0, temp.getWidth(null), temp.getHeight(null));
|
||||
g.drawImage(temp, 0, 0, null);
|
||||
g.dispose();
|
||||
|
||||
final float[] softenArray = {0, softenFactor, 0, softenFactor, 1 - (softenFactor * 4), softenFactor, 0,
|
||||
softenFactor, 0};
|
||||
final Kernel kernel = new Kernel(3, 3, softenArray);
|
||||
final ConvolveOp cOp = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
|
||||
|
||||
return cOp.filter(bufferedImage, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the difference in percent between two buffered images.
|
||||
*
|
||||
* @param img1 the first image.
|
||||
* @param img2 the second image.
|
||||
* @return difference percent.
|
||||
*/
|
||||
public static float getDifferencePercent(BufferedImage img1, BufferedImage img2) {
|
||||
int width = img1.getWidth();
|
||||
int height = img1.getHeight();
|
||||
|
||||
long diff = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
diff += pixelDiff(img1.getRGB(x, y), img2.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
long maxDiff = 3L * 255 * width * height;
|
||||
|
||||
return (float) (100.0 * diff / maxDiff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two pixels
|
||||
*
|
||||
* @param rgb1 the first rgb
|
||||
* @param rgb2 the second rgn
|
||||
* @return the difference.
|
||||
*/
|
||||
public static int pixelDiff(int rgb1, int rgb2) {
|
||||
int r1 = (rgb1 >> 16) & 0xff;
|
||||
int g1 = (rgb1 >> 8) & 0xff;
|
||||
int b1 = rgb1 & 0xff;
|
||||
int r2 = (rgb2 >> 16) & 0xff;
|
||||
int g2 = (rgb2 >> 8) & 0xff;
|
||||
int b2 = rgb2 & 0xff;
|
||||
return Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2);
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.github.romankh3.image.comparison.exception;
|
||||
|
||||
/**
|
||||
* A {@link RuntimeException} that is thrown in case of an image comparison failures.
|
||||
*/
|
||||
public final class ImageComparisonException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ImageComparisonException} with the specified detail message.
|
||||
* The cause is not initialized, and may subsequently be initialized by a call to
|
||||
* Throwable.initCause(java.lang.Throwable).
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for later retrieval by the Throwable.getMessage()
|
||||
* method.
|
||||
*/
|
||||
public ImageComparisonException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ImageComparisonException} with the specified detail message and cause.
|
||||
* Note that the detail message associated with cause is not automatically incorporated in this {@link
|
||||
* ImageComparisonException}'s detail message.
|
||||
*
|
||||
* @param message the detail message (which is saved for later retrieval by the Throwable.getMessage() method).
|
||||
* @param throwable the cause (which is saved for later retrieval by the Throwable.getCause() method).
|
||||
* (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
|
||||
*/
|
||||
public ImageComparisonException(String message, Throwable throwable) {
|
||||
super(message, throwable);
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.github.romankh3.image.comparison.exception;
|
||||
|
||||
/**
|
||||
* {@link RuntimeException} that is thrown in case of an image getting failures.
|
||||
*/
|
||||
public class ImageNotFoundException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs a new {@link ImageNotFoundException} with the specified detail message.
|
||||
* The cause is not initialized, and may subsequently be initialized by a call to
|
||||
* Throwable.initCause(java.lang.Throwable).
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for later retrieval by the Throwable.getMessage()
|
||||
* method.
|
||||
*/
|
||||
public ImageNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.github.romankh3.image.comparison.model;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The area that will be excluded, masked, in the image.
|
||||
*/
|
||||
public class ExcludedAreas {
|
||||
|
||||
/**
|
||||
* The collection of the areas which would be excluded from the comparison.
|
||||
*/
|
||||
private final List<Rectangle> excluded;
|
||||
|
||||
/**
|
||||
* Create empty instance of the {@link ExcludedAreas}.
|
||||
*/
|
||||
public ExcludedAreas() {
|
||||
excluded = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instance of the {@link ExcludedAreas} with provided {@link Rectangle} areas.
|
||||
*
|
||||
* @param excluded provided collection of the {@link Rectangle} objects.
|
||||
*/
|
||||
public ExcludedAreas(List<Rectangle> excluded) {
|
||||
this.excluded = excluded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this {@link Point} contains in the {@link ExcludedAreas#excluded}
|
||||
* collection of the {@link Rectangle}.
|
||||
*
|
||||
* @param point the {@link Point} object to be checked.
|
||||
*
|
||||
* @return {@code true} if this {@link Point} contains in areas from {@link ExcludedAreas#excluded}.
|
||||
*/
|
||||
public boolean contains(Point point) {
|
||||
return excluded.stream().anyMatch(rectangle -> rectangle.containsPoint(point));
|
||||
}
|
||||
|
||||
public List<Rectangle> getExcluded() {
|
||||
return excluded;
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
package com.github.romankh3.image.comparison.model;
|
||||
|
||||
import com.github.romankh3.image.comparison.ImageComparisonUtil;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data transfer objects which contains all the needed data for result of the comparison.
|
||||
*/
|
||||
public class ImageComparisonResult {
|
||||
|
||||
/**
|
||||
* {@link BufferedImage} object of the expected.
|
||||
*/
|
||||
private BufferedImage expected;
|
||||
|
||||
/**
|
||||
* {@link BufferedImage} object of the actual.
|
||||
*/
|
||||
private BufferedImage actual;
|
||||
|
||||
/**
|
||||
* {@link BufferedImage} object of the comparison result.
|
||||
*/
|
||||
private BufferedImage result;
|
||||
|
||||
/**
|
||||
* State of the comparison.
|
||||
*/
|
||||
private ImageComparisonState imageComparisonState;
|
||||
|
||||
/**
|
||||
* The difference percentage between two images.
|
||||
*/
|
||||
private float differencePercent;
|
||||
|
||||
/**
|
||||
* Rectangles of the differences
|
||||
*/
|
||||
private List<Rectangle> rectangles;
|
||||
|
||||
/**
|
||||
* Create default instance of the {@link ImageComparisonResult} with {@link ImageComparisonState#SIZE_MISMATCH}.
|
||||
*
|
||||
* @param expected expected {@link BufferedImage} object.
|
||||
* @param actual actual {@link BufferedImage} object.
|
||||
* @param differencePercent the percent of the differences between images.
|
||||
* @return instance of the {@link ImageComparisonResult} object.
|
||||
*/
|
||||
public static ImageComparisonResult defaultSizeMisMatchResult(BufferedImage expected, BufferedImage actual,
|
||||
float differencePercent) {
|
||||
return new ImageComparisonResult()
|
||||
.setImageComparisonState(ImageComparisonState.SIZE_MISMATCH)
|
||||
.setDifferencePercent(differencePercent)
|
||||
.setExpected(expected)
|
||||
.setActual(actual)
|
||||
.setResult(actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default instance of the {@link ImageComparisonResult} with {@link ImageComparisonState#MISMATCH}.
|
||||
*
|
||||
* @param expected expected {@link BufferedImage} object.
|
||||
* @param actual actual {@link BufferedImage} object.
|
||||
* @param differencePercent the persent of the differences between images.
|
||||
* @return instance of the {@link ImageComparisonResult} object.
|
||||
*/
|
||||
public static ImageComparisonResult defaultMisMatchResult(BufferedImage expected, BufferedImage actual, float differencePercent) {
|
||||
return new ImageComparisonResult()
|
||||
.setImageComparisonState(ImageComparisonState.MISMATCH)
|
||||
.setDifferencePercent(differencePercent)
|
||||
.setExpected(expected)
|
||||
.setActual(actual)
|
||||
.setResult(actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default instance of the {@link ImageComparisonResult} with {@link ImageComparisonState#MATCH}.
|
||||
*
|
||||
* @param expected expected {@link BufferedImage} object.
|
||||
* @param actual actual {@link BufferedImage} object.
|
||||
* @return instance of the {@link ImageComparisonResult} object.
|
||||
*/
|
||||
public static ImageComparisonResult defaultMatchResult(BufferedImage expected, BufferedImage actual) {
|
||||
return new ImageComparisonResult()
|
||||
.setImageComparisonState(ImageComparisonState.MATCH)
|
||||
.setExpected(expected)
|
||||
.setActual(actual)
|
||||
.setResult(actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the image to the provided {@link File} object.
|
||||
*
|
||||
* @param file the provided {@link File} object.
|
||||
* @return this {@link ImageComparisonResult} object.
|
||||
*/
|
||||
public ImageComparisonResult writeResultTo(File file) {
|
||||
ImageComparisonUtil.saveImage(file, result);
|
||||
return this;
|
||||
}
|
||||
|
||||
public BufferedImage getExpected() {
|
||||
return expected;
|
||||
}
|
||||
|
||||
public ImageComparisonResult setExpected(BufferedImage expected) {
|
||||
this.expected = expected;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BufferedImage getActual() {
|
||||
return actual;
|
||||
}
|
||||
|
||||
public ImageComparisonResult setActual(BufferedImage actual) {
|
||||
this.actual = actual;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BufferedImage getResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
public ImageComparisonResult setResult(BufferedImage result) {
|
||||
this.result = result;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ImageComparisonState getImageComparisonState() {
|
||||
return imageComparisonState;
|
||||
}
|
||||
|
||||
public ImageComparisonResult setImageComparisonState(ImageComparisonState imageComparisonState) {
|
||||
this.imageComparisonState = imageComparisonState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public float getDifferencePercent() {
|
||||
return differencePercent;
|
||||
}
|
||||
|
||||
ImageComparisonResult setDifferencePercent(float differencePercent) {
|
||||
this.differencePercent = differencePercent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<Rectangle> getRectangles() {
|
||||
return rectangles;
|
||||
}
|
||||
|
||||
public ImageComparisonResult setRectangles(List<Rectangle> rectangles) {
|
||||
this.rectangles = rectangles;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.github.romankh3.image.comparison.model;
|
||||
|
||||
/**
|
||||
* Enum for telling the result of the Comparison.
|
||||
* For example {@link ImageComparisonState#MATCH} - means that the images are equal.
|
||||
*/
|
||||
public enum ImageComparisonState {
|
||||
|
||||
/**
|
||||
* Result state of the comparison, where mismatch of the image sizes.
|
||||
*/
|
||||
SIZE_MISMATCH,
|
||||
|
||||
/**
|
||||
* Result state of the comparison, where mismatch of the images.
|
||||
*/
|
||||
MISMATCH,
|
||||
|
||||
/**
|
||||
* Result state of the images, where images are equal, e.g. match.
|
||||
*/
|
||||
MATCH
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.github.romankh3.image.comparison.model;
|
||||
|
||||
import static java.lang.Integer.max;
|
||||
import static java.lang.Integer.min;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Object contained data for a rectangle.
|
||||
*/
|
||||
public class Rectangle {
|
||||
|
||||
/**
|
||||
* Left Top {@link Point} on the {@link Rectangle}.
|
||||
*/
|
||||
private Point minPoint;
|
||||
|
||||
/**
|
||||
* Right bottom {@link Point} on the {@link Rectangle}.
|
||||
*/
|
||||
private Point maxPoint;
|
||||
|
||||
/**
|
||||
* Create empty instance of the {@link Rectangle}.
|
||||
*/
|
||||
private Rectangle() {
|
||||
minPoint = new Point();
|
||||
maxPoint = new Point();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create clone object based on the provided {@link Rectangle}.
|
||||
*
|
||||
* @param rectangle provided {@link Rectangle} object.
|
||||
*/
|
||||
public Rectangle(Rectangle rectangle) {
|
||||
this.minPoint = new Point(rectangle.getMinPoint().x, rectangle.getMinPoint().y);
|
||||
this.maxPoint = new Point(rectangle.getMaxPoint().x, rectangle.getMaxPoint().y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instance of the {@link Rectangle} based on the provided coordinates.
|
||||
*
|
||||
* @param minX minimal X-coordinate.
|
||||
* @param minY minimal Y-coordinate.
|
||||
* @param maxX maximal X-coordinate.
|
||||
* @param maxY maximal Y-coordinate.
|
||||
*/
|
||||
public Rectangle(int minX, int minY, int maxX, int maxY) {
|
||||
this.minPoint = new Point(minX, minY);
|
||||
this.maxPoint = new Point(maxX, maxY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default {@link Rectangle} object.
|
||||
*
|
||||
* @return default rectangle {@link Rectangle}.
|
||||
*/
|
||||
public static Rectangle createDefault() {
|
||||
Rectangle defaultRectangle = new Rectangle();
|
||||
|
||||
defaultRectangle.setDefaultValues();
|
||||
|
||||
return defaultRectangle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create instance with zero points.
|
||||
*
|
||||
* @return created {@link Rectangle} instance.
|
||||
*/
|
||||
public static Rectangle createZero() {
|
||||
Rectangle rectangle = new Rectangle();
|
||||
rectangle.makeZeroRectangle();
|
||||
return rectangle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link Rectangle} via merging this and that.
|
||||
*
|
||||
* @param that {@link Rectangle} for merging with this.
|
||||
* @return new merged {@link Rectangle}.
|
||||
*/
|
||||
public Rectangle merge(Rectangle that) {
|
||||
return new Rectangle(min(this.getMinPoint().x, that.getMinPoint().x),
|
||||
min(this.getMinPoint().y, that.getMinPoint().y),
|
||||
max(this.getMaxPoint().x, that.getMaxPoint().x),
|
||||
max(this.getMaxPoint().y, that.getMaxPoint().y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is that rectangle overlapp this.
|
||||
*
|
||||
* @param that {@link Rectangle} which checks with this.
|
||||
* @return true if this over lapp that, false otherwise.
|
||||
*/
|
||||
public boolean isOverlapping(Rectangle that) {
|
||||
if (this.getMaxPoint().getY() < that.getMinPoint().getY() ||
|
||||
that.getMaxPoint().getY() < this.getMinPoint().getY()) {
|
||||
return false;
|
||||
}
|
||||
return this.getMaxPoint().getX() >= that.getMinPoint().getX() &&
|
||||
that.getMaxPoint().getX() >= this.getMinPoint().getX();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values for rectangle.
|
||||
*/
|
||||
public void setDefaultValues() {
|
||||
this.maxPoint = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
|
||||
this.minPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make zero rectagle.
|
||||
*/
|
||||
public void makeZeroRectangle() {
|
||||
this.minPoint = new Point();
|
||||
this.maxPoint = new Point();
|
||||
}
|
||||
|
||||
/**
|
||||
* Size of the {@link Rectangle}, counted as width x height.
|
||||
*
|
||||
* @return the size of the {@link Rectangle}.
|
||||
*/
|
||||
public Integer size() {
|
||||
return getWidth() * getHeight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the width of the {@link Rectangle}.
|
||||
* Min and max point are included, so real width is +1px
|
||||
*
|
||||
* @return rectangle width.
|
||||
*/
|
||||
public int getWidth() {
|
||||
return maxPoint.x - minPoint.x + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the height of the {@link Rectangle}.
|
||||
* Min and max point are included, so real width is +1px.
|
||||
*
|
||||
* @return rectangle height.
|
||||
*/
|
||||
public int getHeight() {
|
||||
return maxPoint.y - minPoint.y + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check in the provided {@link Point} contains in the {@link Rectangle}.
|
||||
*
|
||||
* @param point provided {@link Point}.
|
||||
* @return {@code true} if provided {@link Point} contains, {@code false} - otherwise.
|
||||
*/
|
||||
boolean containsPoint(Point point) {
|
||||
return point.x >= minPoint.x && point.x<= maxPoint.x && point.y >= minPoint.y && point.y <= maxPoint.y;
|
||||
}
|
||||
|
||||
public Point getMinPoint() {
|
||||
return minPoint;
|
||||
}
|
||||
|
||||
public void setMinPoint(Point minPoint) {
|
||||
this.minPoint = minPoint;
|
||||
}
|
||||
|
||||
public Point getMaxPoint() {
|
||||
return maxPoint;
|
||||
}
|
||||
|
||||
public void setMaxPoint(Point maxPoint) {
|
||||
this.maxPoint = maxPoint;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Rectangle rectangle = (Rectangle) o;
|
||||
return minPoint.equals(rectangle.minPoint) &&
|
||||
maxPoint.equals(rectangle.maxPoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(minPoint, maxPoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.diylc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||
import org.diylc.appframework.miscutils.InMemoryConfigurationManager;
|
||||
import org.diylc.core.IView;
|
||||
import org.diylc.presenter.Presenter;
|
||||
import org.diylc.swing.plugins.file.ProjectDrawingProvider;
|
||||
import org.diylc.swingframework.export.DrawingExporter;
|
||||
import com.github.romankh3.image.comparison.ImageComparison;
|
||||
import com.github.romankh3.image.comparison.ImageComparisonUtil;
|
||||
import com.github.romankh3.image.comparison.model.ImageComparisonResult;
|
||||
import com.github.romankh3.image.comparison.model.ImageComparisonState;
|
||||
|
||||
public class RegressionTestRunner {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 2) {
|
||||
System.out.println("No path and command provided");
|
||||
return;
|
||||
}
|
||||
|
||||
ConfigurationManager.getInstance().initialize("diylc-test");
|
||||
|
||||
String basePath = args[0];
|
||||
String command = args[1];
|
||||
System.out.println("Running command " + command + " in: " + basePath);
|
||||
File inputsDir = new File(basePath + File.separator + "input" + File.separator + "diy");
|
||||
if (!inputsDir.exists()) {
|
||||
System.out.println("Could not find the input directory: " + inputsDir.getAbsolutePath());
|
||||
return;
|
||||
}
|
||||
File[] files = inputsDir.listFiles();
|
||||
int okCount = 0;
|
||||
int failCount = 0;
|
||||
for (File file : files) {
|
||||
boolean fileOk = processFile(file, basePath, command);
|
||||
if (fileOk) {
|
||||
okCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
System.out.println("Finished! OK files: " + okCount + "; Failed files: " + failCount);
|
||||
}
|
||||
|
||||
private static boolean processFile(File file, String basePath, String command) {
|
||||
System.out.print("Processing file: " + file.getName() + "...");
|
||||
try {
|
||||
IView view = new MockView();
|
||||
Presenter presenter = new Presenter(view, InMemoryConfigurationManager.getInstance());
|
||||
ProjectDrawingProvider drawingProvider = new ProjectDrawingProvider(presenter, false, true, false);
|
||||
presenter.loadProjectFromFile(file.getAbsolutePath());
|
||||
if ("PREPARE".equalsIgnoreCase(command)) {
|
||||
// generate PNG input image for comparison
|
||||
File pngFile = new File(basePath + File.separator + "input" + File.separator + "png" + File.separator + (file.getName().replace(".diy", ".png")));
|
||||
if (!pngFile.exists()) {
|
||||
DrawingExporter.getInstance().exportPNG(drawingProvider, pngFile);
|
||||
}
|
||||
} else if ("TEST".equalsIgnoreCase(command)) {
|
||||
// test save
|
||||
File outputFile = new File(basePath + File.separator + "output" + File.separator + "diy" + File.separator + file.getName());
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete();
|
||||
}
|
||||
presenter.saveProjectToFile(outputFile.getAbsolutePath(), false);
|
||||
|
||||
// test export png
|
||||
File pngOutputFile = new File(basePath + File.separator + "output" + File.separator + "png" + File.separator + (file.getName().replace(".diy", ".png")));
|
||||
if (pngOutputFile.exists()) {
|
||||
pngOutputFile.delete();
|
||||
}
|
||||
DrawingExporter.getInstance().exportPNG(drawingProvider, pngOutputFile);
|
||||
File pngInputFile = new File(basePath + File.separator + "input" + File.separator + "png" + File.separator + (file.getName().replace(".diy", ".png")));
|
||||
if (!pngInputFile.exists()) {
|
||||
System.out.println("Input image does not exist!");
|
||||
return false;
|
||||
}
|
||||
|
||||
BufferedImage expectedImage = ImageComparisonUtil.readImageFromResources(pngInputFile.getAbsolutePath());
|
||||
BufferedImage actualImage = ImageComparisonUtil.readImageFromResources(pngOutputFile.getAbsolutePath());
|
||||
|
||||
//Create ImageComparison object and compare the images.
|
||||
ImageComparisonResult imageComparisonResult = new ImageComparison(expectedImage, actualImage).compareImages();
|
||||
|
||||
//Check the result
|
||||
if (ImageComparisonState.MATCH != imageComparisonResult.getImageComparisonState()) {
|
||||
System.out.println("Images do not match!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
System.out.println("OK");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
System.out.println("ERROR: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user