com.googlecode.fightinglayoutbugs.helpers.ImageHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.googlecode.fightinglayoutbugs.helpers.ImageHelper.java

Source

/*
 * Copyright 2009-2012 Michael Tamm
 *
 * 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.googlecode.fightinglayoutbugs.helpers;

import org.apache.commons.io.IOUtils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.Raster;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;

import static com.googlecode.fightinglayoutbugs.helpers.FileHelper.createParentDirectoryIfNeeded;

public class ImageHelper {

    public static BufferedImage urlToImage(URL imageUrl) {
        BufferedImage image;
        try {
            image = ImageIO.read(imageUrl);
        } catch (IOException e) {
            throw new RuntimeException("Failed to read image from URL: " + imageUrl, e);
        }
        return image;
    }

    public static BufferedImage fileToImage(File imageFile) {
        BufferedImage image;
        try {
            image = ImageIO.read(imageFile);
        } catch (IOException e) {
            throw new RuntimeException("Failed to read image file: " + imageFile, e);
        }
        return image;
    }

    public static int[][] urlToPixels(URL imageUrl) {
        BufferedImage image = urlToImage(imageUrl);
        int[][] pixels = imageToPixels(image);
        return pixels;
    }

    public static int[][] fileToPixels(File imageFile) {
        BufferedImage image = fileToImage(imageFile);
        int[][] pixels = imageToPixels(image);
        return pixels;
    }

    public static int[][] pngToPixels(byte[] png) {
        InputStream in = new ByteArrayInputStream(png);
        try {
            BufferedImage image = ImageIO.read(in);
            return imageToPixels(image);
        } catch (IOException e) {
            throw new RuntimeException("Should never happen.", e);
        } finally {
            IOUtils.closeQuietly(in);
        }
    }

    public static void pixelsToPngFile(int[][] pixels, File pngFile) {
        BufferedImage image = pixelsToImage(pixels);
        createParentDirectoryIfNeeded(pngFile);
        imageToPngFile(image, pngFile);
    }

    public static void imageToPngFile(BufferedImage image, File pngFile) {
        try {
            ImageIO.write(image, "png", pngFile);
        } catch (IOException e) {
            throw new RuntimeException("Failed to write image to file: " + pngFile, e);
        }
    }

    public static void pixelsToPngFile(boolean[][] pixels, File pngFile) {
        BufferedImage image = pixelsToImage(pixels);
        createParentDirectoryIfNeeded(pngFile);
        imageToPngFile(image, pngFile);
    }

    public static int[][] imageToPixels(BufferedImage image) {
        if (image == null) {
            return null;
        }
        int w = image.getWidth();
        int h = image.getHeight();
        int[][] pixels = new int[w][h];
        Raster raster = image.getRaster();
        if (raster.getTransferType() == DataBuffer.TYPE_BYTE) {
            byte[] bytes = (byte[]) raster.getDataElements(0, 0, w, h, null);
            int bytesPerPixel = (bytes.length / (w * h));
            ColorModel colorModel = image.getColorModel();
            byte[] buf = new byte[bytesPerPixel];
            for (int x = 0; x < w; ++x) {
                for (int y = 0; y < h; ++y) {
                    System.arraycopy(bytes, (x + y * w) * bytesPerPixel, buf, 0, bytesPerPixel);
                    pixels[x][y] = colorModel.getRGB(buf) & 0xFFFFFF;
                }
            }
            return pixels;
        } else {
            throw new RuntimeException("transfer type " + raster.getTransferType() + " not implemented yet");
        }
    }

    public static BufferedImage pixelsToImage(int[][] pixels) {
        if (pixels == null) {
            return null;
        }
        int w = pixels.length;
        if (w > 0) {
            int h = pixels[0].length;
            if (h > 0) {
                BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
                for (int x = 0; x < w; ++x) {
                    int[] column = pixels[x];
                    for (int y = 0; y < h; ++y) {
                        image.setRGB(x, y, column[y]);
                    }
                }
                return image;
            } else {
                throw new IllegalArgumentException("pixels[0].length must not be 0.");
            }
        } else {
            throw new IllegalArgumentException("pixels.length must not be 0.");
        }
    }

    public static BufferedImage pixelsToImage(boolean[][] pixels) {
        if (pixels == null) {
            return null;
        }
        int w = pixels.length;
        if (w > 0) {
            int h = pixels[0].length;
            if (h > 0) {
                BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
                for (int x = 0; x < w; ++x) {
                    boolean[] column = pixels[x];
                    for (int y = 0; y < h; ++y) {
                        image.setRGB(x, y, column[y] ? 0 : 0xFFFFFF);
                    }
                }
                return image;
            } else {
                throw new IllegalArgumentException("pixels[0].length must not be 0.");
            }
        } else {
            throw new IllegalArgumentException("pixels.length must not be 0.");
        }
    }

    public static int[][] copyOf(int[][] pixels) {
        int[][] copy;
        if (pixels == null) {
            copy = null;
        } else {
            int w = pixels.length;
            if (w == 0) {
                copy = new int[0][];
            } else {
                int h = pixels[0].length;
                copy = new int[w][h];
                if (h > 0) {
                    for (int x = 0; x < w; ++x) {
                        System.arraycopy(pixels[x], 0, copy[x], 0, h);
                    }
                }
            }
        }
        return copy;
    }

    /**
     * Returns {@code null} if the given subImage can not be found in the given image.
     */
    @Nullable
    public static RectangularRegion findFirstSubImageInImage(@Nonnull BufferedImage subImage,
            @Nonnull BufferedImage image) {
        List<RectangularRegion> temp = findSubImageInImage(subImage, image, 1);
        return (temp.isEmpty() ? null : temp.get(0));
    }

    /**
     * Returns all rectangular regions where the given {@code subImage} is found in the given {@code image}.
     * Returns an empty collection if no occurrence is found.
     * Pixels of {@code subImage} with an alpha value lower than 255 are ignored.
     */
    @Nonnull
    public static Collection<RectangularRegion> findSubImageInImage(@Nonnull BufferedImage subImage,
            @Nonnull BufferedImage image) {
        return findSubImageInImage(subImage, image, Integer.MAX_VALUE);
    }

    private static List<RectangularRegion> findSubImageInImage(BufferedImage subImage, BufferedImage image,
            int max) {
        Map<Integer, List<Point>> rgb2offsets = new HashMap<Integer, List<Point>>();
        int sw = subImage.getWidth();
        int sh = subImage.getHeight();
        for (int x = 0; x < sw; ++x) {
            for (int y = 0; y < sh; ++y) {
                int argb = subImage.getRGB(x, y);
                int a = argb >>> 24;
                if (a == 255) {
                    Integer rgb = argb & 0xFFFFFF;
                    List<Point> offsets = rgb2offsets.get(rgb);
                    if (offsets == null) {
                        offsets = new ArrayList<Point>();
                        rgb2offsets.put(rgb, offsets);
                    }
                    offsets.add(new Point(x, y));
                }
            }
        }
        List<RectangularRegion> result = new ArrayList<RectangularRegion>();
        int w = image.getWidth();
        int h = image.getHeight();
        int[][] p = new int[w][h];
        Raster raster = image.getRaster();
        if (raster.getTransferType() == DataBuffer.TYPE_BYTE) {
            byte[] bytes = (byte[]) raster.getDataElements(0, 0, w, h, null);
            int bytesPerPixel = (bytes.length / (w * h));
            ColorModel colorModel = image.getColorModel();
            byte[] buf = new byte[bytesPerPixel];
            for (int x = 0; x < w; ++x) {
                for (int y = 0; y < h; ++y) {
                    System.arraycopy(bytes, (x + y * w) * bytesPerPixel, buf, 0, bytesPerPixel);
                    p[x][y] = colorModel.getRGB(buf) & 0xFFFFFF;
                }
            }
        } else if (raster.getTransferType() == DataBuffer.TYPE_INT) {
            for (int x = 0; x < w; ++x) {
                p[x] = (int[]) raster.getDataElements(x, 0, 1, h, null);
            }
        } else {
            throw new RuntimeException("findSubImageInImage not implemented for image transfer type "
                    + raster.getTransferType() + " yet.");
        }
        for (int x = 0; x < w; ++x) {
            for (int y = 0; y < h; ++y) {
                Iterator<Map.Entry<Integer, List<Point>>> i = rgb2offsets.entrySet().iterator();
                compareWithSubImageLoop: while (i.hasNext()) {
                    Map.Entry<Integer, List<Point>> mapEntry = i.next();
                    int expectedRgb = mapEntry.getKey();
                    for (Point offset : mapEntry.getValue()) {
                        int xx = x + offset.x;
                        int yy = y + offset.y;
                        if (xx >= w || yy >= h || expectedRgb != p[xx][yy]) {
                            break compareWithSubImageLoop;
                        }
                    }
                    if (!i.hasNext()) {
                        result.add(new RectangularRegion(x, y, x + (sw - 1), y + (sh - 1)));
                        if (result.size() == max) {
                            return result;
                        }
                    }
                }
            }
        }
        return result;
    }

    public static void applyConvolutionFilter(int[][] pixels, float[][] kernel) {
        if (kernel == null) {
            throw new IllegalArgumentException("Method parameter kernel must not be null.");
        }
        int kernelSize = kernel.length;
        if (kernelSize % 2 == 0) {
            throw new IllegalArgumentException("Method parameter kernel must have odd size. (e.g. 3, 5, 7, ...)");
        }
        if (kernel[0].length != kernelSize) {
            throw new IllegalArgumentException(
                    "Method parameter kernel must have square dimensions. (e.g. 3x3, 5x5, 7x7, ...)");
        }
        if (pixels != null) {
            int w = pixels.length;
            if (w > 0) {
                int h = pixels[0].length;
                if (h > 0) {
                    int[][] r = new int[w][h];
                    int[][] g = new int[w][h];
                    int[][] b = new int[w][h];
                    int[][] a = splitIntoChannels(pixels, r, g, b);
                    if (a != null) {
                        applyConvolutionFilterToChannel(a, kernel);
                    }
                    applyConvolutionFilterToChannel(r, kernel);
                    applyConvolutionFilterToChannel(g, kernel);
                    applyConvolutionFilterToChannel(b, kernel);
                    if (a != null) {
                        combineChannels(a, r, g, b, pixels);
                    } else {
                        combineChannels(r, g, b, pixels);
                    }
                }
            }
        }
    }

    public static void combineChannels(int[][] a, int[][] r, int[][] g, int[][] b, int[][] pixels) {
        int w = pixels.length;
        int h = pixels[0].length;
        for (int x = 0; x < w; ++x) {
            for (int y = 0; y < h; ++y) {
                pixels[x][y] = (a[x][y] << 24) | (r[x][y] << 16) | (g[x][y] << 8) | b[x][y];
            }
        }
    }

    public static void combineChannels(int[][] r, int[][] g, int[][] b, int[][] pixels) {
        int w = pixels.length;
        int h = pixels[0].length;
        for (int x = 0; x < w; ++x) {
            for (int y = 0; y < h; ++y) {
                pixels[x][y] = (r[x][y] << 16) | (g[x][y] << 8) | b[x][y];
            }
        }
    }

    /**
     * If <code>pixels</code> has alpha channel, another <code>int[][]</code> array is allocated,
     * filled with the alpha values, and returned, otherwise <code>null</code> is returned.
     */
    public static int[][] splitIntoChannels(int[][] pixels, int[][] r, int[][] g, int[][] b) {
        int[][] a = null;
        int w = pixels.length;
        int h = pixels[0].length;
        for (int x = 0; x < w; ++x) {
            for (int y = 0; y < h; ++y) {
                int p = pixels[x][y];
                int alpha = p >>> 24;
                if (alpha > 0) {
                    if (a == null) {
                        a = new int[w][h];
                    }
                    a[x][y] = alpha;
                }
                r[x][y] = (p & 0xFF0000) >> 16;
                g[x][y] = (p & 0xFF00) >> 8;
                b[x][y] = (p & 0xFF);
            }
        }
        return a;
    }

    public static void applyConvolutionFilterToChannel(int[][] channel, float[][] kernel) {
        int w = channel.length;
        int h = channel[0].length;
        int kernelSize = kernel.length;
        int r = (kernelSize / 2);
        int xx, yy;
        float n, d;
        // for each column ...
        for (int x = 0; x < w; ++x) {
            // for each row ...
            for (int y = 0; y < h; ++y) {
                n = d = 0;
                // for each kernel column ...
                for (int i = 0; i < kernelSize; ++i) {
                    xx = x + (i - r);
                    if (0 <= xx && xx < w) {
                        // for each kernel row ...
                        for (int j = 0; j < kernelSize; ++j) {
                            yy = y + (j - r);
                            if (0 <= yy && yy < h) {
                                float k = kernel[i][j];
                                int oldValue = channel[xx][yy];
                                assert 0 <= oldValue && oldValue <= 255;
                                n += k * oldValue;
                                d += k;
                            }
                        }
                    }
                }
                int newValue = Math.round(n / d);
                assert 0 <= newValue && newValue <= 255;
                channel[x][y] = newValue;
            }
        }
    }

    public static void gaussianBlur(int[][] pixels, float sigma) {
        int kernelSize = (int) Math.ceil(6 * sigma);
        if (kernelSize % 2 == 0) {
            ++kernelSize;
        }
        if (kernelSize < 3) {
            kernelSize = 3;
        }
        float[][] kernel = new float[kernelSize][kernelSize];
        int m = kernelSize / 2;
        double q = 2 * sigma * sigma;
        double f = 1 / (Math.PI * q);
        for (int x = 0; x < kernelSize; ++x) {
            int dx = x - m;
            for (int y = 0; y < kernelSize; ++y) {
                int dy = y - m;
                kernel[x][y] = (float) (f * Math.exp(-((dx * dx + dy * dy) / q)));
            }
        }
        applyConvolutionFilter(pixels, kernel);
    }

    /**
     * Blends pixelsWithAlpha into pixels.
     */
    public static void blend(int[][] pixels, int[][] pixelsWithAlpha) {
        if (pixels != null && pixelsWithAlpha != null) {
            int w = Math.min(pixels.length, pixelsWithAlpha.length);
            if (w > 0) {
                int h = Math.min(pixels[0].length, pixelsWithAlpha[0].length);
                if (h > 0) {
                    for (int x = 0; x < w; ++x) {
                        for (int y = 0; y < h; ++y) {
                            int p2 = pixelsWithAlpha[x][y];
                            int a = p2 >>> 24;
                            if (a < 0xFF) {
                                if (a == 0) {
                                    pixels[x][y] = p2;
                                } else {
                                    float a2 = ((float) (0xFF - a)) / 0xFF;
                                    assert 0 < a2 && a2 < 1;
                                    float a1 = 1 - a2;
                                    int p1 = pixels[x][y];
                                    int r = (p1 & 0xFF0000) >> 16;
                                    int g = (p1 & 0xFF00) >> 8;
                                    int b = (p1 & 0xFF);
                                    r = Math.round(a1 * r + a2 * ((p2 & 0xFF0000) >> 16));
                                    assert r <= 0xFF;
                                    g = Math.round(a1 * g + a2 * ((p2 & 0xFF00) >> 8));
                                    assert g <= 0xFF;
                                    b = Math.round(a1 * b + a2 * ((p2 & 0xFF)));
                                    assert b <= 0xFF;
                                    pixels[x][y] = (r << 16) | (g << 8) | b;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Find the outlines of all areas where <code>pixels[x][y]</code> is <code>true</code>.
     */
    public static boolean[][] findOutlines(boolean[][] pixels) {
        int w = pixels.length;
        int h = pixels[0].length;
        int w1 = w - 1;
        int h1 = h - 1;
        boolean[][] outlines = new boolean[w][h];
        // Find starting point ...
        int x0 = 0;
        int y0 = 0;
        // Look for starting point on top border ...
        while (x0 < w && pixels[x0][y0]) {
            // ... and bottom border ...
            if (!pixels[x0][h1]) {
                y0 = h1;
                break;
            }
            ++x0;
        }
        if (x0 == w) {
            // Look for starting point on left border ...
            x0 = 1;
            // ... and right border ...
            while (y0 < h && pixels[x0][y0]) {
                if (!pixels[w1][y0]) {
                    x0 = w1;
                    break;
                }
                ++y0;
            }
        }
        if (y0 == h) {
            // No starting point found, therefore ...
            return outlines;
        }
        // Find outlines ...
        Queue<Point> todo = new LinkedList<Point>();
        todo.add(new Point(x0, y0));
        boolean[][] visited = new boolean[w][h];
        while (!todo.isEmpty()) {
            Point p = todo.poll();
            int x = p.x;
            int y = p.y;
            if (!visited[x][y]) {
                visited[x][y] = true;
                if (!pixels[x][y]) {
                    // Compare with pixel above ...
                    if (y > 0) {
                        int y1 = y - 1;
                        if (pixels[x][y1]) {
                            outlines[x][y] = true;
                        } else if (!visited[x][y1]) {
                            todo.add(new Point(x, y1));
                        }
                    }
                    // Compare with pixel to the right ...
                    if (x < w1) {
                        int x1 = x + 1;
                        if (pixels[x1][y]) {
                            outlines[x][y] = true;
                        } else if (!visited[x1][y]) {
                            todo.add(new Point(x1, y));
                        }
                    }
                    // Compare with pixel below ...
                    if (y < h1) {
                        int y1 = y + 1;
                        if (pixels[x][y1]) {
                            outlines[x][y] = true;
                        } else if (!visited[x][y1]) {
                            todo.add(new Point(x, y1));
                        }
                    }
                    // Compare with pixel to the left ...
                    if (x > 0) {
                        int x1 = x - 1;
                        if (pixels[x1][y]) {
                            outlines[x][y] = true;
                        } else if (!visited[x1][y]) {
                            todo.add(new Point(x1, y));
                        }
                    }
                }
            }
        }
        return outlines;
    }

    /**
     * Determines the contrast between the two given pixels based on the
     * <a href="http://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-procedure">WCAG 2.0 formula</a>.
     */
    public static double getContrast(int rgb1, int rgb2) {
        double l1 = getLuminance(rgb1);
        double l2 = getLuminance(rgb2);
        return ((l1 >= l2) ? (l1 + 0.05) / (l2 + 0.05) : (l2 + 0.05) / (l1 + 0.05));
    }

    private static double[] PRE_CALCULATED_LUMINANCE_TABLE = new double[256];
    static {
        for (int i = 0; i < 256; ++i) {
            double x = i / 255.0;
            PRE_CALCULATED_LUMINANCE_TABLE[i] = (x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4));
        }
    }

    private static double getLuminance(int rgb) {
        double r = PRE_CALCULATED_LUMINANCE_TABLE[(rgb & 0xFF0000) >> 16];
        double g = PRE_CALCULATED_LUMINANCE_TABLE[(rgb & 0xFF00) >> 8];
        double b = PRE_CALCULATED_LUMINANCE_TABLE[(rgb & 0xFF)];
        return 0.2126 * r + 0.7152 * g + 0.0722 * b;
    }

    protected ImageHelper() {
    }
}