Detecting pictures that are too dark (#926)

* Made the following changes:
->Added OpenCV library to the project
->Added functionality to detect if an image being uploaded is too dark
->Added functionality to detect if an image being uploaded is blurred

* Made corrections and changes based on gradle checkstyle requirements

* Updated gitignore to remove binary files related to OpenCV from project

* Image blurriness detection was undone. Images are checked only for being too dark now

* Removed OpenCV documentation folder containing a lot of html files

* Removed unnecessary buildScript usage in build.gradle file for opencv library and also added abi splits

* Removed OpenCV library usages and references from project

* Removed OpenCV library folder from project
This commit is contained in:
Vishan Seru 2018-02-18 15:01:42 +05:30 committed by Suchit Kar
parent fbfc1d16f2
commit e50adf858a
5 changed files with 238 additions and 8 deletions

7
.gitignore vendored
View file

@ -27,3 +27,10 @@ app/gradle/wrapper/gradle-wrapper.jar
app/gradlew app/gradlew
app/gradlew.bat app/gradlew.bat
app/gradle/wrapper/gradle-wrapper.properties app/gradle/wrapper/gradle-wrapper.properties
#related to OpenCV
/libraries/opencv/build
app/src/main/jniLibs
#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at:
#https://docs.opencv.org/3.3.0/
/libraries/opencv/javadoc/

View file

@ -38,6 +38,26 @@ dependencies {
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
// Because RxAndroid releases are few and far between, it is recommended you also // Because RxAndroid releases are few and far between, it is recommended you also
// explicitly depend on RxJava's latest version for bug fixes and new features. // explicitly depend on RxJava's latest version for bug fixes and new features.
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
compile 'com.facebook.fresco:fresco:1.3.0'
compile 'com.facebook.stetho:stetho:1.5.0'
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.4'
testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestCompile "com.android.support:support-annotations:${project.SUPPORT_LIB_VERSION}"
androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.1'
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
implementation 'io.reactivex.rxjava2:rxjava:2.1.2' implementation 'io.reactivex.rxjava2:rxjava:2.1.2'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
@ -91,6 +111,8 @@ android {
targetSdkVersion project.targetSdkVersion targetSdkVersion project.targetSdkVersion
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
multiDexEnabled true
} }
sourceSets { sourceSets {

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.BitmapRegionDecoder;
import android.net.Uri;
import android.os.AsyncTask;
import java.io.IOException;
import fr.free.nrw.commons.utils.ImageUtils;
import timber.log.Timber;
/**
* Created by bluesir9 on 16/9/17.
*
* <p>Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
* away completely black,fuzzy/blurry pictures(for now).
*
* <p>todo: Detect selfies?
*/
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
interface Callback {
void onResult(ImageUtils.Result result);
}
private final Callback callback;
private final String imageMediaFilePath;
DetectUnwantedPicturesAsync(String imageMediaFilePath, Callback callback) {
this.callback = callback;
this.imageMediaFilePath = imageMediaFilePath;
}
@Override
protected ImageUtils.Result doInBackground(Void... voids) {
try {
Timber.d("FilePath: " + imageMediaFilePath);
if (imageMediaFilePath == null) {
return ImageUtils.Result.IMAGE_OK;
}
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
return ImageUtils.checkIfImageIsTooDark(decoder);
} catch (IOException ioe) {
Timber.e(ioe, "IO Exception");
return ImageUtils.Result.IMAGE_OK;
}
}
@Override
protected void onPostExecute(ImageUtils.Result result) {
super.onPostExecute(result);
//callback to UI so that it can take necessary decision based on the result obtained
callback.onResult(result);
}
}

View file

@ -23,7 +23,6 @@ import java.io.IOException;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.util.Date; import java.util.Date;
import fr.free.nrw.commons.CommonsApplication;
import timber.log.Timber; import timber.log.Timber;
public class FileUtils { public class FileUtils {
@ -42,6 +41,7 @@ public class FileUtils {
@Nullable @Nullable
public static String getPath(Context context, Uri uri) { public static String getPath(Context context, Uri uri) {
String returnPath = null;
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider // DocumentProvider
@ -53,7 +53,7 @@ public class FileUtils {
final String type = split[0]; final String type = split[0];
if ("primary".equalsIgnoreCase(type)) { if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1]; returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
} }
} else if (isDownloadsDocument(uri)) { // DownloadsProvider } else if (isDownloadsDocument(uri)) { // DownloadsProvider
@ -61,8 +61,9 @@ public class FileUtils {
final Uri contentUri = ContentUris.withAppendedId( final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null); returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider } else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri); final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":"); final String[] split = docId.split(":");
final String type = split[0]; final String type = split[0];
@ -87,17 +88,19 @@ public class FileUtils {
split[1] split[1]
}; };
return getDataColumn(context, contentUri, selection, selectionArgs); returnPath = getDataColumn(context, contentUri, selection, selectionArgs);
} }
} }
// MediaStore (and general) // MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) { else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null); returnPath = getDataColumn(context, uri, null, null);
} }
// File // File
else if ("file".equalsIgnoreCase(uri.getScheme())) { else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath(); returnPath = uri.getPath();
} else { }
if(returnPath == null) {
//fetching path may fail depending on the source URI and all hope is lost //fetching path may fail depending on the source URI and all hope is lost
//so we will create and use a copy of the file, which seems to work //so we will create and use a copy of the file, which seems to work
String copyPath = null; String copyPath = null;
@ -105,6 +108,7 @@ public class FileUtils {
ParcelFileDescriptor descriptor ParcelFileDescriptor descriptor
= context.getContentResolver().openFileDescriptor(uri, "r"); = context.getContentResolver().openFileDescriptor(uri, "r");
if (descriptor != null) { if (descriptor != null) {
SharedPreferences sharedPref = PreferenceManager SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(CommonsApplication.getInstance()); .getDefaultSharedPreferences(CommonsApplication.getInstance());
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true); boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
@ -131,7 +135,10 @@ public class FileUtils {
Timber.w(e, "Error in file " + copyPath); Timber.w(e, "Error in file " + copyPath);
return null; return null;
} }
} else {
return returnPath;
} }
return null; return null;
} }
@ -150,7 +157,7 @@ public class FileUtils {
String[] selectionArgs) { String[] selectionArgs) {
Cursor cursor = null; Cursor cursor = null;
final String column = "_data"; final String column = MediaStore.Images.ImageColumns.DATA;
final String[] projection = { final String[] projection = {
column column
}; };

View file

@ -0,0 +1,135 @@
package fr.free.nrw.commons.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Color;
import android.graphics.Rect;
import timber.log.Timber;
/**
* Created by bluesir9 on 3/10/17.
*/
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 {
IMAGE_DARK,
IMAGE_OK
}
/**
* 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
* @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
*/
public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
if (bitmapRegionDecoder == null) {
Timber.e("Expected bitmapRegionDecoder was null");
return Result.IMAGE_OK;
}
int loadImageHeight = bitmapRegionDecoder.getHeight();
int loadImageWidth = bitmapRegionDecoder.getWidth();
int checkImageTopPosition = 0;
int checkImageBottomPosition = loadImageHeight / 10;
int checkImageLeftPosition = 0;
int checkImageRightPosition = loadImageWidth / 10;
int totalDividedRectangles = 0;
int numberOfDarkRectangles = 0;
while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) {
while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) {
Timber.d("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition);
Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition);
totalDividedRectangles++;
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
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_OK;
}
/**
* 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
* and then applying the formula to calculate its "Luminance". If this brightness value is less than
* 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels
* are dark then the entire bitmap is considered to be dark.
*
* <p>For more information on this brightness/darkness calculation technique refer the accepted answer
* on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745
* SO question and follow the trail.
*
* @param bitmap The bitmap that needs to be checked.
* @return true if bitmap is dark or null, false if bitmap is bright
*/
private static boolean checkIfImageIsDark(Bitmap bitmap) {
if (bitmap == null) {
Timber.e("Expected bitmap was null");
return true;
}
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
int allPixelsCount = bitmapWidth * bitmapHeight;
int[] bitmapPixels = new int[allPixelsCount];
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
boolean isImageDark = false;
int darkPixelsCount = 0;
for (int pixel : bitmapPixels) {
int r = Color.red(pixel);
int g = Color.green(pixel);
int b = Color.blue(pixel);
int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b);
if (brightness < 50) {
//pixel is dark
darkPixelsCount++;
if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) {
isImageDark = true;
break;
}
}
}
return isImageDark;
}
}