/* * Copyright 2013 JavaANPR contributors * Copyright 2006 Ondrej Martinsky * Licensed under the Educational Community 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.osedu.org/licenses/ECL-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 net.sf.javaanpr.imageanalysis; import net.sf.javaanpr.configurator.Configurator; import javax.imageio.ImageIO; import java.awt.*; import java.awt.geom.AffineTransform; import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.awt.image.LookupOp; import java.awt.image.ShortLookupTable; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; public class Photo implements AutoCloseable, Cloneable { private BufferedImage image; public Photo(BufferedImage bi) { image = bi; } public Photo(InputStream is) throws IOException { loadImage(is); } public static void setBrightness(BufferedImage image, int x, int y, float value) { image.setRGB(x, y, new Color(value, value, value).getRGB()); } public static float getBrightness(BufferedImage image, int x, int y) { int r = image.getRaster().getSample(x, y, 0); int g = image.getRaster().getSample(x, y, 1); int b = image.getRaster().getSample(x, y, 2); float[] hsb = Color.RGBtoHSB(r, g, b, null); return hsb[2]; } public static float getSaturation(BufferedImage image, int x, int y) { int r = image.getRaster().getSample(x, y, 0); int g = image.getRaster().getSample(x, y, 1); int b = image.getRaster().getSample(x, y, 2); float[] hsb = Color.RGBtoHSB(r, g, b, null); return hsb[1]; } public static float getHue(BufferedImage image, int x, int y) { int r = image.getRaster().getSample(x, y, 0); int g = image.getRaster().getSample(x, y, 1); int b = image.getRaster().getSample(x, y, 2); float[] hsb = Color.RGBtoHSB(r, g, b, null); return hsb[0]; } /** * Converts a given Image into a BufferedImage. * * @param img The Image to be converted * @return the converted BufferedImage */ public static BufferedImage toBufferedImage(Image img) { if (img instanceof BufferedImage) { return (BufferedImage) img; } // Create a buffered image with transparency BufferedImage bimage = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB); // Draw the image on to the buffered image Graphics2D bGr = bimage.createGraphics(); bGr.drawImage(img, 0, 0, null); bGr.dispose(); // Return the buffered image return bimage; } public static BufferedImage linearResizeBi(BufferedImage origin, int width, int height) { BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = resizedImage.createGraphics(); float xScale = (float) width / origin.getWidth(); float yScale = (float) height / origin.getHeight(); AffineTransform at = AffineTransform.getScaleInstance(xScale, yScale); g.drawRenderedImage(origin, at); g.dispose(); return resizedImage; } public static BufferedImage duplicateBufferedImage(BufferedImage image) { BufferedImage imageCopy = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); imageCopy.setData(image.getData()); return imageCopy; } static void thresholding(BufferedImage bi) { // TODO Optimize short[] threshold = new short[256]; for (short i = 0; i < 36; i++) { threshold[i] = 0; } for (short i = 36; i < 256; i++) { threshold[i] = i; } BufferedImageOp thresholdOp = new LookupOp(new ShortLookupTable(0, threshold), null); thresholdOp.filter(bi, bi); } public static BufferedImage arrayToBufferedImage(float[][] array, int w, int h) { BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { Photo.setBrightness(bi, x, y, array[x][y]); } } return bi; } public static BufferedImage createBlankBi(BufferedImage image) { return new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); } @Override public Photo clone() { try { super.clone(); } catch (CloneNotSupportedException e) { throw new IllegalStateException("Super clone not supported."); } return new Photo(duplicateBufferedImage(image)); } public int getWidth() { return image.getWidth(); } public int getHeight() { return image.getHeight(); } public int getRGB(int x, int y) { return image.getRGB(x, y); } public BufferedImage getImage() { return image; } /** * Package-protected! * <p> * TODO: Should really be private??? * * @param image the new image */ void setImage(BufferedImage image) { this.image = image; } public BufferedImage getBiWithAxes() { BufferedImage axis = new BufferedImage(image.getWidth() + 40, image.getHeight() + 40, BufferedImage.TYPE_INT_RGB); Graphics2D graphicAxis = axis.createGraphics(); graphicAxis.setColor(Color.LIGHT_GRAY); Rectangle backRect = new Rectangle(0, 0, image.getWidth() + 40, image.getHeight() + 40); graphicAxis.fill(backRect); graphicAxis.draw(backRect); graphicAxis.drawImage(image, 35, 5, null); graphicAxis.setColor(Color.BLACK); graphicAxis.drawRect(35, 5, image.getWidth(), image.getHeight()); for (int ax = 0; ax < image.getWidth(); ax += 50) { graphicAxis.drawString(Integer.toString(ax), ax + 35, axis.getHeight() - 10); graphicAxis.drawLine(ax + 35, image.getHeight() + 5, ax + 35, image.getHeight() + 15); } for (int ay = 0; ay < image.getHeight(); ay += 50) { graphicAxis.drawString(Integer.toString(ay), 3, ay + 15); graphicAxis.drawLine(25, ay + 5, 35, ay + 5); } graphicAxis.dispose(); return axis; } public void setBrightness(int x, int y, float value) { image.setRGB(x, y, new Color(value, value, value).getRGB()); } public float getBrightness(int x, int y) { return Photo.getBrightness(image, x, y); } public float getSaturation(int x, int y) { return Photo.getSaturation(image, x, y); } public float getHue(int x, int y) { return Photo.getHue(image, x, y); } public void loadImage(InputStream is) throws IOException { BufferedImage image = ImageIO.read(is); BufferedImage outImage = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB); Graphics2D g = outImage.createGraphics(); g.drawImage(image, 0, 0, null); g.dispose(); this.image = outImage; } public void saveImage(String filepath) throws IOException { String type = filepath.substring(filepath.lastIndexOf('.') + 1, filepath.length()).toUpperCase(); if (!type.equals("BMP") && !type.equals("JPG") && !type.equals("JPEG") && !type.equals("PNG")) { throw new IOException("Unsupported file format"); } File destination = new File(filepath); ImageIO.write(image, type, destination); } public void normalizeBrightness(float coef) { Statistics stats = new Statistics(this); for (int x = 0; x < getWidth(); x++) { for (int y = 0; y < getHeight(); y++) { Photo.setBrightness(image, x, y, stats.thresholdBrightness(Photo.getBrightness(image, x, y), coef)); } } } // FILTERS public void linearResize(int width, int height) { image = Photo.linearResizeBi(image, width, height); } public void averageResize(int width, int height) { image = averageResizeBi(image, width, height); } public BufferedImage averageResizeBi(BufferedImage origin, int width, int height) { // TODO Doesn't work well for characters of size similar to the target size if ((origin.getWidth() < width) || (origin.getHeight() < height)) { // average height doesn't play well with zooming in; if we are zooming in in direction x or y, // use linear transformation return Photo.linearResizeBi(origin, width, height); } // Java traditionally make images smaller with the bilinear method (linear mapping), which brings large // information loss. Fourier transformation would be ideal, but it is too slow. // Therefore We use the method of weighted average. BufferedImage resized = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); float xScale = (float) origin.getWidth() / width; float yScale = (float) origin.getHeight() / height; for (int x = 0; x < width; x++) { int x0min = Math.round(x * xScale); int x0max = Math.round((x + 1) * xScale); for (int y = 0; y < height; y++) { int y0min = Math.round(y * yScale); int y0max = Math.round((y + 1) * yScale); // do a neigborhood average and save to resizedImage float sum = 0; int sumCount = 0; for (int x0 = x0min; x0 < x0max; x0++) { for (int y0 = y0min; y0 < y0max; y0++) { sum += Photo.getBrightness(origin, x0, y0); sumCount++; } } sum /= sumCount; Photo.setBrightness(resized, x, y, sum); } } return resized; } public Photo duplicate() { return new Photo(Photo.duplicateBufferedImage(image)); } public void verticalEdgeDetector(BufferedImage source) { BufferedImage destination = Photo.duplicateBufferedImage(source); float[] data1 = {-1, 0, 1, -2, 0, 2, -1, 0, 1}; new ConvolveOp(new Kernel(3, 3, data1), ConvolveOp.EDGE_NO_OP, null).filter(destination, source); } public float[][] bufferedImageToArray(BufferedImage image, int w, int h) { float[][] array = new float[w][h]; for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { array[x][y] = Photo.getBrightness(image, x, y); } } return array; } public float[][] bufferedImageToArrayWithBounds(BufferedImage image, int w, int h) { float[][] array = new float[w + 2][h + 2]; for (int x = 0; x < w; x++) { for (int y = 0; y < h; y++) { array[x + 1][y + 1] = Photo.getBrightness(image, x, y); } } // clear the edges for (int x = 0; x < (w + 2); x++) { array[x][0] = 1; array[x][h + 1] = 1; } for (int y = 0; y < (h + 2); y++) { array[0][y] = 1; array[w + 1][y] = 1; } return array; } public BufferedImage createBlankBi(int width, int height) { return new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); } /** * Used by edge detectors. * * @param image1 first image * @param image2 second image * @return their "sum" */ public BufferedImage sumBufferedImages(BufferedImage image1, BufferedImage image2) { BufferedImage out = new BufferedImage(Math.min(image1.getWidth(), image2.getWidth()), Math.min(image1.getHeight(), image2.getHeight()), BufferedImage.TYPE_INT_RGB); for (int x = 0; x < out.getWidth(); x++) { for (int y = 0; y < out.getHeight(); y++) { Photo.setBrightness(out, x, y, (float) Math.min(1.0, Photo.getBrightness(image1, x, y) + Photo.getBrightness(image2, x, y))); } } return out; } public void plainThresholding(Statistics stat) { int width = getWidth(); int height = getHeight(); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { setBrightness(x, y, stat.thresholdBrightness(getBrightness(x, y), 1.0f)); } } } /** * Adaptive thresholding through GetNeighborhood. * * @deprecated The only use of this function should be in the constructor of {@link Plate}. */ public void adaptiveThresholding() { Statistics stat = new Statistics(this); int radius = Configurator.getConfigurator().getIntProperty("photo_adaptivethresholdingradius"); if (radius == 0) { plainThresholding(stat); return; } int width = getWidth(); int height = getHeight(); float[][] sourceArray = bufferedImageToArray(image, width, height); float[][] destinationArray = bufferedImageToArray(image, width, height); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { // compute neighborhood int count = 0; float neighborhood = 0.0f; for (int ix = x - radius; ix <= (x + radius); ix++) { for (int iy = y - radius; iy <= (y + radius); iy++) { if ((ix >= 0) && (iy >= 0) && (ix < width) && (iy < height)) { neighborhood += sourceArray[ix][iy]; count++; } } } neighborhood /= count; if (destinationArray[x][y] < neighborhood) { destinationArray[x][y] = 0f; } else { destinationArray[x][y] = 1f; } } } image = Photo.arrayToBufferedImage(destinationArray, width, height); } public HoughTransformation getHoughTransformation() { HoughTransformation hough = new HoughTransformation(getWidth(), getHeight()); for (int x = 0; x < getWidth(); x++) { for (int y = 0; y < getHeight(); y++) { hough.addLine(x, y, getBrightness(x, y)); } } return hough; } @Override public void close() { image.flush(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Photo photo = (Photo) o; if (getWidth() != photo.getWidth() || getHeight() != photo.getHeight()) { return false; } for (int i = 0; i < getWidth(); i++) { for (int j = 0; j < getHeight(); j++) { if (getRGB(i, j) != getRGB(i, j)) { return false; } } } return true; } @Override public int hashCode() { BigInteger rgbSum = BigInteger.ZERO; for (int i = 0; i < getWidth(); i++) { for (int j = 0; j < getHeight(); j++) { rgbSum = rgbSum.add(BigInteger.valueOf(getRGB(i, j))); } } return rgbSum.hashCode(); } }