/* * This file is part of the LIRE project: http://lire-project.net * LIRE is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * LIRE is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LIRE; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * We kindly ask you to refer the any or one of the following publications in * any publication mentioning or employing Lire: * * Lux Mathias, Savvas A. Chatzichristofis. Lire: Lucene Image Retrieval – * An Extensible Java CBIR Library. In proceedings of the 16th ACM International * Conference on Multimedia, pp. 1085-1088, Vancouver, Canada, 2008 * URL: http://doi.acm.org/10.1145/1459359.1459577 * * Lux Mathias. Content Based Image Retrieval with LIRE. In proceedings of the * 19th ACM International Conference on Multimedia, pp. 735-738, Scottsdale, * Arizona, USA, 2011 * URL: http://dl.acm.org/citation.cfm?id=2072432 * * Mathias Lux, Oge Marques. Visual Information Retrieval using Java and LIRE * Morgan & Claypool, 2013 * URL: http://www.morganclaypool.com/doi/abs/10.2200/S00468ED1V01Y201301ICR025 * * Copyright statement: * ==================== * (c) 2002-2013 by Mathias Lux (mathias@juggle.at) * http://www.semanticmetadata.net/lire, http://www.lire-project.net * * Updated: 30.11.14 13:51 */ package net.semanticmetadata.lire.utils; import java.awt.*; import java.awt.image.BufferedImage; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.awt.image.WritableRaster; import java.util.Arrays; /** * Some little helper methods.<br> * This file is part of the Caliph and Emir project: http://www.SemanticMetadata.net * <br>Date: 02.02.2006 * <br>Time: 23:33:36 * * @author Mathias Lux, mathias@juggle.at */ public class ImageUtils { /** * Scales down an image into a box of maxSideLength x maxSideLength. * * @param image the image to scale down. It remains untouched. * @param maxSideLength the maximum side length of the scaled down instance. Has to be > 0. * @return the scaled image if the original image is bigger than the scaled version, the original instance otherwise. */ public static BufferedImage scaleImage(BufferedImage image, int maxSideLength) { assert (maxSideLength > 0); double originalWidth = image.getWidth(); double originalHeight = image.getHeight(); double scaleFactor = 0.0; if (originalWidth > originalHeight) { scaleFactor = ((double) maxSideLength / originalWidth); } else { scaleFactor = ((double) maxSideLength / originalHeight); } // create new image if (scaleFactor < 1 && (int) Math.round(originalWidth * scaleFactor) > 1 && (int) Math.round(originalHeight * scaleFactor) > 1) { // BufferedImage img = new BufferedImage((int) Math.round(originalWidth * scaleFactor), (int) Math.round(originalHeight * scaleFactor), BufferedImage.TYPE_INT_RGB); BufferedImage img = new BufferedImage((int) Math.round(originalWidth * scaleFactor), (int) Math.round(originalHeight * scaleFactor), image.getType()); // fast scale (Java 1.4 & 1.5) Graphics g = img.getGraphics(); // ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(image, 0, 0, img.getWidth(), img.getHeight(), null); return img; } else return image; } /** * Scale image to an arbitrary shape not retaining proportions and aspect ratio. * * @param image * @param width * @param height * @return */ public static BufferedImage scaleImage(BufferedImage image, int width, int height) { assert (width > 0 && height > 0); // create image of new size BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); // fast scale (Java 1.4 & 1.5) Graphics g = img.getGraphics(); ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(image, 0, 0, img.getWidth(), img.getHeight(), null); return img; } public static BufferedImage cropImage(BufferedImage image, int fromX, int fromY, int width, int height) { assert (width > 0 && height > 0); int toX = Math.min(fromX + width, image.getWidth()); int toY = Math.min(fromY + height, image.getHeight()); // create smaller image BufferedImage cropped = new BufferedImage(toX - fromX, toY - fromY, BufferedImage.TYPE_INT_RGB); // fast scale (Java 1.4 & 1.5) Graphics g = cropped.getGraphics(); g.drawImage(image, 0, 0, cropped.getWidth(), cropped.getHeight(), fromX, fromY, toX, toY, null); return cropped; } /** * Converts an image to grey. Use instead of color conversion op, which yields strange results. * * @param image */ public static BufferedImage getGrayscaleImage(BufferedImage image) { BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); result.getGraphics().drawImage(image, 0, 0, null); return result; } /** * Inverts a grey scale image. * * @param image */ public static void invertImage(BufferedImage image) { WritableRaster inRaster = image.getRaster(); int[] p = new int[3]; // float v = 0; for (int x = 0; x < inRaster.getWidth(); x++) { for (int y = 0; y < inRaster.getHeight(); y++) { inRaster.getPixel(x, y, p); p[0] = 255 - p[0]; inRaster.setPixel(x, y, p); } } } /** * Converts an image to a standard internal representation. * Taken from OpenIMAJ. Thanks to these guys! * http://sourceforge.net/p/openimaj * * @param bimg * @return */ public static BufferedImage createWorkingCopy(BufferedImage bimg) { BufferedImage image; if (bimg.getType() == BufferedImage.TYPE_3BYTE_BGR) { image = bimg; } else { image = new BufferedImage(bimg.getWidth(), bimg.getHeight(), BufferedImage.TYPE_3BYTE_BGR); Graphics2D g2d = image.createGraphics(); g2d.drawImage(bimg, null, 0, 0); } return image; } /** * Trims the white border around an image. * * @param img * @return a new image, hopefully trimmed. */ public static BufferedImage trimWhiteSpace(BufferedImage img) { return trimWhiteSpace(img, 250, 0, 0, 0, 0); } /** * Trims the white border around an image. * * @param img * @return a new image, hopefully trimmed. */ public static BufferedImage trimWhiteSpace(BufferedImage img, int whiteThreshold, int startTop, int startRight, int startBottom, int startLeft) { return trimWhiteSpace(img, img, whiteThreshold, startTop, startRight, startBottom, startLeft); } public static BufferedImage trimWhiteSpace(BufferedImage src, BufferedImage tgt, int whiteThreshold, int startTop, int startRight, int startBottom, int startLeft) { // idea is to scan lines of an image starting from each side. // As soon as a scan line encounters non-white (or non-black) pixels we know there is actual image content. WritableRaster raster = getGrayscaleImage(src).getRaster(); int[] pixels = new int[Math.max(raster.getWidth(), raster.getHeight())]; int thresholdWhite = whiteThreshold; int trimTop = startTop, trimBottom = startBottom, trimLeft = startLeft, trimRight = startRight; boolean white = true; while (white) { raster.getPixels(0, trimTop, raster.getWidth(), 1, pixels); for (int i = 0; i < raster.getWidth(); i++) { if (pixels[i] < thresholdWhite) white = false; } if (white) { trimTop++; // handling white only images .. if (trimTop > raster.getHeight() - 10) { System.err.println("Only white ..."); return src; } } } // bottom: white = true; while (white) { raster.getPixels(0, raster.getHeight() - 1 - trimBottom, raster.getWidth(), 1, pixels); for (int i = 0; i < raster.getWidth(); i++) { if (pixels[i] < thresholdWhite) white = false; } if (white) { trimBottom++; } } // left: white = true; while (white) { raster.getPixels(trimLeft, 0, 1, raster.getHeight(), pixels); for (int i = 0; i < raster.getHeight(); i++) { if (pixels[i] < thresholdWhite) white = false; } if (white) { trimLeft++; } } // left: white = true; while (white) { raster.getPixels(raster.getWidth() - 1 - trimRight, 0, 1, raster.getHeight(), pixels); for (int i = 0; i < raster.getHeight(); i++) { if (pixels[i] < thresholdWhite) white = false; } if (white) { trimRight++; } } // System.out.println("trimTop = " + trimTop); // System.out.println("trimBottom = " + trimBottom); // System.out.println("trimLeft = " + trimLeft); // System.out.println("trimRight = " + trimRight); BufferedImage result = new BufferedImage(raster.getWidth() - (trimLeft + trimRight), raster.getHeight() - (trimTop + trimBottom), BufferedImage.TYPE_INT_RGB); result.getGraphics().drawImage(tgt, 0, 0, result.getWidth(), result.getHeight(), trimLeft, trimTop, src.getWidth() - trimRight, src.getHeight() - trimBottom, null); return result; } /** * Non-local means denoising. * * @param img * @return */ public static BufferedImage denoiseImage(BufferedImage img) { double h = 5d; double sigma = 2d; int distance = 5; double sigma2 = 2 * sigma * sigma; double h2 = h * h; // make it grayscale BufferedImage result = getGrayscaleImage(img); WritableRaster raster = result.getRaster(); int[] p = new int[1]; // actual pixel int[] pCol = new int[img.getHeight()]; // actual pixels int[] pColD = new int[img.getHeight()]; // actual pixel // now for each pixel: for (int x = 0; x < raster.getWidth(); x++) { raster.getPixels(x, 0, 1, raster.getHeight(), pCol); for (int y = 0; y < raster.getHeight(); y++) { // get pixel value: // raster.getPixel(x, y, p); if (pCol[y] < 250) { // speed up by only checking non-white pixels. double weightSum = 0; double weight = 0; double graySum = 0; for (int dx = Math.max(0, x - distance); dx < Math.min(raster.getWidth(), x + distance); dx++) { raster.getPixels(dx, 0, 1, raster.getHeight(), pColD); for (int dy = Math.max(0, y - distance); dy < Math.min(y + distance, raster.getHeight()); dy++) { if (dx != x && dy != y) { double d2 = (pCol[y] - pColD[dy]) * (pCol[y] - pColD[dy]); // double d2 = (x - dx) * (x - dx) + (y - dy) * (y - dy); weight = Math.exp(-1d / (h2) * Math.max(0d, d2 - sigma2)); weightSum += weight; // raster.getPixel(dx, dy, dp); graySum += pColD[dy] * weight; } } } p[0] = ((int) (graySum / weightSum)); raster.setPixel(x, y, p); } } } return result; } public static BufferedImage removeScratches(BufferedImage img) { int thresholdGray = 196; int thresholdCount = 12; BufferedImage result = getGrayscaleImage(img); WritableRaster raster; int[] pCol = new int[img.getHeight()]; int[] pRow = new int[img.getWidth()]; if (false) { // equalize histogram or not. raster = result.getRaster(); // histogram equalization, uniform: int min = 255, max = 0; for (int x = 0; x < raster.getWidth(); x++) { raster.getPixels(x, 0, 1, raster.getHeight(), pCol); for (int y = 0; y < raster.getHeight(); y++) { min = Math.min(pCol[y], min); max = Math.max(pCol[y], max); } } for (int x = 0; x < raster.getWidth(); x++) { raster.getPixels(x, 0, 1, raster.getHeight(), pCol); for (int y = 0; y < raster.getHeight(); y++) { pCol[y] = ((int) (255* (pCol[y] - min) / ((double) max - min) )); } raster.setPixels(x, 0, 1, raster.getHeight(), pCol); } } ConvolveOp op = new ConvolveOp(new Kernel(5, 5, ImageUtils.makeGaussianKernel(5, 2.0f))); result = op.filter(result, null); raster = result.getRaster(); Arrays.fill(pCol, 255); Arrays.fill(pRow, 255); int[] p = new int[1]; // repair from the convolveop raster.setPixels(0, 0, 1, raster.getHeight(), pCol); raster.setPixels(1, 0, 1, raster.getHeight(), pCol); raster.setPixels(raster.getWidth() - 1, 0, 1, raster.getHeight(), pCol); raster.setPixels(raster.getWidth() - 2, 0, 1, raster.getHeight(), pCol); raster.setPixels(0, 0, raster.getWidth(), 1, pRow); raster.setPixels(0, 1, raster.getWidth(), 1, pRow); raster.setPixels(0, raster.getHeight() - 1, raster.getWidth(), 1, pRow); raster.setPixels(0, raster.getHeight() - 2, raster.getWidth(), 1, pRow); // now for each pixel: for (int x = 0; x < raster.getWidth(); x++) { raster.getPixels(x, 0, 1, raster.getHeight(), pCol); for (int y = 0; y < raster.getHeight(); y++) { int[] count = {0}; raster.getPixel(x, y, p); if (p[0] < thresholdGray) { // thresholding // track and find out how many are connected. checkNeighbour(raster, x, y, thresholdGray, thresholdCount, count); if (count[0] < thresholdCount) { p[0] = 255; } } else { p[0] = 255; } raster.setPixel(x, y, p); } // raster.setPixels(x, 0, 1, raster.getHeight(), pCol); } return result; } private static void checkNeighbour(WritableRaster raster, int x, int y, int thresholdGray, int thresholdCount, int[] count) { if (count[0] > thresholdCount) return; int[] pixel = new int[1]; if (x >= raster.getWidth()) return; if (y >= raster.getHeight()) return; raster.getPixel(x, y, pixel); if (pixel[0] < thresholdGray) { count[0] += 1; if (count[0] < thresholdCount) { // checkNeighbour(raster, x, y, thresholdGray, thresholdCount, count); checkNeighbour(raster, x, y + 1, thresholdGray, thresholdCount, count); checkNeighbour(raster, x + 1, y, thresholdGray, thresholdCount, count); checkNeighbour(raster, x + 1, y + 1, thresholdGray, thresholdCount, count); checkNeighbour(raster, x - 1, y + 1, thresholdGray, thresholdCount, count); } } } /** * creates a Gaussian kernel for ConvolveOp for blurring an image * * @param radius the radius, i.e. 5 * @param sigma sigma, i.e. 1.4f * @return */ public static float[] makeGaussianKernel(int radius, float sigma) { float[] kernel = new float[radius * radius]; float sum = 0; for (int y = 0; y < radius; y++) { for (int x = 0; x < radius; x++) { int off = y * radius + x; int xx = x - radius / 2; int yy = y - radius / 2; kernel[off] = (float) Math.pow(Math.E, -(xx * xx + yy * yy) / (2 * (sigma * sigma))); sum += kernel[off]; } } for (int i = 0; i < kernel.length; i++) kernel[i] /= sum; return kernel; } public static BufferedImage differenceOfGaussians(BufferedImage image) { BufferedImage img1 = getGrayscaleImage(image); BufferedImage img2 = getGrayscaleImage(image); ConvolveOp gaussian1 = new ConvolveOp(new Kernel(5, 5, ImageUtils.makeGaussianKernel(5, 1.0f))); ConvolveOp gaussian2 = new ConvolveOp(new Kernel(5, 5, ImageUtils.makeGaussianKernel(5, 2.0f))); img1 = gaussian1.filter(img1, null); img2 = gaussian2.filter(img2, null); WritableRaster r1 = img1.getRaster(); WritableRaster r2 = img2.getRaster(); int[] tmp1 = new int[3]; int[] tmp2 = new int[3]; for (int x = 0; x < img1.getWidth(); x++) { for (int y = 0; y < img1.getHeight(); y++) { r1.getPixel(x, y, tmp1); r2.getPixel(x, y, tmp2); tmp1[0] = Math.abs(tmp1[0] - tmp2[0]); // System.out.println("tmp1 = " + tmp1[0]); if (tmp1[0] > 5) tmp1[0] = 0; else tmp1[0] = 255; r1.setPixel(x, y, tmp1); } } return img1; } /** * Converts an image (RGB, RGBA, ... whatever) to a binary one based on given threshold * * @param image the image to convert. Remains untouched. * @param threshold the threshold in [0,255] * @return a new BufferedImage instance of TYPE_BYTE_GRAY with only 0'S and 255's */ public static BufferedImage thresholdImage(BufferedImage image, int threshold) { BufferedImage result = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY); result.getGraphics().drawImage(image, 0, 0, null); WritableRaster raster = result.getRaster(); int[] pixels = new int[image.getWidth()]; for (int y = 0; y < image.getHeight(); y++) { raster.getPixels(0, y, image.getWidth(), 1, pixels); for (int i = 0; i < pixels.length; i++) { if (pixels[i] < threshold) pixels[i] = 0; else pixels[i] = 255; } raster.setPixels(0, y, image.getWidth(), 1, pixels); } return result; } /** * Check if the image is fail safe for color based features that are actually using 8 bits per pixel RGB. * * @param bufferedImage * @return */ public static BufferedImage get8BitRGBImage(BufferedImage bufferedImage) { // check if it's (i) RGB and (ii) 8 bits per pixel. if (bufferedImage.getType() != BufferedImage.TYPE_INT_RGB || bufferedImage.getSampleModel().getSampleSize(0) != 8) { BufferedImage img = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_INT_RGB); img.getGraphics().drawImage(bufferedImage, 0, 0, null); bufferedImage = img; } return bufferedImage; } }