diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..db64b6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +*.swp +*.iml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000..5c92b75 Binary files /dev/null and b/.idea/caches/build_file_checksums.ser differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..30aa626 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..5644ccc --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..e0d5b93 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..db8a813 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6df551c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: android +script: "./gradlew assembleDebug" + +jdk: + - oraclejdk8 + +android: + components: + # The BuildTools version used by your project + - tools + - platform-tools + - build-tools-23.0.3 + # The SDK version used to compile your project + - android-23 + - extra-android-m2repository + - extra-android-support + # Additional components + #- extra-android-m2repository + licenses: + - android-sdk-license-.+ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..42159e7 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2015-present Tzutalin + + 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. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fd1eb11 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +all: + ./gradlew assembleDebug + +clean: + ./gradlew clean + +install: + adb install ./app/build/outputs/apk/app-debug.apk diff --git a/README.md b/README.md new file mode 100644 index 0000000..19f0ce0 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Android App for Real-time Face Landmark Detection + +Fast Face is an android application which detects facial landmark . It detects 68 landmarks of human face
+chin to eyebrow in real-time. Also, it can detect people up to 3 if you guys show your frontal faces.
+ +It is an upgraded version of [dlib-android](https://github.com/tzutalin/dlib-android), Not only revising the code but additional task for optimizing dlib library was needed. +As a result, Fast Face speeds up 2x or more from the original. Higher resoluton, Higher speed.
+ +I think it is not the best one, there are some issues that can be more speedy one.
+So, if you guys already improved or want to improve this code, feel free to contact me. Test and Enjoy it :)
+
+ +## Screenshot + +
+ +## Environments +* DEVICE : SAMSUNG-A8 2015(@cortex-a53 core) +* API : 23 (Android 6.0.1) +* TIME : 50ms ~ 70ms +
+ +## Features + +* Support HOG detector +* HOG Face detection +* Facial Landmark/Expression +
+ +## Sample code + +Facial landmark detection +```java + +// detecs every 3 frames +if(mframeNum % 3 == 0){ + synchronized (OnGetImageListener.this) { + results = mFaceDet.detect(mResizedBitmap); + } +} + +// Draw on bitmap +if (results.size() != 0) { + for (final VisionDetRet ret : results) { + float resizeRatio = 4.5f; + Canvas canvas = new Canvas(mInversedBipmap); + + // Draw landmark + ArrayList landmarks = ret.getFaceLandmarks(); + for (Point point : landmarks) { + int pointX = (int) (point.x * resizeRatio); + int pointY = (int) (point.y * resizeRatio); + canvas.drawCircle(pointX, pointY, 4, mFaceLandmardkPaint); + } + } +} +``` +
+ +## License +[License](LICENSE.md) diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..91e01db --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,58 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion '25.0.2' + + defaultConfig { + applicationId "com.tzutalin.dlibtest" + minSdkVersion 21 + targetSdkVersion 25 + versionCode 1 + versionName "${rootProject.ext.releaseVersionName}" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' + } +} + +repositories { + mavenCentral() + mavenLocal() +} + +apply plugin: 'com.neenbedankt.android-apt' +def AAVersion = '4.0.0' + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile "com.android.support:appcompat-v7:${rootProject.ext.androidSupportSdkVersion}" + compile 'com.android.support:design:25.2.0' + compile 'com.github.dexafree:materiallist:3.0.1' + compile 'com.jakewharton.timber:timber:4.5.1' + compile project(':dlib') + apt "org.androidannotations:androidannotations:$AAVersion" + compile "org.androidannotations:androidannotations-api:$AAVersion" + + // Add AndroidJUnit + androidTestCompile "com.android.support:support-annotations:${rootProject.ext.androidSupportSdkVersion}" + androidTestCompile 'com.android.support.test:runner:0.5' + androidTestCompile 'com.android.support.test:rules:0.5' + // Optional -- Hamcrest library + androidTestCompile 'org.hamcrest:hamcrest-library:1.3' +} +apply plugin: 'com.jakewharton.hugo' diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..acfa448 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/darrenl/tools/android-sdk-linux/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/DLibFunctionsTest.java b/app/src/androidTest/java/DLibFunctionsTest.java new file mode 100644 index 0000000..2ea7f18 --- /dev/null +++ b/app/src/androidTest/java/DLibFunctionsTest.java @@ -0,0 +1,38 @@ +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import com.tzutalin.dlib.PedestrianDet; +import com.tzutalin.dlib.VisionDetRet; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class DLibFunctionsTest { + + private Context mInstrumantationCtx; + + @Before + public void setup() { + mInstrumantationCtx = InstrumentationRegistry.getTargetContext(); + } + + @Test + public void testFacialLandmark() { + PedestrianDet peopleDet = new PedestrianDet(); + List results = peopleDet.detect("/sdcard/test.bmp"); + for (final VisionDetRet ret : results) { + String label = ret.getLabel(); + int rectLeft = ret.getLeft(); + int rectTop= ret.getTop(); + int rectRight = ret.getRight(); + int rectBottom = ret.getBottom(); + } + } +} diff --git a/app/src/androidTest/java/FaceDetTest.java b/app/src/androidTest/java/FaceDetTest.java new file mode 100644 index 0000000..42f1a38 --- /dev/null +++ b/app/src/androidTest/java/FaceDetTest.java @@ -0,0 +1,113 @@ +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + +import com.tzutalin.dlib.Constants; +import com.tzutalin.dlib.FaceDet; +import com.tzutalin.dlib.VisionDetRet; + +import junit.framework.Assert; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +/** + * Created by houzhi on 16-10-20. + */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class FaceDetTest { + + @Before + public void setup() { + } + + @After + public void tearDown() { + } + + @Test + public void testDetBitmapFace() { + FaceDet faceDet = new FaceDet(Constants.getFaceShapeModelPath()); + Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.jpg"); + assertThat(bitmap, notNullValue()); + List results = faceDet.detect(bitmap); + for (final VisionDetRet ret : results) { + String label = ret.getLabel(); + int rectLeft = ret.getLeft(); + int rectTop = ret.getTop(); + int rectRight = ret.getRight(); + int rectBottom = ret.getBottom(); + assertThat(label, is("face")); + Assert.assertTrue(rectLeft > 0); + } + faceDet.release(); + } + + @Test + public void testDetFace() { + Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.jpg"); + assertThat(bitmap, notNullValue()); + FaceDet faceDet = new FaceDet(Constants.getFaceShapeModelPath()); + List results = faceDet.detect("/sdcard/test.jpg"); + for (final VisionDetRet ret : results) { + String label = ret.getLabel(); + int rectLeft = ret.getLeft(); + int rectTop = ret.getTop(); + int rectRight = ret.getRight(); + int rectBottom = ret.getBottom(); + assertThat(label, is("face")); + Assert.assertTrue(rectLeft > 0); + } + faceDet.release(); + } + + @Test + public void testDetBitmapFaceLandmarkDect() { + Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.jpg"); + assertThat(bitmap, notNullValue()); + FaceDet faceDet = new FaceDet(Constants.getFaceShapeModelPath()); + List results = faceDet.detect(bitmap); + for (final VisionDetRet ret : results) { + String label = ret.getLabel(); + int rectLeft = ret.getLeft(); + int rectTop = ret.getTop(); + int rectRight = ret.getRight(); + int rectBottom = ret.getBottom(); + ArrayList landmarks = ret.getFaceLandmarks(); + assertThat(label, is("face")); + Assert.assertTrue(landmarks.size() > 0); + Assert.assertTrue(rectLeft > 0); + } + faceDet.release(); + } + + @Test + public void testDetFaceLandmark() { + FaceDet faceDet = new FaceDet(Constants.getFaceShapeModelPath()); + List results = faceDet.detect("/sdcard/test.jpg"); + for (final VisionDetRet ret : results) { + String label = ret.getLabel(); + int rectLeft = ret.getLeft(); + int rectTop = ret.getTop(); + int rectRight = ret.getRight(); + int rectBottom = ret.getBottom(); + ArrayList landmarks = ret.getFaceLandmarks(); + assertThat(label, is("face")); + Assert.assertTrue(landmarks.size() > 0); + Assert.assertTrue(rectLeft > 0); + } + faceDet.release(); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..97343b4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/tzutalin/dlibtest/AutoFitTextureView.java b/app/src/main/java/com/tzutalin/dlibtest/AutoFitTextureView.java new file mode 100644 index 0000000..bd9c070 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/AutoFitTextureView.java @@ -0,0 +1,74 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.TextureView; + +/** + * A {@link TextureView} that can be adjusted to a specified aspect ratio. + */ +public class AutoFitTextureView extends TextureView { + private int mRatioWidth = 0; + private int mRatioHeight = 0; + + public AutoFitTextureView(final Context context) { + this(context, null); + } + + public AutoFitTextureView(final Context context, final AttributeSet attrs) { + this(context, attrs, 0); + } + + public AutoFitTextureView(final Context context, final AttributeSet attrs, final int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio + * calculated from the parameters. Note that the actual sizes of parameters don't matter, that + * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result. + * + * @param width Relative horizontal size + * @param height Relative vertical size + */ + public void setAspectRatio(final int width, final int height) { + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Size cannot be negative."); + } + mRatioWidth = width; + mRatioHeight = height; + requestLayout(); + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + final int width = MeasureSpec.getSize(widthMeasureSpec); + final int height = MeasureSpec.getSize(heightMeasureSpec); + if (0 == mRatioWidth || 0 == mRatioHeight) { + setMeasuredDimension(width, height); + } else { + if (width < height * mRatioWidth / mRatioHeight) { + setMeasuredDimension(width, width * mRatioHeight / mRatioWidth); + } else { + setMeasuredDimension(height * mRatioWidth / mRatioHeight, height); + } + } + } +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/CameraActivity.java b/app/src/main/java/com/tzutalin/dlibtest/CameraActivity.java new file mode 100644 index 0000000..2e2fcdd --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/CameraActivity.java @@ -0,0 +1,71 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.view.WindowManager; +import android.widget.Toast; + +/** + * Created by darrenl on 2016/5/20. + */ +public class CameraActivity extends Activity { + + private static int OVERLAY_PERMISSION_REQ_CODE = 1; + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + setContentView(R.layout.activity_camera); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!Settings.canDrawOverlays(this.getApplicationContext())) { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); + startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); + } + } + + if (null == savedInstanceState) { + getFragmentManager() + .beginTransaction() + .replace(R.id.container, CameraConnectionFragment.newInstance()) + .commit(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (!Settings.canDrawOverlays(this.getApplicationContext())) { + Toast.makeText(CameraActivity.this, "CameraActivity\", \"SYSTEM_ALERT_WINDOW, permission not granted...", Toast.LENGTH_SHORT).show(); + } else { + Intent intent = getIntent(); + finish(); + startActivity(intent); + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tzutalin/dlibtest/CameraConnectionFragment.java b/app/src/main/java/com/tzutalin/dlibtest/CameraConnectionFragment.java new file mode 100644 index 0000000..ea6c827 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/CameraConnectionFragment.java @@ -0,0 +1,683 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.Context; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.ImageReader; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.support.v4.app.ActivityCompat; +import android.util.Log; +import android.util.Range; +import android.util.Size; +import android.util.SparseArray; +import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import java.io.File; +import java.io.FileWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import hugo.weaving.DebugLog; +import timber.log.Timber; + +public class CameraConnectionFragment extends Fragment { + + /** + * The camera preview size will be chosen to be the smallest frame by pixel size capable of + * containing a DESIRED_SIZE x DESIRED_SIZE square. + */ + private static final int MINIMUM_PREVIEW_SIZE = 320; + private static final String TAG = "CameraConnectionFragment"; + private static Range[] fpsRanges; + + private TrasparentTitleView mScoreView; + + /** + * Conversion from screen rotation to JPEG orientation. + */ + private static final SparseIntArray ORIENTATIONS = new SparseIntArray(); + private static final String FRAGMENT_DIALOG = "dialog"; + + static { + ORIENTATIONS.append(Surface.ROTATION_0, 0); + ORIENTATIONS.append(Surface.ROTATION_90, 90); + ORIENTATIONS.append(Surface.ROTATION_180, 180); + ORIENTATIONS.append(Surface.ROTATION_270, 270); + } + + /** + * {@link android.view.TextureView.SurfaceTextureListener} handles several lifecycle events on a + * {@link TextureView}. + */ + private final TextureView.SurfaceTextureListener surfaceTextureListener = + new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable( + final SurfaceTexture texture, final int width, final int height) { + openCamera(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged( + final SurfaceTexture texture, final int width, final int height) { + configureTransform(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(final SurfaceTexture texture) { + return true; + } + + @Override + public void onSurfaceTextureUpdated(final SurfaceTexture texture) { + } + }; + + /** + * ID of the current {@link CameraDevice}. + */ + private String cameraId; + + /** + * An {@link AutoFitTextureView} for camera preview. + */ + private AutoFitTextureView textureView; + + /** + * A {@link CameraCaptureSession } for camera preview. + */ + private CameraCaptureSession captureSession; + + /** + * A reference to the opened {@link CameraDevice}. + */ + private CameraDevice cameraDevice; + + /** + * The {@link android.util.Size} of camera preview. + */ + private Size previewSize; + + /** + * {@link android.hardware.camera2.CameraDevice.StateCallback} + * is called when {@link CameraDevice} changes its state. + */ + private final CameraDevice.StateCallback stateCallback = + new CameraDevice.StateCallback() { + @Override + public void onOpened(final CameraDevice cd) { + // This method is called when the camera is opened. We start camera preview here. + cameraOpenCloseLock.release(); + cameraDevice = cd; + createCameraPreviewSession(); + } + + @Override + public void onDisconnected(final CameraDevice cd) { + cameraOpenCloseLock.release(); + cd.close(); + cameraDevice = null; + + if (mOnGetPreviewListener != null) { + mOnGetPreviewListener.deInitialize(); + } + } + + @Override + public void onError(final CameraDevice cd, final int error) { + cameraOpenCloseLock.release(); + cd.close(); + cameraDevice = null; + final Activity activity = getActivity(); + if (null != activity) { + activity.finish(); + } + + if (mOnGetPreviewListener != null) { + mOnGetPreviewListener.deInitialize(); + } + } + }; + + /** + * An additional thread for running tasks that shouldn't block the UI. + */ + private HandlerThread backgroundThread; + + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler backgroundHandler; + + /** + * An additional thread for running inference so as not to block the camera. + */ + private HandlerThread inferenceThread; + + /** + * A {@link Handler} for running tasks in the background. + */ + private Handler inferenceHandler; + + /** + * An {@link ImageReader} that handles preview frame capture. + */ + private ImageReader previewReader; + + /** + * {@link android.hardware.camera2.CaptureRequest.Builder} for the camera preview + */ + private CaptureRequest.Builder previewRequestBuilder; + + /** + * {@link CaptureRequest} generated by {@link #previewRequestBuilder} + */ + private CaptureRequest previewRequest; + + /** + * A {@link Semaphore} to prevent the app from exiting before closing the camera. + */ + private final Semaphore cameraOpenCloseLock = new Semaphore(1); + + /** + * Shows a {@link Toast} on the UI thread. + * + * @param text The message to show + */ + private void showToast(final String text) { + final Activity activity = getActivity(); + if (activity != null) { + activity.runOnUiThread( + new Runnable() { + @Override + public void run() { + Toast.makeText(activity, text, Toast.LENGTH_SHORT).show(); + } + }); + } + } + + /** + * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose + * width and height are at least as large as the respective requested values, and whose aspect + * ratio matches with the specified value. + * + * @param choices The list of sizes that the camera supports for the intended output class + * @param width The minimum desired width + * @param height The minimum desired height + * @param aspectRatio The aspect ratio + * @return The optimal {@code Size}, or an arbitrary one if none were big enough + */ + @SuppressLint("LongLogTag") + @DebugLog + private static Size chooseOptimalSize( + final Size[] choices, final int width, final int height, final Size aspectRatio) { + // Collect the supported resolutions that are at least as big as the preview Surface + final List bigEnough = new ArrayList(); + for (final Size option : choices) { + if (option.getHeight() >= MINIMUM_PREVIEW_SIZE && option.getWidth() >= MINIMUM_PREVIEW_SIZE) { + Timber.tag(TAG).i("Adding size: " + option.getWidth() + "x" + option.getHeight()); + bigEnough.add(option); + } else { + Timber.tag(TAG).i("Not adding size: " + option.getWidth() + "x" + option.getHeight()); + } + } + + // Pick the smallest of those, assuming we found any + if (bigEnough.size() > 0) { + final Size chosenSize = Collections.min(bigEnough, new CompareSizesByArea()); + Timber.tag(TAG).i("Chosen size: " + chosenSize.getWidth() + "x" + chosenSize.getHeight()); + return chosenSize; + } else { + Timber.tag(TAG).e("Couldn't find any suitable preview size"); + return choices[0]; + } + } + + public static CameraConnectionFragment newInstance() { + return new CameraConnectionFragment(); + } + + @Override + public View onCreateView( + final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { + return inflater.inflate(R.layout.camera_connection_fragment, container, false); + } + + @Override + public void onViewCreated(final View view, final Bundle savedInstanceState) { + textureView = (AutoFitTextureView) view.findViewById(R.id.texture); + mScoreView = (TrasparentTitleView) view.findViewById(R.id.results); + } + + @Override + public void onActivityCreated(final Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + } + + @Override + public void onResume() { + super.onResume(); + startBackgroundThread(); + + // When the screen is turned off and turned back on, the SurfaceTexture is already + // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open + // a camera and start preview from here (otherwise, we wait until the surface is ready in + // the SurfaceTextureListener). + if (textureView.isAvailable()) { + openCamera(textureView.getWidth(), textureView.getHeight()); + } else { + textureView.setSurfaceTextureListener(surfaceTextureListener); + } + } + + @Override + public void onPause() { + closeCamera(); + stopBackgroundThread(); + super.onPause(); + } + + /** + * Sets up member variables related to camera. + * + * @param width The width of available size for camera preview + * @param height The height of available size for camera preview + */ + @DebugLog + @SuppressLint("LongLogTag") + private void setUpCameraOutputs(final int width, final int height) { + final Activity activity = getActivity(); + final CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + try { + SparseArray cameraFaceTypeMap = new SparseArray<>(); + // Check the facing types of camera devices + for (final String cameraId : manager.getCameraIdList()) { + final CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + final Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); + + if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) { + if (cameraFaceTypeMap.get(CameraCharacteristics.LENS_FACING_FRONT) != null) { + cameraFaceTypeMap.append(CameraCharacteristics.LENS_FACING_FRONT, cameraFaceTypeMap.get(CameraCharacteristics.LENS_FACING_FRONT) + 1); + } else { + cameraFaceTypeMap.append(CameraCharacteristics.LENS_FACING_FRONT, 1); + } + } + + if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) { + if (cameraFaceTypeMap.get(CameraCharacteristics.LENS_FACING_FRONT) != null) { + cameraFaceTypeMap.append(CameraCharacteristics.LENS_FACING_BACK, cameraFaceTypeMap.get(CameraCharacteristics.LENS_FACING_BACK) + 1); + } else { + cameraFaceTypeMap.append(CameraCharacteristics.LENS_FACING_BACK, 1); + } + } + } + + Integer num_facing_back_camera = cameraFaceTypeMap.get(CameraCharacteristics.LENS_FACING_BACK); + for (final String cameraId : manager.getCameraIdList()) { + final CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); + final Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); + // If facing back camera or facing external camera exist, we won't use facing front camera + if (num_facing_back_camera != null && num_facing_back_camera > 0) { + // We don't use a front facing camera in this sample if there are other camera device facing types + if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) { + continue; + } + } + + final StreamConfigurationMap map = + characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + + if (map == null) { + continue; + } + + // For still image captures, we use the largest available size. + final Size largest = + Collections.max( + Arrays.asList(map.getOutputSizes(ImageFormat.YUV_420_888)), + new CompareSizesByArea()); + + // Danger, W.R.! Attempting to use too large a preview size could exceed the camera + // bus' bandwidth limitation, resulting in gorgeous previews but the storage of + // garbage capture data. + previewSize = chooseOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height, largest); + + // We fit the aspect ratio of TextureView to the size of preview we picked. + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + textureView.setAspectRatio(previewSize.getWidth(), previewSize.getHeight()); + } else { + textureView.setAspectRatio(previewSize.getHeight(), previewSize.getWidth()); + } + + CameraConnectionFragment.this.cameraId = cameraId; + return; + } + } catch (final CameraAccessException e) { + Timber.tag(TAG).e("Exception!", e); + } catch (final NullPointerException e) { + // Currently an NPE is thrown when the Camera2API is used but not supported on the + // device this code runs. + ErrorDialog.newInstance(getString(R.string.camera_error)) + .show(getChildFragmentManager(), FRAGMENT_DIALOG); + } + } + + /** + * Opens the camera specified by {@link CameraConnectionFragment#cameraId}. + */ + @SuppressLint("LongLogTag") + @DebugLog + private void openCamera(final int width, final int height) { + Log.d(TAG, "width "+width+"height "+height); + setUpCameraOutputs(width, height); + configureTransform(width, height); + final Activity activity = getActivity(); + final CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + try { + if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { + throw new RuntimeException("Time out waiting to lock camera opening."); + } + if (ActivityCompat.checkSelfPermission(this.getActivity(), Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + Timber.tag(TAG).w("checkSelfPermission CAMERA"); + } + //we just use facing front camera to detect + manager.openCamera("1", stateCallback, backgroundHandler); + Timber.tag(TAG).d("open Camera"); + } catch (final CameraAccessException e) { + Timber.tag(TAG).e("Exception!", e); + } catch (final InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera opening.", e); + } + } + + /** + * Closes the current {@link CameraDevice}. + */ + @DebugLog + private void closeCamera() { + try { + cameraOpenCloseLock.acquire(); + if (null != captureSession) { + captureSession.close(); + captureSession = null; + } + if (null != cameraDevice) { + cameraDevice.close(); + cameraDevice = null; + } + if (null != previewReader) { + previewReader.close(); + previewReader = null; + } + if (null != mOnGetPreviewListener) { + mOnGetPreviewListener.deInitialize(); + } + } catch (final InterruptedException e) { + throw new RuntimeException("Interrupted while trying to lock camera closing.", e); + } finally { + cameraOpenCloseLock.release(); + } + } + + /** + * Starts a background thread and its {@link Handler}. + */ + @DebugLog + private void startBackgroundThread() { + backgroundThread = new HandlerThread("ImageListener"); + backgroundThread.start(); + backgroundHandler = new Handler(backgroundThread.getLooper()); + + inferenceThread = new HandlerThread("InferenceThread"); + inferenceThread.start(); + inferenceHandler = new Handler(inferenceThread.getLooper()); + } + + /** + * Stops the background thread and its {@link Handler}. + */ + @SuppressLint("LongLogTag") + @DebugLog + private void stopBackgroundThread() { + backgroundThread.quitSafely(); + inferenceThread.quitSafely(); + try { + backgroundThread.join(); + backgroundThread = null; + backgroundHandler = null; + + inferenceThread.join(); + inferenceThread = null; + inferenceThread = null; + } catch (final InterruptedException e) { + Timber.tag(TAG).e("error", e); + } + } + + private final OnGetImageListener mOnGetPreviewListener = new OnGetImageListener(); + + private final CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureProgressed( + final CameraCaptureSession session, + final CaptureRequest request, + final CaptureResult partialResult) { + } + + @Override + public void onCaptureCompleted( + final CameraCaptureSession session, + final CaptureRequest request, + final TotalCaptureResult result) { + } + }; + + /** + * Creates a new {@link CameraCaptureSession} for camera preview. + */ + @SuppressLint("LongLogTag") + @DebugLog + private void createCameraPreviewSession() { + try { +// final SurfaceTexture texture = textureView.getSurfaceTexture(); +// assert texture != null; + + // We configure the size of default buffer to be the size of camera preview we want. +// texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + //texture.setOnFrameAvailableListener(); + + // This is the output Surface we need to start preview. +// final Surface surface = new Surface(texture); + + // We set up a CaptureRequest.Builder with the output Surface. + previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + //previewRequestBuilder.addTarget(surface); + + // 设置预览画面的帧率 视实际情况而定选择一个帧率范围 +// previewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRanges[0]); + + // Create the reader for the preview frames. + previewReader = + ImageReader.newInstance( + previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); + + previewReader.setOnImageAvailableListener(mOnGetPreviewListener, backgroundHandler); + previewRequestBuilder.addTarget(previewReader.getSurface()); + + // Here, we create a CameraCaptureSession for camera preview. + cameraDevice.createCaptureSession( + //Arrays.asList(surface,previewReader.getSurface()), new CameraCaptureSession.StateCallback() { + Arrays.asList(previewReader.getSurface()), new CameraCaptureSession.StateCallback() { + + @Override + public void onConfigured(final CameraCaptureSession cameraCaptureSession) { + // The camera is already closed + if (null == cameraDevice) { + return; + } + + // When the session is ready, we start displaying the preview. + captureSession = cameraCaptureSession; + try { + // Auto focus should be continuous for camera preview. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + // Flash is automatically enabled when necessary. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + + // Finally, we start displaying the camera preview. + previewRequest = previewRequestBuilder.build(); + captureSession.setRepeatingRequest( + previewRequest, captureCallback, backgroundHandler); + } catch (final CameraAccessException e) { + Timber.tag(TAG).e("Exception!", e); + } + } + + @Override + public void onConfigureFailed(final CameraCaptureSession cameraCaptureSession) { + showToast("Failed"); + } + }, + null); + } catch (final CameraAccessException e) { + Timber.tag(TAG).e("Exception!", e); + } + + mOnGetPreviewListener.initialize(getActivity().getApplicationContext(), getActivity().getAssets(), mScoreView, inferenceHandler); + } + + /** + * Configures the necessary {@link android.graphics.Matrix} transformation to `mTextureView`. + * This method should be called after the camera preview size is determined in + * setUpCameraOutputs and also the size of `mTextureView` is fixed. + * + * @param viewWidth The width of `mTextureView` + * @param viewHeight The height of `mTextureView` + */ + @DebugLog + private void configureTransform(final int viewWidth, final int viewHeight) { + final Activity activity = getActivity(); + if (null == textureView || null == previewSize || null == activity) { + return; + } + final int rotation = activity.getWindowManager().getDefaultDisplay().getRotation(); + final Matrix matrix = new Matrix(); + final RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); + final RectF bufferRect = new RectF(0, 0, previewSize.getHeight(), previewSize.getWidth()); + final float centerX = viewRect.centerX(); + final float centerY = viewRect.centerY(); + if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { + bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); + matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); + final float scale = + Math.max( + (float) viewHeight / previewSize.getHeight(), + (float) viewWidth / previewSize.getWidth()); + matrix.postScale(scale, scale, centerX, centerY); + matrix.postRotate(90 * (rotation - 2), centerX, centerY); + } else if (Surface.ROTATION_180 == rotation) { + matrix.postRotate(180, centerX, centerY); + } + textureView.setTransform(matrix); + } + + /** + * Compares two {@code Size}s based on their areas. + */ + static class CompareSizesByArea implements Comparator { + @Override + public int compare(final Size lhs, final Size rhs) { + // We cast here to ensure the multiplications won't overflow + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } + + /** + * Shows an error message dialog. + */ + public static class ErrorDialog extends DialogFragment { + private static final String ARG_MESSAGE = "message"; + + public static ErrorDialog newInstance(final String message) { + final ErrorDialog dialog = new ErrorDialog(); + final Bundle args = new Bundle(); + args.putString(ARG_MESSAGE, message); + dialog.setArguments(args); + return dialog; + } + + @Override + public Dialog onCreateDialog(final Bundle savedInstanceState) { + final Activity activity = getActivity(); + return new AlertDialog.Builder(activity) + .setMessage(getArguments().getString(ARG_MESSAGE)) + .setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(final DialogInterface dialogInterface, final int i) { + activity.finish(); + } + }) + .create(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tzutalin/dlibtest/DebugLogFileTree.java b/app/src/main/java/com/tzutalin/dlibtest/DebugLogFileTree.java new file mode 100644 index 0000000..758e4e9 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/DebugLogFileTree.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2017-present. Tzutalin + * 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.tzutalin.dlibtest; + +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.ref.WeakReference; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import timber.log.Timber; + +/** + * Created by tzutalin on 2017/2/23. + */ +public class DebugLogFileTree extends Timber.DebugTree { + + private static final String TWO_SPACE = " "; + private final String mLogDir; + private final String mFilePath; + private final LogWriterWorker mLogWriterWorker; + + public DebugLogFileTree(String dir) { + mLogDir = dir; + mFilePath = mLogDir + File.separator + "dlib.log"; + mLogWriterWorker = new LogWriterWorker(); + mLogWriterWorker.start(this); + } + + @Override + protected void log(int priority, String tag, String message, Throwable t) { + super.log(priority, tag, message, t); + mLogWriterWorker.put(formatLog(priority, tag, message, t)); + } + + private String formatLog(int priority, String tag, String message, Throwable throwable) { + StringBuilder sb = new StringBuilder(); + if (throwable != null && message == null) { + message = getStackTraceString(throwable); + } + if (message == null) { + message = "No message/exception is set"; + } + + sb.append(getTimeStamp()).append(TWO_SPACE); + sb.append(getThreadSignature()).append(TWO_SPACE); + sb.append(tag).append(":").append(getPriorityString(priority)).append(TWO_SPACE); + sb.append(message); + return sb.toString(); + } + + private String getStackTraceString(Throwable tr) { + if (tr == null) { + return ""; + } + + // This is to reduce the amount of log spew that apps do in the non-error + // condition of the network being unavailable. + Throwable t = tr; + while (t != null) { + if (t instanceof UnknownHostException) { + return ""; + } + t = t.getCause(); + } + + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + tr.printStackTrace(pw); + pw.flush(); + return sw.toString(); + } + + private String getThreadSignature() { + Thread t = Thread.currentThread(); + String name = t.getName(); + int id = android.os.Process.myTid(); + return String.format("%s:%d", name, id); + } + + private String getTimeStamp() { + return new SimpleDateFormat("HH:mm:ss.SSS").format(new Date()); + } + + public String getPriorityString(int priority) { + if (priority == Log.ASSERT) { + return "A"; + } else if (priority == Log.ERROR) { + return "E"; + } else if (priority == Log.WARN) { + return "W"; + } else if (priority == Log.INFO) { + return "I"; + } else if (priority == Log.DEBUG) { + return "D"; + } else if (priority == Log.VERBOSE) { + return "V"; + } + return ""; + } + + private static class LogWriterWorker implements Runnable { + private WeakReference mWeakRef; + private BufferedWriter mBufferedWriter; + private BlockingQueue mQueue; + + public void start(@NonNull DebugLogFileTree tree) { + if (isStart() == false) { + mWeakRef = new WeakReference(tree); + mQueue = new LinkedBlockingQueue<>(); + new Thread(this).start(); + } + } + + // Producer on any thread + public void put(@NonNull String msg) { + if (mQueue == null) return; + try { + mQueue.put(msg); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + // Consumer on LogWriterWorker's thread + @Override + public void run() { + // Open a new log file + if (isLogFileOpen() == false) + open(mWeakRef.get().mFilePath); + String log; + try { + while ((log = mQueue.take()) != null) { + appendLog(log); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + mQueue.clear(); + mQueue = null; + close(); + } + } + + private boolean isStart() { + return mQueue != null; + } + + private boolean isLogFileOpen() { + return mBufferedWriter != null; + } + + private boolean open(@NonNull String newFileName) { + if (TextUtils.isEmpty(newFileName)) return false; + File logFile = new File(newFileName); + // Create log file if not exists. + if (!logFile.exists()) { + try { + File parent = logFile.getParentFile(); + if (!parent.exists()) { + parent.mkdirs(); + } + logFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + // Create buffered writer. + try { + mBufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); + } catch (Exception e) { + e.printStackTrace(); + close(); + return false; + } + return true; + } + + private boolean close() { + if (mBufferedWriter != null) { + try { + mBufferedWriter.close(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + mBufferedWriter = null; + } + } + return true; + } + + private void appendLog(@NonNull String flattenedLog) { + try { + mBufferedWriter.write(flattenedLog); + mBufferedWriter.newLine(); + mBufferedWriter.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} + diff --git a/app/src/main/java/com/tzutalin/dlibtest/DlibDemoApp.java b/app/src/main/java/com/tzutalin/dlibtest/DlibDemoApp.java new file mode 100644 index 0000000..96b9e5f --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/DlibDemoApp.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2017. Tzutalin + * 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.tzutalin.dlibtest; + +import android.app.Application; +import android.util.Log; + +import timber.log.Timber; + +/** + * Created by tzutalin on 2017/2/23. + */ +public class DlibDemoApp extends Application { + @Override + public void onCreate() { + super.onCreate(); + + if (BuildConfig.DEBUG) { + Timber.plant(new Timber.DebugTree()); + //Timber.plant(new DebugLogFileTree(Environment.getExternalStorageDirectory().toString())); + } else { + Timber.plant(new ReleaseTree()); + } + } + + /** + * A tree which logs important information + */ + private static class ReleaseTree extends Timber.DebugTree { + @Override + protected void log(int priority, String tag, String message, Throwable t) { + if (priority == Log.VERBOSE || priority == Log.DEBUG) { + return; + } + super.log(priority, tag, message, t); + } + } +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/FileUtils.java b/app/src/main/java/com/tzutalin/dlibtest/FileUtils.java new file mode 100644 index 0000000..fdde049 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/FileUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.RawRes; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Created by Tzutalin on 2016/3/30. + */ +public class FileUtils { + @NonNull + public static final void copyFileFromRawToOthers(@NonNull final Context context, @RawRes int id, @NonNull final String targetPath) { + InputStream in = context.getResources().openRawResource(id); + FileOutputStream out = null; + try { + out = new FileOutputStream(targetPath); + byte[] buff = new byte[1024]; + int read = 0; + while ((read = in.read(buff)) > 0) { + out.write(buff, 0, read); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/FloatingCameraWindow.java b/app/src/main/java/com/tzutalin/dlibtest/FloatingCameraWindow.java new file mode 100644 index 0000000..e854d38 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/FloatingCameraWindow.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2016-present Tzuta Lin + * + * 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 + * imitations under the License. + */ + +package com.tzutalin.dlibtest; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.UiThread; +import android.util.Log; +import android.view.Display; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +/** + * Created by Tzutalin on 2016/5/25 + */ +public class FloatingCameraWindow { + private static final String TAG = "FloatingCameraWindow"; + private Context mContext; + private WindowManager.LayoutParams mWindowParam; + private WindowManager mWindowManager; + private FloatCamView mRootView; + private Handler mUIHandler; + + private int mWindowWidth; + private int mWindowHeight; + + private int mScreenMaxWidth; + private int mScreenMaxHeight; + + private float mScaleWidthRatio = 1.0f; + private float mScaleHeightRatio = 1.0f; + + private static final boolean DEBUG = true; + + public FloatingCameraWindow(Context context) { + mContext = context; + mUIHandler = new Handler(Looper.getMainLooper()); + + // Get screen max size + Point size = new Point(); + Display display = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + display.getSize(size); + mScreenMaxWidth = size.x; + mScreenMaxHeight = size.y; + } else { + mScreenMaxWidth = display.getWidth(); + mScreenMaxHeight = display.getHeight(); + } + // Default window size + mWindowWidth = mScreenMaxWidth; // / 2; + mWindowHeight = mScreenMaxHeight; // / 2; + +// mWindowWidth = mWindowWidth > 0 && mWindowWidth < mScreenMaxWidth ? mWindowWidth : mScreenMaxWidth; +// mWindowHeight = mWindowHeight > 0 && mWindowHeight < mScreenMaxHeight ? mWindowHeight : mScreenMaxHeight; + } + + public FloatingCameraWindow(Context context, int windowWidth, int windowHeight) { + this(context); + + if (windowWidth < 0 || windowWidth > mScreenMaxWidth || windowHeight < 0 || windowHeight > mScreenMaxHeight) { + throw new IllegalArgumentException("Window size is illegal"); + } + + mScaleWidthRatio = (float) windowWidth / mWindowHeight; + mScaleHeightRatio = (float) windowHeight / mWindowHeight; + + if (DEBUG) { + Log.d(TAG, "mScaleWidthRatio: " + mScaleWidthRatio); + Log.d(TAG, "mScaleHeightRatio: " + mScaleHeightRatio); + } + + mWindowWidth = windowWidth; + mWindowHeight = windowHeight; + } + + private void init() { + mUIHandler.postAtFrontOfQueue(new Runnable() { + @Override + public void run() { + if (mWindowManager == null || mRootView == null) { + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + mRootView = new FloatCamView(FloatingCameraWindow.this); + mWindowManager.addView(mRootView, initWindowParameter()); + } + } + }); + } + + public void release() { + mUIHandler.postAtFrontOfQueue(new Runnable() { + @Override + public void run() { + if (mWindowManager != null) { + mWindowManager.removeViewImmediate(mRootView); + mRootView = null; + } + mUIHandler.removeCallbacksAndMessages(null); + } + }); + } + + private WindowManager.LayoutParams initWindowParameter() { + mWindowParam = new WindowManager.LayoutParams(); + + mWindowParam.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; + mWindowParam.format = 1; + mWindowParam.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mWindowParam.flags = mWindowParam.flags | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; + mWindowParam.flags = mWindowParam.flags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + + mWindowParam.alpha = 1.0f; + + mWindowParam.gravity = Gravity.BOTTOM | Gravity.RIGHT; + mWindowParam.x = 0; + mWindowParam.y = 0; + mWindowParam.width = mWindowWidth; + mWindowParam.height = mWindowHeight; + return mWindowParam; + } + + public void setRGBBitmap(final Bitmap rgb) { + checkInit(); + mUIHandler.post(new Runnable() { + @Override + public void run() { + mRootView.setRGBImageView(rgb); + } + }); + } + + public void setInformation(final String info) { + checkInit(); + mUIHandler.post(new Runnable() { + @Override + public void run() { + checkInit(); + mRootView.setInformation(info); + } + }); + } + + public void setMoreInformation(final String info) { + checkInit(); + mUIHandler.post(new Runnable() { + @Override + public void run() { + checkInit(); + mRootView.setMoreInformation(info); + } + }); + } + + private void checkInit() { + if (mRootView == null) { + init(); + } + } + + @UiThread + private final class FloatCamView extends FrameLayout { + private WeakReference mWeakRef; + private static final int MOVE_THRESHOLD = 10; + private int mLastX; + private int mLastY; + private int mFirstX; + private int mFirstY; + private LayoutInflater mLayoutInflater; + private ImageView mColorView; + private TextView mFPSText; + private TextView mInfoText; + private boolean mIsMoving = false; + + public FloatCamView(FloatingCameraWindow window) { + super(window.mContext); + mWeakRef = new WeakReference(window); + // mLayoutInflater = LayoutInflater.from(context); + mLayoutInflater = (LayoutInflater) window.mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + FrameLayout body = (FrameLayout) this; + body.setOnTouchListener(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + return false; + } + }); + + View floatView = mLayoutInflater.inflate(R.layout.cam_window_view, body, true); + mColorView = (ImageView) findViewById(R.id.imageView_c); + mFPSText = (TextView) findViewById(R.id.fps_textview); + mInfoText = (TextView) findViewById(R.id.info_textview); + mFPSText.setVisibility(View.GONE); + mInfoText.setVisibility(View.GONE); + + int colorMaxWidth = (int) (mWindowWidth* window.mScaleWidthRatio); + int colorMaxHeight = (int) (mWindowHeight * window.mScaleHeightRatio); + + mColorView.getLayoutParams().width = colorMaxWidth; + mColorView.getLayoutParams().height = colorMaxHeight; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + mLastX = (int) event.getRawX(); + mLastY = (int) event.getRawY(); + mFirstX = mLastX; + mFirstY = mLastY; + break; + case MotionEvent.ACTION_MOVE: + int deltaX = (int) event.getRawX() - mLastX; + int deltaY = (int) event.getRawY() - mLastY; + mLastX = (int) event.getRawX(); + mLastY = (int) event.getRawY(); + int totalDeltaX = mLastX - mFirstX; + int totalDeltaY = mLastY - mFirstY; + + if (mIsMoving + || Math.abs(totalDeltaX) >= MOVE_THRESHOLD + || Math.abs(totalDeltaY) >= MOVE_THRESHOLD) { + mIsMoving = true; + WindowManager windowMgr = mWeakRef.get().mWindowManager; + WindowManager.LayoutParams parm = mWeakRef.get().mWindowParam; + if (event.getPointerCount() == 1 && windowMgr != null) { + parm.x -= deltaX; + parm.y -= deltaY; + windowMgr.updateViewLayout(this, parm); + } + } + break; + + case MotionEvent.ACTION_UP: + mIsMoving = false; + break; + } + return true; + } + + public void setRGBImageView(Bitmap rgb) { + if (rgb != null && !rgb.isRecycled()) { + mColorView.setImageBitmap(rgb); + } + } + + public void setInformation(String info) { + if (mFPSText != null) { + if (mFPSText.getVisibility() == View.GONE) { + mFPSText.setVisibility(View.VISIBLE); + } + mFPSText.setText(info); + } + } + + public void setMoreInformation(String info) { + if (mInfoText != null) { + if (mInfoText.getVisibility() == View.GONE) { + mInfoText.setVisibility(View.VISIBLE); + } + mInfoText.setText(info); + } + } + } + +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/ImageUtils.java b/app/src/main/java/com/tzutalin/dlibtest/ImageUtils.java new file mode 100644 index 0000000..42a05d3 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/ImageUtils.java @@ -0,0 +1,161 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.graphics.Bitmap; +import android.os.Environment; +import android.support.annotation.Keep; + +import java.io.File; +import java.io.FileOutputStream; + +import timber.log.Timber; + +/** + * Utility class for manipulating images. + **/ +public class ImageUtils { + private static final String TAG = ImageUtils.class.getSimpleName(); + + /** + * Utility method to compute the allocated size in bytes of a YUV420SP image + * of the given dimensions. + */ + public static int getYUVByteSize(final int width, final int height) { + // The luminance plane requires 1 byte per pixel. + final int ySize = width * height; + + // The UV plane works on 2x2 blocks, so dimensions with odd size must be rounded up. + // Each 2x2 block takes 2 bytes to encode, one each for U and V. + final int uvSize = ((width + 1) / 2) * ((height + 1) / 2) * 2; + + return ySize + uvSize; + } + + /** + * Saves a Bitmap object to disk for analysis. + * + * @param bitmap The bitmap to save. + */ + public static void saveBitmap(final Bitmap bitmap) { + final String root = + Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "dlib"; + Timber.tag(TAG).d(String.format("Saving %dx%d bitmap to %s.", bitmap.getWidth(), bitmap.getHeight(), root)); + final File myDir = new File(root); + + if (!myDir.mkdirs()) { + Timber.tag(TAG).e("Make dir failed"); + } + + final String fname = "preview.png"; + final File file = new File(myDir, fname); + if (file.exists()) { + file.delete(); + } + try { + final FileOutputStream out = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 99, out); + out.flush(); + out.close(); + } catch (final Exception e) { + Timber.tag(TAG).e("Exception!", e); + } + } + + /** + * Converts YUV420 semi-planar data to ARGB 8888 data using the supplied width + * and height. The input and output must already be allocated and non-null. + * For efficiency, no error checking is performed. + * + * @param input The array of YUV 4:2:0 input data. + * @param output A pre-allocated array for the ARGB 8:8:8:8 output data. + * @param width The width of the input image. + * @param height The height of the input image. + * @param halfSize If true, downsample to 50% in each dimension, otherwise not. + */ + public static native void convertYUV420SPToARGB8888( + byte[] input, int[] output, int width, int height, boolean halfSize); + + /** + * Converts YUV420 semi-planar data to ARGB 8888 data using the supplied width + * and height. The input and output must already be allocated and non-null. + * For efficiency, no error checking is performed. + * + * @param y + * @param u + * @param v + * @param uvPixelStride + * @param width The width of the input image. + * @param height The height of the input image. + * @param halfSize If true, downsample to 50% in each dimension, otherwise not. + * @param output A pre-allocated array for the ARGB 8:8:8:8 output data. + */ + @Keep + public static native void convertYUV420ToARGB8888( + byte[] y, + byte[] u, + byte[] v, + int[] output, + int width, + int height, + int yRowStride, + int uvRowStride, + int uvPixelStride, + boolean halfSize); + + /** + * Converts YUV420 semi-planar data to RGB 565 data using the supplied width + * and height. The input and output must already be allocated and non-null. + * For efficiency, no error checking is performed. + * + * @param input The array of YUV 4:2:0 input data. + * @param output A pre-allocated array for the RGB 5:6:5 output data. + * @param width The width of the input image. + * @param height The height of the input image. + */ + @Keep + public static native void convertYUV420SPToRGB565( + byte[] input, byte[] output, int width, int height); + + /** + * Converts 32-bit ARGB8888 image data to YUV420SP data. This is useful, for + * instance, in creating data to feed the classes that rely on raw camera + * preview frames. + * + * @param input An array of input pixels in ARGB8888 format. + * @param output A pre-allocated array for the YUV420SP output data. + * @param width The width of the input image. + * @param height The height of the input image. + */ + @Keep + public static native void convertARGB8888ToYUV420SP( + int[] input, byte[] output, int width, int height); + + /** + * Converts 16-bit RGB565 image data to YUV420SP data. This is useful, for + * instance, in creating data to feed the classes that rely on raw camera + * preview frames. + * + * @param input An array of input pixels in RGB565 format. + * @param output A pre-allocated array for the YUV420SP output data. + * @param width The width of the input image. + * @param height The height of the input image. + */ + @Keep + public static native void convertRGB565ToYUV420SP( + byte[] input, byte[] output, int width, int height); +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/MainActivity.java b/app/src/main/java/com/tzutalin/dlibtest/MainActivity.java new file mode 100644 index 0000000..7b6185b --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/MainActivity.java @@ -0,0 +1,385 @@ +/* +* Copyright (C) 2015-present TzuTaLin +*/ + +package com.tzutalin.dlibtest; + +import android.Manifest; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.annotation.NonNull; +import android.support.design.widget.FloatingActionButton; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.widget.Toast; + +import com.dexafree.materialList.card.Card; +import com.dexafree.materialList.card.provider.BigImageCardProvider; +import com.dexafree.materialList.view.MaterialListView; +import com.tzutalin.dlib.Constants; +import com.tzutalin.dlib.FaceDet; +import com.tzutalin.dlib.PedestrianDet; +import com.tzutalin.dlib.VisionDetRet; + +import org.androidannotations.annotations.AfterViews; +import org.androidannotations.annotations.Background; +import org.androidannotations.annotations.Click; +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.UiThread; +import org.androidannotations.annotations.ViewById; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import hugo.weaving.DebugLog; +import timber.log.Timber; + +@EActivity(R.layout.activity_main) +public class MainActivity extends AppCompatActivity { + private static final int RESULT_LOAD_IMG = 1; + private static final int REQUEST_CODE_PERMISSION = 2; + + private static final String TAG = "MainActivity"; + + // Storage Permissions + private static String[] PERMISSIONS_REQ = { + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.CAMERA + }; + + protected String mTestImgPath; + // UI + @ViewById(R.id.material_listview) + protected MaterialListView mListView; + @ViewById(R.id.fab) + protected FloatingActionButton mFabActionBt; + @ViewById(R.id.fab_cam) + protected FloatingActionButton mFabCamActionBt; + @ViewById(R.id.toolbar) + protected Toolbar mToolbar; + + FaceDet mFaceDet; + PedestrianDet mPersonDet; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mListView = (MaterialListView) findViewById(R.id.material_listview); + setSupportActionBar(mToolbar); + // Just use hugo to print log + isExternalStorageWritable(); + isExternalStorageReadable(); + + // For API 23+ you need to request the read/write permissions even if they are already in your manifest. + int currentapiVersion = android.os.Build.VERSION.SDK_INT; + + if (currentapiVersion >= Build.VERSION_CODES.M) { + verifyPermissions(this); + } + + //we set "noDetection" as default detectionMode + SharedPreferences mSharedPreferences = getSharedPreferences("userInfo", MODE_PRIVATE); + SharedPreferences.Editor edit = mSharedPreferences.edit(); + edit.putString("detectionMode", "noDetection"); + edit.commit(); + + } + + @AfterViews + protected void setupUI() { + mToolbar.setTitle(getString(R.string.app_name)); + Toast.makeText(MainActivity.this, getString(R.string.description_info), Toast.LENGTH_LONG).show(); + + FloatingActionButton mSettingBt = (FloatingActionButton) findViewById(R.id.setting); + mSettingBt.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(MainActivity.this, Setting.class)); + } + }); + + } + + @Click({R.id.fab}) + protected void launchGallery() { + Toast.makeText(MainActivity.this, "Pick one image", Toast.LENGTH_SHORT).show(); + Intent galleryIntent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(galleryIntent, RESULT_LOAD_IMG); + } + + @Click({R.id.fab_cam}) + protected void launchCameraPreview() { + startActivity(new Intent(this, CameraActivity.class)); + } + + /** + * Checks if the app has permission to write to device storage or open camera + * If the app does not has permission then the user will be prompted to grant permissions + * + * @param activity + */ + @DebugLog + private static boolean verifyPermissions(Activity activity) { + // Check if we have write permission + int write_permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); + int read_persmission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); + int camera_permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA); + + if (write_permission != PackageManager.PERMISSION_GRANTED || + read_persmission != PackageManager.PERMISSION_GRANTED || + camera_permission != PackageManager.PERMISSION_GRANTED) { + // We don't have permission so prompt the user + ActivityCompat.requestPermissions( + activity, + PERMISSIONS_REQ, + REQUEST_CODE_PERMISSION + ); + return false; + } else { + return true; + } + } + + /* Checks if external storage is available for read and write */ + @DebugLog + private boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state)) { + return true; + } + return false; + } + + /* Checks if external storage is available to at least read */ + @DebugLog + private boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + if (Environment.MEDIA_MOUNTED.equals(state) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { + return true; + } + return false; + } + + @DebugLog + protected void demoStaticImage() { + if (mTestImgPath != null) { + Timber.tag(TAG).d("demoStaticImage() launch a task to det"); + runDetectAsync(mTestImgPath); + } else { + Timber.tag(TAG).d("demoStaticImage() mTestImgPath is null, go to gallery"); + Toast.makeText(MainActivity.this, "Pick an image to run algorithms", Toast.LENGTH_SHORT).show(); + // Create intent to Open Image applications like Gallery, Google Photos + Intent galleryIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(galleryIntent, RESULT_LOAD_IMG); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == REQUEST_CODE_PERMISSION) { + Toast.makeText(MainActivity.this, "Demo using static images", Toast.LENGTH_SHORT).show(); + demoStaticImage(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + try { + // When an Image is picked + if (requestCode == RESULT_LOAD_IMG && resultCode == RESULT_OK && null != data) { + // Get the Image from data + Uri selectedImage = data.getData(); + String[] filePathColumn = {MediaStore.Images.Media.DATA}; + // Get the cursor + Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null); + cursor.moveToFirst(); + int columnIndex = cursor.getColumnIndex(filePathColumn[0]); + mTestImgPath = cursor.getString(columnIndex); + cursor.close(); + if (mTestImgPath != null) { + runDetectAsync(mTestImgPath); + //Toast.makeText(this, "Img Path:" + mTestImgPath, Toast.LENGTH_SHORT).show(); + } + } else { + Toast.makeText(this, "You haven't picked Image", Toast.LENGTH_LONG).show(); + } + } catch (Exception e) { + Toast.makeText(this, "Something went wrong", Toast.LENGTH_LONG).show(); + } + } + + // ========================================================== + // Tasks inner class + // ========================================================== + private ProgressDialog mDialog; + + @Background + @NonNull + protected void runDetectAsync(@NonNull String imgPath) { + showDiaglog(); + + final String targetPath = Constants.getFaceShapeModelPath(); + if (!new File(targetPath).exists()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(MainActivity.this, "Copy landmark model to " + targetPath, Toast.LENGTH_SHORT).show(); + } + }); + FileUtils.copyFileFromRawToOthers(getApplicationContext(), R.raw.shape_predictor_68_face_landmarks, targetPath); + } + // Init + if (mPersonDet == null) { + mPersonDet = new PedestrianDet(); + } + if (mFaceDet == null) { + mFaceDet = new FaceDet(Constants.getFaceShapeModelPath()); + } + + Timber.tag(TAG).d("Image path: " + imgPath); + List cardrets = new ArrayList<>(); + List faceList = mFaceDet.detect(imgPath); + if (faceList.size() > 0) { + Card card = new Card.Builder(MainActivity.this) + .withProvider(BigImageCardProvider.class) + .setDrawable(drawRect(imgPath, faceList, Color.GREEN)) + .setTitle("Face det") + .endConfig() + .build(); + cardrets.add(card); + } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), "No face", Toast.LENGTH_SHORT).show(); + } + }); + } + + List personList = mPersonDet.detect(imgPath); + if (personList.size() > 0) { + Card card = new Card.Builder(MainActivity.this) + .withProvider(BigImageCardProvider.class) + .setDrawable(drawRect(imgPath, personList, Color.BLUE)) + .setTitle("Person det") + .endConfig() + .build(); + cardrets.add(card); + } else { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), "No person", Toast.LENGTH_SHORT).show(); + } + }); + } + + addCardListView(cardrets); + dismissDialog(); + } + + @UiThread + protected void addCardListView(List cardrets) { + for (Card each : cardrets) { + mListView.add(each); + } + } + + @UiThread + protected void showDiaglog() { + mDialog = ProgressDialog.show(MainActivity.this, "Wait", "Face detection", true); + } + + @UiThread + protected void dismissDialog() { + if (mDialog != null) { + mDialog.dismiss(); + } + } + + @DebugLog + protected BitmapDrawable drawRect(String path, List results, int color) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = 1; + Bitmap bm = BitmapFactory.decodeFile(path, options); + android.graphics.Bitmap.Config bitmapConfig = bm.getConfig(); + // set default bitmap config if none + if (bitmapConfig == null) { + bitmapConfig = android.graphics.Bitmap.Config.ARGB_8888; + } + // resource bitmaps are imutable, + // so we need to convert it to mutable one + bm = bm.copy(bitmapConfig, true); + int width = bm.getWidth(); + int height = bm.getHeight(); + // By ratio scale + float aspectRatio = bm.getWidth() / (float) bm.getHeight(); + + final int MAX_SIZE = 512; + int newWidth = MAX_SIZE; + int newHeight = MAX_SIZE; + float resizeRatio = 1; + newHeight = Math.round(newWidth / aspectRatio); + if (bm.getWidth() > MAX_SIZE && bm.getHeight() > MAX_SIZE) { + Timber.tag(TAG).d("Resize Bitmap"); + bm = getResizedBitmap(bm, newWidth, newHeight); + resizeRatio = (float) bm.getWidth() / (float) width; + Timber.tag(TAG).d("resizeRatio " + resizeRatio); + } + + // Create canvas to draw + Canvas canvas = new Canvas(bm); + Paint paint = new Paint(); + paint.setColor(color); + paint.setStrokeWidth(2); + paint.setStyle(Paint.Style.STROKE); + // Loop result list + for (VisionDetRet ret : results) { + Rect bounds = new Rect(); + bounds.left = (int) (ret.getLeft() * resizeRatio); + bounds.top = (int) (ret.getTop() * resizeRatio); + bounds.right = (int) (ret.getRight() * resizeRatio); + bounds.bottom = (int) (ret.getBottom() * resizeRatio); + canvas.drawRect(bounds, paint); + // Get landmark + ArrayList landmarks = ret.getFaceLandmarks(); + for (Point point : landmarks) { + int pointX = (int) (point.x * resizeRatio); + int pointY = (int) (point.y * resizeRatio); + canvas.drawCircle(pointX, pointY, 2, paint); + } + } + + return new BitmapDrawable(getResources(), bm); + } + + @DebugLog + protected Bitmap getResizedBitmap(Bitmap bm, int newWidth, int newHeight) { + Bitmap resizedBitmap = Bitmap.createScaledBitmap(bm, newWidth, newHeight, true); + return resizedBitmap; + } +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/OnGetImageListener.java b/app/src/main/java/com/tzutalin/dlibtest/OnGetImageListener.java new file mode 100644 index 0000000..ab1764c --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/OnGetImageListener.java @@ -0,0 +1,220 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Point; +import android.media.Image; +import android.media.Image.Plane; +import android.media.ImageReader; +import android.media.ImageReader.OnImageAvailableListener; +import android.os.Handler; +import android.os.Trace; +import android.util.Log; +import android.view.Display; +import android.view.WindowManager; + +import junit.framework.Assert; + +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Class that takes in preview frames and converts the image to Bitmaps to process with dlib lib. + */ +public class OnGetImageListener implements OnImageAvailableListener { + + private static int INPUT_SIZE = 960; + private static final String TAG = "OnGetImageListener"; + + private int mScreenRotation = 90; + + private int mPreviewWdith = 0; + private int mPreviewHeight = 0; + private byte[][] mYUVBytes; + private int[] mRGBBytes = null; + private Bitmap mRGBframeBitmap = null; + private Bitmap mCroppedBitmap = null; + private Bitmap mResizedBitmap = null; + private Bitmap mInversedBipmap = null; + + private Handler mInferenceHandler; + + private Context mContext; + private TrasparentTitleView mTransparentTitleView; + + private int mframeNum = 0; + private float r = 4f; + //Queue + private ProcessWithQueue processFrameQueue; + private LinkedBlockingQueue frameQueue; + private LinkedBlockingQueue frameQueueForDisplay; + + + public void initialize( + final Context context, + final AssetManager assetManager, + final TrasparentTitleView scoreView, + final Handler handler) { + this.mContext = context; + this.mTransparentTitleView = scoreView; + this.mInferenceHandler = handler; + + frameQueue = new LinkedBlockingQueue<>(); + frameQueueForDisplay = new LinkedBlockingQueue<>(); + processFrameQueue = new ProcessWithQueue(frameQueue, frameQueueForDisplay, mContext, mTransparentTitleView, handler); + + } + + public void deInitialize() { + synchronized (OnGetImageListener.this) { + if (processFrameQueue != null){ + processFrameQueue.release(); + } + } + } + + private void drawResizedBitmap(final Bitmap src, final Bitmap dst) { + + Display getOrient = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + int orientation = Configuration.ORIENTATION_UNDEFINED; + Point point = new Point(); + getOrient.getSize(point); + int screen_width = point.x; + int screen_height = point.y; + Log.d(TAG, String.format("screen size (%d,%d)", screen_width, screen_height)); + if (screen_width < screen_height) { + orientation = Configuration.ORIENTATION_PORTRAIT; + mScreenRotation = -90; + } else { + orientation = Configuration.ORIENTATION_LANDSCAPE; + mScreenRotation = 0; + } + + Assert.assertEquals(dst.getWidth(), dst.getHeight()); + final float minDim = Math.min(src.getWidth(), src.getHeight()); + + final Matrix matrix = new Matrix(); + + // We only want the center square out of the original rectangle. + final float translateX = -Math.max(0, (src.getWidth() - minDim) / 2); + final float translateY = -Math.max(0, (src.getHeight() - minDim) / 2); + matrix.preTranslate(translateX, translateY); + + final float scaleFactor = dst.getHeight() / minDim; + matrix.postScale(scaleFactor, scaleFactor); + + // Rotate around the center if necessary. + if (mScreenRotation != 0) { + matrix.postTranslate(-dst.getWidth() / 2.0f, -dst.getHeight() / 2.0f); + matrix.postRotate(mScreenRotation); + matrix.postTranslate(dst.getWidth() / 2.0f, dst.getHeight() / 2.0f); + } + + final Canvas canvas = new Canvas(dst); + canvas.drawBitmap(src, matrix, null); + } + + public Bitmap imageSideInversion(Bitmap src){ + Matrix sideInversion = new Matrix(); + sideInversion.setScale(-1, 1); + Bitmap inversedImage = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), sideInversion, false); + return inversedImage; + } + + @Override + public void onImageAvailable(final ImageReader reader) { + + Image image = null; + try { + image = reader.acquireNextImage(); + mframeNum++; + Log.i("frameInListener", String.valueOf(mframeNum)); + if (image == null) { + return; + } + + Trace.beginSection("imageAvailable"); + + final Plane[] planes = image.getPlanes(); + + // Initialize the storage bitmaps once when the resolution is known. + if (mPreviewWdith != image.getWidth() || mPreviewHeight != image.getHeight()) { + mPreviewWdith = image.getWidth(); + mPreviewHeight = image.getHeight(); + + //Log.d(TAG, String.format("Initializing at size %dx%d", mPreviewWdith, mPreviewHeight)); + mRGBBytes = new int[mPreviewWdith * mPreviewHeight]; + mRGBframeBitmap = Bitmap.createBitmap(mPreviewWdith, mPreviewHeight, Config.ARGB_8888); + mCroppedBitmap = Bitmap.createBitmap(INPUT_SIZE, INPUT_SIZE, Config.ARGB_8888); + + mYUVBytes = new byte[planes.length][]; + for (int i = 0; i < planes.length; ++i) { + mYUVBytes[i] = new byte[planes[i].getBuffer().capacity()]; + } + } + + for (int i = 0; i < planes.length; ++i) { + planes[i].getBuffer().get(mYUVBytes[i]); + } + + final int yRowStride = planes[0].getRowStride(); + final int uvRowStride = planes[1].getRowStride(); + final int uvPixelStride = planes[1].getPixelStride(); + ImageUtils.convertYUV420ToARGB8888( + mYUVBytes[0], + mYUVBytes[1], + mYUVBytes[2], + mRGBBytes, + mPreviewWdith, + mPreviewHeight, + yRowStride, + uvRowStride, + uvPixelStride, + false); + + image.close(); + } catch (final Exception e) { + if (image != null) { + image.close(); + } + Log.e(TAG, "Exception!", e); + Trace.endSection(); + return; + } + + mRGBframeBitmap.setPixels(mRGBBytes, 0, mPreviewWdith, 0, 0, mPreviewWdith, mPreviewHeight); + drawResizedBitmap(mRGBframeBitmap, mCroppedBitmap); + + mInversedBipmap = imageSideInversion(mCroppedBitmap); + mResizedBitmap = Bitmap.createScaledBitmap(mInversedBipmap, (int)(INPUT_SIZE/r), (int)(INPUT_SIZE/r), true); + + try { + frameQueueForDisplay.put(mInversedBipmap); + frameQueue.put(mResizedBitmap); + } catch (InterruptedException e) { + e.printStackTrace(); + } + Log.i("queueInListener", String.valueOf(frameQueue.size())); + } +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/ProcessWithQueue.java b/app/src/main/java/com/tzutalin/dlibtest/ProcessWithQueue.java new file mode 100644 index 0000000..4d9887b --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/ProcessWithQueue.java @@ -0,0 +1,469 @@ +package com.tzutalin.dlibtest; + +import android.content.Context; +import android.content.SharedPreferences; +import android.support.v7.app.AppCompatActivity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Point; +import android.media.Image; +import android.os.Environment; +import android.os.Handler; +import android.util.Log; +import android.view.Display; +import android.view.WindowManager; + +import com.tzutalin.dlib.Constants; +import com.tzutalin.dlib.FaceDet; +import com.tzutalin.dlib.VisionDetRet; + +import junit.framework.Assert; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +import static android.content.Context.MODE_PRIVATE; + +public class ProcessWithQueue extends Thread { + private static final String TAG = "Queue"; + private static final String NoDetection = "noDetection"; + private static final String EyesBlinkDetection = "eyesBlinkDetection"; + private static final String headOrientationDetection = "headOrientationDetection"; + + private LinkedBlockingQueue mQueue; + private LinkedBlockingQueue frameForDisplay; + + private static String checkMode = null; + + private List results; + + private Handler mInferenceHandler; + private Context mContext; + private FaceDet mFaceDet; + private TrasparentTitleView mTransparentTitleView; + private FloatingCameraWindow mWindow; + private Paint mFaceLandmardkPaint; + + private int mframeNum = 0; + + private double ear = 0; + private int x = 0; + private boolean ear_array_removed = false; + private boolean drop_array_appended = false; + private double THRESH = 0.04; + private double DROP_THRESH = 0.065; + private ArrayList ear_array = new ArrayList<>(); + private ArrayList ax = new ArrayList<>(); + private ArrayList ay = new ArrayList<>(); + private int continuous_Increment = 0; + private int continuous_Decrement = 0; + private double drop = 0; + private ArrayList drop_array = new ArrayList<>(); + private int temp = 0; + private double closeEyes_drop = -5; + private double openEyes_drop = 0; + private int closeEyes_end = 0; + private int openEyes_start = 0; + private int blink = 0; + private int frames_notFoundFace = 0; + private Point keyPoint_right = null; + private Point keyPoint_left = null; + private Point keyPoint_nose = null; + private double rightHalfFace = 0; + private double leftHalfFace = 0; + private String headToward = "front"; + + private double ratio = 0; + + private SharedPreferences mSharedPreferences; + + + public ProcessWithQueue(LinkedBlockingQueue frameQueue, LinkedBlockingQueue frameQueueForDisplay, Context context, TrasparentTitleView scoreView, Handler handler) { + this.mContext = context; + this.mTransparentTitleView = scoreView; + this.mInferenceHandler = handler; + + mQueue = frameQueue; + frameForDisplay = frameQueueForDisplay; + + mFaceDet = new FaceDet(Constants.getFaceShapeModelPath()); + mWindow = new FloatingCameraWindow(mContext); + + mFaceLandmardkPaint = new Paint(); + mFaceLandmardkPaint.setColor(Color.GREEN); + mFaceLandmardkPaint.setStrokeWidth(2); + mFaceLandmardkPaint.setStyle(Paint.Style.STROKE); + + mSharedPreferences = context.getSharedPreferences("userInfo", MODE_PRIVATE); + checkMode = mSharedPreferences.getString("detectionMode",""); + + start(); + } + + public void release(){ + if (mFaceDet != null) { + mFaceDet.release(); + } + + if (mWindow != null) { + mWindow.release(); + } + } + + @Override + public void run() { + while (true) { + Bitmap frameData = null; + Bitmap framefordisplay = null; + try { + frameData = mQueue.take(); + framefordisplay = frameForDisplay.take(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (frameData == null) { + break; + } + processFrame(frameData, framefordisplay); + } + } + + private void processFrame(final Bitmap frameData, final Bitmap framefordisplay) { + + if(frameData != null){ + mInferenceHandler.post( + new Runnable() { + @Override + public void run() { + + if (!new File(Constants.getFaceShapeModelPath()).exists()) { + mTransparentTitleView.setText("Copying landmark model to " + Constants.getFaceShapeModelPath()); + FileUtils.copyFileFromRawToOthers(mContext, R.raw.shape_predictor_68_face_landmarks, Constants.getFaceShapeModelPath()); + } + + mframeNum++; +// saveBitmap(frameData, "frames", String.valueOf(mframeNum) + ".jpg"); + + switch (checkMode){ + + case NoDetection:{ + mWindow.setRGBBitmap(framefordisplay); + mWindow.setInformation("frame: " + String.valueOf(mframeNum)); + mTransparentTitleView.setText("noDetection"); + }break; + + case EyesBlinkDetection:{ + + long startTime = System.currentTimeMillis(); + + results = mFaceDet.detect(frameData); + + if (results.size() != 0) { + for (final VisionDetRet ret : results) { + float resizeRatio = 4f; + Canvas canvas = new Canvas(framefordisplay); + + ArrayList landmarks = ret.getFaceLandmarks(); + + int i = 1; + + //get the 6 key point from 68_face_landmarks + Point[] leftEye = new Point[6]; + Point[] rightEye = new Point[6]; + + for (Point point : landmarks) { + if (i > 36 && i < 43) { + //for more efficient procession, the data we process were zoomed out + //So the point must be magnified , to display correctly in the original image. + int pointX = (int) (point.x * resizeRatio); + int pointY = (int) (point.y * resizeRatio); + leftEye[i - 37] = new Point(pointX, pointY); + //canvas.drawCircle(pointX, pointY, 2, mFaceLandmardkPaint); + } else if (i > 42 && i < 49) { + int pointX = (int) (point.x * resizeRatio); + int pointY = (int) (point.y * resizeRatio); + rightEye[i - 43] = new Point(pointX, pointY); + //canvas.drawCircle(pointX, pointY, 2, mFaceLandmardkPaint); + } + if (i > 48) { + break; + } + i++; + } + + canvas.drawPath(getPath(leftEye), mFaceLandmardkPaint); + canvas.drawPath(getPath(rightEye), mFaceLandmardkPaint); + //saveBitmap(frameData, "Pframes", String.valueOf(mframeNum) + ".jpg"); + + double leftEAR = eye_aspect_ratio(leftEye); + double rightEAR = eye_aspect_ratio(rightEye); + ear = (leftEAR + rightEAR) / 2.0; + } + } else { + frames_notFoundFace++; + Log.i("frames_notFoundFace", String.valueOf(frames_notFoundFace)); + } + + if (ear != 0) { + + //codes below are difficult to read,but it dose work + x += 1; + ear_array.add(ear); + ax.add(x); + ay.add(ear); + ear_array_removed = filter_unexpected_values(ear_array, ax, THRESH); + + if (ear_array.size() > 2 && !ear_array_removed) { + if (ear_array.get(ear_array.size() - 2) > ear_array.get(ear_array.size() - 3)) { + continuous_Increment += 1; + if (continuous_Decrement != 0) { + drop = ear_array.get(ear_array.size() - 3) - ear_array.get(ear_array.size() - 3 - continuous_Decrement); + if (continuous_Decrement != 1) { + drop_array.add(drop); + drop_array_appended = true; + } + temp = continuous_Decrement; + continuous_Decrement = 0; + } + } else if (ear_array.get(ear_array.size() - 2) < ear_array.get(ear_array.size() - 3)) { + continuous_Decrement += 1; + if (continuous_Increment != 0) { + drop = ear_array.get(ear_array.size() - 3) - ear_array.get(ear_array.size() - 3 - continuous_Increment); + if (continuous_Increment != 1) { + drop_array.add(drop); + drop_array_appended = true; + } + temp = continuous_Increment; + continuous_Increment = 0; + } + } + } + + if (drop_array_appended) { + if (drop_array.get(drop_array.size() - 1) < -DROP_THRESH) { + closeEyes_drop = drop_array.get(drop_array.size() - 1); + closeEyes_end = ax.get(ax.size() - 3); + } + if (drop_array.get(drop_array.size() - 1) > DROP_THRESH) { + openEyes_drop = drop_array.get(drop_array.size() - 1); + openEyes_start = ax.get(ax.size() - 3 - temp); + if (Math.abs(closeEyes_drop + openEyes_drop) < 0.1 && ear_array.get(ear_array.size() - 3 - temp) < 0.21 + && openEyes_start - closeEyes_end < 20) { + blink += 1; + closeEyes_drop = -5; + } + } + } + + } + long endTime = System.currentTimeMillis(); + mTransparentTitleView.setText("noFace: " + String.valueOf(frames_notFoundFace) + " blink: " + String.valueOf(blink) + " TimeCost: " + String.valueOf((endTime - startTime) / 1000f)); + + //save the ear data to local,for further analysis + //When mframeNum is equal to 1, it means that the recognition is restarted, so the previous data is overwritten. + //if(mframeNum == 1){ + //saveStringToTxt(String.valueOf(mframeNum) + " " + doubleToString(ear), "ear.txt", false); + //}else{ + //saveStringToTxt(String.valueOf(mframeNum) + " " + doubleToString(ear), "ear.txt",true); + //} + + Log.i("processingFrame", String.valueOf(mframeNum)); + mWindow.setRGBBitmap(framefordisplay); + mWindow.setInformation("frame: " + String.valueOf(mframeNum)); + mWindow.setMoreInformation("ear: " + String.valueOf(ear)); + + }break; + case headOrientationDetection:{ + + long startTime = System.currentTimeMillis(); + + results = mFaceDet.detect(frameData); + + if (results.size() != 0) { + for (final VisionDetRet ret : results) { + float resizeRatio = 4f; + Canvas canvas = new Canvas(framefordisplay); + + ArrayList landmarks = ret.getFaceLandmarks(); + + + for (Point point : landmarks) { + int pointX = (int) (point.x * resizeRatio); + int pointY = (int) (point.y * resizeRatio); + canvas.drawCircle(pointX, pointY, 2, mFaceLandmardkPaint); + } + + keyPoint_left = landmarks.get(2); + keyPoint_right = landmarks.get(14); + keyPoint_nose = landmarks.get(30); + + rightHalfFace = euclidean(keyPoint_nose, keyPoint_right); + leftHalfFace = euclidean(keyPoint_nose, keyPoint_left); + + ratio = Math.min(rightHalfFace, leftHalfFace) / Math.max(rightHalfFace, leftHalfFace); + } + } else { + frames_notFoundFace++; + Log.i("frames_notFoundFace", String.valueOf(frames_notFoundFace)); + } + + if (ratio>0.6 && ratio<1){ + headToward = "front"; + }else { + //if your right face is bigger than the left, then your head toward left + headToward = rightHalfFace > leftHalfFace ? "left" : "right"; + } + + long endTime = System.currentTimeMillis(); + + mTransparentTitleView.setText("headToward:" + headToward + " TimeCost: " + String.valueOf((endTime - startTime) / 1000f)); + Log.i("processingFrame", String.valueOf(mframeNum)); + mWindow.setInformation("frame: " + String.valueOf(mframeNum)); + mWindow.setMoreInformation("ratio: " + String.valueOf(ratio)); + mWindow.setRGBBitmap(framefordisplay); + } + break; + + } + } + + }); + } + } + + //眼睛的高和长的比值 + private double eye_aspect_ratio(Point[] eye){ + double ear = 0; + double A = euclidean(eye[1],eye[5]); + double B = euclidean(eye[2],eye[4]); + double C = euclidean(eye[0],eye[3]); + ear = (A + B) / (2.0 * C); + return ear; + } + + //两点间的欧式距离 + private double euclidean(Point p1, Point p2){ + double result = 0; + result = Math.sqrt(Math.pow((p1.x-p2.x),2)+Math.pow((p1.y-p2.y),2)); + return result; + } + + //保存数据到文件,追加or覆盖 + private static void saveStringToTxt(String str, String fileName, boolean appendOrNot){ + + String filePath = null; + + boolean hasSDCard =Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + + if (hasSDCard) { + + filePath =Environment.getExternalStorageDirectory().toString() + File.separator +fileName; + + } else + + filePath =Environment.getDownloadCacheDirectory().toString() + File.separator +fileName; + try { + File file = new File(filePath); + + if (!file.exists()) { + + file.createNewFile(); + + } + FileWriter fw = new FileWriter(file,appendOrNot);//SD卡中的路径 + fw.flush(); + fw.write(str+"\r\n"); + fw.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + //double转string + private static String doubleToString(double num){ + //使用0.00不足位补0,#.##仅保留有效位 + return new DecimalFormat("0.0000").format(num); + } + + /** + + * 过滤异常值,以便更好的计算连续落差Calculate_continuous_drop() + + * + + * @param AL 存有ear值的ArrayList + * @param ax ear值对应的帧,AL看作是Y轴的话,那么ax就是X轴,当AL过滤某一ear值时,其对应的帧也应删除 + * @param THRESH 阈值,小于它才过滤 + * @return 返回此次调用是否发生了过滤,过滤了返回true,无需过滤返回false + + */ + private static boolean filter_unexpected_values(ArrayList AL, ArrayList ax, double THRESH){ + if(AL.size()>2){ + if(AL.get(AL.size()-3) < AL.get(AL.size()-1)){ + if(AL.get(AL.size()-3) > AL.get(AL.size()-2) && AL.get(AL.size()-3)-AL.get(AL.size()-2)>THRESH){ + AL.remove(AL.size()-2); + ax.remove(AL.size()-2); + return true; + } + }else if(AL.get(AL.size()-3) > AL.get(AL.size()-1)){ + if(AL.get(AL.size()-2) > AL.get(AL.size()-3) && AL.get(AL.size()-2)-AL.get(AL.size()-3)>THRESH){ + AL.remove(AL.size()-2); + ax.remove(AL.size()-2); + return true; + } + } + } + return false; + } + + //返回点的闭合路径,通过canvas可画出 + private Path getPath(Point[] points){ + Path path = new Path(); + path.moveTo(points[0].x, points[0].y);//起点 + //添加中间连接点 + for(int i = 1; i < points.length; i++){ + path.lineTo(points[i].x, points[i].y); + } + path.close(); // 使这些点构成封闭的多边形 + return path; + } + + private void saveBitmap(Bitmap bm, String directory, String fileName){ + String filePath = null; + + boolean hasSDCard =Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + + if (hasSDCard) { + + filePath =Environment.getExternalStorageDirectory().toString() + File.separator + directory+ File.separator+ fileName; + + } else + + filePath =Environment.getDownloadCacheDirectory().toString() + File.separator + directory+ File.separator+ fileName; + try { + File file = new File(filePath); + + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } + FileOutputStream fos = new FileOutputStream(file); + bm.compress(Bitmap.CompressFormat.JPEG, 100, fos); + fos.flush(); + fos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/Setting.java b/app/src/main/java/com/tzutalin/dlibtest/Setting.java new file mode 100644 index 0000000..1d5d735 --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/Setting.java @@ -0,0 +1,44 @@ +package com.tzutalin.dlibtest; + +import android.content.SharedPreferences; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.Spinner; +import android.widget.Toast; + +public class Setting extends AppCompatActivity { + + private SharedPreferences mSharedPreferences; + private Spinner mSpinner; + private static int spinnerStatus = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_setting); + + mSharedPreferences = getSharedPreferences("userInfo", MODE_PRIVATE); + mSpinner = (Spinner) findViewById(R.id.spinner); + mSpinner.setSelection(spinnerStatus); + + Button mSettingBt = (Button) findViewById(R.id.button); + mSettingBt.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + String selectedItem = mSpinner.getSelectedItem().toString(); + spinnerStatus = mSpinner.getSelectedItemPosition(); + String detectionMode = null; + detectionMode = selectedItem; + + SharedPreferences.Editor edit = mSharedPreferences.edit(); + edit.putString("detectionMode", detectionMode); + edit.commit(); + Toast.makeText(Setting.this, "Successfully saved", Toast.LENGTH_SHORT).show(); + } + }); + + } +} diff --git a/app/src/main/java/com/tzutalin/dlibtest/TrasparentTitleView.java b/app/src/main/java/com/tzutalin/dlibtest/TrasparentTitleView.java new file mode 100644 index 0000000..e2efa3a --- /dev/null +++ b/app/src/main/java/com/tzutalin/dlibtest/TrasparentTitleView.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-present Tzutalin + * + * 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.tzutalin.dlibtest; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.support.annotation.NonNull; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; + +public class TrasparentTitleView extends View { + private static final float TEXT_SIZE_DIP = 24; + private String mShowText; + private final float mTextSizePx; + private final Paint mFgPaint; + private final Paint mBgPaint; + + public TrasparentTitleView(final Context context, final AttributeSet set) { + super(context, set); + + mTextSizePx = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DIP, getResources().getDisplayMetrics()); + mFgPaint = new Paint(); + mFgPaint.setTextSize(mTextSizePx); + + mBgPaint = new Paint(); + mBgPaint.setColor(0xcc4285f4); + } + + @NonNull + public void setText(@NonNull String text) { + this.mShowText = text; + postInvalidate(); + } + + @Override + public void onDraw(final Canvas canvas) { + final int x = 10; + int y = (int) (mFgPaint.getTextSize() * 1.5f); + + canvas.drawPaint(mBgPaint); + + if (mShowText != null) { + canvas.drawText(mShowText, x, y, mFgPaint); + } + } +} diff --git a/app/src/main/res/drawable/gallery.png b/app/src/main/res/drawable/gallery.png new file mode 100644 index 0000000..4e9cd50 Binary files /dev/null and b/app/src/main/res/drawable/gallery.png differ diff --git a/app/src/main/res/drawable/ic_camera_alt_black_36dp.png b/app/src/main/res/drawable/ic_camera_alt_black_36dp.png new file mode 100644 index 0000000..3c25621 Binary files /dev/null and b/app/src/main/res/drawable/ic_camera_alt_black_36dp.png differ diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml new file mode 100644 index 0000000..91017b8 --- /dev/null +++ b/app/src/main/res/layout/activity_camera.xml @@ -0,0 +1,22 @@ + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..8ae8d45 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_setting.xml b/app/src/main/res/layout/activity_setting.xml new file mode 100644 index 0000000..bf0e815 --- /dev/null +++ b/app/src/main/res/layout/activity_setting.xml @@ -0,0 +1,31 @@ + + + + + + + +