/* * Copyright 2015 Daniel Dittmar * * 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 dan.dit.whatsthat.util.image; import android.graphics.Bitmap; import android.graphics.Color; import android.util.Log; import java.nio.ByteBuffer; /** * Created by daniel on 08.04.15. */ public class BitmapUtil { /** * The resulting bitmap fits inside the required * dimensions but does not necessarily gets resized to fit one dimension exactly. * The aspect ratio is the same to the original image. */ public static final int MODE_FIT_NO_GROW = 0; /** * The resulting bitmap fits inside the given * dimensions and is resized so that one dimension fits exactly * and the aspect ratio is the same to the original image. */ public static final int MODE_FIT_INSIDE = 1; /** * The resulting bitmap fits inside the given * dimensions and is resized so that one dimension fits exactly * and the aspect ratio is almost the same to the original image. * This can result in the resulting bitmap fitting the given dimensions * exactly even though the given bitmap did not. */ public static final int MODE_FIT_INSIDE_GENEROUS = 2; /** * The resulting bitmap fits inside the required * dimensions and is resized so that both dimensions fit exactly to * the given dimensions. */ public static final int MODE_FIT_EXACT = 3; public static final double CONTRAST_WEAK_THRESHOLD = 0.3; // everything below is bad public static final double CONTRAST_STRONG_THRESHOLD = 0.6; // everything between this and weak is ok, everything above is great public static final double GREYNESS_STRONG_THRESHOLD = 0.15; // everything below is very grey (0 would be black and white) public static final double GREYNESS_MEDIUM_THRESHOLD = 0.3; // everything between this and STRONG is medium grey, everything above is getting very colorful public static double calculateContrast(Bitmap image) { final int depth = 64; int[] frequencies = new int[depth]; for (int x = 0; x < image.getWidth(); x++) { for (int y = 0; y < image.getHeight(); y++) { int rgba = image.getPixel(x, y); int value = (int) ((depth - 1) * ColorAnalysisUtil.getBrightnessWithAlpha(rgba)); frequencies[value]++; } } //wolfram alpha: interpolating polynomial | {{0, 1}, {11, 0.2}, {32, 0}, {52, 0.2}, {63, 1}} //1 - 0.12162 x + 0.00562105 x^2 - 0.000117161 x^3 + 9.298497201723005*^-7 x^4 double contrast = 0.; for (int i = 1; i < depth; i++) { contrast += frequencies[i] * (1. + i * (-0.12162 + i * (0.00562105 + i * (-0.000117161 + i * 9.298497201723005E-7)))); } return contrast / ((double) (image.getWidth() * image.getHeight())); } public static double calculateGreyness(Bitmap image) { // calculate average greyness of pixels double greyness = 0.; for (int x = 0; x < image.getWidth(); x++) { for (int y = 0; y < image.getHeight(); y++) { int rgb = image.getPixel(x, y); greyness += ColorAnalysisUtil.getGreyness(Color.red(rgb), Color.green(rgb), Color.blue(rgb)); } } return greyness / ((double) (image.getWidth() * image.getHeight())); } public static Bitmap improveContrast(Bitmap originalImage) { Bitmap result = Bitmap.createBitmap(originalImage.getWidth(), originalImage.getHeight(), originalImage.getConfig()); // http://de.wikipedia.org/wiki/Punktoperator_%28Bildverarbeitung%29, Histogrammäqualisation // calculate relative frequencies of occurances of certain brightness values final int depth = 256; int[] frequencies = new int[depth]; for (int x = 0; x < originalImage.getWidth(); x++) { for (int y = 0; y < originalImage.getHeight(); y++) { int oldRgba = originalImage.getPixel(x, y); int value = (int) ((depth - 1) * ColorAnalysisUtil.getBrightnessWithAlpha(oldRgba)); // use alpha=255 to restore value without rounding error, save alpha in blue result.setPixel(x, y, ColorAnalysisUtil.toRGB(value, value, Color.alpha(oldRgba), 255)); frequencies[value]++; } } // accumulate frequencies for (int i = 1; i < depth; i++) { frequencies[i] = frequencies[i] + frequencies[i - 1]; } // Histogrammhyperbolisation final double power = 3./2.; for (int x = 0; x < result.getWidth(); x++) { for (int y = 0; y < result.getHeight(); y++) { int rgba = result.getPixel(x, y); int value = Color.red(rgba); value = Math.max(Math.min(depth - 1, value), 0); value = (int) (value * Math.pow(frequencies[value] / ((double) (result.getWidth() * result.getHeight())), power)); value = Math.max(Math.min(depth - 1, value), 0); result.setPixel(x, y, ColorAnalysisUtil.toRGB(value, value, value, Color.blue(rgba))); } } return result; } /** * Resizes the given image to the wanted height and width * using the specified render options. * * @param originalImage The image that will be resized. * @param wantedWidth The new width of the resized image. * @param wantedHeight The new height of the resized image. * @return A new Bitmap with the given height and width, rendered * with the given options. <code>null</code> if given image is <code>null</code>. */ public static Bitmap resize(Bitmap originalImage, int wantedWidth, int wantedHeight) { if (originalImage == null) { return null; } if (wantedHeight <= 0 || wantedWidth <= 0) { return originalImage; } // Create new Image return Bitmap.createScaledBitmap(originalImage, wantedWidth, wantedHeight, true); } public static class ByteBufferHolder { private ByteBuffer mBuffer; public byte[] array() { return mBuffer == null ? null : mBuffer.array(); } } /** * Extract the data bytes from the given png image. * * @param buffer A buffer holder to use to copy pixel data into, can be uninitialized to allocate new storage. * @param image The image in png format. */ public static void extractDataFromBitmap(ByteBufferHolder buffer, Bitmap image) { int requiredCapacity = image.getByteCount(); if (buffer.mBuffer == null || buffer.mBuffer.capacity() < requiredCapacity) { // we need a new or bigger buffer Runtime runtime = Runtime.getRuntime(); runtime.gc(); long maxRemainingBytes = (runtime.maxMemory() - runtime.totalMemory() + runtime.freeMemory()) / 2; // we only talk half if (requiredCapacity < maxRemainingBytes) { buffer.mBuffer = ByteBuffer.allocate(requiredCapacity); buffer.mBuffer.mark(); image.copyPixelsToBuffer(buffer.mBuffer); return; } else { // we dont have so much memory left, panic mode if (buffer.mBuffer == null) { buffer.mBuffer = ByteBuffer.allocate((int) maxRemainingBytes); buffer.mBuffer.mark(); } else { buffer.mBuffer.reset(); } for (int i = 0; i < Math.min(image.getHeight(), Math.max(maxRemainingBytes, buffer.mBuffer.capacity())) / 4; i++) { int x = Math.min(i, image.getWidth() - 1); int y = i; if (y >= image.getHeight()) { break; } int pixel = image.getPixel(x, y); buffer.mBuffer.put((byte) ((pixel & 0xFF000000) >> 6)); buffer.mBuffer.put((byte) ((pixel & 0x00FF0000) >> 4)); buffer.mBuffer.put((byte) ((pixel & 0x0000FF00) >> 2)); buffer.mBuffer.put((byte) ((pixel & 0x000000FF))); } Log.e("Image", "Too little memory to exract all data from bitmap: " + maxRemainingBytes + ", required: " + requiredCapacity); return; } } buffer.mBuffer.reset(); image.copyPixelsToBuffer(buffer.mBuffer); } public static Bitmap attemptBitmapScaling(Bitmap result, int reqWidth, int reqHeight, int mode) { if (reqWidth <= 0 || reqHeight <= 0) { return result; } int imageWidth = result.getWidth(); int imageHeight = result.getHeight(); if (imageWidth == reqWidth && imageHeight == reqHeight) { return result; } if (mode == MODE_FIT_NO_GROW) { if (imageWidth <= reqWidth && imageHeight <= reqHeight) { return result; // we already fit inside, do not grow } // else scale down to fit inside, keeping aspect ratio } // for fitting similar aspect ratios, calculate how bad it is to forced scale the image to desired dimensions if (mode == MODE_FIT_EXACT || (mode == MODE_FIT_INSIDE_GENEROUS && ImageUtil.areAspectRatiosSimilar(reqWidth, reqHeight, result.getWidth(), result.getHeight()))) { // scale the image exactly to required dimensions, will most likely break the aspect ratio but not too hard return Bitmap.createScaledBitmap(result, reqWidth, reqHeight, true); } // scale the bitmap so that bitmaps dimensions are smaller or equal to required dimensions, keeping aspect ratio double scalingFactor = Math.min(reqHeight / ((double) result.getHeight()), reqWidth / ((double) result.getWidth())); return Bitmap.createScaledBitmap(result, (int) (result.getWidth() * scalingFactor), (int) (result.getHeight() * scalingFactor), true); } public static Bitmap attemptBitmapScaling(Bitmap result, int reqWidth, int reqHeight, boolean enforceDimension) { if (reqWidth <= 0 || reqHeight <= 0) { return result; } if (result.getWidth() == reqWidth && result.getHeight() == reqHeight) { return result; } else { // calculate how bad it is to forced scale the image to desired dimensions if (enforceDimension || ImageUtil.areAspectRatiosSimilar(reqWidth, reqHeight, result.getWidth(), result.getHeight())) { // scale the image exactly to required dimensions, will most likely break the aspect ratio but not too hard result = Bitmap.createScaledBitmap(result, reqWidth, reqHeight, true); } else { // scale the bitmap so that bitmaps dimensions are smaller or equal to required dimensions, keeping aspect ratio double scalingFactor = Math.min(reqHeight / ((double) result.getHeight()), reqWidth / ((double) result.getWidth())); result = Bitmap.createScaledBitmap(result, (int) (result.getWidth() * scalingFactor), (int) (result.getHeight() * scalingFactor), true); } return result; } } }