Improve the algorithm for detecting unwanted images. (#1798)

* improve the algorithm for checking if the image is dark.

ImageUtils.java: algorithm used previously gave the 'TOO DARK' message for images that were perfectly valid.
The modified algorithm solves this problem.

* Clean up the code
This commit is contained in:
Mansi Agarwal 2018-09-07 05:18:38 +05:30 committed by Vivek Maskara
parent 92a8b5e37c
commit 13c377aa17

View file

@ -8,6 +8,7 @@ import android.graphics.Color;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log;
import com.facebook.common.executors.CallerThreadExecutor; import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference; import com.facebook.common.references.CloseableReference;
@ -29,11 +30,6 @@ import timber.log.Timber;
*/ */
public class ImageUtils { public class ImageUtils {
//atleast 50% of the image in question should be considered dark for the entire image to be dark
private static final double MINIMUM_DARKNESS_FACTOR = 0.50;
//atleast 50% of the image in question should be considered blurry for the entire image to be blurry
private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50;
private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70;
public enum Result { public enum Result {
IMAGE_DARK, IMAGE_DARK,
@ -41,13 +37,6 @@ public class ImageUtils {
} }
/** /**
* BitmapRegionDecoder allows us to process a large bitmap by breaking it down into smaller rectangles. The rectangles
* are obtained by setting an initial width, height and start position of the rectangle as a factor of the width and
* height of the original bitmap and then manipulating the width, height and position to loop over the entire original
* bitmap. Each individual rectangle is independently processed to check if its too dark. Based on
* the factor of "bright enough" individual rectangles amongst the total rectangles into which the image
* was divided, we will declare the image as wanted/unwanted
*
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process * @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
* @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null * @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
* Result.IMAGE_DARK if image is too dark * Result.IMAGE_DARK if image is too dark
@ -62,39 +51,15 @@ public class ImageUtils {
int loadImageWidth = bitmapRegionDecoder.getWidth(); int loadImageWidth = bitmapRegionDecoder.getWidth();
int checkImageTopPosition = 0; int checkImageTopPosition = 0;
int checkImageBottomPosition = loadImageHeight / 10;
int checkImageLeftPosition = 0; int checkImageLeftPosition = 0;
int checkImageRightPosition = loadImageWidth / 10;
int totalDividedRectangles = 0; Timber.v("left: " + checkImageLeftPosition + " right: " + loadImageWidth + " top: " + checkImageTopPosition + " bottom: " + loadImageHeight);
int numberOfDarkRectangles = 0;
while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) { Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition, loadImageWidth, loadImageHeight);
while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) {
Timber.v("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition);
Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition);
totalDividedRectangles++;
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null); Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
if (checkIfImageIsDark(processBitmap)) { if (checkIfImageIsDark(processBitmap)) {
numberOfDarkRectangles++;
}
checkImageTopPosition = checkImageBottomPosition;
checkImageBottomPosition += (checkImageBottomPosition < (loadImageHeight - checkImageBottomPosition)) ? checkImageBottomPosition : (loadImageHeight - checkImageBottomPosition);
}
checkImageTopPosition = 0; //reset to start
checkImageBottomPosition = loadImageHeight / 10; //reset to start
checkImageLeftPosition = checkImageRightPosition;
checkImageRightPosition += (checkImageRightPosition < (loadImageWidth - checkImageRightPosition)) ? checkImageRightPosition : (loadImageWidth - checkImageRightPosition);
}
Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles);
if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) {
return Result.IMAGE_DARK; return Result.IMAGE_DARK;
} }
@ -104,14 +69,12 @@ public class ImageUtils {
/** /**
* Pulls the pixels into an array and then runs through it while checking the brightness of each pixel. * Pulls the pixels into an array and then runs through it while checking the brightness of each pixel.
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel * The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel
* and then applying the formula to calculate its "Luminance". If this brightness value is less than * and then applying the formula to calculate its "Luminance".
* 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels * Pixels with luminance greater than 40% are considered to be bright pixels while the ones with luminance
* are dark then the entire bitmap is considered to be dark. * greater than 26% but less than 40% are considered to be pixels with medium brightness. The rest are
* * dark pixels.
* <p>For more information on this brightness/darkness calculation technique refer the accepted answer * If the number of bright pixels is more than 2.5% or the number of pixels with medium brightness is
* on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745 * more than 30% of the total number of pixels then the image is considered to be OK else dark.
* SO question and follow the trail.
*
* @param bitmap The bitmap that needs to be checked. * @param bitmap The bitmap that needs to be checked.
* @return true if bitmap is dark or null, false if bitmap is bright * @return true if bitmap is dark or null, false if bitmap is bright
*/ */
@ -126,28 +89,45 @@ public class ImageUtils {
int allPixelsCount = bitmapWidth * bitmapHeight; int allPixelsCount = bitmapWidth * bitmapHeight;
int[] bitmapPixels = new int[allPixelsCount]; int[] bitmapPixels = new int[allPixelsCount];
Log.e("total", Integer.toString(allPixelsCount));
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight); bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
boolean isImageDark = false; int numberOfBrightPixels = 0;
int darkPixelsCount = 0; int numberOfMediumBrightnessPixels = 0;
double brightPixelThreshold = 0.025*allPixelsCount;
double mediumBrightPixelThreshold = 0.3*allPixelsCount;
for (int pixel : bitmapPixels) { for (int pixel : bitmapPixels) {
int r = Color.red(pixel); int r = Color.red(pixel);
int g = Color.green(pixel); int g = Color.green(pixel);
int b = Color.blue(pixel); int b = Color.blue(pixel);
int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b); int secondMax = r>g ? r:g;
if (brightness < 50) { double max = (secondMax>b ? secondMax:b)/255.0;
//pixel is dark
darkPixelsCount++; int secondMin = r<g ? r:g;
if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) { double min = (secondMin<b ? secondMin:b)/255.0;
isImageDark = true;
break; double luminance = ((max+min)/2.0)*100;
int highBrightnessLuminance = 40;
int mediumBrightnessLuminance = 26;
if (luminance<highBrightnessLuminance){
if (luminance>mediumBrightnessLuminance){
numberOfMediumBrightnessPixels++;
} }
} }
else {
numberOfBrightPixels++;
} }
return isImageDark; if (numberOfBrightPixels>=brightPixelThreshold || numberOfMediumBrightnessPixels>=mediumBrightPixelThreshold){
return false;
}
}
return true;
} }
/** /**