From 6fac275f1b42bb8a5bf3715de3e4b6bcae454a24 Mon Sep 17 00:00:00 2001 From: hunghd Date: Mon, 10 Apr 2017 16:30:15 +0700 Subject: [PATCH 01/16] add an example code how to use OpenCV in Android --- JavaCvAndroidExmaple/.gitignore | 9 + JavaCvAndroidExmaple/.idea/compiler.xml | 22 + .../.idea/copyright/profiles_settings.xml | 3 + JavaCvAndroidExmaple/.idea/encodings.xml | 6 + JavaCvAndroidExmaple/.idea/gradle.xml | 18 + JavaCvAndroidExmaple/.idea/misc.xml | 62 + JavaCvAndroidExmaple/.idea/modules.xml | 9 + .../.idea/runConfigurations.xml | 12 + JavaCvAndroidExmaple/app/.gitignore | 1 + JavaCvAndroidExmaple/app/build.gradle | 41 + JavaCvAndroidExmaple/app/proguard-rules.pro | 25 + .../exmaple/ExampleInstrumentedTest.java | 26 + .../app/src/main/AndroidManifest.xml | 38 + .../android/exmaple/CvCameraPreview.java | 855 + .../javacv/android/exmaple/MainActivity.java | 78 + .../android/exmaple/OpenCvActivity.java | 86 + .../android/exmaple/RecordActivity.java | 283 + .../android/exmaple/utils/StorageHelper.java | 155 + .../res/drawable/bg_green_circle_button.xml | 7 + .../res/drawable/bg_red_circle_button.xml | 7 + .../app/src/main/res/layout/activity_main.xml | 32 + .../src/main/res/layout/activity_opencv.xml | 15 + .../src/main/res/layout/activity_record.xml | 26 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4208 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2555 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6114 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10056 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 14696 bytes .../app/src/main/res/raw/frontalface.xml | 33314 ++++++++++++++++ .../app/src/main/res/values/attrs.xml | 13 + .../app/src/main/res/values/colors.xml | 6 + .../app/src/main/res/values/dimens.xml | 7 + .../app/src/main/res/values/strings.xml | 5 + .../app/src/main/res/values/styles.xml | 11 + .../android/exmaple/ExampleUnitTest.java | 17 + JavaCvAndroidExmaple/build.gradle | 23 + JavaCvAndroidExmaple/gradle.properties | 17 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + JavaCvAndroidExmaple/gradlew | 160 + JavaCvAndroidExmaple/gradlew.bat | 90 + JavaCvAndroidExmaple/settings.gradle | 1 + 47 files changed, 35486 insertions(+) create mode 100644 JavaCvAndroidExmaple/.gitignore create mode 100644 JavaCvAndroidExmaple/.idea/compiler.xml create mode 100644 JavaCvAndroidExmaple/.idea/copyright/profiles_settings.xml create mode 100644 JavaCvAndroidExmaple/.idea/encodings.xml create mode 100644 JavaCvAndroidExmaple/.idea/gradle.xml create mode 100644 JavaCvAndroidExmaple/.idea/misc.xml create mode 100644 JavaCvAndroidExmaple/.idea/modules.xml create mode 100644 JavaCvAndroidExmaple/.idea/runConfigurations.xml create mode 100644 JavaCvAndroidExmaple/app/.gitignore create mode 100644 JavaCvAndroidExmaple/app/build.gradle create mode 100644 JavaCvAndroidExmaple/app/proguard-rules.pro create mode 100644 JavaCvAndroidExmaple/app/src/androidTest/java/com/javacv/android/exmaple/ExampleInstrumentedTest.java create mode 100644 JavaCvAndroidExmaple/app/src/main/AndroidManifest.xml create mode 100755 JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/CvCameraPreview.java create mode 100644 JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/MainActivity.java create mode 100644 JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/OpenCvActivity.java create mode 100644 JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/RecordActivity.java create mode 100644 JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/utils/StorageHelper.java create mode 100644 JavaCvAndroidExmaple/app/src/main/res/drawable/bg_green_circle_button.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/drawable/bg_red_circle_button.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/layout/activity_main.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/layout/activity_opencv.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/layout/activity_record.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 JavaCvAndroidExmaple/app/src/main/res/raw/frontalface.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/values/attrs.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/values/colors.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/values/dimens.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/values/strings.xml create mode 100644 JavaCvAndroidExmaple/app/src/main/res/values/styles.xml create mode 100644 JavaCvAndroidExmaple/app/src/test/java/com/javacv/android/exmaple/ExampleUnitTest.java create mode 100644 JavaCvAndroidExmaple/build.gradle create mode 100644 JavaCvAndroidExmaple/gradle.properties create mode 100644 JavaCvAndroidExmaple/gradle/wrapper/gradle-wrapper.jar create mode 100644 JavaCvAndroidExmaple/gradle/wrapper/gradle-wrapper.properties create mode 100755 JavaCvAndroidExmaple/gradlew create mode 100644 JavaCvAndroidExmaple/gradlew.bat create mode 100644 JavaCvAndroidExmaple/settings.gradle diff --git a/JavaCvAndroidExmaple/.gitignore b/JavaCvAndroidExmaple/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/JavaCvAndroidExmaple/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/JavaCvAndroidExmaple/.idea/compiler.xml b/JavaCvAndroidExmaple/.idea/compiler.xml new file mode 100644 index 0000000..43b000d --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/.idea/copyright/profiles_settings.xml b/JavaCvAndroidExmaple/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/.idea/encodings.xml b/JavaCvAndroidExmaple/.idea/encodings.xml new file mode 100644 index 0000000..97626ba --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/.idea/gradle.xml b/JavaCvAndroidExmaple/.idea/gradle.xml new file mode 100644 index 0000000..7ac24c7 --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/.idea/misc.xml b/JavaCvAndroidExmaple/.idea/misc.xml new file mode 100644 index 0000000..7158618 --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/misc.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1.8 + + + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/.idea/modules.xml b/JavaCvAndroidExmaple/.idea/modules.xml new file mode 100644 index 0000000..20593bb --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/.idea/runConfigurations.xml b/JavaCvAndroidExmaple/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/JavaCvAndroidExmaple/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/app/.gitignore b/JavaCvAndroidExmaple/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/JavaCvAndroidExmaple/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/JavaCvAndroidExmaple/app/build.gradle b/JavaCvAndroidExmaple/app/build.gradle new file mode 100644 index 0000000..99347ab --- /dev/null +++ b/JavaCvAndroidExmaple/app/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "com.javacv.android.exmaple" + minSdkVersion 16 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + jcenter() + mavenCentral() +} + +dependencies { + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + testCompile 'junit:junit:4.12' + + compile 'com.android.support:appcompat-v7:25.3.1' + + compile 'com.android.support:appcompat-v7:25.3.1' + compile group: 'org.bytedeco', name: 'javacv', version: '1.3.2' + compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.2.0-1.3', classifier: 'android-arm' + compile group: 'org.bytedeco.javacpp-presets', name: 'opencv', version: '3.2.0-1.3', classifier: 'android-x86' + compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '3.2.1-1.3', classifier: 'android-arm' + compile group: 'org.bytedeco.javacpp-presets', name: 'ffmpeg', version: '3.2.1-1.3', classifier: 'android-x86' +} diff --git a/JavaCvAndroidExmaple/app/proguard-rules.pro b/JavaCvAndroidExmaple/app/proguard-rules.pro new file mode 100644 index 0000000..6e69731 --- /dev/null +++ b/JavaCvAndroidExmaple/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/hunghd/Library/Android/sdk/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 *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/JavaCvAndroidExmaple/app/src/androidTest/java/com/javacv/android/exmaple/ExampleInstrumentedTest.java b/JavaCvAndroidExmaple/app/src/androidTest/java/com/javacv/android/exmaple/ExampleInstrumentedTest.java new file mode 100644 index 0000000..5b27dce --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/androidTest/java/com/javacv/android/exmaple/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.javacv.android.exmaple; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.javacv.android.exmaple", appContext.getPackageName()); + } +} diff --git a/JavaCvAndroidExmaple/app/src/main/AndroidManifest.xml b/JavaCvAndroidExmaple/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..4985643 --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/CvCameraPreview.java b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/CvCameraPreview.java new file mode 100755 index 0000000..2f61f5c --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/CvCameraPreview.java @@ -0,0 +1,855 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.javacv.android.exmaple; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ImageFormat; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.PictureCallback; +import android.hardware.Camera.PreviewCallback; +import android.hardware.Camera.Size; +import android.os.Build; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacv.AndroidFrameConverter; +import org.bytedeco.javacv.FFmpegFrameFilter; +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.FrameFilter; +import org.bytedeco.javacv.OpenCVFrameConverter; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; + +import static org.bytedeco.javacpp.avutil.AV_PIX_FMT_NV21; + +/** + * This is the graphical object used to display a real-time preview of the Camera. + * It MUST be an extension of the {@link SurfaceView} class.
+ * It also needs to implement some other interfaces like {@link SurfaceHolder.Callback} + * (to react to SurfaceView events) + * + * @author alessandrofrancesconi, hunghd + */ +public class CvCameraPreview extends SurfaceView implements SurfaceHolder.Callback, PreviewCallback { + + private final String LOG_TAG = "CvCameraPreview"; + + private static final int STOPPED = 0; + private static final int STARTED = 1; + + private static final int CAMERA_BACK = 99; + private static final int CAMERA_FRONT = 98; + + private static final int MAGIC_TEXTURE_ID = 10; + + /** + * ASPECT_RATIO_W and ASPECT_RATIO_H define the aspect ratio + * of the Surface. They are used when {@link #onMeasure(int, int)} + * is called. + */ + private final float ASPECT_RATIO_W = 4.0f; + private final float ASPECT_RATIO_H = 3.0f; + + /** + * The maximum dimension (in pixels) of the preview frames that are produced + * by the Camera object. Note that this should not be intended as + * the final, exact, dimension because the device could not support + * it and a lower value is required (but the aspect ratio should remain the same).
+ * See {@link CvCameraPreview#getBestSize(List, int)} for more information. + */ + private final int PREVIEW_MAX_WIDTH = 640; + + /** + * The maximum dimension (in pixels) of the images produced when a + * {@link PictureCallback#onPictureTaken(byte[], Camera)} event is + * fired. Again, this is a maximum value and could not be the + * real one implemented by the device. + */ + private final int PICTURE_MAX_WIDTH = 1280; + + /** + * In this example we look at camera preview buffer functionality too.
+ * This is the array that will be filled everytime a single preview frame is + * ready to be processed (for example when we want to show to the user + * a transformed preview instead of the original one, or when we want to + * make some image analysis in real-time without taking full-sized pictures). + */ + private byte[] previewBuffer; + + /** + * The "holder" is the underlying surface. + */ + private SurfaceHolder surfaceHolder; + + private FFmpegFrameFilter filter; + private int chainIdx = 0; + private boolean stopThread = false; + private boolean cameraFrameReady = false; + protected boolean enabled = true; + private boolean surfaceExist; + private Thread thread; + private CvCameraViewListener listener; + private AndroidFrameConverter converterToBitmap = new AndroidFrameConverter(); + private OpenCVFrameConverter.ToMat converterToMat = new OpenCVFrameConverter.ToMat(); + private Bitmap cacheBitmap; + protected Frame[] cameraFrame; + private int state = STOPPED; + private final Object syncObject = new Object(); + private int cameraId = -1; + private int cameraType = Camera.CameraInfo.CAMERA_FACING_BACK; + private Camera cameraDevice; + private SurfaceTexture surfaceTexture; + private int frameWidth, frameHeight; + + public CvCameraPreview(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.CvCameraPreview); + int type = array.getInt(R.styleable.CvCameraPreview_camera_type, CAMERA_BACK); + array.recycle(); + + initializer(type == CAMERA_BACK ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT); + } + + public CvCameraPreview(Context context, int camType) { + super(context); + + initializer(camType); + } + + private void initializer(int camType) { + this.surfaceHolder = this.getHolder(); + this.surfaceHolder.addCallback(this); + + this.cameraType = camType; + + // deprecated setting, but required on Android versions prior to API 11 + if (Build.VERSION.SDK_INT < 11) { + this.surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + } + + public void setCvCameraViewListener(CvCameraViewListener listener) { + this.listener = listener; + } + + public int getCameraId() { + return cameraId; + } + + /** + * Called when the surface is created for the first time. It sets all the + * required {@link #cameraDevice}'s parameters and starts the preview stream. + * + * @param holder + */ + @Override + public void surfaceCreated(SurfaceHolder holder) { + /* Do nothing. Wait until surfaceChanged delivered */ + } + + /** + * [IMPORTANT!] A SurfaceChanged event means that the parent graphic has changed its layout + * (for example when the orientation changes). It's necessary to update the {@link CvCameraPreview} + * orientation, so the preview is stopped, then updated, then re-activated. + * + * @param holder The SurfaceHolder whose surface has changed + * @param format The new PixelFormat of the surface + * @param w The new width of the surface + * @param h The new height of the surface + */ + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + if (this.surfaceHolder.getSurface() == null) { + Log.e(LOG_TAG, "surfaceChanged(): surfaceHolder is null, nothing to do."); + return; + } + + synchronized (syncObject) { + if (!surfaceExist) { + surfaceExist = true; + checkCurrentState(); + } else { + /** Surface changed. We need to stop camera and restart with new parameters */ + /* Pretend that old surface has been destroyed */ + surfaceExist = false; + checkCurrentState(); + /* Now use new surface. Say we have it now */ + surfaceExist = true; + checkCurrentState(); + } + } + } + + /** + * Called when mSyncObject lock is held + */ + private void checkCurrentState() { + Log.d(LOG_TAG, "call checkCurrentState"); + int targetState; + + if (enabled && surfaceExist && getVisibility() == VISIBLE) { + targetState = STARTED; + } else { + targetState = STOPPED; + } + + if (targetState != state) { + /* The state change detected. Need to exit the current state and enter target state */ + processExitState(state); + state = targetState; + processEnterState(state); + } + } + + private void processExitState(int state) { + Log.d(LOG_TAG, "call processExitState: " + state); + switch (state) { + case STARTED: + onExitStartedState(); + break; + case STOPPED: + onExitStoppedState(); + break; + } + ; + } + + private void processEnterState(int state) { + Log.d(LOG_TAG, "call processEnterState: " + state); + switch (state) { + case STARTED: + onEnterStartedState(); + if (listener != null) { + listener.onCameraViewStarted(frameWidth, frameHeight); + } + break; + case STOPPED: + onEnterStoppedState(); + if (listener != null) { + listener.onCameraViewStopped(); + } + break; + } + } + + private void onEnterStoppedState() { + /* nothing to do */ + } + + private void onExitStoppedState() { + /* nothing to do */ + } + + // NOTE: The order of bitmap constructor and camera connection is important for android 4.1.x + // Bitmap must be constructed before surface + private void onEnterStartedState() { + Log.d(LOG_TAG, "call onEnterStartedState"); + /* Connect camera */ + if (!connectCamera()) { + AlertDialog ad = new AlertDialog.Builder(getContext()).create(); + ad.setCancelable(false); // This blocks the 'BACK' button + ad.setMessage("It seems that you device does not support camera (or it is locked). Application will be closed."); + ad.setButton(DialogInterface.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + ((Activity) getContext()).finish(); + } + }); + ad.show(); + + } + } + + private void onExitStartedState() { + disconnectCamera(); + if (cacheBitmap != null) { + cacheBitmap.recycle(); + } + if (filter != null) { + try { + filter.release(); + } catch (FrameFilter.Exception e) { + e.printStackTrace(); + } + } + } + + /** + * surfaceDestroyed does nothing, because Camera release is + * performed in the parent Activity + * + * @param holder + */ + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + synchronized (syncObject) { + surfaceExist = false; + checkCurrentState(); + } + } + + /** + * [IMPORTANT!] Probably the most important method here. Lots of users experience bad + * camera behaviors because they don't override this guy. + * In fact, some Android devices are very strict about the size of the surface + * where the preview is printed: if its ratio is different from the + * original one, it results in errors like "startPreview failed".
+ * This methods takes care on this and applies the right size to the + * {@link CvCameraPreview}. + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent. + * @param heightMeasureSpec vertical space requirements as imposed by the parent. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int height = MeasureSpec.getSize(heightMeasureSpec); + int width = MeasureSpec.getSize(widthMeasureSpec); + + // do some ultra high precision math... + float ratio = ASPECT_RATIO_H / ASPECT_RATIO_W; + if (width > height * ratio) { + width = (int) (height / ratio + .5); + } else { + height = (int) (width / ratio + .5); + } + + setMeasuredDimension(width, height); + Log.i(LOG_TAG, "onMeasure(): set surface dimension to " + width + "x" + height); + } + + private boolean connectCamera() { + /* 1. We need to instantiate camera + * 2. We need to start thread which will be getting frames + */ + /* First step - initialize camera connection */ + Log.d(LOG_TAG, "Connecting to camera"); + if (!initializeCamera()) + return false; + + /* now we can start update thread */ + Log.d(LOG_TAG, "Starting processing thread"); + stopThread = false; + thread = new Thread(new CameraWorker()); + thread.start(); + + return true; + } + + private void disconnectCamera() { + /* 1. We need to stop thread which updating the frames + * 2. Stop camera and release it + */ + Log.d(LOG_TAG, "Disconnecting from camera"); + try { + stopThread = true; + Log.d(LOG_TAG, "Notify thread"); + synchronized (this) { + this.notify(); + } + Log.d(LOG_TAG, "Wating for thread"); + if (thread != null) + thread.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + thread = null; + } + + stopCameraPreview(); + + /* Now release camera */ + releaseCamera(); + + cameraFrameReady = false; + } + + private boolean initializeCamera() { + synchronized (this) { + if (this.cameraDevice != null) { + // do the job only if the camera is not already set + Log.i(LOG_TAG, "initializeCamera(): camera is already set, nothing to do"); + return true; + } + + // warning here! starting from API 9, we can retrieve one from the multiple + // hardware cameras (ex. front/back) + if (Build.VERSION.SDK_INT >= 9) { + + if (this.cameraId < 0) { + // at this point, it's the first time we request for a camera + Camera.CameraInfo camInfo = new Camera.CameraInfo(); + for (int i = 0; i < Camera.getNumberOfCameras(); i++) { + Camera.getCameraInfo(i, camInfo); + + if (camInfo.facing == cameraType) { + // in this example we'll request specifically the back camera + try { + this.cameraDevice = Camera.open(i); + this.cameraId = i; // assign to cameraId this camera's ID (O RLY?) + break; + } catch (RuntimeException e) { + // something bad happened! this camera could be locked by other apps + Log.e(LOG_TAG, "initializeCamera(): trying to open camera #" + i + " but it's locked", e); + } + } + } + } else { + // at this point, a previous camera was set, we try to re-instantiate it + try { + this.cameraDevice = Camera.open(this.cameraId); + } catch (RuntimeException e) { + Log.e(LOG_TAG, "initializeCamera(): trying to re-open camera #" + this.cameraId + " but it's locked", e); + } + } + } + + // we could reach this point in two cases: + // - the API is lower than 9 + // - previous code block failed + // hence, we try the classic method, that doesn't ask for a particular camera + if (this.cameraDevice == null) { + try { + this.cameraDevice = Camera.open(); + this.cameraId = 0; + } catch (RuntimeException e) { + // this is REALLY bad, the camera is definitely locked by the system. + Log.e(LOG_TAG, + "initializeCamera(): trying to open default camera but it's locked. " + + "The camera is not available for this app at the moment.", e + ); + return false; + } + } + + // here, the open() went good and the camera is available + Log.i(LOG_TAG, "initializeCamera(): successfully set camera #" + this.cameraId); + + setupCamera(); + + updateCameraDisplayOrientation(); + + initFilter(frameWidth, frameHeight); + + startCameraPreview(this.surfaceHolder); + } + + return true; + } + + /** + * It sets all the required parameters for the Camera object, like preview + * and picture size, format, flash modes and so on. + * In this particular example it initializes the {@link #previewBuffer} too. + */ + private boolean setupCamera() { + if (cameraDevice == null) { + Log.e(LOG_TAG, "setupCamera(): warning, camera is null"); + return false; + } + try { + Camera.Parameters parameters = cameraDevice.getParameters(); + List sizes = parameters.getSupportedPreviewSizes(); + if (sizes != null) { + Size bestPreviewSize = getBestSize(sizes, PREVIEW_MAX_WIDTH); + Size bestPictureSize = getBestSize(sizes, PICTURE_MAX_WIDTH); + + frameWidth = bestPreviewSize.width; + frameHeight = bestPreviewSize.height; + + parameters.setPreviewSize(bestPreviewSize.width, bestPreviewSize.height); + parameters.setPictureSize(bestPictureSize.width, bestPictureSize.height); + + parameters.setPreviewFormat(ImageFormat.NV21); // NV21 is the most supported format for preview frames +// parameters.setPictureFormat(ImageFormat.JPEG); // JPEG for full resolution images + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && !Build.MODEL.equals("GT-I9100")) + parameters.setRecordingHint(true); + + List FocusModes = parameters.getSupportedFocusModes(); + if (FocusModes != null && FocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); + } + + cameraDevice.setParameters(parameters); // save everything + + // print saved parameters + int prevWidth = cameraDevice.getParameters().getPreviewSize().width; + int prevHeight = cameraDevice.getParameters().getPreviewSize().height; + int picWidth = cameraDevice.getParameters().getPictureSize().width; + int picHeight = cameraDevice.getParameters().getPictureSize().height; + + Log.d(LOG_TAG, "setupCamera(): settings applied:\n\t" + + "preview size: " + prevWidth + "x" + prevHeight + "\n\t" + + "picture size: " + picWidth + "x" + picHeight + ); + + // here: previewBuffer initialization. It will host every frame that comes out + // from the preview, so it must be big enough. + // After that, it's linked to the camera with the setCameraCallback() method. + try { + this.previewBuffer = new byte[prevWidth * prevHeight * ImageFormat.getBitsPerPixel(cameraDevice.getParameters().getPreviewFormat()) / 8]; + setCameraCallback(); + } catch (IOException e) { + Log.e(LOG_TAG, "setupCamera(): error setting camera callback.", e); + } + + return true; + } else { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + protected void releaseCamera() { + synchronized (this) { + if (cameraDevice != null) { + cameraDevice.release(); + } + cameraDevice = null; + } + } + + /** + * [IMPORTANT!] Sets the {@link #previewBuffer} to be the default buffer where the + * preview frames will be copied. Also sets the callback function + * when a frame is ready. + * + * @throws IOException + */ + private void setCameraCallback() throws IOException { + Log.d(LOG_TAG, "setCameraCallback()"); + cameraDevice.addCallbackBuffer(this.previewBuffer); + cameraDevice.setPreviewCallbackWithBuffer(this); + } + + /** + * [IMPORTANT!] This is a convenient function to determine what's the proper + * preview/picture size to be assigned to the camera, by looking at + * the list of supported sizes and the maximum value given + * + * @param sizes sizes that are currently supported by the camera hardware, + * retrived with {@link Camera.Parameters#getSupportedPictureSizes()} or {@link Camera.Parameters#getSupportedPreviewSizes()} + * @param widthThreshold the maximum value we want to apply + * @return an optimal size <= widthThreshold + */ + private Size getBestSize(List sizes, int widthThreshold) { + Size bestSize = null; + + for (Size currentSize : sizes) { + boolean isDesiredRatio = ((currentSize.width / ASPECT_RATIO_W) == (currentSize.height / ASPECT_RATIO_H)); + boolean isBetterSize = (bestSize == null || currentSize.width > bestSize.width); + boolean isInBounds = currentSize.width <= widthThreshold; + + if (isDesiredRatio && isInBounds && isBetterSize) { + bestSize = currentSize; + } + } + + if (bestSize == null) { + bestSize = sizes.get(0); + Log.e(LOG_TAG, "determineBestSize(): can't find a good size. Setting to the very first..."); + } + + Log.i(LOG_TAG, "determineBestSize(): bestSize is " + bestSize.width + "x" + bestSize.height); + return bestSize; + } + + /** + * In addition to calling {@link Camera#startPreview()}, it also + * updates the preview display that could be changed in some situations + * + * @param holder the current {@link SurfaceHolder} + */ + private synchronized void startCameraPreview(SurfaceHolder holder) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + surfaceTexture = new SurfaceTexture(MAGIC_TEXTURE_ID); + cameraDevice.setPreviewTexture(surfaceTexture); + } else + cameraDevice.setPreviewDisplay(holder); + cameraDevice.startPreview(); + filter.start(); + } catch (Exception e) { + Log.e(LOG_TAG, "startCameraPreview(): error starting camera preview", e); + } + } + + /** + * It "simply" calls {@link Camera#stopPreview()} and checks + * for errors + */ + private synchronized void stopCameraPreview() { + try { + cameraDevice.stopPreview(); + cameraDevice.setPreviewCallback(null); + filter.stop(); + } catch (Exception e) { + // ignored: tried to stop a non-existent preview + Log.i(LOG_TAG, "stopCameraPreview(): tried to stop a non-running preview, this is not an error"); + } + } + + /** + * Gets the current screen rotation in order to understand how much + * the surface needs to be rotated + */ + private void updateCameraDisplayOrientation() { + if (cameraDevice == null) { + Log.e(LOG_TAG, "updateCameraDisplayOrientation(): warning, camera is null"); + return; + } + + int degree = getRotationDegree(); + + cameraDevice.setDisplayOrientation(degree); // save settings + } + + private int getRotationDegree() { + int result = 0; + Activity parentActivity = (Activity) this.getContext(); + + int rotation = parentActivity.getWindowManager().getDefaultDisplay().getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + } + + if (Build.VERSION.SDK_INT >= 9) { + // on >= API 9 we can proceed with the CameraInfo method + // and also we have to keep in mind that the camera could be the front one + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { + // back-facing + result = (info.orientation - degrees + 360) % 360; + } + } else { + // TODO: on the majority of API 8 devices, this trick works good + // and doesn't produce an upside-down preview. + // ... but there is a small amount of devices that don't like it! + result = Math.abs(degrees - 90); + } + return result; + } + + private void initFilter(int width, int height) { + int degree = getRotationDegree(); + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraId, info); + boolean isFrontFaceCamera = info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT; + Log.i(LOG_TAG, "init filter with width = " + width + " and height = " + height + " and degree = " + + degree + " and isFrontFaceCamera = " + isFrontFaceCamera); + String transposeCode; + String formatCode = "format=pix_fmts=rgba"; + /* + 0 = 90CounterCLockwise and Vertical Flip (default) + 1 = 90Clockwise + 2 = 90CounterClockwise + 3 = 90Clockwise and Vertical Flip + */ + switch (degree) { + case 0: + transposeCode = isFrontFaceCamera ? "transpose=3,transpose=2" : "transpose=1,transpose=2"; + break; + case 90: + transposeCode = isFrontFaceCamera ? "transpose=3" : "transpose=1"; + break; + case 180: + transposeCode = isFrontFaceCamera ? "transpose=0,transpose=2" : "transpose=2,transpose=2"; + break; + case 270: + transposeCode = isFrontFaceCamera ? "transpose=0" : "transpose=2"; + break; + default: + transposeCode = isFrontFaceCamera ? "transpose=3,transpose=2" : "transpose=1,transpose=2"; + } + + if (cameraFrame == null) { + cameraFrame = new Frame[2]; + cameraFrame[0] = new Frame(width, height, Frame.DEPTH_UBYTE, 2); + cameraFrame[1] = new Frame(width, height, Frame.DEPTH_UBYTE, 2); + } + + filter = new FFmpegFrameFilter(transposeCode + "," + formatCode, width, height); + filter.setPixelFormat(AV_PIX_FMT_NV21); + + Log.i(LOG_TAG, "filter initialize success"); + } + + @Override + public void onPreviewFrame(byte[] raw, Camera cam) { + processFrame(previewBuffer, cam); + // [IMPORTANT!] remember to reset the CallbackBuffer at the end of every onPreviewFrame event. + // Seems weird, but it works + cam.addCallbackBuffer(previewBuffer); + } + + /** + * [IMPORTANT!] It's the callback that's fired when a preview frame is ready. Here + * we can do some real-time analysis of the preview's contents. + * Just remember that the buffer array is a list of pixels represented in + * Y'UV420sp (NV21) format, so you could have to convert it to RGB before. + * + * @param raw the preview buffer + * @param cam the camera that filled the buffer + * @see YUV Conversion - Wikipedia + */ + private void processFrame(byte[] raw, Camera cam) { + if (cameraFrame != null) { + synchronized (this) { + ((ByteBuffer) cameraFrame[chainIdx].image[0].position(0)).put(raw); + cameraFrameReady = true; + this.notify(); + } + } + } + + private class CameraWorker implements Runnable { + public void run() { + do { + boolean hasFrame = false; + synchronized (CvCameraPreview.this) { + try { + while (!cameraFrameReady && !stopThread) { + CvCameraPreview.this.wait(); + } + } catch (InterruptedException e) { + Log.e(LOG_TAG, "CameraWorker interrupted", e); + } + if (cameraFrameReady) { + chainIdx = 1 - chainIdx; + cameraFrameReady = false; + hasFrame = true; + } + } + + if (!stopThread && hasFrame) { + if (cameraFrame[1 - chainIdx] != null) { + try { + Frame frame; + filter.push(cameraFrame[1 - chainIdx]); + while ((frame = filter.pull()) != null) { + deliverAndDrawFrame(frame); + } + } catch (FrameFilter.Exception e) { + e.printStackTrace(); + } + } + } + } while (!stopThread); + Log.d(LOG_TAG, "Finish processing thread"); + } + } + + /** + * This method shall be called by the subclasses when they have valid + * object and want it to be delivered to external client (via callback) and + * then displayed on the screen. + * + * @param frame - the current frame to be delivered + */ + protected void deliverAndDrawFrame(Frame frame) { + Mat processedMat = null; + + if (listener != null) { + Mat mat = converterToMat.convert(frame); + processedMat = listener.onCameraFrame(mat); + frame = converterToMat.convert(processedMat); + if (mat != null) { + mat.release(); + } + } + cacheBitmap = converterToBitmap.convert(frame); + if (cacheBitmap != null) { + Canvas canvas = getHolder().lockCanvas(); + if (canvas != null) { + int width = canvas.getWidth(); + int height = cacheBitmap.getHeight() * canvas.getWidth() / cacheBitmap.getWidth(); + canvas.drawColor(0, android.graphics.PorterDuff.Mode.CLEAR); + canvas.drawBitmap(cacheBitmap, + new Rect(0, 0, cacheBitmap.getWidth(), cacheBitmap.getHeight()), + new Rect(0, + (canvas.getHeight() - height) / 2, + width, + (canvas.getHeight() - height) / 2 + height), null); + getHolder().unlockCanvasAndPost(canvas); + } + } + + if (processedMat != null) { + processedMat.release(); + } + } + + public interface CvCameraViewListener { + /** + * This method is invoked when camera preview has started. After this method is invoked + * the frames will start to be delivered to client via the onCameraFrame() callback. + * + * @param width - the width of the frames that will be delivered + * @param height - the height of the frames that will be delivered + */ + public void onCameraViewStarted(int width, int height); + + /** + * This method is invoked when camera preview has been stopped for some reason. + * No frames will be delivered via onCameraFrame() callback after this method is called. + */ + public void onCameraViewStopped(); + + /** + * This method is invoked when delivery of the frame needs to be done. + * The returned values - is a modified frame which needs to be displayed on the screen. + * TODO: pass the parameters specifying the format of the frame (BPP, YUV or RGB and etc) + */ + public Mat onCameraFrame(Mat mat); + } + +} diff --git a/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/MainActivity.java b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/MainActivity.java new file mode 100644 index 0000000..847e05d --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/MainActivity.java @@ -0,0 +1,78 @@ +package com.javacv.android.exmaple; + +import android.Manifest; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.ActivityCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.view.View; + +import java.util.HashMap; +import java.util.Map; + +public class MainActivity extends AppCompatActivity { + + private boolean mPermissionReady; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + findViewById(R.id.btnRecord).setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(View v) { + if (mPermissionReady) { + startActivity(new Intent(MainActivity.this, RecordActivity.class)); + } + } + }); + findViewById(R.id.btnOpenCv).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mPermissionReady) { + startActivity(new Intent(MainActivity.this, OpenCvActivity.class)); + } + } + }); + + int cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA); + int storagePermssion = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + mPermissionReady = cameraPermission == PackageManager.PERMISSION_GRANTED + && storagePermssion == PackageManager.PERMISSION_GRANTED; + if (!mPermissionReady) + requirePermissions(); + } + + private void requirePermissions() { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, 11); + } + + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Map perm = new HashMap<>(); + perm.put(Manifest.permission.CAMERA, PackageManager.PERMISSION_DENIED); + perm.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, PackageManager.PERMISSION_DENIED); + for (int i = 0; i < permissions.length; i++) { + perm.put(permissions[i], grantResults[i]); + } + if (perm.get(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED + && perm.get(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + mPermissionReady = true; + } else { + if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA) + || !ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + new AlertDialog.Builder(this) + .setMessage(R.string.permission_warning) + .setPositiveButton(R.string.dismiss, null) + .show(); + } + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } +} diff --git a/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/OpenCvActivity.java b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/OpenCvActivity.java new file mode 100644 index 0000000..b5fd718 --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/OpenCvActivity.java @@ -0,0 +1,86 @@ +package com.javacv.android.exmaple; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import com.javacv.android.exmaple.utils.StorageHelper; + +import org.bytedeco.javacpp.opencv_core; +import org.bytedeco.javacpp.opencv_core.Point; +import org.bytedeco.javacpp.opencv_core.RectVector; +import org.bytedeco.javacpp.opencv_core.Size; + +import static org.bytedeco.javacpp.opencv_core.LINE_8; +import static org.bytedeco.javacpp.opencv_core.Mat; +import static org.bytedeco.javacpp.opencv_imgproc.CV_BGR2GRAY; +import static org.bytedeco.javacpp.opencv_imgproc.cvtColor; +import static org.bytedeco.javacpp.opencv_imgproc.rectangle; +import static org.bytedeco.javacpp.opencv_objdetect.CascadeClassifier; + +/** + * Created by hunghd on 4/10/17. + */ + +public class OpenCvActivity extends Activity implements CvCameraPreview.CvCameraViewListener { + + final String TAG = "OpenCvActivity"; + + private CascadeClassifier faceDetector; + private int absoluteFaceSize = 0; + private CvCameraPreview cameraView; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_opencv); + + cameraView = (CvCameraPreview) findViewById(R.id.camera_view); + cameraView.setCvCameraViewListener(this); + + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + faceDetector = StorageHelper.loadClassifierCascade(OpenCvActivity.this, R.raw.frontalface); + return null; + } + }.execute(); + } + + @Override + public void onCameraViewStarted(int width, int height) { + absoluteFaceSize = (int) (width * 0.32f); + } + + @Override + public void onCameraViewStopped() { + + } + + @Override + public Mat onCameraFrame(Mat rgbaMat) { + if (faceDetector != null) { + Mat grayMat = new Mat(rgbaMat.rows(), rgbaMat.cols()); + + cvtColor(rgbaMat, grayMat, CV_BGR2GRAY); + + RectVector faces = new RectVector(); + faceDetector.detectMultiScale(grayMat, faces, 1.25f, 3, 1, + new Size(absoluteFaceSize, absoluteFaceSize), + new Size(4 * absoluteFaceSize, 4 * absoluteFaceSize)); + if (faces.size() == 1) { + int x = faces.get(0).x(); + int y = faces.get(0).y(); + int w = faces.get(0).width(); + int h = faces.get(0).height(); + rectangle(rgbaMat, new Point(x, y), new Point(x + w, y + h), opencv_core.Scalar.GREEN, 2, LINE_8, 0); + } + + grayMat.release(); + } + + return rgbaMat; + } +} diff --git a/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/RecordActivity.java b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/RecordActivity.java new file mode 100644 index 0000000..77ddcfb --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/RecordActivity.java @@ -0,0 +1,283 @@ +package com.javacv.android.exmaple; + +import android.app.Activity; +import android.content.Context; +import android.hardware.Camera; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.PowerManager; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Surface; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.Toast; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacv.FFmpegFrameRecorder; +import org.bytedeco.javacv.Frame; +import org.bytedeco.javacv.FrameRecorder; +import org.bytedeco.javacv.OpenCVFrameConverter; + +import java.io.File; + +import static org.bytedeco.javacpp.avcodec.AV_CODEC_ID_MPEG4; + +public class RecordActivity extends Activity implements OnClickListener, CvCameraPreview.CvCameraViewListener { + + private final static String CLASS_LABEL = "RecordActivity"; + private final static String LOG_TAG = CLASS_LABEL; + private PowerManager.WakeLock wakeLock; + private boolean recording; + private CvCameraPreview cameraView; + private Button btnRecorderControl; + private File savePath = new File(Environment.getExternalStorageDirectory(), "stream.mp4"); + private FFmpegFrameRecorder recorder; + private long startTime = 0; + private OpenCVFrameConverter.ToMat converterToMat = new OpenCVFrameConverter.ToMat(); + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, + WindowManager.LayoutParams.FLAG_FULLSCREEN); + + setContentView(R.layout.activity_record); + + cameraView = (CvCameraPreview) findViewById(R.id.camera_view); + + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, CLASS_LABEL); + wakeLock.acquire(); + + initLayout(); + } + + @Override + protected void onResume() { + super.onResume(); + + if (wakeLock == null) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, CLASS_LABEL); + wakeLock.acquire(); + } + } + + @Override + protected void onPause() { + super.onPause(); + + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (wakeLock != null) { + wakeLock.release(); + wakeLock = null; + } + + if (recorder != null) { + try { + recorder.release(); + } catch (FrameRecorder.Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + + if (keyCode == KeyEvent.KEYCODE_BACK) { + if (recording) { + stopRecording(); + } + + finish(); + + return true; + } + + return super.onKeyDown(keyCode, event); + } + + private void initLayout() { + btnRecorderControl = (Button) findViewById(R.id.recorder_control); + btnRecorderControl.setText("Start"); + btnRecorderControl.setOnClickListener(this); + + cameraView.setCvCameraViewListener(this); + } + + private void initRecorder(int width, int height) { + int degree = getRotationDegree(); + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraView.getCameraId(), info); + boolean isFrontFaceCamera = info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT; + Log.i(LOG_TAG, "init recorder with width = " + width + " and height = " + height + " and degree = " + + degree + " and isFrontFaceCamera = " + isFrontFaceCamera); + int frameWidth, frameHeight; + /* + 0 = 90CounterCLockwise and Vertical Flip (default) + 1 = 90Clockwise + 2 = 90CounterClockwise + 3 = 90Clockwise and Vertical Flip + */ + switch (degree) { + case 0: + frameWidth = width; + frameHeight = height; + break; + case 90: + frameWidth = height; + frameHeight = width; + break; + case 180: + frameWidth = width; + frameHeight = height; + break; + case 270: + frameWidth = height; + frameHeight = width; + break; + default: + frameWidth = width; + frameHeight = height; + } + + Log.i(LOG_TAG, "saved file path: " + savePath.getAbsolutePath()); + recorder = new FFmpegFrameRecorder(savePath, frameWidth, frameHeight, 0); + recorder.setFormat("mp4"); + recorder.setVideoCodec(AV_CODEC_ID_MPEG4); + recorder.setVideoQuality(1); + // Set in the surface changed method + recorder.setFrameRate(16); + + Log.i(LOG_TAG, "recorder initialize success"); + } + + private int getRotationDegree() { + int result; + + int rotation = getWindowManager().getDefaultDisplay().getRotation(); + int degrees = 0; + switch (rotation) { + case Surface.ROTATION_0: + degrees = 0; + break; + case Surface.ROTATION_90: + degrees = 90; + break; + case Surface.ROTATION_180: + degrees = 180; + break; + case Surface.ROTATION_270: + degrees = 270; + break; + } + + if (Build.VERSION.SDK_INT >= 9) { + // on >= API 9 we can proceed with the CameraInfo method + // and also we have to keep in mind that the camera could be the front one + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(cameraView.getCameraId(), info); + + if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { + result = (info.orientation + degrees) % 360; + result = (360 - result) % 360; // compensate the mirror + } else { + // back-facing + result = (info.orientation - degrees + 360) % 360; + } + } else { + // TODO: on the majority of API 8 devices, this trick works good + // and doesn't produce an upside-down preview. + // ... but there is a small amount of devices that don't like it! + result = Math.abs(degrees - 90); + } + return result; + } + + public void startRecording() { + try { + recorder.start(); + startTime = System.currentTimeMillis(); + recording = true; + } catch (FFmpegFrameRecorder.Exception e) { + e.printStackTrace(); + } + } + + public void stopRecording() { + if (recorder != null && recording) { + recording = false; + Log.v(LOG_TAG, "Finishing recording, calling stop and release on recorder"); + try { + recorder.stop(); + recorder.release(); + } catch (FFmpegFrameRecorder.Exception e) { + e.printStackTrace(); + } + recorder = null; + } + } + + @Override + public void onClick(View v) { + if (!recording) { + startRecording(); + recording = true; + Log.w(LOG_TAG, "Start Button Pushed"); + btnRecorderControl.setText("Stop"); + btnRecorderControl.setBackgroundResource(R.drawable.bg_red_circle_button); + } else { + // This will trigger the audio recording loop to stop and then set isRecorderStart = false; + stopRecording(); + recording = false; + Log.w(LOG_TAG, "Stop Button Pushed"); +// btnRecorderControl.setText("Start"); + btnRecorderControl.setVisibility(View.GONE); + Toast.makeText(this, "Video file was saved to \"" + savePath + "\"", Toast.LENGTH_LONG).show(); + } + } + + @Override + public void onCameraViewStarted(int width, int height) { + initRecorder(width, height); + } + + @Override + public void onCameraViewStopped() { + stopRecording(); + } + + @Override + public Mat onCameraFrame(Mat mat) { + if (recording && mat != null) { + try { + Frame frame = converterToMat.convert(mat); + long t = 1000 * (System.currentTimeMillis() - startTime); + if (t > recorder.getTimestamp()) { + recorder.setTimestamp(t); + } + recorder.record(frame); + } catch (FFmpegFrameRecorder.Exception e) { + Log.v(LOG_TAG, e.getMessage()); + e.printStackTrace(); + } + } + return mat; + } +} \ No newline at end of file diff --git a/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/utils/StorageHelper.java b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/utils/StorageHelper.java new file mode 100644 index 0000000..fa1dcf0 --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/java/com/javacv/android/exmaple/utils/StorageHelper.java @@ -0,0 +1,155 @@ +// +// Copyright (c) nganluong. All rights reserved. +// Licensed under the MIT license. +// +// nganluong Cognitive Services (formerly Project FaceApi): https://www.nganluong.com/cognitive-services +// +// nganluong Cognitive Services (formerly Project FaceApi) GitHub: +// https://github.com/nganluong/Cognitive-Face-Android +// +// Copyright (c) nganluong Corporation +// All rights reserved. +// +// MIT License: +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED ""AS IS"", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +package com.javacv.android.exmaple.utils; + +import android.content.Context; +import android.util.Log; + +import org.bytedeco.javacpp.opencv_core.Mat; +import org.bytedeco.javacpp.opencv_objdetect.CascadeClassifier; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.bytedeco.javacpp.opencv_imgcodecs.imwrite; +import static org.bytedeco.javacpp.opencv_imgproc.COLOR_RGB2BGR; +import static org.bytedeco.javacpp.opencv_imgproc.cvtColor; + +/** + * Defined several functions to manage local storage. + */ +public class StorageHelper { + final static String TAG = "StorageHelper"; + + public static CascadeClassifier loadClassifierCascade(Context context, int resId) { + FileOutputStream fos = null; + InputStream inputStream; + + inputStream = context.getResources().openRawResource(resId); + File xmlDir = context.getDir("xml", Context.MODE_PRIVATE); + File cascadeFile = new File(xmlDir, "temp.xml"); + try { + fos = new FileOutputStream(cascadeFile); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + fos.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + Log.d(TAG, "Can\'t load the cascade file"); + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + CascadeClassifier detector = new CascadeClassifier(cascadeFile.getAbsolutePath()); + if (detector.isNull()) { + Log.e(TAG, "Failed to load cascade classifier"); + detector = null; + } else { + Log.i(TAG, "Loaded cascade classifier from " + cascadeFile.getAbsolutePath()); + } + // delete the temporary directory + cascadeFile.delete(); + + return detector; + } + + public static File loadXmlFromRes2File(Context context, int resId, String filename) { + FileOutputStream fos = null; + InputStream inputStream; + + inputStream = context.getResources().openRawResource(resId); + File trainDir = context.getDir("xml", Context.MODE_PRIVATE); + File trainFile = new File(trainDir, filename); + try { + fos = new FileOutputStream(trainFile); + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + fos.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + Log.d(TAG, "Can\'t load the train file"); + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return trainFile; + } + + public static File saveMat2File(Mat mat, String filePath, String fileName) { + File path = new File(filePath); + if (!path.exists()) { + path.mkdir(); + } + File file = new File(path, fileName); + Mat mat2Save = new Mat(); + cvtColor(mat, mat2Save, COLOR_RGB2BGR); + boolean result = imwrite(file.toString(), mat2Save); + mat2Save.release(); + if (result) + return file; + else + return null; + } +} diff --git a/JavaCvAndroidExmaple/app/src/main/res/drawable/bg_green_circle_button.xml b/JavaCvAndroidExmaple/app/src/main/res/drawable/bg_green_circle_button.xml new file mode 100644 index 0000000..253f651 --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/res/drawable/bg_green_circle_button.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/app/src/main/res/drawable/bg_red_circle_button.xml b/JavaCvAndroidExmaple/app/src/main/res/drawable/bg_red_circle_button.xml new file mode 100644 index 0000000..0e50e1e --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/res/drawable/bg_red_circle_button.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/JavaCvAndroidExmaple/app/src/main/res/layout/activity_main.xml b/JavaCvAndroidExmaple/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ced6267 --- /dev/null +++ b/JavaCvAndroidExmaple/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,32 @@ + + + + + +