diff --git a/README.md b/README.md new file mode 100644 index 00000000..a96dc608 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# AdvancedAndroid_Emojify + +This is the toy app for the Libraries lesson of the [Advanced Android App Development course on Udacity](https://www.udacity.com/course/advanced-android-app-development--ud855). + +## How to use this repo while taking the course + +Each code repository in this class has a chain of commits that looks like this: + +![listofcommits](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe2e_listofcommits/listofcommits.png) + +These commits show every step you'll take to create the app. They include **Exercise** commits and **Solution** commits. + +Exercise commits contain instructions for completing the exercise, while solution commits show the completed exercise. You can tell what a commit is by looking at its commit message. + +For example, **TFCM.01-Exercise-AddGradleDependencies** is the first code step in the Firebase Cloud Messaging (FCM) lesson. This is the exercise commit, and the exercise is called Add Gradle Dependencies. + +Each commit also has a **branch** associated with it of the same name as the commit message, seen below: + +![branches](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590390fe_branches-ud855/branches-ud855.png +) +Access all branches from this tab + +![listofbranches](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe76_listofbranches/listofbranches.png +) + + +![branchesdropdown](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590391a3_branches-dropdown-ud855/branches-dropdown-ud855.png +) + + +The branches are also accessible from the drop-down in the "Code" tab + + +## Working with the Course Code + +Here are the basic steps for working with and completing exercises in the repo. This information is linked whenever you start a new exercise project, so don't feel you need to memorize all of this! In fact, skim it now, make sure that you know generally how to do the different tasks, and then come back when you start your first exercise. + +The basic steps are: + +1. Clone the repo +2. Checkout the exercise branch +3. Find and complete the TODOs +4. Optionally commit your code changes +5. Compare with the solution + + +**Step 1: Clone the repo** + +As you go through the course, you'll be instructed to clone the different exercise repositories, so you don't need to set these up now. You can clone a repository from github in a folder of your choice with the command: + +```bash +git clone https://github.com/udacity/REPOSITORY_NAME.git +``` + +**Step 2: Checkout the exercise branch** + +As you do different exercises in the code, you'll be told which exercise you're on, as seen below: +![exerciseexample](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0087_exerciseexample/exerciseexample.png +) + +To complete an exercise, you'll want to check out the branch associated with that exercise. For the exercise above, the command to check out that branch would be: + +```bash +git checkout TFCM.01-Exercise-AddGradleDependencies +``` + +**Step 3: Find and complete the TODOs** + +This branch should always have **Exercise** in the title. Once you've checked out the branch, you'll have the code in the exact state you need. You'll even have TODOs, which are special comments that tell you all the steps you need to complete the exercise. You can easily navigate to all the TODOs using Android Studio's TODO tool. To open the TODO tool, click the button at the bottom of the screen that says TODO. This will display a list of all comments with TODO in the project. + +We've numbered the TODO steps so you can do them in order: +![todos](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00e7_todos/todos.png +) + +**Step 4: Optionally commit your code changes** + +After You've completed the TODOs, you can optionally commit your changes. This will allow you to see the code you wrote whenever you return to the branch. The following git code will add and save **all** your changes. + +```bash +git add . +git commit -m "Your commit message" +``` + +**Step 5: Compare with the solution** + +Most exercises will have a list of steps for you to check off in the classroom. Once you've checked these off, you'll see a pop up window with a link to the solution code. Note the **Diff** link: + +![solutionwindow](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00f9_solutionwindow/solutionwindow.png +) + +The **Diff** link will take you to a Github diff as seen below: +![diff](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0108_diffsceenshot/diffsceenshot.png +) + +All of the code that was added in the solution is in green, and the removed code (which will usually be the TODO comments) is in red. +## Report Issues +Notice any issues with a repository? Please file a github issue in the repository. diff --git a/app/build.gradle b/app/build.gradle index 1240229e..6b2aca1b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" + buildToolsVersion '27.0.3' defaultConfig { applicationId "com.example.android.emojify" minSdkVersion 15 @@ -20,12 +20,15 @@ android { } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + implementation fileTree(dir: 'libs', include: ['*.jar']) + androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile 'com.android.support:design:25.2.0' - compile 'com.android.support:appcompat-v7:25.2.0' - // TODO (1): Add Google Mobile Vision Library dependency - testCompile 'junit:junit:4.12' + implementation 'com.android.support:design:25.2.0' + implementation 'com.android.support:appcompat-v7:25.2.0' + implementation 'com.google.android.gms:play-services-vision:10.2.0' + implementation 'com.jakewharton:butterknife:8.4.0' + annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' + implementation 'com.jakewharton.timber:timber:4.5.0' + testImplementation 'junit:junit:4.12' } diff --git a/app/src/main/java/com/example/android/emojify/Emojifier.java b/app/src/main/java/com/example/android/emojify/Emojifier.java new file mode 100644 index 00000000..a0c1d2d1 --- /dev/null +++ b/app/src/main/java/com/example/android/emojify/Emojifier.java @@ -0,0 +1,234 @@ +/* +* Copyright (C) 2017 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +package com.example.android.emojify; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.util.SparseArray; +import android.widget.Toast; + +import com.google.android.gms.vision.Frame; +import com.google.android.gms.vision.face.Face; +import com.google.android.gms.vision.face.FaceDetector; + +import timber.log.Timber; + +class Emojifier { + + + private static final float EMOJI_SCALE_FACTOR = .9f; + private static final double SMILING_PROB_THRESHOLD = .15; + private static final double EYE_OPEN_PROB_THRESHOLD = .5; + + /** + * Method for detecting faces in a bitmap, and drawing emoji depending on the facial + * expression. + * + * @param context The application context. + * @param picture The picture in which to detect the faces. + */ + static Bitmap detectFacesandOverlayEmoji(Context context, Bitmap picture) { + + // Create the face detector, disable tracking and enable classifications + FaceDetector detector = new FaceDetector.Builder(context) + .setTrackingEnabled(false) + .setClassificationType(FaceDetector.ALL_CLASSIFICATIONS) + .build(); + + // Build the frame + Frame frame = new Frame.Builder().setBitmap(picture).build(); + + // Detect the faces + SparseArray faces = detector.detect(frame); + + // Log the number of faces + Timber.d("detectFaces: number of faces = " + faces.size()); + + // Initialize result bitmap to original picture + Bitmap resultBitmap = picture; + + // If there are no faces detected, show a Toast message + if (faces.size() == 0) { + Toast.makeText(context, R.string.no_faces_message, Toast.LENGTH_SHORT).show(); + } else { + + // Iterate through the faces + for (int i = 0; i < faces.size(); ++i) { + Face face = faces.valueAt(i); + + Bitmap emojiBitmap; + switch (whichEmoji(face)) { + case SMILE: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.smile); + break; + case FROWN: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.frown); + break; + case LEFT_WINK: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.leftwink); + break; + case RIGHT_WINK: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.rightwink); + break; + case LEFT_WINK_FROWN: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.leftwinkfrown); + break; + case RIGHT_WINK_FROWN: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.rightwinkfrown); + break; + case CLOSED_EYE_SMILE: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.closed_smile); + break; + case CLOSED_EYE_FROWN: + emojiBitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.closed_frown); + break; + default: + emojiBitmap = null; + Toast.makeText(context, R.string.no_emoji, Toast.LENGTH_SHORT).show(); + } + + // Add the emojiBitmap to the proper position in the original image + resultBitmap = addBitmapToFace(resultBitmap, emojiBitmap, face); + } + } + + + // Release the detector + detector.release(); + + return resultBitmap; + } + + + /** + * Determines the closest emoji to the expression on the face, based on the + * odds that the person is smiling and has each eye open. + * + * @param face The face for which you pick an emoji. + */ + + private static Emoji whichEmoji(Face face) { + // Log all the probabilities + Timber.d("whichEmoji: smilingProb = " + face.getIsSmilingProbability()); + Timber.d("whichEmoji: leftEyeOpenProb = " + + face.getIsLeftEyeOpenProbability()); + Timber.d("whichEmoji: rightEyeOpenProb = " + + face.getIsRightEyeOpenProbability()); + + + boolean smiling = face.getIsSmilingProbability() > SMILING_PROB_THRESHOLD; + + boolean leftEyeClosed = face.getIsLeftEyeOpenProbability() < EYE_OPEN_PROB_THRESHOLD; + boolean rightEyeClosed = face.getIsRightEyeOpenProbability() < EYE_OPEN_PROB_THRESHOLD; + + + // Determine and log the appropriate emoji + Emoji emoji; + if(smiling) { + if (leftEyeClosed && !rightEyeClosed) { + emoji = Emoji.LEFT_WINK; + } else if(rightEyeClosed && !leftEyeClosed){ + emoji = Emoji.RIGHT_WINK; + } else if (leftEyeClosed){ + emoji = Emoji.CLOSED_EYE_SMILE; + } else { + emoji = Emoji.SMILE; + } + } else { + if (leftEyeClosed && !rightEyeClosed) { + emoji = Emoji.LEFT_WINK_FROWN; + } else if(rightEyeClosed && !leftEyeClosed){ + emoji = Emoji.RIGHT_WINK_FROWN; + } else if (leftEyeClosed){ + emoji = Emoji.CLOSED_EYE_FROWN; + } else { + emoji = Emoji.FROWN; + } + } + + + // Log the chosen Emoji + Timber.d("whichEmoji: " + emoji.name()); + + // return the chosen Emoji + return emoji; + } + + /** + * Combines the original picture with the emoji bitmaps + * + * @param backgroundBitmap The original picture + * @param emojiBitmap The chosen emoji + * @param face The detected face + * @return The final bitmap, including the emojis over the faces + */ + private static Bitmap addBitmapToFace(Bitmap backgroundBitmap, Bitmap emojiBitmap, Face face) { + + // Initialize the results bitmap to be a mutable copy of the original image + Bitmap resultBitmap = Bitmap.createBitmap(backgroundBitmap.getWidth(), + backgroundBitmap.getHeight(), backgroundBitmap.getConfig()); + + // Scale the emoji so it looks better on the face + float scaleFactor = EMOJI_SCALE_FACTOR; + + // Determine the size of the emoji to match the width of the face and preserve aspect ratio + int newEmojiWidth = (int) (face.getWidth() * scaleFactor); + int newEmojiHeight = (int) (emojiBitmap.getHeight() * + newEmojiWidth / emojiBitmap.getWidth() * scaleFactor); + + + // Scale the emoji + emojiBitmap = Bitmap.createScaledBitmap(emojiBitmap, newEmojiWidth, newEmojiHeight, false); + + // Determine the emoji position so it best lines up with the face + float emojiPositionX = + (face.getPosition().x + face.getWidth() / 2) - emojiBitmap.getWidth() / 2; + float emojiPositionY = + (face.getPosition().y + face.getHeight() / 2) - emojiBitmap.getHeight() / 3; + + // Create the canvas and draw the bitmaps to it + Canvas canvas = new Canvas(resultBitmap); + canvas.drawBitmap(backgroundBitmap, 0, 0, null); + canvas.drawBitmap(emojiBitmap, emojiPositionX, emojiPositionY, null); + + return resultBitmap; + } + + + // Enum for all possible Emojis + private enum Emoji { + SMILE, + FROWN, + LEFT_WINK, + RIGHT_WINK, + LEFT_WINK_FROWN, + RIGHT_WINK_FROWN, + CLOSED_EYE_SMILE, + CLOSED_EYE_FROWN + } + +} diff --git a/app/src/main/java/com/example/android/emojify/MainActivity.java b/app/src/main/java/com/example/android/emojify/MainActivity.java index a4b4d2d2..be311a78 100644 --- a/app/src/main/java/com/example/android/emojify/MainActivity.java +++ b/app/src/main/java/com/example/android/emojify/MainActivity.java @@ -39,21 +39,27 @@ import java.io.File; import java.io.IOException; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import timber.log.Timber; + public class MainActivity extends AppCompatActivity { + private static final int REQUEST_IMAGE_CAPTURE = 1; private static final int REQUEST_STORAGE_PERMISSION = 1; private static final String FILE_PROVIDER_AUTHORITY = "com.example.android.fileprovider"; - private ImageView mImageView; + @BindView(R.id.image_view) ImageView mImageView; - private Button mEmojifyButton; - private FloatingActionButton mShareFab; - private FloatingActionButton mSaveFab; - private FloatingActionButton mClearFab; + @BindView(R.id.emojify_button) Button mEmojifyButton; + @BindView(R.id.share_button) FloatingActionButton mShareFab; + @BindView(R.id.save_button) FloatingActionButton mSaveFab; + @BindView(R.id.clear_button) FloatingActionButton mClearFab; - private TextView mTitleTextView; + @BindView(R.id.title_text_view) TextView mTitleTextView; private String mTempPhotoPath; @@ -66,20 +72,17 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(R.layout.activity_main); // Bind the views - mImageView = (ImageView) findViewById(R.id.image_view); - mEmojifyButton = (Button) findViewById(R.id.emojify_button); - mShareFab = (FloatingActionButton) findViewById(R.id.share_button); - mSaveFab = (FloatingActionButton) findViewById(R.id.save_button); - mClearFab = (FloatingActionButton) findViewById(R.id.clear_button); - mTitleTextView = (TextView) findViewById(R.id.title_text_view); + ButterKnife.bind(this); + + // Set up Timber + Timber.plant(new Timber.DebugTree()); } /** * OnClick method for "Emojify Me!" Button. Launches the camera app. - * - * @param view The emojify me button. */ - public void emojifyMe(View view) { + @OnClick(R.id.emojify_button) + public void emojifyMe() { // Check for the external storage permission if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) @@ -181,6 +184,10 @@ private void processAndSetImage() { // Resample the saved image to fit the ImageView mResultsBitmap = BitmapUtils.resamplePic(this, mTempPhotoPath); + + // Detect the faces and overlay the appropriate emoji + mResultsBitmap = Emojifier.detectFacesandOverlayEmoji(this, mResultsBitmap); + // Set the new bitmap to the ImageView mImageView.setImageBitmap(mResultsBitmap); } @@ -188,10 +195,9 @@ private void processAndSetImage() { /** * OnClick method for the save button. - * - * @param view The save button. */ - public void saveMe(View view) { + @OnClick(R.id.save_button) + public void saveMe() { // Delete the temporary image file BitmapUtils.deleteImageFile(this, mTempPhotoPath); @@ -201,10 +207,9 @@ public void saveMe(View view) { /** * OnClick method for the share button, saves and shares the new bitmap. - * - * @param view The share button. */ - public void shareMe(View view) { + @OnClick(R.id.share_button) + public void shareMe() { // Delete the temporary image file BitmapUtils.deleteImageFile(this, mTempPhotoPath); @@ -217,10 +222,9 @@ public void shareMe(View view) { /** * OnClick for the clear button, resets the app to original state. - * - * @param view The clear button. */ - public void clearImage(View view) { + @OnClick(R.id.clear_button) + public void clearImage() { // Clear the image and toggle the view visibility mImageView.setImageResource(0); mEmojifyButton.setVisibility(View.VISIBLE); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4c42b60b..4051050d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License.--> + @@ -61,8 +61,8 @@ android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_alignParentTop="true" - android:onClick="clearImage" android:src="@drawable/ic_clear" + tools:visibility="visible" android:visibility="gone" app:backgroundTint="@android:color/white" app:fabSize="mini" /> @@ -77,8 +77,8 @@ android:layout_marginBottom="@dimen/fab_margins" android:layout_marginEnd="@dimen/fab_margins" android:layout_marginRight="@dimen/fab_margins" - android:onClick="saveMe" android:src="@drawable/ic_save" + tools:visibility="visible" android:visibility="gone" app:backgroundTint="@android:color/white" /> @@ -92,8 +92,8 @@ android:layout_marginBottom="@dimen/fab_margins" android:layout_marginLeft="@dimen/fab_margins" android:layout_marginStart="@dimen/fab_margins" - android:onClick="shareMe" android:src="@drawable/ic_share" + tools:visibility="visible" android:visibility="gone" app:backgroundTint="@android:color/white" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 623c8520..334c966e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,4 +23,5 @@ GO Permission denied The imageview that contains the emojified picture + No Faces Detected diff --git a/build.gradle b/build.gradle index 74b2ab0d..a443fad3 100644 --- a/build.gradle +++ b/build.gradle @@ -2,10 +2,11 @@ buildscript { repositories { + google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' + classpath 'com.android.tools.build:gradle:3.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -14,6 +15,7 @@ buildscript { allprojects { repositories { + google() jcenter() } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 04e285f3..35e5ec14 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Dec 28 10:00:20 PST 2015 +#Thu Feb 07 20:58:16 EST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip