mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
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:
parent
62aee8c34d
commit
054fa93460
7 changed files with 295 additions and 17 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -27,3 +27,10 @@ app/gradle/wrapper/gradle-wrapper.jar
|
|||
app/gradlew
|
||||
app/gradlew.bat
|
||||
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/
|
||||
|
|
|
|||
|
|
@ -38,6 +38,26 @@ dependencies {
|
|||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
|
||||
// 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.
|
||||
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 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||
|
|
@ -91,6 +111,8 @@ android {
|
|||
targetSdkVersion project.targetSdkVersion
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,6 @@ import java.io.IOException;
|
|||
import java.nio.channels.FileChannel;
|
||||
import java.util.Date;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class FileUtils {
|
||||
|
|
@ -42,6 +41,7 @@ public class FileUtils {
|
|||
@Nullable
|
||||
public static String getPath(Context context, Uri uri) {
|
||||
|
||||
String returnPath = null;
|
||||
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
|
||||
// DocumentProvider
|
||||
|
|
@ -53,7 +53,7 @@ public class FileUtils {
|
|||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
|
||||
}
|
||||
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
|
||||
|
||||
|
|
@ -61,8 +61,9 @@ public class FileUtils {
|
|||
final Uri contentUri = ContentUris.withAppendedId(
|
||||
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
|
||||
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
|
@ -87,17 +88,19 @@ public class FileUtils {
|
|||
split[1]
|
||||
};
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
returnPath = getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
return getDataColumn(context, uri, null, null);
|
||||
returnPath = getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
} else {
|
||||
returnPath = uri.getPath();
|
||||
}
|
||||
|
||||
if(returnPath == null) {
|
||||
//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
|
||||
String copyPath = null;
|
||||
|
|
@ -105,6 +108,7 @@ public class FileUtils {
|
|||
ParcelFileDescriptor descriptor
|
||||
= context.getContentResolver().openFileDescriptor(uri, "r");
|
||||
if (descriptor != null) {
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager
|
||||
.getDefaultSharedPreferences(context);
|
||||
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
|
||||
|
|
@ -131,7 +135,10 @@ public class FileUtils {
|
|||
Timber.w(e, "Error in file " + copyPath);
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
return returnPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +157,7 @@ public class FileUtils {
|
|||
String[] selectionArgs) {
|
||||
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String column = MediaStore.Images.ImageColumns.DATA;
|
||||
final String[] projection = {
|
||||
column
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload;
|
|||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
|
|
@ -18,6 +19,7 @@ import android.support.graphics.drawable.VectorDrawableCompat;
|
|||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
|
@ -47,11 +49,14 @@ import fr.free.nrw.commons.caching.CacheController;
|
|||
import fr.free.nrw.commons.category.CategorizationFragment;
|
||||
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.ContributionsActivity;
|
||||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
||||
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
||||
import fr.free.nrw.commons.mwapi.EventLog;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -287,7 +292,7 @@ public class ShareActivity
|
|||
REQUEST_PERM_ON_CREATE_LOCATION);
|
||||
}
|
||||
}
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
|
||||
|
||||
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
|
||||
|
|
@ -311,7 +316,7 @@ public class ShareActivity
|
|||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
backgroundImageView.setImageURI(mediaUri);
|
||||
storagePermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -319,7 +324,7 @@ public class ShareActivity
|
|||
if (grantResults.length >= 1
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
locationPermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -328,12 +333,12 @@ public class ShareActivity
|
|||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
backgroundImageView.setImageURI(mediaUri);
|
||||
storagePermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
if (grantResults.length >= 2
|
||||
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
|
||||
locationPermitted = true;
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -344,7 +349,7 @@ public class ShareActivity
|
|||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
//It is OK to call this at both (1) and (4) because if perm had been granted at
|
||||
//snackbar, user should not be prompted at submit button
|
||||
performPreuploadProcessingOfFile();
|
||||
performPreUploadProcessingOfFile();
|
||||
|
||||
//Uploading only begins if storage permission granted from arrow icon
|
||||
uploadBegins();
|
||||
|
|
@ -355,7 +360,7 @@ public class ShareActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void performPreuploadProcessingOfFile() {
|
||||
private void performPreUploadProcessingOfFile() {
|
||||
if (!useNewPermissions || storagePermitted) {
|
||||
if (!duplicateCheckPassed) {
|
||||
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
||||
|
|
@ -370,7 +375,17 @@ public class ShareActivity
|
|||
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
|
||||
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|
||||
|| result == NO_DUPLICATE);
|
||||
}, mwApi);
|
||||
/*
|
||||
TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means
|
||||
we are processing images that are already on server???...
|
||||
*/
|
||||
|
||||
if (duplicateCheckPassed) {
|
||||
//image can be uploaded, so now check if its a useless picture or not
|
||||
performUnwantedPictureDetectionProcess();
|
||||
}
|
||||
|
||||
},mwApi);
|
||||
fileAsyncTask.execute();
|
||||
} catch (IOException e) {
|
||||
Timber.d(e, "IO Exception: ");
|
||||
|
|
@ -384,6 +399,37 @@ public class ShareActivity
|
|||
}
|
||||
}
|
||||
|
||||
private void performUnwantedPictureDetectionProcess() {
|
||||
String imageMediaFilePath = FileUtils.getPath(this,mediaUri);
|
||||
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync = new DetectUnwantedPicturesAsync(imageMediaFilePath, result -> {
|
||||
|
||||
if (result != ImageUtils.Result.IMAGE_OK) {
|
||||
//show appropriate error message
|
||||
String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? getString(R.string.upload_image_too_dark) : getString(R.string.upload_image_blurry);
|
||||
AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this);
|
||||
errorDialogBuilder.setMessage(errorMessage);
|
||||
errorDialogBuilder.setTitle(getString(R.string.warning));
|
||||
errorDialogBuilder.setPositiveButton(getString(R.string.no), (dialogInterface, i) -> {
|
||||
//user does not wish to upload the picture, take them back to ContributionsActivity
|
||||
Intent intent = new Intent(ShareActivity.this, ContributionsActivity.class);
|
||||
dialogInterface.dismiss();
|
||||
startActivity(intent);
|
||||
});
|
||||
errorDialogBuilder.setNegativeButton(getString(R.string.yes), (dialogInterface, i) -> {
|
||||
//user wishes to go ahead with the upload of this picture, just dismiss this dialog
|
||||
dialogInterface.dismiss();
|
||||
});
|
||||
|
||||
AlertDialog errorDialog = errorDialogBuilder.create();
|
||||
if (!isFinishing()) {
|
||||
errorDialog.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
detectUnwantedPicturesAsync.execute();
|
||||
}
|
||||
|
||||
private Snackbar requestPermissionUsingSnackBar(String rationale,
|
||||
final String[] perms,
|
||||
final int code) {
|
||||
|
|
|
|||
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal file
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -205,12 +205,14 @@
|
|||
<string name="error_while_cache">Error while caching pictures</string>
|
||||
<string name="title_info">A unique descriptive title for the file, which will serve as a filename. You may use plain language with spaces. Do not include the file extension</string>
|
||||
<string name="description_info">Please describe the media as much as possible: Where was it taken? What does it show? What is the context? Please describe the objects or persons. Reveal information that can not be easily guessed, for instance the time of day if it is a landscape. If the media shows something unusual, please explain what makes it unusual.</string>
|
||||
<string name="upload_image_too_dark">This picture is too dark, are you sure you want to upload it? Wikimedia Commons is only for pictures with encyclopedic value.</string>
|
||||
<string name="upload_image_blurry">This picture is blurry, are you sure you want to upload it? Wikimedia Commons is only for pictures with encyclopedic value.</string>
|
||||
<string name="give_permission">Give permission</string>
|
||||
<string name="use_external_storage">Use external storage</string>
|
||||
<string name="use_external_storage_summary">Save pictures taken with the in-app camera on your device</string>
|
||||
<string name="login_to_your_account">Login to your account</string>
|
||||
<string name="send_log_file">Send log file</string>
|
||||
<string name="send_log_file_description">Send log file to developers via email</string>
|
||||
<string name="login_to_your_account">Login to your account</string>
|
||||
|
||||
<string name="nearby_location_has_not_changed">Location has not changed.</string>
|
||||
<string name="nearby_location_not_available">Location not available.</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue