diff --git a/app/build.gradle b/app/build.gradle index 169054de61..491413d839 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,10 +162,6 @@ android { exclude 'META-INF/proguard/androidx-annotations.pro' } - aaptOptions { - ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl' - } - buildTypes { debug { if (keystores['debug'] != null) { @@ -315,8 +311,6 @@ dependencies { implementation "androidx.camera:camera-view:1.0.0-alpha18" implementation "androidx.concurrent:concurrent-futures:1.0.0" implementation "androidx.autofill:autofill:1.0.0" - implementation 'com.google.firebase:firebase-ml-vision:24.0.3' - implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1' implementation ('com.google.firebase:firebase-messaging:20.2.0') { exclude group: 'com.google.firebase', module: 'firebase-core' diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/AndroidFaceDetector.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/AndroidFaceDetector.java new file mode 100644 index 0000000000..1e08760740 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/AndroidFaceDetector.java @@ -0,0 +1,98 @@ +package org.thoughtcrime.securesms.scribbles; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.PointF; +import android.graphics.RectF; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + +import org.signal.core.util.logging.Log; + +import java.util.List; +import java.util.Locale; + +/** + * Detects faces with the built in Android face detection. + */ +final class AndroidFaceDetector implements FaceDetector { + + private static final String TAG = Log.tag(AndroidFaceDetector.class); + + private static final int MAX_FACES = 20; + + @Override + public List detect(@NonNull Bitmap source) { + long startTime = System.currentTimeMillis(); + + Log.d(TAG, String.format(Locale.US, "Bitmap format is %dx%d %s", source.getWidth(), source.getHeight(), source.getConfig())); + + boolean createBitmap = source.getConfig() != Bitmap.Config.RGB_565 || source.getWidth() % 2 != 0; + Bitmap bitmap; + + if (createBitmap) { + Log.d(TAG, "Changing colour format to 565, with even width"); + bitmap = Bitmap.createBitmap(source.getWidth() & ~0x1, source.getHeight(), Bitmap.Config.RGB_565); + new Canvas(bitmap).drawBitmap(source, 0, 0, null); + } else { + bitmap = source; + } + + try { + android.media.FaceDetector faceDetector = new android.media.FaceDetector(bitmap.getWidth(), bitmap.getHeight(), MAX_FACES); + android.media.FaceDetector.Face[] faces = new android.media.FaceDetector.Face[MAX_FACES]; + int foundFaces = faceDetector.findFaces(bitmap, faces); + + Log.d(TAG, String.format(Locale.US, "Found %d faces", foundFaces)); + + return Stream.of(faces) + .limit(foundFaces) + .map(AndroidFaceDetector::faceToFace) + .toList(); + } finally { + if (createBitmap) { + bitmap.recycle(); + } + + Log.d(TAG, "Finished in " + (System.currentTimeMillis() - startTime) + " ms"); + } + } + + private static Face faceToFace(@NonNull android.media.FaceDetector.Face face) { + PointF point = new PointF(); + face.getMidPoint(point); + + float halfWidth = face.eyesDistance() * 1.4f; + float yOffset = face.eyesDistance() * 0.4f; + RectF bounds = new RectF(point.x - halfWidth, point.y - halfWidth + yOffset, point.x + halfWidth, point.y + halfWidth + yOffset); + + return new DefaultFace(bounds, face.confidence()); + } + + private static class DefaultFace implements Face { + private final RectF bounds; + private final float certainty; + + public DefaultFace(@NonNull RectF bounds, float confidence) { + this.bounds = bounds; + this.certainty = confidence; + } + + @Override + public RectF getBounds() { + return bounds; + } + + @Override + public Class getDetectorClass() { + return AndroidFaceDetector.class; + } + + @Override + public float getConfidence() { + return certainty; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java index 9f3e514cd2..4365dbd3e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FaceDetector.java @@ -3,8 +3,18 @@ package org.thoughtcrime.securesms.scribbles; import android.graphics.Bitmap; import android.graphics.RectF; +import androidx.annotation.NonNull; + import java.util.List; interface FaceDetector { - List detect(Bitmap bitmap); + List detect(@NonNull Bitmap bitmap); + + interface Face { + RectF getBounds(); + + Class getDetectorClass(); + + float getConfidence(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java deleted file mode 100644 index ffbe85c87d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/FirebaseFaceDetector.java +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.scribbles; - -import android.graphics.Bitmap; -import android.graphics.RectF; -import android.os.Build; - -import com.annimon.stream.Stream; -import com.google.firebase.ml.vision.FirebaseVision; -import com.google.firebase.ml.vision.common.FirebaseVisionImage; -import com.google.firebase.ml.vision.face.FirebaseVisionFace; -import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetector; -import com.google.firebase.ml.vision.face.FirebaseVisionFaceDetectorOptions; - -import org.signal.core.util.logging.Log; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -class FirebaseFaceDetector implements FaceDetector { - - private static final String TAG = Log.tag(FirebaseFaceDetector.class); - - private static final long MAX_SIZE = 1000 * 1000; - - @Override - public List detect(Bitmap source) { - long startTime = System.currentTimeMillis(); - - int performanceMode = getPerformanceMode(source); - Log.d(TAG, "Using performance mode " + performanceMode + " (API " + Build.VERSION.SDK_INT + ", " + source.getWidth() + "x" + source.getHeight() + ")"); - - FirebaseVisionFaceDetectorOptions options = new FirebaseVisionFaceDetectorOptions.Builder() - .setPerformanceMode(performanceMode) - .setMinFaceSize(0.05f) - .setContourMode(FirebaseVisionFaceDetectorOptions.NO_CONTOURS) - .setLandmarkMode(FirebaseVisionFaceDetectorOptions.NO_LANDMARKS) - .setClassificationMode(FirebaseVisionFaceDetectorOptions.NO_CLASSIFICATIONS) - .build(); - - FirebaseVisionImage image = FirebaseVisionImage.fromBitmap(source); - List output = new ArrayList<>(); - - try (FirebaseVisionFaceDetector detector = FirebaseVision.getInstance().getVisionFaceDetector(options)) { - CountDownLatch latch = new CountDownLatch(1); - - detector.detectInImage(image) - .addOnSuccessListener(firebaseVisionFaces -> { - output.addAll(Stream.of(firebaseVisionFaces) - .map(FirebaseVisionFace::getBoundingBox) - .map(r -> new RectF(r.left, r.top, r.right, r.bottom)) - .toList()); - latch.countDown(); - }) - .addOnFailureListener(e -> latch.countDown()); - - latch.await(15, TimeUnit.SECONDS); - } catch (IOException e) { - Log.w(TAG, "Failed to close!", e); - } catch (InterruptedException e) { - Log.w(TAG, e); - } - - Log.d(TAG, "Finished in " + (System.currentTimeMillis() - startTime) + " ms"); - - return output; - } - - private static int getPerformanceMode(Bitmap source) { - if (Build.VERSION.SDK_INT < 28) { - return FirebaseVisionFaceDetectorOptions.FAST; - } - - return source.getWidth() * source.getHeight() < MAX_SIZE ? FirebaseVisionFaceDetectorOptions.ACCURATE - : FirebaseVisionFaceDetectorOptions.FAST; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 7189109365..27355c3d0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -375,7 +375,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu if (mainImage.getRenderer() != null) { Bitmap bitmap = ((UriGlideRenderer) mainImage.getRenderer()).getBitmap(); if (bitmap != null) { - FaceDetector detector = new FirebaseFaceDetector(); + FaceDetector detector = new AndroidFaceDetector(); Point size = model.getOutputSizeMaxWidth(1000); Bitmap render = model.render(ApplicationDependencies.getApplication(), size); @@ -486,7 +486,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } private void renderFaceBlurs(@NonNull FaceDetectionResult result) { - List faces = result.rects; + List faces = result.faces; if (faces.isEmpty()) { cachedFaceDetection = null; @@ -497,12 +497,12 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu Matrix faceMatrix = new Matrix(); - for (RectF face : faces) { - FaceBlurRenderer faceBlurRenderer = new FaceBlurRenderer(); + for (FaceDetector.Face face : faces) { + Renderer faceBlurRenderer = new FaceBlurRenderer(); EditorElement element = new EditorElement(faceBlurRenderer, EditorModel.Z_MASK); Matrix localMatrix = element.getLocalMatrix(); - faceMatrix.setRectToRect(Bounds.FULL_BOUNDS, face, Matrix.ScaleToFit.FILL); + faceMatrix.setRectToRect(Bounds.FULL_BOUNDS, face.getBounds(), Matrix.ScaleToFit.FILL); localMatrix.set(result.position); localMatrix.preConcat(faceMatrix); @@ -574,11 +574,11 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu } private static class FaceDetectionResult { - private final List rects; - private final Matrix position; + private final List faces; + private final Matrix position; - private FaceDetectionResult(@NonNull List rects, @NonNull Point imageSize, @NonNull Matrix position) { - this.rects = rects; + private FaceDetectionResult(@NonNull List faces, @NonNull Point imageSize, @NonNull Matrix position) { + this.faces = faces; this.position = new Matrix(position); Matrix imageProjectionMatrix = new Matrix(); diff --git a/app/witness-verifications.gradle b/app/witness-verifications.gradle index 4503a9d1b6..e0e8f93428 100644 --- a/app/witness-verifications.gradle +++ b/app/witness-verifications.gradle @@ -258,8 +258,8 @@ dependencyVerification { ['com.google.android.gms:play-services-auth-api-phone:16.0.0', '19365818b9ceb048ef48db12b5ffadd5eb86dbeb2c7c7b823bfdd89c665f42e5'], - ['com.google.android.gms:play-services-auth-base:17.0.0', - 'c494d23d5cdc7e4c33721877592868d3dc16085cab535c3f589c03052524f737'], + ['com.google.android.gms:play-services-auth-base:16.0.0', + '51dc02ad2f8d1d9dff7b5b52c4df2c6c12ef7df55d752e919d5cb4dd6002ecd0'], ['com.google.android.gms:play-services-auth:16.0.1', 'aec9e1c584d442cb9f59481a50b2c66dc191872607c04d97ecb82dd0eb5149ec'], @@ -270,36 +270,15 @@ dependencyVerification { ['com.google.android.gms:play-services-basement:17.0.0', 'd324a1785bbc48bfe3639fc847cfd3cf43d49e967b5caf2794240a854557a39c'], - ['com.google.android.gms:play-services-clearcut:17.0.0', - 'cce72073c269c2b4cff301304751f2faa2cd1b0344fef581a59da63665f9a4b4'], - - ['com.google.android.gms:play-services-flags:17.0.0', - '746e66b850c5d2b3a0c73871d3fe71ad1b98b62abc0625bbd5badabb73c82cf2'], - ['com.google.android.gms:play-services-maps:16.1.0', 'ff50cae9e4059416202375597d99cdc8ddefd9cea3f1dc2ff53779a3a12eb480'], - ['com.google.android.gms:play-services-phenotype:17.0.0', - '53d40a205e48ad4e35923a01f04d9850acbd7403b3d30fb388e586fad1540ece'], - ['com.google.android.gms:play-services-stats:17.0.0', 'e8ae5b40512b71e2258bfacd8cd3da398733aa4cde3b32d056093f832b83a6fe'], ['com.google.android.gms:play-services-tasks:17.0.0', '2e6d1738b73647f3fe7a038b9780b97717b3746eae258009197e36e7bf3112a5'], - ['com.google.android.gms:play-services-vision-common:19.0.2', - 'b1d93b40a8b49d63d86dfd88ddc4030ab7231d839c5ff3adeb876de94d44b970'], - - ['com.google.android.gms:play-services-vision-face-contour-internal:16.0.0', - '79e5be6ea321a7c10822f190c45612f1999d37c7bc846d8b01a35478eeb0f985'], - - ['com.google.android.gms:play-services-vision-image-label:18.0.3', - 'aea181d214e170a07f13f537c165750cf81fe4522c4e3df6a845b9aa1dcaa06d'], - - ['com.google.android.gms:play-services-vision:20.0.0', - '0386c1c32b06c3c771dd518220d47bb5828fa3d415863ecd6859909b52cc4f6f'], - ['com.google.android.material:material:1.2.1', 'd3d0cc776f2341da8e572586c7d390a5b356ce39a0deb2768071dc40b364ac80'], @@ -339,15 +318,6 @@ dependencyVerification { ['com.google.firebase:firebase-messaging:20.2.0', 'f49cfba49ab33c6fb7436fe9b790b16d3f1265a29955b48fccc1fb1f231da2d8'], - ['com.google.firebase:firebase-ml-common:22.1.1', - '74ac365da2578a07b7dd5cd6ca4ae6d7279c7010153025d081afa5db0dce6d57'], - - ['com.google.firebase:firebase-ml-vision-face-model:20.0.1', - 'e81fc985d9e680be0b18891fa8d108f546173c5da2fd923d787fd13759db3b8a'], - - ['com.google.firebase:firebase-ml-vision:24.0.3', - 'afe0d27eebcb8c52a1e40f1e147b750456e7e02747b7e8f3b9d7f3aa58922c78'], - ['com.google.guava:listenablefuture:1.0', 'e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069'],