Search code examples
javaandroidandroid-camera2

Simple Image Correction for Older Android Cameras


I have an android application where I am trying to let the user capture an image of a document with the native camera and then send it to the server to be made black and white.

In the android app I'm following this example to capture a full size photo from the camera.

The issue I'm having is that an old Nexus 4 camera results in a darker image, and therefore parts of the document are made black instead of white (mostly around the edges of the document). But with a newer Nexus 6 camera there is no issue.

Using this code to change the contrast/brightness works great for increasing the quality of the the Nexus 4 image, but washes out the newer Nexus 6 image.

Is there a way to automatically detect a darker, poor quality image from an older android device in order to apply the image correction only to those images?

Simply calculating the average brightness of the image with this code is thrown off by the possibility of a dark background surrounding a document.


Solution

  • I ended up finding this algorithm in C++ OpenCV and I wrote an implementation in Java for our backend server. It seems to handle darker images from older cameras well. Although it only seems to work for JPG images, not PNGs.

    private BufferedImage autoCorrectBrightnessAndContrast(final BufferedImage image) {
        final int[] histogram = makeHistogram(image);
    
        if (LOG.isDebugEnabled()) {
            LOG.debug("Histogram: {}", Arrays.toString(histogram));
        }
    
        final int[] cumulativeHistogram = getCumulativeHistogram(histogram);
        /*
         * Finding the minimum and maximum indices in the histogram where the value at the index is greater than a given
         * threshold, computed from CLIP_PERCENT.
         */
        final int min = getMinIndex(cumulativeHistogram);
        final int max = getMaxIndex(cumulativeHistogram);
        LOG.debug("Min: {} Max: {}", min, max);
    
        /*
         * alpha is the scaling factor, or the amount that we need to expand the histogram graph horizontally so that it
         * fills the entire width. Essentially increasing contrast.
         */
        double alpha = 1;
        if (max != min) {
            alpha = MAX_COLOR / (double) (max - min);
        }
        /*
         * beta is the translating factor, or the amount that we need to slide the histogram graph left or right so that
         * it lines up with the origin. Essentially adjusting brightness.
         */
        final double beta = -min * alpha;
    
        return adjustBrightnessAndContrast(image, alpha, beta);
    }
    
    /**
     * Creates an int array of length MAX_COLOR representing a histogram from the input image. Each index represents the
     * number of pixels of that greyscale shade in the image.
     * 
     * @param image
     *            the bufferedImage to create a histogram from.
     * @return an int array represenation of the histogram.
     * 
     * @see <a href="https://stackoverflow.com/a/10109389/1088659">Plot a histogram for a buffered image</a>
     */
    private static int[] makeHistogram(final BufferedImage image) {
        final int[] histogram = new int[MAX_COLOR];
    
        final ColorSpace colorSpace = image.getColorModel().getColorSpace();
        LOG.debug("Color space type: {}, is RGB = {}", colorSpace.getType(), colorSpace.isCS_sRGB());
    
        for (int x = 0; x < image.getWidth(); x++) {
            for (int y = 0; y < image.getHeight(); y++) {
                final int color = image.getRGB(x, y);
                final int red =   (color & 0x00ff0000) >> 16;
                final int green = (color & 0x0000ff00) >> 8;
                final int blue =   color & 0x000000ff;
                // Constructing a weighted average of the three color bands
                // based on how much they contribute to the overall brightness
                // of a pixel. (Relative luminance - https://en.wikipedia.org/wiki/Relative_luminance)
                final double greyscaleBrightness = .2126 * red + .7152 * green + .0722 * blue;
                histogram[(int) greyscaleBrightness]++;
            }
        }
        return histogram;
    }
    
    /**
     * @param histogram
     * @return an int array representing the cumulative histogram of the given histogram.
     * @see <a href="https://en.wikipedia.org/wiki/Histogram#Cumulative_histogram">Cumulative Histogram</a>
     */
    private static int[] getCumulativeHistogram(final int[] histogram) {
        final int[] cumulativeHistogram = new int[histogram.length];
    
        cumulativeHistogram[0] = histogram[0];
        for (int i = 1; i < histogram.length; i++) {
            cumulativeHistogram[i] = cumulativeHistogram[i - 1] + histogram[i];
        }
        return cumulativeHistogram;
    }
    
    /**
     * @param cumulativeHistogram
     * @return the minimum index where the cumulative histogram goes above the threshold set by CLIP_PERCENT.
     */
    private static int getMinIndex(final int[] cumulativeHistogram) {
        final int maxValue = cumulativeHistogram[cumulativeHistogram.length - 1];
        final double clipThreshold = CLIP_PERCENT * (maxValue / 100.0) * .5;
        int minIndex = 0;
    
        while (cumulativeHistogram[minIndex] < clipThreshold) {
            minIndex++;
        }
        return minIndex;
    }
    
    /**
     * @param cumulativeHistogram
     * @return the maximum index where the cumulative histogram goes below the threshold set by CLIP_PERCENT.
     */
    private static int getMaxIndex(final int[] cumulativeHistogram) {
        final int maxValue = cumulativeHistogram[cumulativeHistogram.length - 1];
        final double clipThreshold = CLIP_PERCENT * (maxValue / 100.0) * .5;
        int maxIndex = cumulativeHistogram.length - 1;
    
        while (cumulativeHistogram[maxIndex] >= maxValue - clipThreshold) {
            maxIndex--;
        }
        return maxIndex;
    }
    
    /**
     * @param image
     * @param alpha
     *            the scaling factor to adjust the contrast.
     * @param beta
     *            the offset factor to adjust the brightness.
     * @return the adjusted image.
     */
    private static BufferedImage adjustBrightnessAndContrast(final BufferedImage image, final double alpha, final double beta) {
        BufferedImage processedImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
        processedImage = new RescaleOp((float) alpha, (float) beta, null).filter(image, processedImage);
        LOG.debug("alpha: {} beta: {}", (float) alpha, (float) beta);
        return processedImage;
    }