/******************************************************************************* * Copyright 2017 Ivan Shubin http://galenframework.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.galenframework.rainbow4j; import com.galenframework.rainbow4j.colorscheme.ColorClassifier; import com.galenframework.rainbow4j.colorscheme.CustomSpectrum; import com.galenframework.rainbow4j.filters.ImageFilter; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; public class Rainbow4J { public static final int DEFAULT_COLOR_TOLERANCE_FOR_SPECTRUM = 3; public static Spectrum readSpectrum(BufferedImage image) throws IOException { return readSpectrum(image, null, 256); } public static Spectrum readSpectrum(BufferedImage image, Rectangle rectangle) throws IOException { return readSpectrum(image, rectangle, 256); } public static Spectrum readSpectrum(BufferedImage image, int precision) throws IOException { return readSpectrum(image, null, precision); } public static ImageCompareResult compare(BufferedImage imageA, BufferedImage imageB, ComparisonOptions options) throws IOException { return compare(imageA, imageB, new Rectangle(0, 0, imageA.getWidth(), imageA.getHeight()), new Rectangle(0, 0, imageB.getWidth(), imageB.getHeight()), options); } public static ImageCompareResult compare(BufferedImage imageA, BufferedImage imageB, Rectangle areaA, Rectangle areaB, ComparisonOptions options) { if (options.getTolerance() < 0 ) { options.setTolerance(0); } if (areaA.width + areaA.x > imageA.getWidth() || areaA.height + areaA.y > imageA.getHeight()) { throw new RuntimeException("Specified area is outside for original image"); } if (areaB.width + areaB.x > imageB.getWidth() || areaB.height + areaB.y > imageB.getHeight()) { throw new RuntimeException("Specified area is outside for secondary image"); } int imageAWidth = imageA.getWidth(); int imageAHeight = imageA.getHeight(); int Cax = areaA.x; int Cay = areaA.y; int Cbx = areaB.x; int Cby = areaB.y; int Wa = areaA.width; int Ha = areaA.height; int Wb = areaB.width; int Hb = areaB.height; double Kx = ((double)Wb) / ((double)Wa); double Ky = ((double)Hb) / ((double)Ha); ImageHandler handlerA = new ImageHandler(imageA); ImageHandler handlerB = new ImageHandler(imageB); applyAllFilters(areaA, areaB, options, handlerA, handlerB); int tolerance = options.getTolerance(); long minMismatchingPixels = Integer.MAX_VALUE; ImageHandler resultingMapHandler = null; int resultingOffsetX = 0; int resultingOffsetY = 0; // Here it moves within a spiral for better performance when analyzing a large offset int offsetX = 0; int offsetY = 0; int spiral_dx = 0; int spiral_dy = -1; int spiral_n = 0; if (options.getAnalyzeOffset() > 0) { spiral_n = options.getAnalyzeOffset() * 2 + 1; } int max_spiral = spiral_n * spiral_n; for (int spiral_i = 0; spiral_i <= max_spiral; spiral_i++) { if ((offsetX == offsetY) || (offsetX < 0 && offsetX == -offsetY) || (offsetX > 0 && offsetX == 1 - offsetY)){ int temp = spiral_dx; spiral_dx = -spiral_dy; spiral_dy = temp; } ImageHandler mapHandler = new ImageHandler(areaA.width, areaA.height); long mismatchingPixels = 0; int x = 0, y = 0; while(y < Ha && minMismatchingPixels > 0) { while (x < Wa && mismatchingPixels < minMismatchingPixels) { int xA = x + Cax + offsetX; int yA = y + Cay + offsetY; if (xA >= 0 && xA < imageAWidth && yA >= 0 && yA < imageAHeight ) { if (!shouldPixelBeIgnored(xA, yA, options)) { Color cA = handlerA.pickColor(xA, yA); int xB, yB; if (options.isStretchToFit()) { xB = (int) Math.round((((double) x) * Kx) + Cbx); yB = (int) Math.round(((double) y) * Ky + Cby); xB = Math.min(xB, Cbx + Wb - 1); yB = Math.min(yB, Cby + Hb - 1); } else { xB = x + Cbx; yB = y + Cby; } Color cB = handlerB.pickColor(xB, yB); long colorError = ImageHandler.colorDiff(cA, cB); if (colorError > tolerance) { Color color = Color.red; int diff = (int) (colorError - tolerance); if (diff > 30 && diff < 80) { color = Color.yellow; } else if (diff <= 30) { color = Color.green; } mapHandler.setRGBA(x, y, color.getRed(), color.getGreen(), color.getBlue(), 255); mismatchingPixels += 1; } else { mapHandler.setRGBA(x, y, 0, 0, 0, 255); } } else { mapHandler.setRGBA(x, y, 0, 0, 0, 160); } } else { mapHandler.setRGBA(x, y, 0, 0, 0, 255); } x += 1; } y += 1; x = 0; } if (mismatchingPixels < minMismatchingPixels) { minMismatchingPixels = mismatchingPixels; resultingOffsetX = offsetX; resultingOffsetY = offsetY; resultingMapHandler = mapHandler; } offsetX += spiral_dx; offsetY += spiral_dy; } applyFilters(resultingMapHandler, options.getMapFilters(), new Rectangle(0, 0, resultingMapHandler.getWidth(), resultingMapHandler.getHeight())); ImageCompareResult result = analyzeComparisonMap(resultingMapHandler); result.setOffsetX(resultingOffsetX); result.setOffsetY(resultingOffsetY); result.setOriginalFilteredImage(handlerA.getImage().getSubimage(areaA.x, areaA.y, areaA.width, areaA.height)); result.setSampleFilteredImage(handlerB.getImage().getSubimage(areaB.x, areaB.y, areaB.width, areaB.height)); return result; } private static boolean shouldPixelBeIgnored(int x, int y, ComparisonOptions options) { if (options != null && options.getIgnoreRegions() != null) { for (Rectangle rectangle : options.getIgnoreRegions()) { if (rectangle.contains(x, y)) { return true; } } } return false; } private static ImageCompareResult analyzeComparisonMap(ImageHandler mapHandler) { ImageCompareResult result = new ImageCompareResult(); long totalMismatchingPixels = 0; ByteBuffer bytes = mapHandler.getBytes(); for (int k = 0; k < bytes.capacity() - ImageHandler.BLOCK_SIZE; k += ImageHandler.BLOCK_SIZE) { if (((int)bytes.get(k) &0xff) > 0 || ((int)bytes.get(k + 1) &0xff) > 0 || ((int)bytes.get(k + 2) &0xff) > 0) { totalMismatchingPixels++; } } double totalPixels = (mapHandler.getWidth() * mapHandler.getHeight()); result.setPercentage(100.0 * totalMismatchingPixels / totalPixels); result.setTotalPixels(totalMismatchingPixels); result.setComparisonMap(mapHandler.getImage()); return result; } private static void applyAllFilters(Rectangle areaA, Rectangle areaB, ComparisonOptions options, ImageHandler handlerA, ImageHandler handlerB) { applyFilters(handlerA, options.getOriginalFilters(), areaA); applyFilters(handlerB, options.getSampleFilters(), areaB); } private static void applyFilters(ImageHandler handler, List<ImageFilter> filters, Rectangle area) { if (filters != null) { for (ImageFilter filter : filters) { handler.applyFilter(filter, area); } } } /** * * @param image an image for calculating the color spectrum * @param precision 8 to 256 value for spectrum accuracy. The bigger value - the better precision, but the more memory it takes * @return * @throws IOException */ public static Spectrum readSpectrum(BufferedImage image, Rectangle area, int precision) throws IOException { if (precision < 8) throw new IllegalArgumentException("Color size should not be less then 8"); if (precision > 256) throw new IllegalArgumentException("Color size should not be bigger then 256"); int spectrum[][][] = new int[precision][precision][precision]; int width = image.getWidth(); int height = image.getHeight(); int[] a = new int[width * height]; image.getRGB(0, 0, width, height, a, 0, width); int spectrumWidth = width; int spectrumHeight = height; if (area == null) { area = new Rectangle(0, 0, width, height); } else { spectrumWidth = area.width; spectrumHeight = area.height; } int k = 0; int r,g,b; for (int y = area.y; y < area.y + area.height; y++) { for (int x = area.x; x < area.x + area.width; x++) { k = y * width + x; r = ((a[k] >> 16) & 0xff) * precision / 256; g = ((a[k] >> 8) & 0xff) * precision / 256; b = ((a[k]) & 0xff) * precision / 256; spectrum[Math.min(r, precision - 1)][Math.min(g, precision - 1)][Math.min(b, precision - 1)] += 1; } } return new Spectrum(spectrum, spectrumWidth, spectrumHeight); } public static CustomSpectrum readCustomSpectrum(BufferedImage image, List<ColorClassifier> colorClassifiers) { return readCustomSpectrum(image, colorClassifiers, new Rectangle(0, 0, image.getWidth(), image.getHeight())); } public static CustomSpectrum readCustomSpectrum(BufferedImage image, List<ColorClassifier> colorClassifiers, Rectangle area) { return readCustomSpectrum(image, colorClassifiers, area, DEFAULT_COLOR_TOLERANCE_FOR_SPECTRUM); } public static CustomSpectrum readCustomSpectrum(BufferedImage image, List<ColorClassifier> colorClassifiers, Rectangle area, int colorTolerance) { int maxColorSquareDistance = colorTolerance*colorTolerance*3; Map<String, AtomicInteger> colorPickers = new HashMap<>(); for (ColorClassifier classifier : colorClassifiers) { colorPickers.put(classifier.getName(), new AtomicInteger(0)); } int amountOfUnmatchedColor = 0; int width = image.getWidth(); int height = image.getHeight(); int[] a = new int[width * height]; image.getRGB(0, 0, width, height, a, 0, width); if (area == null) { area = new Rectangle(0, 0, width, height); } int k = 0; int r,g,b; for (int y = area.y; y < area.y + area.height; y++) { for (int x = area.x; x < area.x + area.width; x++) { k = y * width + x; r = ((a[k] >> 16) & 0xff); g = ((a[k] >> 8) & 0xff); b = ((a[k]) & 0xff); boolean colorMatched = false; for (ColorClassifier classifier : colorClassifiers) { if (classifier.holdsColor(r, g, b, maxColorSquareDistance)) { colorPickers.get(classifier.getName()).incrementAndGet(); colorMatched = true; } } if (!colorMatched) { amountOfUnmatchedColor += 1; } } } int totalPixels = area.height * area.width; Map<String, Integer> collectedColors = colorPickers.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get())); return new CustomSpectrum(collectedColors, amountOfUnmatchedColor, totalPixels); } public static BufferedImage loadImage(String filePath) throws IOException{ return ImageIO.read(new File(filePath)); } public static BufferedImage loadImage(InputStream stream) throws IOException { return ImageIO.read(stream); } public static void saveImage(BufferedImage image, File file) throws IOException { ImageIO.write(image, "png", file); } }