diff options
author | Benjamin Hendricks <coolbnjmn@google.com> | 2013-09-19 14:40:45 -0700 |
---|---|---|
committer | Igor Murashkin <iam@google.com> | 2013-10-15 15:11:34 -0700 |
commit | 227b47625d7482b5b47ad0e4c70ce0a246236ade (patch) | |
tree | d8a4107d4d04e89e6006b5b1898850d970bd2db9 /tests/Camera2Tests/SmartCamera | |
parent | f847c3b014ab49fae6bbbc9dd7aac199b31467df (diff) | |
download | frameworks_base-227b47625d7482b5b47ad0e4c70ce0a246236ade.zip frameworks_base-227b47625d7482b5b47ad0e4c70ce0a246236ade.tar.gz frameworks_base-227b47625d7482b5b47ad0e4c70ce0a246236ade.tar.bz2 |
Camera2Tests: Add SmartCamera App
Bug: 10818732
Change-Id: I6ac08ecab3a1e04116be2f7764d0d5d4f29c5cd9
Diffstat (limited to 'tests/Camera2Tests/SmartCamera')
148 files changed, 20290 insertions, 0 deletions
diff --git a/tests/Camera2Tests/SmartCamera/Android.mk b/tests/Camera2Tests/SmartCamera/Android.mk new file mode 100644 index 0000000..3fa8f54 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/Android.mk @@ -0,0 +1,14 @@ +# Copyright 2013 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. +include $(call all-subdir-makefiles) diff --git a/tests/Camera2Tests/SmartCamera/README.txt b/tests/Camera2Tests/SmartCamera/README.txt new file mode 100644 index 0000000..1fff3ab --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/README.txt @@ -0,0 +1,60 @@ +Copyright 2013 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. + + +Smart Camera / Auto Snapshot (formerly named SimpleCamera) ReadMe + +Created by: Benjamin W Hendricks + +How to build the application: +From root: make SmartCamera will build the apk for generic +Otherwise, to build the application for a specific device, lunch to that device +and then run mm while in the SimpleCamera directory. +Then take the given Install path (out/target/.../SmartCamera.apk) +and run adb install out/target/.../SmartCamera.apk. The application should +then appear in the launcher of your device. +You might also need to run adb sync after building to sync the +libsmartcamera_jni library +Summarized: + make SmartCamera + adb remount + adb sync + adb install -r $ANDROID_PRODUCT_OUT/data/app/SmartCamera.apk + +How to run the application: +On a Nexus 7, open up the application from the launcher, and the camera preview +should appear. From there, you can go to the gallery with the gallery button or +press start to start capturing images. You can also change the number of images +to be captured by changing the number on the spinner (between 1-10). + +What does it do: +The application tries to take good pictures for you automatically when in the +start mode. On stop, the application will capture whatever images are in the +bottom preview and save them to the Gallery. It does this by looking at the +following image features: + - Sharpness + - Brightness + - Motion of the device + - Colorfulness + - Contrast + - Exposure (over/under) + +By comparing each of these features frame by frame, a score is calculated to +determine whether an image is better or worse than the previous few frames, +and from that score I can determine the great images from the bad ones. + +What libraries does it use: +- Mobile Filter Framework (MFF) +- Camera2 API +- Renderscript diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/.classpath b/tests/Camera2Tests/SmartCamera/SimpleCamera/.classpath new file mode 100644 index 0000000..3f9691c --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/.classpath @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/> + <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/> + <classpathentry kind="src" path="src"/> + <classpathentry kind="src" path="gen"/> + <classpathentry kind="output" path="bin/classes"/> +</classpath> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/.project b/tests/Camera2Tests/SmartCamera/SimpleCamera/.project new file mode 100644 index 0000000..2517e2d --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/.project @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>CameraShoot</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>com.android.ide.eclipse.adt.ApkBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>com.android.ide.eclipse.adt.AndroidNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/Android.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/Android.mk new file mode 100644 index 0000000..ed0c294 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/Android.mk @@ -0,0 +1,40 @@ +# Copyright (C) 2013 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. + +ifneq ($(TARGET_BUILD_JAVA_SUPPORT_LEVEL),) + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_PROGUARD_ENABLED := disabled + +# comment it out for now since we need use some hidden APIs +LOCAL_SDK_VERSION := current + +LOCAL_SRC_FILES := \ + $(call all-java-files-under, src) \ + $(call all-renderscript-files-under, src) + +LOCAL_PACKAGE_NAME := SmartCamera +LOCAL_JNI_SHARED_LIBRARIES := libsmartcamera_jni + +include $(BUILD_PACKAGE) + +# Include packages in subdirectories +include $(call all-makefiles-under,$(LOCAL_PATH)) + +endif diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/AndroidManifest.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/AndroidManifest.xml new file mode 100644 index 0000000..0681868 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/AndroidManifest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2013 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + android:versionCode="1" + android:versionName="1.0" + package="androidx.media.filterfw.samples.simplecamera"> + <uses-sdk android:minSdkVersion="18" android:targetSdkVersion="19"/> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> + <application android:label="Smart Camera" + android:debuggable="true"> + <uses-library android:name="com.google.android.media.effects" + android:required="false" /> + + <activity android:name=".SmartCamera" + android:label="Smart Camera" + android:screenOrientation="portrait"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> +</manifest> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/0002_000390.jpg b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/0002_000390.jpg Binary files differnew file mode 100644 index 0000000..9b4bce4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/0002_000390.jpg diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLeyesclosed_100.emd b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLeyesclosed_100.emd Binary files differnew file mode 100644 index 0000000..8c3d811 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLeyesclosed_100.emd diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLjoy_100.emd b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLjoy_100.emd Binary files differnew file mode 100644 index 0000000..4ae3fbd --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/assets/frsdk_expression_modules/BCLjoy_100.emd diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/ic_launcher-web.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/ic_launcher-web.png Binary files differnew file mode 100644 index 0000000..f142216 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/ic_launcher-web.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Android.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Android.mk new file mode 100644 index 0000000..616a11b --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Android.mk @@ -0,0 +1,49 @@ +# Copyright (C) 2013 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. +# + +FILTERFW_NATIVE_PATH := $(call my-dir) + + +# +# Build module libfilterframework +# +LOCAL_PATH := $(FILTERFW_NATIVE_PATH) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SDK_VERSION := 14 + +LOCAL_MODULE := libsmartcamera_jni + +LOCAL_SRC_FILES := contrast.cpp \ + brightness.cpp \ + exposure.cpp \ + colorspace.cpp \ + histogram.cpp \ + frametovalues.cpp \ + pixelutils.cpp \ + sobeloperator.cpp \ + stats_scorer.cpp + +LOCAL_STATIC_LIBRARIES += \ + libcutils + +LOCAL_C_INCLUDES += \ + system/core/include \ + +LOCAL_NDK_STL_VARIANT := stlport_static + +include $(BUILD_SHARED_LIBRARY) diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Application.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Application.mk new file mode 100644 index 0000000..2b93b3c --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/Application.mk @@ -0,0 +1,16 @@ +# Copyright (C) 2013 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. +# + +APP_STL := stlport_static diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.cpp new file mode 100644 index 0000000..998fd4c --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.cpp @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2013 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. + */ + +// Native function to extract brightness from image (handed down as ByteBuffer). + +#include "brightness.h" + +#include <math.h> +#include <string.h> +#include <jni.h> +#include <unistd.h> +#include <android/log.h> + +jfloat +Java_androidx_media_filterfw_samples_simplecamera_AvgBrightnessFilter_brightnessOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) { + + if (imageBuffer == 0) { + return 0.0f; + } + float pixelTotals[] = { 0.0f, 0.0f, 0.0f }; + const int numPixels = width * height; + unsigned char* srcPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + for (int i = 0; i < numPixels; i++) { + pixelTotals[0] += *(srcPtr + 4 * i); + pixelTotals[1] += *(srcPtr + 4 * i + 1); + pixelTotals[2] += *(srcPtr + 4 * i + 2); + } + float avgPixels[] = { 0.0f, 0.0f, 0.0f }; + + avgPixels[0] = pixelTotals[0] / numPixels; + avgPixels[1] = pixelTotals[1] / numPixels; + avgPixels[2] = pixelTotals[2] / numPixels; + float returnValue = sqrt(0.241f * avgPixels[0] * avgPixels[0] + + 0.691f * avgPixels[1] * avgPixels[1] + + 0.068f * avgPixels[2] * avgPixels[2]); + + return returnValue / 255; +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.h new file mode 100644 index 0000000..c09e3b5 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/brightness.h @@ -0,0 +1,36 @@ +/* Copyright (C) 2013 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. + */ + +// Native function to extract brightness from image (handed down as ByteBuffer). + +#ifndef ANDROID_FILTERFW_JNI_BRIGHTNESS_H +#define ANDROID_FILTERFW_JNI_BRIGHTNESS_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + + JNIEXPORT jfloat JNICALL + Java_androidx_media_filterfw_samples_simplecamera_AvgBrightnessFilter_brightnessOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer); + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_BRIGHTNESS_H + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.cpp new file mode 100644 index 0000000..63e2ebf --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.cpp @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2013 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. + */ + +#include "colorspace.h" + +#include <jni.h> +#include <stdint.h> + +typedef uint8_t uint8; +typedef uint32_t uint32; +typedef int32_t int32; + +// RGBA helper struct allows access as int and individual channels +// WARNING: int value depends on endianness and should not be used to analyze individual channels. +union Rgba { + uint32 color; + uint8 channel[4]; +}; + +// Channel index constants +static const uint8 kRed = 0; +static const uint8 kGreen = 1; +static const uint8 kBlue = 2; +static const uint8 kAlpha = 3; + +// Clamp to range 0-255 +static inline uint32 clamp(int32 x) { + return x > 255 ? 255 : (x < 0 ? 0 : x); +} + +// Convert YUV to RGBA +// This uses the ITU-R BT.601 coefficients. +static inline Rgba convertYuvToRgba(int32 y, int32 u, int32 v) { + Rgba color; + color.channel[kRed] = clamp(y + static_cast<int>(1.402 * v)); + color.channel[kGreen] = clamp(y - static_cast<int>(0.344 * u + 0.714 * v)); + color.channel[kBlue] = clamp(y + static_cast<int>(1.772 * u)); + color.channel[kAlpha] = 0xFF; + return color; +} + +// Colorspace conversion functions ///////////////////////////////////////////////////////////////// +void JNI_COLORSPACE_METHOD(nativeYuv420pToRgba8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) { + uint8* const pInput = static_cast<uint8*>(env->GetDirectBufferAddress(input)); + Rgba* const pOutput = static_cast<Rgba*>(env->GetDirectBufferAddress(output)); + + const int size = width * height; + + uint8* pInY = pInput; + uint8* pInU = pInput + size; + uint8* pInV = pInput + size + size / 4; + Rgba* pOutColor = pOutput; + + const int u_offset = size; + const int v_offset = u_offset + size / 4; + + for (int y = 0; y < height; y += 2) { + for (int x = 0; x < width; x += 2) { + int u, v, y1, y2, y3, y4; + + y1 = pInY[0]; + y2 = pInY[1]; + y3 = pInY[width]; + y4 = pInY[width + 1]; + + u = *pInU - 128; + v = *pInV - 128; + + pOutColor[0] = convertYuvToRgba(y1, u, v); + pOutColor[1] = convertYuvToRgba(y2, u, v); + pOutColor[width] = convertYuvToRgba(y3, u, v); + pOutColor[width + 1] = convertYuvToRgba(y4, u, v); + + pInY += 2; + pInU++; + pInV++; + pOutColor += 2; + } + pInY += width; + pOutColor += width; + } +} + +void JNI_COLORSPACE_METHOD(nativeArgb8888ToRgba8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) { + Rgba* pInput = static_cast<Rgba*>(env->GetDirectBufferAddress(input)); + Rgba* pOutput = static_cast<Rgba*>(env->GetDirectBufferAddress(output)); + + for (int i = 0; i < width * height; ++i) { + Rgba color_in = *pInput++; + Rgba& color_out = *pOutput++; + color_out.channel[kRed] = color_in.channel[kGreen]; + color_out.channel[kGreen] = color_in.channel[kBlue]; + color_out.channel[kBlue] = color_in.channel[kAlpha]; + color_out.channel[kAlpha] = color_in.channel[kRed]; + } +} + +void JNI_COLORSPACE_METHOD(nativeRgba8888ToHsva8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) { + Rgba* pInput = static_cast<Rgba*>(env->GetDirectBufferAddress(input)); + Rgba* pOutput = static_cast<Rgba*>(env->GetDirectBufferAddress(output)); + + int r, g, b, a, h, s, v, c_max, c_min; + float delta; + for (int i = 0; i < width * height; ++i) { + Rgba color_in = *pInput++; + Rgba& color_out = *pOutput++; + r = color_in.channel[kRed]; + g = color_in.channel[kGreen]; + b = color_in.channel[kBlue]; + a = color_in.channel[kAlpha]; + + if (r > g) { + c_min = (g > b) ? b : g; + c_max = (r > b) ? r : b; + } else { + c_min = (r > b) ? b : r; + c_max = (g > b) ? g : b; + } + delta = c_max -c_min; + + float scaler = 255 * 60 / 360.0f; + if (c_max == r) { + h = (g > b) ? static_cast<int>(scaler * (g - b) / delta) : + static_cast<int>(scaler * ((g - b) / delta + 6)); + } else if (c_max == g) { + h = static_cast<int>(scaler * ((b - r) / delta + 2)); + } else { // Cmax == b + h = static_cast<int>(scaler * ((r - g) / delta + 4)); + } + s = (delta == 0.0f) ? 0 : static_cast<unsigned char>(delta / c_max * 255); + v = c_max; + + color_out.channel[kRed] = h; + color_out.channel[kGreen] = s; + color_out.channel[kBlue] = v; + color_out.channel[kAlpha] = a; + } +} + +void JNI_COLORSPACE_METHOD(nativeRgba8888ToYcbcra8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height) { + Rgba* pInput = static_cast<Rgba*>(env->GetDirectBufferAddress(input)); + Rgba* pOutput = static_cast<Rgba*>(env->GetDirectBufferAddress(output)); + + int r, g, b; + for (int i = 0; i < width * height; ++i) { + Rgba color_in = *pInput++; + Rgba& color_out = *pOutput++; + r = color_in.channel[kRed]; + g = color_in.channel[kGreen]; + b = color_in.channel[kBlue]; + + color_out.channel[kRed] = + static_cast<unsigned char>((65.738 * r + 129.057 * g + 25.064 * b) / 256 + 16); + color_out.channel[kGreen] = + static_cast<unsigned char>((-37.945 * r - 74.494 * g + 112.439 * b) / 256 + 128); + color_out.channel[kBlue] = + static_cast<unsigned char>((112.439 * r - 94.154 * g - 18.285 * b) / 256 + 128); + color_out.channel[kAlpha] = color_in.channel[kAlpha]; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.h new file mode 100644 index 0000000..c332749 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/colorspace.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2013 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. + */ + +#ifndef ANDROID_FILTERFW_JNI_COLORSPACE_H +#define ANDROID_FILTERFW_JNI_COLORSPACE_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define JNI_COLORSPACE_METHOD(METHOD_NAME) \ + Java_androidx_media_filterfw_ColorSpace_ ## METHOD_NAME + +JNIEXPORT void JNICALL +JNI_COLORSPACE_METHOD(nativeYuv420pToRgba8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height); + +JNIEXPORT void JNICALL +JNI_COLORSPACE_METHOD(nativeArgb8888ToRgba8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height); + +JNIEXPORT void JNICALL +JNI_COLORSPACE_METHOD(nativeRgba8888ToHsva8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height); + +JNIEXPORT void JNICALL +JNI_COLORSPACE_METHOD(nativeRgba8888ToYcbcra8888)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height); + + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_COLORSPACE_H diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.cpp new file mode 100644 index 0000000..222f738 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 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. + */ + +// Native function to extract contrast ratio from image (handed down as ByteBuffer). + +#include "contrast.h" + +#include <math.h> +#include <string.h> +#include <jni.h> +#include <unistd.h> +#include <android/log.h> + +jfloat +Java_androidx_media_filterfw_samples_simplecamera_ContrastRatioFilter_contrastOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) { + + if (imageBuffer == 0) { + return 0.0f; + } + float total = 0; + const int numPixels = width * height; + unsigned char* srcPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + float* lumArray = new float[numPixels]; + for (int i = 0; i < numPixels; i++) { + lumArray[i] = (0.2126f * *(srcPtr + 4 * i) + 0.7152f * + *(srcPtr + 4 * i + 1) + 0.0722f * *(srcPtr + 4 * i + 2)) / 255; + total += lumArray[i]; + } + const float avg = total / numPixels; + float sum = 0; + + for (int i = 0; i < numPixels; i++) { + sum += (lumArray[i] - avg) * (lumArray[i] - avg); + } + delete[] lumArray; + return ((float) sqrt(sum / numPixels)); +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.h new file mode 100644 index 0000000..ddcd3d4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/contrast.h @@ -0,0 +1,36 @@ +/* Copyright (C) 2013 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. + */ + +// Native function to extract contrast from image (handed down as ByteBuffer). + +#ifndef ANDROID_FILTERFW_JNI_CONTRAST_H +#define ANDROID_FILTERFW_JNI_CONTRAST_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + + JNIEXPORT jfloat JNICALL + Java_androidx_media_filterfw_samples_simplecamera_ContrastRatioFilter_contrastOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer); + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_CONTRAST_H + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.cpp new file mode 100644 index 0000000..b2853f7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 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. + */ + +// Native function to extract exposure from image (handed down as ByteBuffer). + +#include "exposure.h" + +#include <math.h> +#include <string.h> +#include <jni.h> +#include <unistd.h> +#include <android/log.h> + + +jfloat +Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_overExposureOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) { + if (imageBuffer == 0) { + return 0.0f; + } + const int numPixels = width * height; + unsigned char* srcPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + int output = 0; + float tempLuminance = 0.0f; + + for (int i = 0; i < numPixels; i++) { + tempLuminance = (0.2126f * *(srcPtr + 4 * i) + + 0.7152f * *(srcPtr + 4 * i + 1) + + 0.0722f * *(srcPtr + 4 * i + 2)); + if (tempLuminance + 5 >= 255) { + output++; + } + } + return (static_cast<float>(output)) / numPixels; +} + +jfloat +Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_underExposureOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer) { + if (imageBuffer == 0) { + return 0.0f; + } + const int numPixels = width * height; + unsigned char* srcPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + int output = 0; + float tempLuminance = 0.0f; + + for (int i = 0; i < numPixels; i++) { + tempLuminance = (0.2126f * *(srcPtr + 4 * i) + + 0.7152f * *(srcPtr + 4 * i + 1) + + 0.0722f * *(srcPtr + 4 * i + 2)); + if (tempLuminance - 5 <= 0) { + output++; + } + } + return (static_cast<float>(output)) / numPixels; +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.h new file mode 100644 index 0000000..bc6e3b1 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/exposure.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2013 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. + */ + +// Native function to extract exposure from image (handed down as ByteBuffer). + +#ifndef ANDROID_FILTERFW_JNI_EXPOSURE_H +#define ANDROID_FILTERFW_JNI_EXPOSURE_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + + JNIEXPORT jfloat JNICALL + Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_underExposureOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer); + + JNIEXPORT jfloat JNICALL + Java_androidx_media_filterfw_samples_simplecamera_ExposureFilter_overExposureOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer); +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_EXPOSURE_H + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.cpp new file mode 100644 index 0000000..2e3a0ec --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 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. + */ + +// Native function to extract histogram from image (handed down as ByteBuffer). + +#include "frametovalues.h" + +#include <string.h> +#include <jni.h> +#include <unistd.h> +#include <android/log.h> + +#include "imgprocutil.h" + +jboolean Java_androidx_media_filterpacks_image_ToGrayValuesFilter_toGrayValues( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject grayBuffer ) +{ + unsigned char* pixelPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + unsigned char* grayPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(grayBuffer)); + + if (pixelPtr == 0 || grayPtr == 0) { + return JNI_FALSE; + } + + int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; + + // TODO: the current implementation is focused on the correctness not performance. + // If performance becomes an issue, it is better to increment pixelPtr directly. + int disp = 0; + for(int idx = 0; idx < numPixels; idx++, disp+=4) { + int R = *(pixelPtr + disp); + int G = *(pixelPtr + disp + 1); + int B = *(pixelPtr + disp + 2); + int gray = getIntensityFast(R, G, B); + *(grayPtr+idx) = static_cast<unsigned char>(gray); + } + + return JNI_TRUE; +} + +jboolean Java_androidx_media_filterpacks_image_ToRgbValuesFilter_toRgbValues( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject rgbBuffer ) +{ + unsigned char* pixelPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + unsigned char* rgbPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(rgbBuffer)); + + if (pixelPtr == 0 || rgbPtr == 0) { + return JNI_FALSE; + } + + int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; + + // TODO: this code could be revised to improve the performance as the TODO above. + int pixelDisp = 0; + int rgbDisp = 0; + for(int idx = 0; idx < numPixels; idx++, pixelDisp += 4, rgbDisp += 3) { + for (int c = 0; c < 3; ++c) { + *(rgbPtr + rgbDisp + c) = *(pixelPtr + pixelDisp + c); + } + } + return JNI_TRUE; +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.h new file mode 100644 index 0000000..4abb848 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/frametovalues.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012 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. + */ + +// Native functions to pack a RGBA frame into either a one channel grayscale buffer +// or a three channel RGB buffer. + +#ifndef ANDROID_FILTERFW_JNI_TOGRAYVALUES_H +#define ANDROID_FILTERFW_JNI_TOGRAYVALUES_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jboolean JNICALL +Java_androidx_media_filterpacks_image_ToGrayValuesFilter_toGrayValues( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject grayBuffer ); + +JNIEXPORT jboolean JNICALL +Java_androidx_media_filterpacks_image_ToRgbValuesFilter_toRgbValues( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject rgbBuffer ); + + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_TOGRAYVALUES_H diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.cpp new file mode 100644 index 0000000..ba060d4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2012 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. + */ + +// Native function to extract histogram from image (handed down as ByteBuffer). + +#include "histogram.h" + +#include <string.h> +#include <jni.h> +#include <unistd.h> +#include <android/log.h> + +#include "imgprocutil.h" + +inline void addPixelToHistogram(unsigned char*& pImg, int* pHist, int numBins) { + int R = *(pImg++); + int G = *(pImg++); + int B = *(pImg++); + ++pImg; + int i = getIntensityFast(R, G, B); + int bin = clamp(0, static_cast<int>(static_cast<float>(i * numBins) / 255.0f), numBins - 1); + ++pHist[bin]; +} + +void Java_androidx_media_filterpacks_histogram_GrayHistogramFilter_extractHistogram( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject maskBuffer, jobject histogramBuffer ) +{ + unsigned char* pImg = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + int* pHist = static_cast<int*>(env->GetDirectBufferAddress(histogramBuffer)); + int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; // 4 bytes per pixel + int numBins = env->GetDirectBufferCapacity(histogramBuffer); + + unsigned char* pMask = NULL; + if(maskBuffer != NULL) { + pMask = static_cast<unsigned char*>(env->GetDirectBufferAddress(maskBuffer)); + } + + for(int i = 0; i < numBins; ++i) pHist[i] = 0; + + if(pMask == NULL) { + for( ; numPixels > 0; --numPixels) { + addPixelToHistogram(pImg, pHist, numBins); + } + } else { + for( ; numPixels > 0; --numPixels) { + if(*pMask == 0){ + pMask += 4; + pImg += 4; // Note that otherwise addPixelToHistogram advances pImg by 4 + continue; + } + pMask += 4; + addPixelToHistogram(pImg, pHist, numBins); + } + } +} + +void Java_androidx_media_filterpacks_histogram_ChromaHistogramFilter_extractChromaHistogram( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer, jint hBins, jint sBins) +{ + unsigned char* pixelIn = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + float* histOut = static_cast<float*>(env->GetDirectBufferAddress(histogramBuffer)); + int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; // 4 bytes per pixel + + for (int i = 0; i < hBins * sBins; ++i) histOut[i] = 0.0f; + + int h, s, v; + float hScaler = hBins / 256.0f; + float sScaler = sBins / 256.0f; + for( ; numPixels > 0; --numPixels) { + h = *(pixelIn++); + s = *(pixelIn++); + v = *(pixelIn++); + pixelIn++; + + int index = static_cast<int>(s * sScaler) * hBins + static_cast<int>(h * hScaler); + histOut[index] += 1.0f; + } +} + +void Java_androidx_media_filterpacks_histogram_NewChromaHistogramFilter_extractChromaHistogram( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer, + jint hueBins, jint saturationBins, jint valueBins, + jint saturationThreshold, jint valueThreshold) { + unsigned char* pixelIn = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + float* histOut = static_cast<float*>(env->GetDirectBufferAddress(histogramBuffer)); + int numPixels = env->GetDirectBufferCapacity(imageBuffer) / 4; // 4 bytes per pixel + + // TODO: add check on the size of histOut + for (int i = 0; i < (hueBins * saturationBins + valueBins); ++i) { + histOut[i] = 0.0f; + } + + for( ; numPixels > 0; --numPixels) { + int h = *(pixelIn++); + int s = *(pixelIn++); + int v = *(pixelIn++); + + pixelIn++; + // If a pixel that is either too dark (less than valueThreshold) or colorless + // (less than saturationThreshold), if will be put in a 1-D value histogram instead. + + int index; + if (s > saturationThreshold && v > valueThreshold) { + int sIndex = s * saturationBins / 256; + + // Shifting hue index by 0.5 such that peaks of red, yellow, green, cyan, blue, pink + // will be at the center of some bins. + int hIndex = ((h * hueBins + 128) / 256) % hueBins; + index = sIndex * hueBins + hIndex; + } else { + index = hueBins * saturationBins + (v * valueBins / 256); + } + histOut[index] += 1.0f; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.h new file mode 100644 index 0000000..b5e88aa --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/histogram.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 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. + */ + +// Native function to extract histogram from image (handed down as ByteBuffer). + +#ifndef ANDROID_FILTERFW_JNI_HISTOGRAM_H +#define ANDROID_FILTERFW_JNI_HISTOGRAM_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL +Java_androidx_media_filterpacks_histogram_GrayHistogramFilter_extractHistogram( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject maskBuffer, jobject histogramBuffer ); + +JNIEXPORT void JNICALL +Java_androidx_media_filterpacks_histogram_ChromaHistogramFilter_extractChromaHistogram( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer, jint hBins, jint sBins); + +JNIEXPORT void JNICALL +Java_androidx_media_filterpacks_histogram_NewChromaHistogramFilter_extractChromaHistogram( + JNIEnv* env, jclass clazz, jobject imageBuffer, jobject histogramBuffer, + jint hueBins, jint saturationBins, jint valueBins, + jint saturationThreshold, jint valueThreshold); + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_HISTOGRAM_H diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/imgprocutil.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/imgprocutil.h new file mode 100644 index 0000000..aef67a5 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/imgprocutil.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 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. + */ + +// Some native low-level image processing functions. + + +#ifndef ANDROID_FILTERFW_JNI_IMGPROCUTIL_H +#define ANDROID_FILTERFW_JNI_IMGPROCUTIL_H + +inline int getIntensityFast(int R, int G, int B) { + return (R + R + R + B + G + G + G + G) >> 3; // see http://stackoverflow.com/a/596241 +} + +inline int clamp(int min, int val, int max) { + return val < min ? min : (val > max ? max : val); + // Note that for performance reasons, this function does *not* check if min < max! +} + +#endif // ANDROID_FILTERFW_JNI_IMGPROCUTIL_H diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.cpp new file mode 100644 index 0000000..596c7c0 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013 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. + */ + +#include "pixelutils.h" + +#include <stdint.h> + +typedef uint32_t uint32; + +void JNI_PIXELUTILS_METHOD(nativeCopyPixels)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height, jint offset, + jint pixStride, jint rowStride) { + uint32* pInPix = static_cast<uint32*>(env->GetDirectBufferAddress(input)); + uint32* pOutput = static_cast<uint32*>(env->GetDirectBufferAddress(output)); + uint32* pOutRow = pOutput + offset; + for (int y = 0; y < height; ++y) { + uint32* pOutPix = pOutRow; + for (int x = 0; x < width; ++x) { + *pOutPix = *(pInPix++); + pOutPix += pixStride; + } + pOutRow += rowStride; + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.h new file mode 100644 index 0000000..be69009 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/pixelutils.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 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. + */ + +#ifndef ANDROID_FILTERFW_JNI_PIXELUTILS_H +#define ANDROID_FILTERFW_JNI_PIXELUTILS_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define JNI_PIXELUTILS_METHOD(METHOD_NAME) \ + Java_androidx_media_filterfw_PixelUtils_ ## METHOD_NAME + +JNIEXPORT void JNICALL +JNI_PIXELUTILS_METHOD(nativeCopyPixels)( + JNIEnv* env, jclass clazz, jobject input, jobject output, jint width, jint height, jint offset, + jint pixStride, jint rowStride); + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_PIXELUTILS_H + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.cpp new file mode 100644 index 0000000..dc5c305 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.cpp @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012 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. + */ + +// Native function to extract histogram from image (handed down as ByteBuffer). + +#include "sobeloperator.h" + +#include <math.h> +#include <string.h> +#include <jni.h> +#include <unistd.h> +#include <android/log.h> + +#include "imgprocutil.h" + +/* + * Perform 1d convolution on 3 channel image either horizontally or vertically. + * Parameters: + * inputHead: pointer to input image + * length: the length of image in the chosen axis. + * fragments: number of lines of the image in the chosen axis. + * step: the 1d pixel distance between adjacent pixels in the chosen axis. + * shift: the 1d pixel distance between adjacent lines in the chosen axis. + * filter: pointer to 1d filter + * halfSize: the length of filter is supposed to be (2 * halfSize + 1) + * outputHead: pointer to output image + */ + +void computeGradient(unsigned char* dataPtr, int width, int height, short* gxPtr, short* gyPtr) { + for (int i = 0; i < height; i++) { + for (int j = 0; j < width; j++) { + const int left = (j > 0)? -4 : 0; + const int right = (j < width - 1) ? 4 : 0; + const int curr = (i * width + j) * 4; + const int above = (i > 0) ? curr - 4 * width : curr; + const int below = (i < height - 1) ? curr + 4 * width : curr; + const int offset = (i * width + j) * 3; + for (int c = 0; c < 3; c++) { + *(gxPtr + offset + c) = + (*(dataPtr + curr + c + right) - *(dataPtr + curr + c + left)) * 2 + + *(dataPtr + above + c + right) - *(dataPtr + above + c + left) + + *(dataPtr + below + c + right) - *(dataPtr + below + c + left); + *(gyPtr + offset + c) = + (*(dataPtr + c + below) - *(dataPtr + c + above)) * 2 + + *(dataPtr + left + c + below) - *(dataPtr + left + c + above) + + *(dataPtr + right + c + below) - *(dataPtr + right + c + above); + } + } + } +} + +jboolean Java_androidx_media_filterpacks_image_SobelFilter_sobelOperator( + JNIEnv* env, jclass clazz, jint width, jint height, jobject imageBuffer, + jobject magBuffer, jobject dirBuffer) { + + if (imageBuffer == 0) { + return JNI_FALSE; + } + unsigned char* srcPtr = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + unsigned char* magPtr = (magBuffer == 0) ? + 0 : static_cast<unsigned char*>(env->GetDirectBufferAddress(magBuffer)); + unsigned char* dirPtr = (dirBuffer == 0) ? + 0 : static_cast<unsigned char*>(env->GetDirectBufferAddress(dirBuffer)); + + int numPixels = width * height; + // TODO: avoid creating and deleting these buffers within this native function. + short* gxPtr = new short[3 * numPixels]; + short* gyPtr = new short[3 * numPixels]; + computeGradient(srcPtr, width, height, gxPtr, gyPtr); + + unsigned char* mag = magPtr; + unsigned char* dir = dirPtr; + for (int i = 0; i < numPixels; ++i) { + for (int c = 0; c < 3; c++) { + int gx = static_cast<int>(*(gxPtr + 3 * i + c) / 8 + 127.5); + int gy = static_cast<int>(*(gyPtr + 3 * i + c) / 8 + 127.5); + + // emulate arithmetic in GPU. + gx = 2 * gx - 255; + gy = 2 * gy - 255; + if (magPtr != 0) { + double value = sqrt(gx * gx + gy * gy); + *(magPtr + 4 * i + c) = static_cast<unsigned char>(value); + } + if (dirPtr != 0) { + *(dirPtr + 4 * i + c) = static_cast<unsigned char>( + (atan(static_cast<double>(gy)/static_cast<double>(gx)) + 3.14) / 6.28); + } + } + //setting alpha change to 1.0 (255) + if (magPtr != 0) { + *(magPtr + 4 * i + 3) = 255; + } + if (dirPtr != 0) { + *(dirPtr + 4 * i + 3) = 255; + } + } + + delete[] gxPtr; + delete[] gyPtr; + + return JNI_TRUE; +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.h new file mode 100644 index 0000000..c7639d2 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/sobeloperator.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 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. + */ + +// Native function to extract histogram from image (handed down as ByteBuffer). + +#ifndef ANDROID_FILTERFW_JNI_SOBELOPERATOR_H +#define ANDROID_FILTERFW_JNI_SOBELOPERATOR_H + +#include <jni.h> + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT jboolean JNICALL +Java_androidx_media_filterpacks_image_SobelFilter_sobelOperator( + JNIEnv* env, jclass clazz, jint width, jint height, + jobject imageBuffer, jobject magBuffer, jobject dirBuffer); + +#ifdef __cplusplus +} +#endif + +#endif // ANDROID_FILTERFW_JNI_SOBELOPERATOR_H diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.cpp b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.cpp new file mode 100644 index 0000000..f282675 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2012 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. + */ + +// Stats (mean and stdev) scoring in the native. + +#include "stats_scorer.h" + +#include <jni.h> +#include <math.h> + +void Java_androidx_media_filterpacks_numeric_StatsFilter_score( + JNIEnv* env, jobject thiz, jobject imageBuffer, jfloatArray statsArray) +{ + unsigned char* pImg = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + int numPixels = env->GetDirectBufferCapacity(imageBuffer); // 1 byte per pixel + float sum = 0.0; + float sumSquares = 0.0; + + for (int i = 0; i < numPixels; ++i) { + float val = static_cast<float>(pImg[i]); + sum += val; + sumSquares += val * val; + } + jfloat result[2]; + result[0] = sum / numPixels; // mean + result[1] = sqrt((sumSquares - numPixels * result[0] * result[0]) / (numPixels - 1)); // stdev. + env->SetFloatArrayRegion(statsArray, 0, 2, result); +} + +void Java_androidx_media_filterpacks_numeric_StatsFilter_regionscore( + JNIEnv* env, jobject thiz, jobject imageBuffer, jint width, jint height, + jfloat left, jfloat top, jfloat right, jfloat bottom, jfloatArray statsArray) +{ + unsigned char* pImg = static_cast<unsigned char*>(env->GetDirectBufferAddress(imageBuffer)); + int xStart = static_cast<int>(width * left); + int xEnd = static_cast<int>(width * right); + int yStart = static_cast<int>(height * top); + int yEnd = static_cast<int>(height * bottom); + int numPixels = (xEnd - xStart) * (yEnd - yStart); + float sum = 0.0; + float sumSquares = 0.0; + + for (int y = yStart; y < yEnd; y++) { + int disp = width * y; + for (int x = xStart; x < xEnd; ++x) { + float val = static_cast<float>(*(pImg + disp + x)); + sum += val; + sumSquares += val * val; + } + } + jfloat result[2]; + result[0] = sum / numPixels; // mean + result[1] = (numPixels == 1) ? + 0 : sqrt((sumSquares - numPixels * result[0] * result[0]) / (numPixels - 1)); // stdev. + env->SetFloatArrayRegion(statsArray, 0, 2, result); +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.h b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.h new file mode 100644 index 0000000..a951ec9 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/jni/stats_scorer.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2012 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. + */ + +// Stats (mean and stdev) scoring in the native. + +#ifndef ANDROID_FILTERFW_JNI_STATS_SCORER_H +#define ANDROID_FILTERFW_JNI_STATS_SCORER_H + +#include <jni.h> + +#define JNI_FES_FUNCTION(name) Java_androidx_media_filterpacks_numeric_StatsFilter_ ## name + +#ifdef __cplusplus +extern "C" { +#endif + +JNIEXPORT void JNICALL +JNI_FES_FUNCTION(score)( + JNIEnv* env, jobject thiz, jobject imageBuffer, jfloatArray statsArray); + +JNIEXPORT void JNICALL +JNI_FES_FUNCTION(regionscore)( + JNIEnv* env, jobject thiz, jobject imageBuffer, jint width, jint height, + jfloat lefp, jfloat top, jfloat right, jfloat bottom, jfloatArray statsArray); + +#ifdef __cplusplus +} +#endif + + +#endif // ANDROID_FILTERFW_JNI_STATS_SCORER_H diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/proguard-project.txt b/tests/Camera2Tests/SmartCamera/SimpleCamera/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# 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/tests/Camera2Tests/SmartCamera/SimpleCamera/project.properties b/tests/Camera2Tests/SmartCamera/SimpleCamera/project.properties new file mode 100644 index 0000000..10149cb --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-16 +android.library.reference.1=../../filterfw diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/black_screen.jpg b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/black_screen.jpg Binary files differnew file mode 100644 index 0000000..702d9fa --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/black_screen.jpg diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..1c7b44a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_launcher.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_gallery.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_gallery.png Binary files differnew file mode 100644 index 0000000..f61bbd8 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_gallery.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_quill.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_quill.png Binary files differnew file mode 100644 index 0000000..7ea01b7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_quill.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_save.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_save.png Binary files differnew file mode 100644 index 0000000..62d0b9a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-hdpi/ic_menu_save.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-ldpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-ldpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..b42e903 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-ldpi/ic_launcher.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-mdpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..d4b4d6b --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-mdpi/ic_launcher.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/android_figure.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/android_figure.png Binary files differnew file mode 100644 index 0000000..71c6d76 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/android_figure.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/oldframe.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/oldframe.png Binary files differnew file mode 100644 index 0000000..8b7ae63 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/oldframe.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/polaroid.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/polaroid.png Binary files differnew file mode 100644 index 0000000..5504c57 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-nodpi/polaroid.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-xhdpi/ic_launcher.png b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..3bb5454 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/drawable-xhdpi/ic_launcher.png diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/imageview.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/imageview.xml new file mode 100644 index 0000000..4e20c3f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/imageview.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copyright 2013 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. +--> +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="180px" + android:layout_height="240px" + android:src="@drawable/black_screen" + android:adjustViewBounds="true" +/> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/simplecamera.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/simplecamera.xml new file mode 100644 index 0000000..8d8ff51 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/layout/simplecamera.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2013 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <RelativeLayout android:id="@+id/surfaceViewLayout" + android:layout_width="wrap_content" + android:layout_height="1240px" + android:layout_alignParentTop="true" > + <SurfaceView android:id="@+id/cameraView" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_weight="1.0" + /> + <Button android:id="@+id/startButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/startButton" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + /> + <Button android:id="@+id/galleryOpenButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/galleryOpenButton" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + /> + <Spinner android:id="@+id/spinner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:entries="@array/number_array" + android:layout_alignParentTop="true" + android:layout_alignParentRight="true" + /> + <TextView android:id="@+id/imagesSavedTextView" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:padding="16dip" + android:text="@string/imagesSavedTextView" + android:layout_centerHorizontal="true" + android:layout_alignParentBottom="true" + android:textColor="#FF0000" + android:textSize="20sp" + /> + </RelativeLayout> + <HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/scrollView" + android:layout_width="fill_parent" + android:layout_height="wrap_content" > + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/scrollViewLinearLayout" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="320px"> + </LinearLayout> + </HorizontalScrollView> + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="wrap_content"> + <TextView android:id="@+id/goodOrBadTextView" + android:layout_width="wrap_content" + android:layout_height="fill_parent" + android:padding="16dip" + android:text="@string/goodOrBadTextView" + /> + <TextView android:id="@+id/fpsTextView" + android:layout_height="fill_parent" + android:layout_width="wrap_content" + android:padding="16dip" + android:text="@string/fpsTextView" + /> + <TextView android:id="@+id/scoreTextView" + android:layout_height="fill_parent" + android:layout_width="wrap_content" + android:padding="16dip" + android:text="@string/scoreTextView" + /> + </LinearLayout> +</LinearLayout> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/raw/camera_graph.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/raw/camera_graph.xml new file mode 100644 index 0000000..6661fd7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/raw/camera_graph.xml @@ -0,0 +1,182 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2013 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. +--> + +<graph> + <!-- Packages --> + <import package="androidx.media.filterpacks.base"/> + <import package="androidx.media.filterpacks.image"/> + <import package="androidx.media.filterpacks.video"/> + <import package="androidx.media.filterpacks.text" /> + <import package="androidx.media.filterpacks.numeric" /> + <import package="androidx.media.filterpacks.face" /> + <import package="androidx.media.filterpacks.transform" /> + <import package="androidx.media.filterpacks.performance" /> + <import package="androidx.media.filterfw.samples.simplecamera" /> + <import package="androidx.media.filterpacks.histogram" /> + <import package="androidx.media.filterpacks.colorspace" /> + <import package="androidx.media.filterpacks.sensors" /> + + <!-- Filters --> + <filter class="ResizeFilter" name="resize" > + <input name="outputWidth" intValue="480" /> + <input name="outputHeight" intValue="640" /> + </filter> + + <filter class="Camera2Source" name="camera"/> + + <filter class="BranchFilter" name="mainBranch" /> + <filter class="BranchFilter" name="preMainBranch" /> + <filter class="BranchFilter" name="featureBranch" /> + + <filter class="SurfaceHolderTarget" name="camViewTarget"/> + + <filter class="ScaleFilter" name="scale" > + <input name="scale" floatValue="0.50"/> + </filter> + + <filter class="SobelFilter" name="sobel" /> + <filter class="StatsFilter" name="statsFilter" /> + <filter class="NormFilter" name="normFilter" /> + <filter class="TextViewTarget" name="goodOrBadTextView" /> + <filter class="ToGrayValuesFilter" name="sobelConverter" /> + <filter class="AverageFilter" name="avgFilter" /> + + <var name="startCapture" /> + <filter class="ImageGoodnessFilter" name="goodnessFilter" > + <input name="capturing" varValue="startCapture" /> + </filter> + + <filter class="ToStringFilter" name="scoreToString" /> + <filter class="TextViewTarget" name="scoreTextView" /> + + <filter class="ExposureFilter" name="exposure" /> + + <filter class="TextViewTarget" name="fpsTextView" /> + <filter class="ToStringFilter" name="throughputToString" /> + + + <filter class="ContrastRatioFilter" name="contrast" /> + + <filter class="ScaleFilter" name="secondaryScale" > + <input name="scale" floatValue="0.50"/> + </filter> + + <filter class="ThroughputFilter" name="throughput" /> + + <filter class="NewChromaHistogramFilter" name="histogram" /> + <filter class="ColorfulnessFilter" name="colorfulness" /> + + <filter class="MotionSensorWTime" name="motion" /> + + <filter class="AvgBrightnessFilter" name="brightness" /> + + <filter class="RotateFilter" name="rotate" /> + + <filter class="BrightnessFilter" name="snapBrightness" /> + <filter class="WaveTriggerFilter" name="snapEffect" /> + <!-- Connections --> + <connect sourceFilter="camera" sourcePort="video" + targetFilter="rotate" targetPort="image" /> + + <connect sourceFilter="camera" sourcePort="orientation" + targetFilter="rotate" targetPort="rotateAngle" /> + + <connect sourceFilter="rotate" sourcePort="image" + targetFilter="resize" targetPort="image" /> + <connect sourceFilter="resize" sourcePort="image" + targetFilter="preMainBranch" targetPort="input" /> + <connect sourceFilter="preMainBranch" sourcePort="toMainBranch" + targetFilter="scale" targetPort="image" /> + <connect sourceFilter="scale" sourcePort="image" + targetFilter="mainBranch" targetPort="input" /> + + <connect sourceFilter="preMainBranch" sourcePort="toGoodnessFilter" + targetFilter="goodnessFilter" targetPort="image" /> + <connect sourceFilter="mainBranch" sourcePort="toFeatureBranch" + targetFilter="secondaryScale" targetPort="image" /> + <connect sourceFilter="secondaryScale" sourcePort="image" + targetFilter="featureBranch" targetPort="input" /> + + <connect sourceFilter="featureBranch" sourcePort="toSobel" + targetFilter="sobel" targetPort="image" /> + + <connect sourceFilter="sobel" sourcePort="magnitude" + targetFilter="sobelConverter" targetPort="image" /> + + <connect sourceFilter="sobelConverter" sourcePort="image" + targetFilter="statsFilter" targetPort="buffer" /> + + <connect sourceFilter="statsFilter" sourcePort="mean" + targetFilter="normFilter" targetPort="x" /> + + <connect sourceFilter="statsFilter" sourcePort="stdev" + targetFilter="normFilter" targetPort="y" /> + + <connect sourceFilter="normFilter" sourcePort="norm" + targetFilter="avgFilter" targetPort="sharpness" /> + + <connect sourceFilter="avgFilter" sourcePort="avg" + targetFilter="goodnessFilter" targetPort="sharpness" /> + + <connect sourceFilter="goodnessFilter" sourcePort="goodOrBadPic" + targetFilter="goodOrBadTextView" targetPort="text" /> + + <connect sourceFilter="featureBranch" sourcePort="toExposure" + targetFilter="exposure" targetPort="image" /> + <connect sourceFilter="exposure" sourcePort="underExposureRating" + targetFilter="goodnessFilter" targetPort="underExposure" /> + <connect sourceFilter="exposure" sourcePort="overExposureRating" + targetFilter="goodnessFilter" targetPort="overExposure" /> + + <connect sourceFilter="goodnessFilter" sourcePort="score" + targetFilter="scoreToString" targetPort="object" /> + <connect sourceFilter="scoreToString" sourcePort="string" + targetFilter="scoreTextView" targetPort="text" /> + + <connect sourceFilter="mainBranch" sourcePort="camView" + targetFilter="throughput" targetPort="frame" /> + <connect sourceFilter="throughput" sourcePort="frame" + targetFilter="snapBrightness" targetPort="image" /> + <connect sourceFilter="snapEffect" sourcePort="value" + targetFilter="snapBrightness" targetPort="brightness" /> + <connect sourceFilter="snapBrightness" sourcePort="image" + targetFilter="camViewTarget" targetPort="image" /> + <connect sourceFilter="throughput" sourcePort="throughput" + targetFilter="throughputToString" targetPort="object" /> + <connect sourceFilter="throughputToString" sourcePort="string" + targetFilter="fpsTextView" targetPort="text" /> + + <connect sourceFilter="featureBranch" sourcePort="contrastRatio" + targetFilter="contrast" targetPort="image" /> + <connect sourceFilter="contrast" sourcePort="contrastRatingToGoodness" + targetFilter="goodnessFilter" targetPort="contrastRating" /> + + <connect sourceFilter="mainBranch" sourcePort="colorfulness" + targetFilter="histogram" targetPort="image" /> + <connect sourceFilter="histogram" sourcePort="histogram" + targetFilter="colorfulness" targetPort="histogram" /> + <connect sourceFilter="colorfulness" sourcePort="score" + targetFilter="goodnessFilter" targetPort="colorfulness" /> + + <connect sourceFilter="motion" sourcePort="values" + targetFilter="goodnessFilter" targetPort="motionValues" /> + + <connect sourceFilter="featureBranch" sourcePort="brightness" + targetFilter="brightness" targetPort="image" /> + <connect sourceFilter="brightness" sourcePort="brightnessRating" + targetFilter="goodnessFilter" targetPort="brightness" /> +</graph> + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v11/styles.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v11/styles.xml new file mode 100644 index 0000000..d408cbc --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v11/styles.xml @@ -0,0 +1,5 @@ +<resources> + + <style name="AppTheme" parent="android:Theme.Holo.Light" /> + +</resources>
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v14/styles.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v14/styles.xml new file mode 100644 index 0000000..1c089a7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values-v14/styles.xml @@ -0,0 +1,5 @@ +<resources> + + <style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar" /> + +</resources>
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/strings.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/strings.xml new file mode 100644 index 0000000..5e6b8ab --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/strings.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2013 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. +--> + +<resources> + <string name="goodOrBadTextView"> Good/Bad Picture </string> + <string name="fpsTextView"> FPS </string> + <string name="scoreTextView"> Score</string> + <string name="gallery"> Go To Gallery </string> + <string name="camera"> Go To Camera </string> + <string name="startButton" > Start </string> + <string name="imagesSavedTextView" > Images Saved </string> + <string name="galleryOpenButton" > Gallery </string> + <string-array name="number_array"> + <item> 1 </item> + <item> 2 </item> + <item> 3 </item> + <item> 4 </item> + <item> 5 </item> + <item> 6 </item> + <item> 7 </item> + <item> 8 </item> + <item> 9 </item> + <item> 10 </item> + </string-array> +</resources> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/styles.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/styles.xml new file mode 100644 index 0000000..bd5027f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/res/values/styles.xml @@ -0,0 +1,5 @@ +<resources> + + <style name="AppTheme" parent="android:Theme.Light" /> + +</resources> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BackingStore.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BackingStore.java new file mode 100644 index 0000000..216e743 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BackingStore.java @@ -0,0 +1,929 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.annotation.TargetApi; +import android.graphics.Bitmap; +import android.os.Build; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.Type; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.Vector; + +final class BackingStore { + + /** Access mode None: Frame data will not be accessed at all. */ + static final int ACCESS_NONE = 0x00; + /** Access mode Bytes: Frame data will be accessed as a ByteBuffer. */ + static final int ACCESS_BYTES = 0x01; + /** Access mode Texture: Frame data will be accessed as a TextureSource. */ + static final int ACCESS_TEXTURE = 0x02; + /** Access mode RenderTarget: Frame data will be accessed as a RenderTarget. */ + static final int ACCESS_RENDERTARGET = 0x04; + /** Access mode Object: Frame data will be accessed as a generic Object. */ + static final int ACCESS_OBJECT = 0x08; + /** Access mode Bitmap: Frame data will be accessed as a Bitmap. */ + static final int ACCESS_BITMAP = 0x10; + /** Access mode Allocation: Frame data will be accessed as a RenderScript Allocation. */ + static final int ACCESS_ALLOCATION = 0x20; + + private static final int BACKING_BYTEBUFFER = 1; + private static final int BACKING_TEXTURE = 2; + private static final int BACKING_OBJECT = 3; + private static final int BACKING_BITMAP = 4; + private static final int BACKING_ALLOCATION = 5; + + private final FrameType mType; + private int[] mDimensions; + private long mTimestamp = Frame.TIMESTAMP_NOT_SET; + + private final FrameManager mFrameManager; + + private Vector<Backing> mBackings = new Vector<Backing>(); + + private boolean mWriteLocked = false; + private int mReadLocks = 0; + + private int mRefCount = 1; + + /** The most up-to-date data backing */ + private Backing mCurrentBacking = null; + + /** The currently locked backing */ + private Backing mLockedBacking = null; + + // Public Methods ////////////////////////////////////////////////////////////////////////////// + public BackingStore(FrameType type, int[] dimensions, FrameManager frameManager) { + mType = type; + mDimensions = dimensions != null ? Arrays.copyOf(dimensions, dimensions.length) : null; + mFrameManager = frameManager; + } + + public FrameType getFrameType() { + return mType; + } + + public Object lockData(int mode, int accessFormat) { + return lockBacking(mode, accessFormat).lock(accessFormat); + } + + public Backing lockBacking(int mode, int access) { + Backing backing = fetchBacking(mode, access); + if (backing == null) { + throw new RuntimeException("Could not fetch frame data!"); + } + lock(backing, mode); + return backing; + } + + public boolean unlock() { + if (mWriteLocked) { + mWriteLocked = false; + } else if (mReadLocks > 0) { + --mReadLocks; + } else { + return false; + } + mLockedBacking.unlock(); + mLockedBacking = null; + return true; + } + + public BackingStore retain() { + if (mRefCount >= 10) { + Log.w("BackingStore", "High ref-count of " + mRefCount + " on " + this + "!"); + } + if (mRefCount <= 0) { + throw new RuntimeException("RETAINING RELEASED"); + } + ++mRefCount; + return this; + } + + public BackingStore release() { + if (mRefCount <= 0) { + throw new RuntimeException("DOUBLE-RELEASE"); + } + --mRefCount; + if (mRefCount == 0) { + releaseBackings(); + return null; + } + return this; + } + + /** + * Resizes the backing store. This invalidates all data in the store. + */ + public void resize(int[] newDimensions) { + Vector<Backing> resized = new Vector<Backing>(); + for (Backing backing : mBackings) { + if (backing.resize(newDimensions)) { + resized.add(backing); + } else { + releaseBacking(backing); + } + } + mBackings = resized; + mDimensions = newDimensions; + } + + public int[] getDimensions() { + return mDimensions; + } + + public int getElementCount() { + int result = 1; + if (mDimensions != null) { + for (int dim : mDimensions) { + result *= dim; + } + } + return result; + } + + public void importStore(BackingStore store) { + // TODO: Better backing selection? + if (store.mBackings.size() > 0) { + importBacking(store.mBackings.firstElement()); + } + mTimestamp = store.mTimestamp; + } + + /** + * @return the timestamp + */ + public long getTimestamp() { + return mTimestamp; + } + + /** + * @param timestamp the timestamp to set + */ + public void setTimestamp(long timestamp) { + mTimestamp = timestamp; + } + + // Internal Methods //////////////////////////////////////////////////////////////////////////// + private Backing fetchBacking(int mode, int access) { + Backing backing = getBacking(mode, access); + if (backing == null) { + backing = attachNewBacking(mode, access); + } + syncBacking(backing); + return backing; + } + + private void syncBacking(Backing backing) { + if (backing != null && backing.isDirty() && mCurrentBacking != null) { + backing.syncTo(mCurrentBacking); + } + } + + private Backing getBacking(int mode, int access) { + // [Non-iterator looping] + for (int i = 0; i < mBackings.size(); ++i) { + final Backing backing = mBackings.get(i); + + int backingAccess = + (mode == Frame.MODE_WRITE) ? backing.writeAccess() : backing.readAccess(); + if ((backingAccess & access) == access) { + return backing; + } + } + return null; + } + + private Backing attachNewBacking(int mode, int access) { + Backing backing = createBacking(mode, access); + if (mBackings.size() > 0) { + backing.markDirty(); + } + mBackings.add(backing); + return backing; + } + + private Backing createBacking(int mode, int access) { + // TODO: If the read/write access flags indicate, make/fetch a GraphicBuffer backing. + Backing backing = null; + int elemSize = mType.getElementSize(); + if (shouldFetchCached(access)) { + backing = mFrameManager.fetchBacking(mode, access, mDimensions, elemSize); + } + if (backing == null) { + switch (access) { + case ACCESS_BYTES: + backing = new ByteBufferBacking(); + break; + case ACCESS_TEXTURE: + case ACCESS_RENDERTARGET: + backing = new TextureBacking(); + break; + case ACCESS_OBJECT: + backing = new ObjectBacking(); + break; + case ACCESS_BITMAP: + backing = new BitmapBacking(); + break; + case ACCESS_ALLOCATION: + if (!AllocationBacking.isSupported()) { + throw new RuntimeException( + "Attempted to create an AllocationBacking in context that does " + + "not support RenderScript!"); + } + backing = new AllocationBacking(mFrameManager.getContext().getRenderScript()); + break; + } + if (backing == null) { + throw new RuntimeException( + "Could not create backing for access type " + access + "!"); + } + if (backing.requiresGpu() && !mFrameManager.getRunner().isOpenGLSupported()) { + throw new RuntimeException( + "Cannot create backing that requires GPU in a runner that does not " + + "support OpenGL!"); + } + backing.setDimensions(mDimensions); + backing.setElementSize(elemSize); + backing.setElementId(mType.getElementId()); + backing.allocate(mType); + mFrameManager.onBackingCreated(backing); + } + return backing; + } + + private void importBacking(Backing backing) { + // TODO: This actually needs synchronization between the two BackingStore threads for the + // general case + int access = backing.requiresGpu() ? ACCESS_BYTES : backing.readAccess(); + Backing newBacking = createBacking(Frame.MODE_READ, access); + newBacking.syncTo(backing); + mBackings.add(newBacking); + mCurrentBacking = newBacking; + } + + private void releaseBackings() { + // [Non-iterator looping] + for (int i = 0; i < mBackings.size(); ++i) { + releaseBacking(mBackings.get(i)); + } + mBackings.clear(); + mCurrentBacking = null; + } + + private void releaseBacking(Backing backing) { + mFrameManager.onBackingAvailable(backing); + } + + private void lock(Backing backingToLock, int mode) { + if (mode == Frame.MODE_WRITE) { + // Make sure frame is not read-locked + if (mReadLocks > 0) { + throw new RuntimeException( + "Attempting to write-lock the read-locked frame " + this + "!"); + } else if (mWriteLocked) { + throw new RuntimeException( + "Attempting to write-lock the write-locked frame " + this + "!"); + } + // Mark all other backings dirty + // [Non-iterator looping] + for (int i = 0; i < mBackings.size(); ++i) { + final Backing backing = mBackings.get(i); + if (backing != backingToLock) { + backing.markDirty(); + } + } + mWriteLocked = true; + mCurrentBacking = backingToLock; + } else { + if (mWriteLocked) { + throw new RuntimeException("Attempting to read-lock locked frame " + this + "!"); + } + ++mReadLocks; + } + mLockedBacking = backingToLock; + } + + private static boolean shouldFetchCached(int access) { + return access != ACCESS_OBJECT; + } + + + // Backings //////////////////////////////////////////////////////////////////////////////////// + static abstract class Backing { + protected int[] mDimensions = null; + private int mElementSize; + private int mElementID; + protected boolean mIsDirty = false; + + int cachePriority = 0; + + public abstract void allocate(FrameType frameType); + + public abstract int readAccess(); + + public abstract int writeAccess(); + + public abstract void syncTo(Backing backing); + + public abstract Object lock(int accessType); + + public abstract int getType(); + + public abstract boolean shouldCache(); + + public abstract boolean requiresGpu(); + + public abstract void destroy(); + + public abstract int getSize(); + + public void unlock() { + // Default implementation does nothing. + } + + public void setData(Object data) { + throw new RuntimeException("Internal error: Setting data on frame backing " + this + + ", which does not support setting data directly!"); + } + + public void setDimensions(int[] dimensions) { + mDimensions = dimensions; + } + + public void setElementSize(int elemSize) { + mElementSize = elemSize; + } + + public void setElementId(int elemId) { + mElementID = elemId; + } + + public int[] getDimensions() { + return mDimensions; + } + + public int getElementSize() { + return mElementSize; + } + + public int getElementId() { + return mElementID; + } + + public boolean resize(int[] newDimensions) { + return false; + } + + public void markDirty() { + mIsDirty = true; + } + + public boolean isDirty() { + return mIsDirty; + } + + protected void assertImageCompatible(FrameType type) { + if (type.getElementId() != FrameType.ELEMENT_RGBA8888) { + throw new RuntimeException("Cannot allocate texture with non-RGBA data type!"); + } else if (mDimensions == null || mDimensions.length != 2) { + throw new RuntimeException("Cannot allocate non 2-dimensional texture!"); + } + } + + } + + static class ObjectBacking extends Backing { + + private Object mObject = null; + + @Override + public void allocate(FrameType frameType) { + mObject = null; + } + + @Override + public int readAccess() { + return ACCESS_OBJECT; + } + + @Override + public int writeAccess() { + return ACCESS_OBJECT; + } + + @Override + public void syncTo(Backing backing) { + switch (backing.getType()) { + case BACKING_OBJECT: + mObject = backing.lock(ACCESS_OBJECT); + backing.unlock(); + break; + case BACKING_BITMAP: + mObject = backing.lock(ACCESS_BITMAP); + backing.unlock(); + break; + default: + mObject = null; + } + mIsDirty = false; + } + + @Override + public Object lock(int accessType) { + return mObject; + } + + @Override + public int getType() { + return BACKING_OBJECT; + } + + @Override + public boolean shouldCache() { + return false; + } + + @Override + public boolean requiresGpu() { + return false; + } + + @Override + public void destroy() { + mObject = null; + } + + @Override + public int getSize() { + return 0; + } + + @Override + public void setData(Object data) { + mObject = data; + } + + } + + static class BitmapBacking extends Backing { + + private Bitmap mBitmap = null; + + @Override + public void allocate(FrameType frameType) { + assertImageCompatible(frameType); + } + + @Override + public int readAccess() { + return ACCESS_BITMAP; + } + + @Override + public int writeAccess() { + return ACCESS_BITMAP; + } + + @Override + public void syncTo(Backing backing) { + int access = backing.readAccess(); + if ((access & ACCESS_BITMAP) != 0) { + mBitmap = (Bitmap) backing.lock(ACCESS_BITMAP); + } else if ((access & ACCESS_BYTES) != 0) { + createBitmap(); + ByteBuffer buffer = (ByteBuffer) backing.lock(ACCESS_BYTES); + mBitmap.copyPixelsFromBuffer(buffer); + buffer.rewind(); + } else if ((access & ACCESS_TEXTURE) != 0) { + createBitmap(); + RenderTarget renderTarget = (RenderTarget) backing.lock(ACCESS_RENDERTARGET); + mBitmap.copyPixelsFromBuffer( + renderTarget.getPixelData(mDimensions[0], mDimensions[1])); + } else if ((access & ACCESS_ALLOCATION) != 0 && AllocationBacking.isSupported()) { + createBitmap(); + syncToAllocationBacking(backing); + } else { + throw new RuntimeException("Cannot sync bytebuffer backing!"); + } + backing.unlock(); + mIsDirty = false; + } + + @TargetApi(11) + private void syncToAllocationBacking(Backing backing) { + Allocation allocation = (Allocation) backing.lock(ACCESS_ALLOCATION); + allocation.copyTo(mBitmap); + } + + @Override + public Object lock(int accessType) { + return mBitmap; + } + + @Override + public int getType() { + return BACKING_BITMAP; + } + + @Override + public boolean shouldCache() { + return false; + } + + @Override + public boolean requiresGpu() { + return false; + } + + @Override + public void destroy() { + // As we share the bitmap with other backings (such as object backings), we must not + // recycle it here. + mBitmap = null; + } + + @Override + public int getSize() { + return 4 * mDimensions[0] * mDimensions[1]; + } + + @Override + public void setData(Object data) { + // We can assume that data will always be a Bitmap instance. + mBitmap = (Bitmap) data; + } + + private void createBitmap() { + mBitmap = Bitmap.createBitmap(mDimensions[0], mDimensions[1], Bitmap.Config.ARGB_8888); + } + } + + static class TextureBacking extends Backing { + + private RenderTarget mRenderTarget = null; + private TextureSource mTexture = null; + + @Override + public void allocate(FrameType frameType) { + assertImageCompatible(frameType); + mTexture = TextureSource.newTexture(); + } + + @Override + public int readAccess() { + return ACCESS_TEXTURE; + } + + @Override + public int writeAccess() { + return ACCESS_RENDERTARGET; + } + + @Override + public void syncTo(Backing backing) { + int access = backing.readAccess(); + if ((access & ACCESS_BYTES) != 0) { + ByteBuffer pixels = (ByteBuffer) backing.lock(ACCESS_BYTES); + mTexture.allocateWithPixels(pixels, mDimensions[0], mDimensions[1]); + } else if ((access & ACCESS_BITMAP) != 0) { + Bitmap bitmap = (Bitmap) backing.lock(ACCESS_BITMAP); + mTexture.allocateWithBitmapPixels(bitmap); + } else if ((access & ACCESS_TEXTURE) != 0) { + TextureSource texture = (TextureSource) backing.lock(ACCESS_TEXTURE); + int w = mDimensions[0]; + int h = mDimensions[1]; + ImageShader.renderTextureToTarget(texture, getRenderTarget(), w, h); + } else if ((access & ACCESS_ALLOCATION) != 0 && AllocationBacking.isSupported()) { + syncToAllocationBacking(backing); + } else { + throw new RuntimeException("Cannot sync bytebuffer backing!"); + } + backing.unlock(); + mIsDirty = false; + } + + @TargetApi(11) + private void syncToAllocationBacking(Backing backing) { + Allocation allocation = (Allocation) backing.lock(ACCESS_ALLOCATION); + ByteBuffer pixels = ByteBuffer.allocateDirect(getSize()); + allocation.copyTo(pixels.array()); + mTexture.allocateWithPixels(pixels, mDimensions[0], mDimensions[1]); + } + + @Override + public Object lock(int accessType) { + switch (accessType) { + case ACCESS_TEXTURE: + return getTexture(); + + case ACCESS_RENDERTARGET: + return getRenderTarget(); + + default: + throw new RuntimeException("Illegal access to texture!"); + } + } + + @Override + public int getType() { + return BACKING_TEXTURE; + } + + @Override + public boolean shouldCache() { + return true; + } + + @Override + public boolean requiresGpu() { + return true; + } + + @Override + public void destroy() { + if (mRenderTarget != null) { + mRenderTarget.release(); + } + if (mTexture.isAllocated()) { + mTexture.release(); + } + } + + @Override + public int getSize() { + return 4 * mDimensions[0] * mDimensions[1]; + } + + private TextureSource getTexture() { + if (!mTexture.isAllocated()) { + mTexture.allocate(mDimensions[0], mDimensions[1]); + } + return mTexture; + } + + private RenderTarget getRenderTarget() { + if (mRenderTarget == null) { + int w = mDimensions[0]; + int h = mDimensions[1]; + mRenderTarget = RenderTarget.currentTarget().forTexture(getTexture(), w, h); + } + return mRenderTarget; + } + + } + + static class ByteBufferBacking extends Backing { + + ByteBuffer mBuffer = null; + + @Override + public void allocate(FrameType frameType) { + int size = frameType.getElementSize(); + for (int dim : mDimensions) { + size *= dim; + } + mBuffer = ByteBuffer.allocateDirect(size); + } + + @Override + public int readAccess() { + return ACCESS_BYTES; + } + + @Override + public int writeAccess() { + return ACCESS_BYTES; + } + + @Override + public boolean requiresGpu() { + return false; + } + + @Override + public void syncTo(Backing backing) { + int access = backing.readAccess(); + if ((access & ACCESS_TEXTURE) != 0) { + RenderTarget target = (RenderTarget) backing.lock(ACCESS_RENDERTARGET); + GLToolbox.readTarget(target, mBuffer, mDimensions[0], mDimensions[1]); + } else if ((access & ACCESS_BITMAP) != 0) { + Bitmap bitmap = (Bitmap) backing.lock(ACCESS_BITMAP); + bitmap.copyPixelsToBuffer(mBuffer); + mBuffer.rewind(); + } else if ((access & ACCESS_BYTES) != 0) { + ByteBuffer otherBuffer = (ByteBuffer) backing.lock(ACCESS_BYTES); + mBuffer.put(otherBuffer); + otherBuffer.rewind(); + } else if ((access & ACCESS_ALLOCATION) != 0 && AllocationBacking.isSupported()) { + syncToAllocationBacking(backing); + } else { + throw new RuntimeException("Cannot sync bytebuffer backing!"); + } + backing.unlock(); + mBuffer.rewind(); + mIsDirty = false; + } + + @TargetApi(11) + private void syncToAllocationBacking(Backing backing) { + Allocation allocation = (Allocation) backing.lock(ACCESS_ALLOCATION); + if (getElementId() == FrameType.ELEMENT_RGBA8888) { + byte[] bytes = mBuffer.array(); + allocation.copyTo(bytes); + } else if (getElementId() == FrameType.ELEMENT_FLOAT32) { + float[] floats = new float[getSize() / 4]; + allocation.copyTo(floats); + mBuffer.asFloatBuffer().put(floats); + } else { + throw new RuntimeException( + "Trying to sync to an allocation with an unsupported element id: " + + getElementId()); + } + } + + @Override + public Object lock(int accessType) { + return mBuffer.rewind(); + } + + @Override + public void unlock() { + mBuffer.rewind(); + } + + @Override + public int getType() { + return BACKING_BYTEBUFFER; + } + + @Override + public boolean shouldCache() { + return true; + } + + @Override + public void destroy() { + mBuffer = null; + } + + @Override + public int getSize() { + return mBuffer.remaining(); + } + + } + + @TargetApi(11) + static class AllocationBacking extends Backing { + + private final RenderScript mRenderScript; + private Allocation mAllocation = null; + + public AllocationBacking(RenderScript renderScript) { + mRenderScript = renderScript; + } + + @Override + public void allocate(FrameType frameType) { + assertCompatible(frameType); + + Element element = null; + switch (frameType.getElementId()) { + case FrameType.ELEMENT_RGBA8888: + element = Element.RGBA_8888(mRenderScript); + break; + case FrameType.ELEMENT_FLOAT32: + element = Element.F32(mRenderScript); + break; + } + Type.Builder imageTypeBuilder = new Type.Builder(mRenderScript, element); + imageTypeBuilder.setX(mDimensions.length >= 1 ? mDimensions[0] : 1); + imageTypeBuilder.setY(mDimensions.length == 2 ? mDimensions[1] : 1); + Type imageType = imageTypeBuilder.create(); + + mAllocation = Allocation.createTyped(mRenderScript, imageType); + } + + @Override + public int readAccess() { + return ACCESS_ALLOCATION; + } + + @Override + public int writeAccess() { + return ACCESS_ALLOCATION; + } + + @Override + public boolean requiresGpu() { + return false; + } + + @Override + public void syncTo(Backing backing) { + int access = backing.readAccess(); + if ((access & ACCESS_TEXTURE) != 0) { + RenderTarget target = (RenderTarget) backing.lock(ACCESS_RENDERTARGET); + ByteBuffer pixels = ByteBuffer.allocateDirect(getSize()); + GLToolbox.readTarget(target, pixels, mDimensions[0], mDimensions[1]); + mAllocation.copyFrom(pixels.array()); + } else if ((access & ACCESS_BITMAP) != 0) { + Bitmap bitmap = (Bitmap) backing.lock(ACCESS_BITMAP); + mAllocation.copyFrom(bitmap); + } else if ((access & ACCESS_BYTES) != 0) { + ByteBuffer buffer = (ByteBuffer) backing.lock(ACCESS_BYTES); + if (buffer.order() != ByteOrder.nativeOrder()) { + throw new RuntimeException( + "Trying to sync to the ByteBufferBacking with non-native byte order!"); + } + byte[] bytes; + if (buffer.hasArray()) { + bytes = buffer.array(); + } else { + bytes = new byte[getSize()]; + buffer.get(bytes); + buffer.rewind(); + } + mAllocation.copyFromUnchecked(bytes); + } else { + throw new RuntimeException("Cannot sync allocation backing!"); + } + backing.unlock(); + mIsDirty = false; + } + + @Override + public Object lock(int accessType) { + return mAllocation; + } + + @Override + public void unlock() { + } + + @Override + public int getType() { + return BACKING_ALLOCATION; + } + + @Override + public boolean shouldCache() { + return true; + } + + @Override + public void destroy() { + if (mAllocation != null) { + mAllocation.destroy(); + mAllocation = null; + } + } + + @Override + public int getSize() { + int elementCount = 1; + for (int dim : mDimensions) { + elementCount *= dim; + } + return getElementSize() * elementCount; + } + + public static boolean isSupported() { + return Build.VERSION.SDK_INT >= 11; + } + + private void assertCompatible(FrameType type) { + // TODO: consider adding support for other data types. + if (type.getElementId() != FrameType.ELEMENT_RGBA8888 + && type.getElementId() != FrameType.ELEMENT_FLOAT32) { + throw new RuntimeException( + "Cannot allocate allocation with a non-RGBA or non-float data type!"); + } + if (mDimensions == null || mDimensions.length > 2) { + throw new RuntimeException( + "Cannot create an allocation with more than 2 dimensions!"); + } + } + + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BranchFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BranchFilter.java new file mode 100644 index 0000000..6e7c014 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BranchFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.base; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public final class BranchFilter extends Filter { + + private boolean mSynchronized = true; + + public BranchFilter(MffContext context, String name) { + super(context, name); + } + + public BranchFilter(MffContext context, String name, boolean synced) { + super(context, name); + mSynchronized = synced; + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("input", Signature.PORT_REQUIRED, FrameType.any()) + .addInputPort("synchronized", Signature.PORT_OPTIONAL,FrameType.single(boolean.class)) + .disallowOtherInputs(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("input")) { + for (OutputPort outputPort : getConnectedOutputPorts()) { + port.attachToOutputPort(outputPort); + } + } else if (port.getName().equals("synchronized")) { + port.bindToFieldNamed("mSynchronized"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected void onOpen() { + updateSynchronization(); + } + + @Override + protected void onProcess() { + Frame inputFrame = getConnectedInputPort("input").pullFrame(); + for (OutputPort outputPort : getConnectedOutputPorts()) { + if (outputPort.isAvailable()) { + outputPort.pushFrame(inputFrame); + } + } + } + + private void updateSynchronization() { + if (mSynchronized) { + for (OutputPort port : getConnectedOutputPorts()) { + port.setWaitsUntilAvailable(true); + } + } else { + for (OutputPort port : getConnectedOutputPorts()) { + port.setWaitsUntilAvailable(false); + } + } + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BrightnessFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BrightnessFilter.java new file mode 100644 index 0000000..5a70776 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/BrightnessFilter.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.image; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public class BrightnessFilter extends Filter { + + private float mBrightness = 1.0f; + private ImageShader mShader; + + private static final String mBrightnessShader = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler_0;\n" + + "uniform float brightness;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " vec4 color = texture2D(tex_sampler_0, v_texcoord);\n" + + " if (brightness < 0.5) {\n" + + " gl_FragColor = color * (2.0 * brightness);\n" + + " } else {\n" + + " vec4 diff = 1.0 - color;\n" + + " gl_FragColor = color + diff * (2.0 * (brightness - 0.5));\n" + + " }\n" + + "}\n"; + + + public BrightnessFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addInputPort("brightness", Signature.PORT_OPTIONAL, FrameType.single(float.class)) + .addOutputPort("image", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("brightness")) { + port.bindToFieldNamed("mBrightness"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected void onPrepare() { + mShader = new ImageShader(mBrightnessShader); + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("image"); + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + int[] dim = inputImage.getDimensions(); + FrameImage2D outputImage = outPort.fetchAvailableFrame(dim).asFrameImage2D(); + mShader.setUniformValue("brightness", mBrightness); + mShader.process(inputImage, outputImage); + outPort.pushFrame(outputImage); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CameraStreamer.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CameraStreamer.java new file mode 100644 index 0000000..d1642fd --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CameraStreamer.java @@ -0,0 +1,1906 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.Camera.CameraInfo; +import android.hardware.Camera.PreviewCallback; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.opengl.GLES20; +import android.os.Build.VERSION; +import android.util.Log; +import android.view.Display; +import android.view.Surface; +import android.view.SurfaceView; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Vector; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +import javax.microedition.khronos.egl.EGLContext; + +/** + * The CameraStreamer streams Frames from a camera to connected clients. + * + * There is one centralized CameraStreamer object per MffContext, and only one stream can be + * active at any time. The CameraStreamer acts as a Camera "server" that streams frames to any + * number of connected clients. Typically, these are CameraSource filters that are part of a + * graph, but other clients can be written as well. + */ +public class CameraStreamer { + + /** Camera Facing: Don't Care: Picks any available camera. */ + public static final int FACING_DONTCARE = 0; + /** Camera Facing: Front: Use the front facing camera. */ + public static final int FACING_FRONT = 1; + /** Camera Facing: Back: Use the rear facing camera. */ + public static final int FACING_BACK = 2; + + /** How long the streamer should wait to acquire the camera before giving up. */ + public static long MAX_CAMERA_WAIT_TIME = 5; + + /** + * The global camera lock, that is closed when the camera is acquired by any CameraStreamer, + * and opened when a streamer is done using the camera. + */ + static ReentrantLock mCameraLock = new ReentrantLock(); + + /** The Camera thread that grabs frames from the camera */ + private CameraRunnable mCameraRunner = null; + + private abstract class CamFrameHandler { + protected int mCameraWidth; + protected int mCameraHeight; + protected int mOutWidth; + protected int mOutHeight; + protected CameraRunnable mRunner; + + /** Map of GLSL shaders (one for each target context) */ + protected HashMap<EGLContext, ImageShader> mTargetShaders + = new HashMap<EGLContext, ImageShader>(); + + /** Map of target textures (one for each target context) */ + protected HashMap<EGLContext, TextureSource> mTargetTextures + = new HashMap<EGLContext, TextureSource>(); + + /** Map of set of clients (one for each target context) */ + protected HashMap<EGLContext, Set<FrameClient>> mContextClients + = new HashMap<EGLContext, Set<FrameClient>>(); + + /** List of clients that are consuming camera frames. */ + protected Vector<FrameClient> mClients = new Vector<FrameClient>(); + + public void initWithRunner(CameraRunnable camRunner) { + mRunner = camRunner; + } + + public void setCameraSize(int width, int height) { + mCameraWidth = width; + mCameraHeight = height; + } + + public void registerClient(FrameClient client) { + EGLContext context = RenderTarget.currentContext(); + Set<FrameClient> clientTargets = clientsForContext(context); + clientTargets.add(client); + mClients.add(client); + onRegisterClient(client, context); + } + + public void unregisterClient(FrameClient client) { + EGLContext context = RenderTarget.currentContext(); + Set<FrameClient> clientTargets = clientsForContext(context); + clientTargets.remove(client); + if (clientTargets.isEmpty()) { + onCleanupContext(context); + } + mClients.remove(client); + } + + public abstract void setupServerFrame(); + public abstract void updateServerFrame(); + public abstract void grabFrame(FrameImage2D targetFrame); + public abstract void release(); + + public void onUpdateCameraOrientation(int orientation) { + if (orientation % 180 != 0) { + mOutWidth = mCameraHeight; + mOutHeight = mCameraWidth; + } else { + mOutWidth = mCameraWidth; + mOutHeight = mCameraHeight; + } + } + + protected Set<FrameClient> clientsForContext(EGLContext context) { + Set<FrameClient> clients = mContextClients.get(context); + if (clients == null) { + clients = new HashSet<FrameClient>(); + mContextClients.put(context, clients); + } + return clients; + } + + protected void onRegisterClient(FrameClient client, EGLContext context) { + } + + protected void onCleanupContext(EGLContext context) { + TextureSource texture = mTargetTextures.get(context); + ImageShader shader = mTargetShaders.get(context); + if (texture != null) { + texture.release(); + mTargetTextures.remove(context); + } + if (shader != null) { + mTargetShaders.remove(context); + } + } + + protected TextureSource textureForContext(EGLContext context) { + TextureSource texture = mTargetTextures.get(context); + if (texture == null) { + texture = createClientTexture(); + mTargetTextures.put(context, texture); + } + return texture; + } + + protected ImageShader shaderForContext(EGLContext context) { + ImageShader shader = mTargetShaders.get(context); + if (shader == null) { + shader = createClientShader(); + mTargetShaders.put(context, shader); + } + return shader; + } + + protected ImageShader createClientShader() { + return null; + } + + protected TextureSource createClientTexture() { + return null; + } + + public boolean isFrontMirrored() { + return true; + } + } + + // Jellybean (and later) back-end + @TargetApi(16) + private class CamFrameHandlerJB extends CamFrameHandlerICS { + + @Override + public void setupServerFrame() { + setupPreviewTexture(mRunner.mCamera); + } + + @Override + public synchronized void updateServerFrame() { + updateSurfaceTexture(); + informClients(); + } + + @Override + public synchronized void grabFrame(FrameImage2D targetFrame) { + TextureSource targetTex = TextureSource.newExternalTexture(); + ImageShader copyShader = shaderForContext(RenderTarget.currentContext()); + if (targetTex == null || copyShader == null) { + throw new RuntimeException("Attempting to grab camera frame from unknown " + + "thread: " + Thread.currentThread() + "!"); + } + mPreviewSurfaceTexture.attachToGLContext(targetTex.getTextureId()); + updateTransform(copyShader); + updateShaderTargetRect(copyShader); + targetFrame.resize(new int[] { mOutWidth, mOutHeight }); + copyShader.process(targetTex, + targetFrame.lockRenderTarget(), + mOutWidth, + mOutHeight); + targetFrame.setTimestamp(mPreviewSurfaceTexture.getTimestamp()); + targetFrame.unlock(); + mPreviewSurfaceTexture.detachFromGLContext(); + targetTex.release(); + } + + @Override + protected void updateShaderTargetRect(ImageShader shader) { + if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) { + shader.setTargetRect(1f, 1f, -1f, -1f); + } else { + shader.setTargetRect(0f, 1f, 1f, -1f); + } + } + + @Override + protected void setupPreviewTexture(Camera camera) { + super.setupPreviewTexture(camera); + mPreviewSurfaceTexture.detachFromGLContext(); + } + + protected void updateSurfaceTexture() { + mPreviewSurfaceTexture.attachToGLContext(mPreviewTexture.getTextureId()); + mPreviewSurfaceTexture.updateTexImage(); + mPreviewSurfaceTexture.detachFromGLContext(); + } + + protected void informClients() { + synchronized (mClients) { + for (FrameClient client : mClients) { + client.onCameraFrameAvailable(); + } + } + } + } + + // ICS (and later) back-end + @TargetApi(15) + private class CamFrameHandlerICS extends CamFrameHandler { + + protected static final String mCopyShaderSource = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES tex_sampler_0;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" + + "}\n"; + + /** The camera transform matrix */ + private float[] mCameraTransform = new float[16]; + + /** The texture the camera streams to */ + protected TextureSource mPreviewTexture = null; + protected SurfaceTexture mPreviewSurfaceTexture = null; + + /** Map of target surface textures (one for each target context) */ + protected HashMap<EGLContext, SurfaceTexture> mTargetSurfaceTextures + = new HashMap<EGLContext, SurfaceTexture>(); + + /** Map of RenderTargets for client SurfaceTextures */ + protected HashMap<SurfaceTexture, RenderTarget> mClientRenderTargets + = new HashMap<SurfaceTexture, RenderTarget>(); + + /** Server side copy shader */ + protected ImageShader mCopyShader = null; + + @Override + public synchronized void setupServerFrame() { + setupPreviewTexture(mRunner.mCamera); + } + + @Override + public synchronized void updateServerFrame() { + mPreviewSurfaceTexture.updateTexImage(); + distributeFrames(); + } + + @Override + public void onUpdateCameraOrientation(int orientation) { + super.onUpdateCameraOrientation(orientation); + mRunner.mCamera.setDisplayOrientation(orientation); + updateSurfaceTextureSizes(); + } + + @Override + public synchronized void onRegisterClient(FrameClient client, EGLContext context) { + final Set<FrameClient> clientTargets = clientsForContext(context); + + // Make sure we have texture, shader, and surfacetexture setup for this context. + TextureSource clientTex = textureForContext(context); + ImageShader copyShader = shaderForContext(context); + SurfaceTexture surfTex = surfaceTextureForContext(context); + + // Listen to client-side surface texture updates + surfTex.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + for (FrameClient clientTarget : clientTargets) { + clientTarget.onCameraFrameAvailable(); + } + } + }); + } + + @Override + public synchronized void grabFrame(FrameImage2D targetFrame) { + // Get the GL objects for the receiver's context + EGLContext clientContext = RenderTarget.currentContext(); + TextureSource clientTex = textureForContext(clientContext); + ImageShader copyShader = shaderForContext(clientContext); + SurfaceTexture surfTex = surfaceTextureForContext(clientContext); + if (clientTex == null || copyShader == null || surfTex == null) { + throw new RuntimeException("Attempting to grab camera frame from unknown " + + "thread: " + Thread.currentThread() + "!"); + } + + // Copy from client ST to client tex + surfTex.updateTexImage(); + targetFrame.resize(new int[] { mOutWidth, mOutHeight }); + copyShader.process(clientTex, + targetFrame.lockRenderTarget(), + mOutWidth, + mOutHeight); + + targetFrame.setTimestamp(mPreviewSurfaceTexture.getTimestamp()); + targetFrame.unlock(); + } + + @Override + public synchronized void release() { + if (mPreviewTexture != null) { + mPreviewTexture.release(); + mPreviewTexture = null; + } + if (mPreviewSurfaceTexture != null) { + mPreviewSurfaceTexture.release(); + mPreviewSurfaceTexture = null; + } + } + + @Override + protected ImageShader createClientShader() { + return new ImageShader(mCopyShaderSource); + } + + @Override + protected TextureSource createClientTexture() { + return TextureSource.newExternalTexture(); + } + + protected void distributeFrames() { + updateTransform(getCopyShader()); + updateShaderTargetRect(getCopyShader()); + + for (SurfaceTexture clientTexture : mTargetSurfaceTextures.values()) { + RenderTarget clientTarget = renderTargetFor(clientTexture); + clientTarget.focus(); + getCopyShader().process(mPreviewTexture, + clientTarget, + mOutWidth, + mOutHeight); + GLToolbox.checkGlError("distribute frames"); + clientTarget.swapBuffers(); + } + } + + protected RenderTarget renderTargetFor(SurfaceTexture surfaceTex) { + RenderTarget target = mClientRenderTargets.get(surfaceTex); + if (target == null) { + target = RenderTarget.currentTarget().forSurfaceTexture(surfaceTex); + mClientRenderTargets.put(surfaceTex, target); + } + return target; + } + + protected void setupPreviewTexture(Camera camera) { + if (mPreviewTexture == null) { + mPreviewTexture = TextureSource.newExternalTexture(); + } + if (mPreviewSurfaceTexture == null) { + mPreviewSurfaceTexture = new SurfaceTexture(mPreviewTexture.getTextureId()); + try { + camera.setPreviewTexture(mPreviewSurfaceTexture); + } catch (IOException e) { + throw new RuntimeException("Could not bind camera surface texture: " + + e.getMessage() + "!"); + } + mPreviewSurfaceTexture.setOnFrameAvailableListener(mOnCameraFrameListener); + } + } + + protected ImageShader getCopyShader() { + if (mCopyShader == null) { + mCopyShader = new ImageShader(mCopyShaderSource); + } + return mCopyShader; + } + + protected SurfaceTexture surfaceTextureForContext(EGLContext context) { + SurfaceTexture surfTex = mTargetSurfaceTextures.get(context); + if (surfTex == null) { + TextureSource texture = textureForContext(context); + if (texture != null) { + surfTex = new SurfaceTexture(texture.getTextureId()); + surfTex.setDefaultBufferSize(mOutWidth, mOutHeight); + mTargetSurfaceTextures.put(context, surfTex); + } + } + return surfTex; + } + + protected void updateShaderTargetRect(ImageShader shader) { + if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) { + shader.setTargetRect(1f, 0f, -1f, 1f); + } else { + shader.setTargetRect(0f, 0f, 1f, 1f); + } + } + + protected synchronized void updateSurfaceTextureSizes() { + for (SurfaceTexture clientTexture : mTargetSurfaceTextures.values()) { + clientTexture.setDefaultBufferSize(mOutWidth, mOutHeight); + } + } + + protected void updateTransform(ImageShader shader) { + mPreviewSurfaceTexture.getTransformMatrix(mCameraTransform); + shader.setSourceTransform(mCameraTransform); + } + + @Override + protected void onCleanupContext(EGLContext context) { + super.onCleanupContext(context); + SurfaceTexture surfaceTex = mTargetSurfaceTextures.get(context); + if (surfaceTex != null) { + surfaceTex.release(); + mTargetSurfaceTextures.remove(context); + } + } + + protected SurfaceTexture.OnFrameAvailableListener mOnCameraFrameListener = + new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + mRunner.signalNewFrame(); + } + }; + } + + // Gingerbread (and later) back-end + @TargetApi(9) + private final class CamFrameHandlerGB extends CamFrameHandler { + + private SurfaceView mSurfaceView; + private byte[] mFrameBufferFront; + private byte[] mFrameBufferBack; + private boolean mWriteToBack = true; + private float[] mTargetCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f }; + final Object mBufferLock = new Object(); + + private String mNV21ToRGBAFragment = + "precision mediump float;\n" + + "\n" + + "uniform sampler2D tex_sampler_0;\n" + + "varying vec2 v_y_texcoord;\n" + + "varying vec2 v_vu_texcoord;\n" + + "varying vec2 v_pixcoord;\n" + + "\n" + + "vec3 select(vec4 yyyy, vec4 vuvu, int s) {\n" + + " if (s == 0) {\n" + + " return vec3(yyyy.r, vuvu.g, vuvu.r);\n" + + " } else if (s == 1) {\n" + + " return vec3(yyyy.g, vuvu.g, vuvu.r);\n" + + " } else if (s == 2) {\n" + + " return vec3(yyyy.b, vuvu.a, vuvu.b);\n" + + " } else {\n" + + " return vec3(yyyy.a, vuvu.a, vuvu.b);\n" + + " }\n" + + "}\n" + + "\n" + + "vec3 yuv2rgb(vec3 yuv) {\n" + + " mat4 conversion = mat4(1.0, 0.0, 1.402, -0.701,\n" + + " 1.0, -0.344, -0.714, 0.529,\n" + + " 1.0, 1.772, 0.0, -0.886,\n" + + " 0, 0, 0, 0);" + + " return (vec4(yuv, 1.0) * conversion).rgb;\n" + + "}\n" + + "\n" + + "void main() {\n" + + " vec4 yyyy = texture2D(tex_sampler_0, v_y_texcoord);\n" + + " vec4 vuvu = texture2D(tex_sampler_0, v_vu_texcoord);\n" + + " int s = int(mod(floor(v_pixcoord.x), 4.0));\n" + + " vec3 yuv = select(yyyy, vuvu, s);\n" + + " vec3 rgb = yuv2rgb(yuv);\n" + + " gl_FragColor = vec4(rgb, 1.0);\n" + + "}"; + + private String mNV21ToRGBAVertex = + "attribute vec4 a_position;\n" + + "attribute vec2 a_y_texcoord;\n" + + "attribute vec2 a_vu_texcoord;\n" + + "attribute vec2 a_pixcoord;\n" + + "varying vec2 v_y_texcoord;\n" + + "varying vec2 v_vu_texcoord;\n" + + "varying vec2 v_pixcoord;\n" + + "void main() {\n" + + " gl_Position = a_position;\n" + + " v_y_texcoord = a_y_texcoord;\n" + + " v_vu_texcoord = a_vu_texcoord;\n" + + " v_pixcoord = a_pixcoord;\n" + + "}\n"; + + private byte[] readBuffer() { + synchronized (mBufferLock) { + return mWriteToBack ? mFrameBufferFront : mFrameBufferBack; + } + } + + private byte[] writeBuffer() { + synchronized (mBufferLock) { + return mWriteToBack ? mFrameBufferBack : mFrameBufferFront; + } + } + + private synchronized void swapBuffers() { + synchronized (mBufferLock) { + mWriteToBack = !mWriteToBack; + } + } + + private PreviewCallback mPreviewCallback = new PreviewCallback() { + + @Override + public void onPreviewFrame(byte[] data, Camera camera) { + swapBuffers(); + camera.addCallbackBuffer(writeBuffer()); + mRunner.signalNewFrame(); + } + + }; + + @Override + public void setupServerFrame() { + checkCameraDimensions(); + Camera camera = mRunner.mCamera; + int bufferSize = mCameraWidth * (mCameraHeight + mCameraHeight/2); + mFrameBufferFront = new byte[bufferSize]; + mFrameBufferBack = new byte[bufferSize]; + camera.addCallbackBuffer(writeBuffer()); + camera.setPreviewCallbackWithBuffer(mPreviewCallback); + SurfaceView previewDisplay = getPreviewDisplay(); + if (previewDisplay != null) { + try { + camera.setPreviewDisplay(previewDisplay.getHolder()); + } catch (IOException e) { + throw new RuntimeException("Could not start camera with given preview " + + "display!"); + } + } + } + + private void checkCameraDimensions() { + if (mCameraWidth % 4 != 0) { + throw new RuntimeException("Camera width must be a multiple of 4!"); + } else if (mCameraHeight % 2 != 0) { + throw new RuntimeException("Camera height must be a multiple of 2!"); + } + } + + @Override + public void updateServerFrame() { + // Server frame has been updated already, simply inform clients here. + informClients(); + } + + @Override + public void grabFrame(FrameImage2D targetFrame) { + EGLContext clientContext = RenderTarget.currentContext(); + + // Copy camera data to the client YUV texture + TextureSource clientTex = textureForContext(clientContext); + int texWidth = mCameraWidth / 4; + int texHeight = mCameraHeight + mCameraHeight / 2; + synchronized(mBufferLock) { // Don't swap buffers while we are reading + ByteBuffer pixels = ByteBuffer.wrap(readBuffer()); + clientTex.allocateWithPixels(pixels, texWidth, texHeight); + } + clientTex.setParameter(GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + clientTex.setParameter(GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + + // Setup the YUV-2-RGBA shader + ImageShader transferShader = shaderForContext(clientContext); + transferShader.setTargetCoords(mTargetCoords); + updateShaderPixelSize(transferShader); + + // Convert pixels into target frame + targetFrame.resize(new int[] { mOutWidth, mOutHeight }); + transferShader.process(clientTex, + targetFrame.lockRenderTarget(), + mOutWidth, + mOutHeight); + targetFrame.unlock(); + } + + @Override + public void onUpdateCameraOrientation(int orientation) { + super.onUpdateCameraOrientation(orientation); + if ((mRunner.mActualFacing == FACING_FRONT) && mRunner.mFlipFront) { + switch (orientation) { + case 0: + mTargetCoords = new float[] { 1f, 0f, 0f, 0f, 1f, 1f, 0f, 1f }; + break; + case 90: + mTargetCoords = new float[] { 0f, 0f, 0f, 1f, 1f, 0f, 1f, 1f }; + break; + case 180: + mTargetCoords = new float[] { 0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f }; + break; + case 270: + mTargetCoords = new float[] { 1f, 1f, 1f, 0f, 0f, 1f, 0f, 0f }; + break; + } + } else { + switch (orientation) { + case 0: + mTargetCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f }; + break; + case 90: + mTargetCoords = new float[] { 1f, 0f, 1f, 1f, 0f, 0f, 0f, 1f }; + break; + case 180: + mTargetCoords = new float[] { 1f, 1f, 0f, 1f, 1f, 0f, 0f, 0f }; + break; + case 270: + mTargetCoords = new float[] { 0f, 1f, 0f, 0f, 1f, 1f, 1f, 0f }; + break; + } + } + } + + @Override + public void release() { + mFrameBufferBack = null; + mFrameBufferFront = null; + } + + @Override + public boolean isFrontMirrored() { + return false; + } + + @Override + protected ImageShader createClientShader() { + ImageShader shader = new ImageShader(mNV21ToRGBAVertex, mNV21ToRGBAFragment); + // TODO: Make this a VBO + float[] yCoords = new float[] { + 0f, 0f, + 1f, 0f, + 0f, 2f / 3f, + 1f, 2f / 3f }; + float[] uvCoords = new float[] { + 0f, 2f / 3f, + 1f, 2f / 3f, + 0f, 1f, + 1f, 1f }; + shader.setAttributeValues("a_y_texcoord", yCoords, 2); + shader.setAttributeValues("a_vu_texcoord", uvCoords, 2); + return shader; + } + + @Override + protected TextureSource createClientTexture() { + TextureSource texture = TextureSource.newTexture(); + texture.setParameter(GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST); + texture.setParameter(GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); + return texture; + } + + private void updateShaderPixelSize(ImageShader shader) { + float[] pixCoords = new float[] { + 0f, 0f, + mCameraWidth, 0f, + 0f, mCameraHeight, + mCameraWidth, mCameraHeight }; + shader.setAttributeValues("a_pixcoord", pixCoords, 2); + } + + private SurfaceView getPreviewDisplay() { + if (mSurfaceView == null) { + mSurfaceView = mRunner.getContext().getDummySurfaceView(); + } + return mSurfaceView; + } + + private void informClients() { + synchronized (mClients) { + for (FrameClient client : mClients) { + client.onCameraFrameAvailable(); + } + } + } + } + + private static class State { + public static final int STATE_RUNNING = 1; + public static final int STATE_STOPPED = 2; + public static final int STATE_HALTED = 3; + + private AtomicInteger mCurrent = new AtomicInteger(STATE_STOPPED); + + public int current() { + return mCurrent.get(); + } + + public void set(int newState) { + mCurrent.set(newState); + } + } + + private static class Event { + public static final int START = 1; + public static final int FRAME = 2; + public static final int STOP = 3; + public static final int HALT = 4; + public static final int RESTART = 5; + public static final int UPDATE = 6; + public static final int TEARDOWN = 7; + + public int code; + + public Event(int code) { + this.code = code; + } + } + + private final class CameraRunnable implements Runnable { + + /** On slower devices the event queue can easily fill up. We bound the queue to this. */ + private final static int MAX_EVENTS = 32; + + /** The runner's state */ + private State mState = new State(); + + /** The CameraRunner's event queue */ + private LinkedBlockingQueue<Event> mEventQueue = new LinkedBlockingQueue<Event>(MAX_EVENTS); + + /** The requested FPS */ + private int mRequestedFramesPerSec = 30; + + /** The actual FPS */ + private int mActualFramesPerSec = 0; + + /** The requested preview width and height */ + private int mRequestedPreviewWidth = 640; + private int mRequestedPreviewHeight = 480; + + /** The requested picture width and height */ + private int mRequestedPictureWidth = 640; + private int mRequestedPictureHeight = 480; + + /** The actual camera width and height */ + private int[] mActualDims = null; + + /** The requested facing */ + private int mRequestedFacing = FACING_DONTCARE; + + /** The actual facing */ + private int mActualFacing = FACING_DONTCARE; + + /** Whether to horizontally flip the front facing camera */ + private boolean mFlipFront = true; + + /** The display the camera streamer is bound to. */ + private Display mDisplay = null; + + /** The camera and screen orientation. */ + private int mCamOrientation = 0; + private int mOrientation = -1; + + /** The camera rotation (used for capture). */ + private int mCamRotation = 0; + + /** The camera flash mode */ + private String mFlashMode = Camera.Parameters.FLASH_MODE_OFF; + + /** The camera object */ + private Camera mCamera = null; + + private MediaRecorder mRecorder = null; + + /** The ID of the currently used camera */ + int mCamId = 0; + + /** The platform-dependent camera frame handler. */ + private CamFrameHandler mCamFrameHandler = null; + + /** The set of camera listeners. */ + private Set<CameraListener> mCamListeners = new HashSet<CameraListener>(); + + private ReentrantLock mCameraReadyLock = new ReentrantLock(true); + // mCameraReady condition is used when waiting for the camera getting ready. + private Condition mCameraReady = mCameraReadyLock.newCondition(); + // external camera lock used to provide the capability of external camera access. + private ExternalCameraLock mExternalCameraLock = new ExternalCameraLock(); + + private RenderTarget mRenderTarget; + private MffContext mContext; + + /** + * This provides the capability of locking and unlocking from different threads. + * The thread will wait until the lock state is idle. Any thread can wake up + * a waiting thread by calling unlock (i.e. signal), provided that unlock + * are called using the same context when lock was called. Using context prevents + * from rogue usage of unlock. + */ + private class ExternalCameraLock { + public static final int IDLE = 0; + public static final int IN_USE = 1; + private int mLockState = IDLE; + private Object mLockContext; + private final ReentrantLock mLock = new ReentrantLock(true); + private final Condition mInUseLockCondition= mLock.newCondition(); + + public boolean lock(Object context) { + if (context == null) { + throw new RuntimeException("Null context when locking"); + } + mLock.lock(); + if (mLockState == IN_USE) { + try { + mInUseLockCondition.await(); + } catch (InterruptedException e) { + return false; + } + } + mLockState = IN_USE; + mLockContext = context; + mLock.unlock(); + return true; + } + + public void unlock(Object context) { + mLock.lock(); + if (mLockState != IN_USE) { + throw new RuntimeException("Not in IN_USE state"); + } + if (context != mLockContext) { + throw new RuntimeException("Lock is not owned by this context"); + } + mLockState = IDLE; + mLockContext = null; + mInUseLockCondition.signal(); + mLock.unlock(); + } + } + + public CameraRunnable(MffContext context) { + mContext = context; + createCamFrameHandler(); + mCamFrameHandler.initWithRunner(this); + launchThread(); + } + + public MffContext getContext() { + return mContext; + } + + public void loop() { + while (true) { + try { + Event event = nextEvent(); + if (event == null) continue; + switch (event.code) { + case Event.START: + onStart(); + break; + case Event.STOP: + onStop(); + break; + case Event.FRAME: + onFrame(); + break; + case Event.HALT: + onHalt(); + break; + case Event.RESTART: + onRestart(); + break; + case Event.UPDATE: + onUpdate(); + break; + case Event.TEARDOWN: + onTearDown(); + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void run() { + loop(); + } + + public void signalNewFrame() { + pushEvent(Event.FRAME, false); + } + + public void pushEvent(int eventId, boolean required) { + try { + if (required) { + mEventQueue.put(new Event(eventId)); + } else { + mEventQueue.offer(new Event(eventId)); + } + } catch (InterruptedException e) { + // We should never get here (as we do not limit capacity in the queue), but if + // we do, we log an error. + Log.e("CameraStreamer", "Dropping event " + eventId + "!"); + } + } + + public void launchThread() { + Thread cameraThread = new Thread(this); + cameraThread.start(); + } + + @Deprecated + public Camera getCamera() { + synchronized (mState) { + return mCamera; + } + } + + public Camera lockCamera(Object context) { + mExternalCameraLock.lock(context); + /** + * since lockCamera can happen right after closeCamera, + * the camera handle can be null, wait until valid handle + * is acquired. + */ + while (mCamera == null) { + mExternalCameraLock.unlock(context); + mCameraReadyLock.lock(); + try { + mCameraReady.await(); + } catch (InterruptedException e) { + throw new RuntimeException("Condition interrupted", e); + } + mCameraReadyLock.unlock(); + mExternalCameraLock.lock(context); + } + return mCamera; + } + + public void unlockCamera(Object context) { + mExternalCameraLock.unlock(context); + } + + public int getCurrentCameraId() { + synchronized (mState) { + return mCamId; + } + } + + public boolean isRunning() { + return mState.current() != State.STATE_STOPPED; + } + + public void addListener(CameraListener listener) { + synchronized (mCamListeners) { + mCamListeners.add(listener); + } + } + + public void removeListener(CameraListener listener) { + synchronized (mCamListeners) { + mCamListeners.remove(listener); + } + } + + public synchronized void bindToDisplay(Display display) { + mDisplay = display; + } + + public synchronized void setDesiredPreviewSize(int width, int height) { + if (width != mRequestedPreviewWidth || height != mRequestedPreviewHeight) { + mRequestedPreviewWidth = width; + mRequestedPreviewHeight = height; + onParamsUpdated(); + } + } + + public synchronized void setDesiredPictureSize(int width, int height) { + if (width != mRequestedPictureWidth || height != mRequestedPictureHeight) { + mRequestedPictureWidth = width; + mRequestedPictureHeight = height; + onParamsUpdated(); + } + } + + public synchronized void setDesiredFrameRate(int fps) { + if (fps != mRequestedFramesPerSec) { + mRequestedFramesPerSec = fps; + onParamsUpdated(); + } + } + + public synchronized void setFacing(int facing) { + if (facing != mRequestedFacing) { + switch (facing) { + case FACING_DONTCARE: + case FACING_FRONT: + case FACING_BACK: + mRequestedFacing = facing; + break; + default: + throw new IllegalArgumentException("Unknown facing value '" + facing + + "' passed to setFacing!"); + } + onParamsUpdated(); + } + } + + public synchronized void setFlipFrontCamera(boolean flipFront) { + if (mFlipFront != flipFront) { + mFlipFront = flipFront; + } + } + + public synchronized void setFlashMode(String flashMode) { + if (!flashMode.equals(mFlashMode)) { + mFlashMode = flashMode; + onParamsUpdated(); + } + } + + public synchronized int getCameraFacing() { + return mActualFacing; + } + + public synchronized int getCameraRotation() { + return mCamRotation; + } + + public synchronized boolean supportsHardwareFaceDetection() { + //return mCamFrameHandler.supportsHardwareFaceDetection(); + // TODO + return true; + } + + public synchronized int getCameraWidth() { + return (mActualDims != null) ? mActualDims[0] : 0; + } + + public synchronized int getCameraHeight() { + return (mActualDims != null) ? mActualDims[1] : 0; + } + + public synchronized int getCameraFrameRate() { + return mActualFramesPerSec; + } + + public synchronized String getFlashMode() { + return mCamera.getParameters().getFlashMode(); + } + + public synchronized boolean canStart() { + // If we can get a camera id without error we should be able to start. + try { + getCameraId(); + } catch (RuntimeException e) { + return false; + } + return true; + } + + public boolean grabFrame(FrameImage2D targetFrame) { + // Make sure we stay in state running while we are grabbing the frame. + synchronized (mState) { + if (mState.current() != State.STATE_RUNNING) { + return false; + } + // we may not have the camera ready, this might happen when in the middle + // of switching camera. + if (mCamera == null) { + return false; + } + mCamFrameHandler.grabFrame(targetFrame); + return true; + } + } + + public CamFrameHandler getCamFrameHandler() { + return mCamFrameHandler; + } + + private void onParamsUpdated() { + pushEvent(Event.UPDATE, true); + } + + private Event nextEvent() { + try { + return mEventQueue.take(); + } catch (InterruptedException e) { + // Ignore and keep going. + Log.w("GraphRunner", "Event queue processing was interrupted."); + return null; + } + } + + private void onStart() { + if (mState.current() == State.STATE_STOPPED) { + mState.set(State.STATE_RUNNING); + getRenderTarget().focus(); + openCamera(); + } + } + + private void onStop() { + if (mState.current() == State.STATE_RUNNING) { + closeCamera(); + RenderTarget.focusNone(); + } + // Set state to stop (halted becomes stopped). + mState.set(State.STATE_STOPPED); + } + + private void onHalt() { + // Only halt if running. Stopped overrides halt. + if (mState.current() == State.STATE_RUNNING) { + closeCamera(); + RenderTarget.focusNone(); + mState.set(State.STATE_HALTED); + } + } + + private void onRestart() { + // Only restart if halted + if (mState.current() == State.STATE_HALTED) { + mState.set(State.STATE_RUNNING); + getRenderTarget().focus(); + openCamera(); + } + } + + private void onUpdate() { + if (mState.current() == State.STATE_RUNNING) { + pushEvent(Event.STOP, true); + pushEvent(Event.START, true); + } + } + private void onFrame() { + if (mState.current() == State.STATE_RUNNING) { + updateRotation(); + mCamFrameHandler.updateServerFrame(); + } + } + + private void onTearDown() { + if (mState.current() == State.STATE_STOPPED) { + // Remove all listeners. This will release their resources + for (CameraListener listener : mCamListeners) { + removeListener(listener); + } + mCamListeners.clear(); + } else { + Log.e("CameraStreamer", "Could not tear-down CameraStreamer as camera still " + + "seems to be running!"); + } + } + + private void createCamFrameHandler() { + // TODO: For now we simply assert that OpenGL is supported. Later on, we should add + // a CamFrameHandler that does not depend on OpenGL. + getContext().assertOpenGLSupported(); + if (VERSION.SDK_INT >= 16) { + mCamFrameHandler = new CamFrameHandlerJB(); + } else if (VERSION.SDK_INT >= 15) { + mCamFrameHandler = new CamFrameHandlerICS(); + } else { + mCamFrameHandler = new CamFrameHandlerGB(); + } + } + + private void updateRotation() { + if (mDisplay != null) { + updateDisplayRotation(mDisplay.getRotation()); + } + } + + private synchronized void updateDisplayRotation(int rotation) { + switch (rotation) { + case Surface.ROTATION_0: + onUpdateOrientation(0); + break; + case Surface.ROTATION_90: + onUpdateOrientation(90); + break; + case Surface.ROTATION_180: + onUpdateOrientation(180); + break; + case Surface.ROTATION_270: + onUpdateOrientation(270); + break; + default: + throw new IllegalArgumentException("Unsupported display rotation constant! Use " + + "one of the Surface.ROTATION_ constants!"); + } + } + + private RenderTarget getRenderTarget() { + if (mRenderTarget == null) { + mRenderTarget = RenderTarget.newTarget(1, 1); + } + return mRenderTarget; + } + + private void updateCamera() { + synchronized (mState) { + mCamId = getCameraId(); + updateCameraOrientation(mCamId); + mCamera = Camera.open(mCamId); + initCameraParameters(); + } + } + + private void updateCameraOrientation(int camId) { + CameraInfo cameraInfo = new CameraInfo(); + Camera.getCameraInfo(camId, cameraInfo); + mCamOrientation = cameraInfo.orientation; + mOrientation = -1; // Forces recalculation to match display + mActualFacing = (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) + ? FACING_FRONT + : FACING_BACK; + } + + private int getCameraId() { + int camCount = Camera.getNumberOfCameras(); + if (camCount == 0) { + throw new RuntimeException("Device does not have any cameras!"); + } else if (mRequestedFacing == FACING_DONTCARE) { + // Simply return first camera if mRequestedFacing is don't care + return 0; + } + + // Attempt to find requested camera + boolean useFrontCam = (mRequestedFacing == FACING_FRONT); + CameraInfo cameraInfo = new CameraInfo(); + for (int i = 0; i < camCount; ++i) { + Camera.getCameraInfo(i, cameraInfo); + if ((cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) == useFrontCam) { + return i; + } + } + throw new RuntimeException("Could not find a camera facing (" + mRequestedFacing + + ")!"); + } + + private void initCameraParameters() { + Camera.Parameters params = mCamera.getParameters(); + + // Find closest preview size + mActualDims = + findClosestPreviewSize(mRequestedPreviewWidth, mRequestedPreviewHeight, params); + mCamFrameHandler.setCameraSize(mActualDims[0], mActualDims[1]); + params.setPreviewSize(mActualDims[0], mActualDims[1]); + // Find closest picture size + int[] dims = + findClosestPictureSize(mRequestedPictureWidth, mRequestedPictureHeight, params); + params.setPictureSize(dims[0], dims[1]); + + // Find closest FPS + int closestRange[] = findClosestFpsRange(mRequestedFramesPerSec, params); + params.setPreviewFpsRange(closestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], + closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); + + // Set flash mode (if supported) + if (params.getFlashMode() != null) { + params.setFlashMode(mFlashMode); + } + + mCamera.setParameters(params); + } + + private int[] findClosestPreviewSize(int width, int height, Camera.Parameters parameters) { + List<Camera.Size> previewSizes = parameters.getSupportedPreviewSizes(); + return findClosestSizeFromList(width, height, previewSizes); + } + + private int[] findClosestPictureSize(int width, int height, Camera.Parameters parameters) { + List<Camera.Size> pictureSizes = parameters.getSupportedPictureSizes(); + return findClosestSizeFromList(width, height, pictureSizes); + } + + private int[] findClosestSizeFromList(int width, int height, List<Camera.Size> sizes) { + int closestWidth = -1; + int closestHeight = -1; + int smallestWidth = sizes.get(0).width; + int smallestHeight = sizes.get(0).height; + for (Camera.Size size : sizes) { + // Best match defined as not being larger in either dimension than + // the requested size, but as close as possible. The below isn't a + // stable selection (reording the size list can give different + // results), but since this is a fallback nicety, that's acceptable. + if ( size.width <= width && + size.height <= height && + size.width >= closestWidth && + size.height >= closestHeight) { + closestWidth = size.width; + closestHeight = size.height; + } + if ( size.width < smallestWidth && + size.height < smallestHeight) { + smallestWidth = size.width; + smallestHeight = size.height; + } + } + if (closestWidth == -1) { + // Requested size is smaller than any listed size; match with smallest possible + closestWidth = smallestWidth; + closestHeight = smallestHeight; + } + int[] closestSize = {closestWidth, closestHeight}; + return closestSize; + } + + private int[] findClosestFpsRange(int fps, Camera.Parameters params) { + List<int[]> supportedFpsRanges = params.getSupportedPreviewFpsRange(); + int[] closestRange = supportedFpsRanges.get(0); + int fpsk = fps * 1000; + int minDiff = 1000000; + for (int[] range : supportedFpsRanges) { + int low = range[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; + int high = range[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; + if (low <= fpsk && high >= fpsk) { + int diff = (fpsk - low) + (high - fpsk); + if (diff < minDiff) { + closestRange = range; + minDiff = diff; + } + } + } + mActualFramesPerSec = closestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] / 1000; + return closestRange; + } + + private void onUpdateOrientation(int orientation) { + // First we calculate the camera rotation. + int rotation = (mActualFacing == FACING_FRONT) + ? (mCamOrientation + orientation) % 360 + : (mCamOrientation - orientation + 360) % 360; + if (rotation != mCamRotation) { + synchronized (this) { + mCamRotation = rotation; + } + } + + // We compensate for mirroring in the orientation. This differs from the rotation, + // where we are invariant to mirroring. + int fixedOrientation = rotation; + if (mActualFacing == FACING_FRONT && mCamFrameHandler.isFrontMirrored()) { + fixedOrientation = (360 - rotation) % 360; // compensate the mirror + } + if (mOrientation != fixedOrientation) { + mOrientation = fixedOrientation; + mCamFrameHandler.onUpdateCameraOrientation(mOrientation); + } + } + + private void openCamera() { + // Acquire lock for camera + try { + if (!mCameraLock.tryLock(MAX_CAMERA_WAIT_TIME, TimeUnit.SECONDS)) { + throw new RuntimeException("Timed out while waiting to acquire camera!"); + } + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted while waiting to acquire camera!"); + } + + // Make sure external entities are not holding camera. We need to hold the lock until + // the preview is started again. + Object lockContext = new Object(); + mExternalCameraLock.lock(lockContext); + + // Need to synchronize this as many of the member values are modified during setup. + synchronized (this) { + updateCamera(); + updateRotation(); + mCamFrameHandler.setupServerFrame(); + } + + mCamera.startPreview(); + + // Inform listeners + synchronized (mCamListeners) { + for (CameraListener listener : mCamListeners) { + listener.onCameraOpened(CameraStreamer.this); + } + } + mExternalCameraLock.unlock(lockContext); + // New camera started + mCameraReadyLock.lock(); + mCameraReady.signal(); + mCameraReadyLock.unlock(); + } + + /** + * Creates an instance of MediaRecorder to be used for the streamer. + * User should call the functions in the following sequence:<p> + * {@link #createRecorder}<p> + * {@link #startRecording}<p> + * {@link #stopRecording}<p> + * {@link #releaseRecorder}<p> + * @param outputPath the output video path for the recorder + * @param profile the recording {@link CamcorderProfile} which has parameters indicating + * the resolution, quality etc. + */ + public void createRecorder(String outputPath, CamcorderProfile profile) { + lockCamera(this); + mCamera.unlock(); + if (mRecorder != null) { + mRecorder.release(); + } + mRecorder = new MediaRecorder(); + mRecorder.setCamera(mCamera); + mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); + mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); + mRecorder.setProfile(profile); + mRecorder.setOutputFile(outputPath); + try { + mRecorder.prepare(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Starts recording video using the created MediaRecorder object + */ + public void startRecording() { + if (mRecorder == null) { + throw new RuntimeException("No recorder created"); + } + mRecorder.start(); + } + + /** + * Stops recording video + */ + public void stopRecording() { + if (mRecorder == null) { + throw new RuntimeException("No recorder created"); + } + mRecorder.stop(); + } + + /** + * Release the resources held by the MediaRecorder, call this after done recording. + */ + public void releaseRecorder() { + if (mRecorder == null) { + throw new RuntimeException("No recorder created"); + } + mRecorder.release(); + mRecorder = null; + mCamera.lock(); + unlockCamera(this); + } + + private void closeCamera() { + Object lockContext = new Object(); + mExternalCameraLock.lock(lockContext); + if (mCamera != null) { + mCamera.stopPreview(); + mCamera.release(); + mCamera = null; + } + mCameraLock.unlock(); + mCamFrameHandler.release(); + mExternalCameraLock.unlock(lockContext); + // Inform listeners + synchronized (mCamListeners) { + for (CameraListener listener : mCamListeners) { + listener.onCameraClosed(CameraStreamer.this); + } + } + } + + } + + /** + * The frame-client callback interface. + * FrameClients, that wish to receive Frames from the camera must implement this callback + * method. + * Note, that this method is called on the Camera server thread. However, the + * {@code getLatestFrame()} method must be called from the client thread. + */ + public static interface FrameClient { + public void onCameraFrameAvailable(); + } + + /** + * The CameraListener callback interface. + * This interface allows observers to monitor the CameraStreamer and respond to stream open + * and close events. + */ + public static interface CameraListener { + /** + * Called when the camera is opened and begins producing frames. + * This is also called when settings have changed that caused the camera to be reopened. + */ + public void onCameraOpened(CameraStreamer camera); + + /** + * Called when the camera is closed and stops producing frames. + */ + public void onCameraClosed(CameraStreamer camera); + } + + /** + * Manually update the display rotation. + * You do not need to call this, if the camera is bound to a display, or your app does not + * support multiple orientations. + */ + public void updateDisplayRotation(int rotation) { + mCameraRunner.updateDisplayRotation(rotation); + } + + /** + * Bind the camera to your Activity's display. + * Use this, if your Activity supports multiple display orientation, and you would like the + * camera to update accordingly when the orientation is changed. + */ + public void bindToDisplay(Display display) { + mCameraRunner.bindToDisplay(display); + } + + /** + * Sets the desired preview size. + * Note that the actual width and height may vary. + * + * @param width The desired width of the preview camera stream. + * @param height The desired height of the preview camera stream. + */ + public void setDesiredPreviewSize(int width, int height) { + mCameraRunner.setDesiredPreviewSize(width, height); + } + + /** + * Sets the desired picture size. + * Note that the actual width and height may vary. + * + * @param width The desired picture width. + * @param height The desired picture height. + */ + public void setDesiredPictureSize(int width, int height) { + mCameraRunner.setDesiredPictureSize(width, height); + } + + /** + * Sets the desired camera frame-rate. + * Note, that the actual frame-rate may vary. + * + * @param fps The desired FPS. + */ + public void setDesiredFrameRate(int fps) { + mCameraRunner.setDesiredFrameRate(fps); + } + + /** + * Sets the camera facing direction. + * + * Specify {@code FACING_DONTCARE} (default) if you would like the CameraStreamer to choose + * the direction. When specifying any other direction be sure to first check whether the + * device supports the desired facing. + * + * @param facing The desired camera facing direction. + */ + public void setFacing(int facing) { + mCameraRunner.setFacing(facing); + } + + /** + * Set whether to flip the camera image horizontally when using the front facing camera. + */ + public void setFlipFrontCamera(boolean flipFront) { + mCameraRunner.setFlipFrontCamera(flipFront); + } + + /** + * Sets the camera flash mode. + * + * This must be one of the String constants defined in the Camera.Parameters class. + * + * @param flashMode A String constant specifying the flash mode. + */ + public void setFlashMode(String flashMode) { + mCameraRunner.setFlashMode(flashMode); + } + + /** + * Returns the current flash mode. + * + * This returns the currently running camera's flash-mode, or NULL if flash modes are not + * supported on that camera. + * + * @return The flash mode String, or NULL if flash modes are not supported. + */ + public String getFlashMode() { + return mCameraRunner.getFlashMode(); + } + + /** + * Get the actual camera facing. + * Returns 0 if actual facing is not yet known. + */ + public int getCameraFacing() { + return mCameraRunner.getCameraFacing(); + } + + /** + * Get the current camera rotation. + * + * Use this rotation if you want to snap pictures from the camera and need to rotate the + * picture to be up-right. + * + * @return the current camera rotation. + */ + public int getCameraRotation() { + return mCameraRunner.getCameraRotation(); + } + + /** + * Specifies whether or not the camera supports hardware face detection. + * @return true, if the camera supports hardware face detection. + */ + public boolean supportsHardwareFaceDetection() { + return mCameraRunner.supportsHardwareFaceDetection(); + } + + /** + * Returns the camera facing that is chosen when DONT_CARE is specified. + * Returns 0 if neither a front nor back camera could be found. + */ + public static int getDefaultFacing() { + int camCount = Camera.getNumberOfCameras(); + if (camCount == 0) { + return 0; + } else { + CameraInfo cameraInfo = new CameraInfo(); + Camera.getCameraInfo(0, cameraInfo); + return (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) + ? FACING_FRONT + : FACING_BACK; + } + } + + /** + * Get the actual camera width. + * Returns 0 if actual width is not yet known. + */ + public int getCameraWidth() { + return mCameraRunner.getCameraWidth(); + } + + /** + * Get the actual camera height. + * Returns 0 if actual height is not yet known. + */ + public int getCameraHeight() { + return mCameraRunner.getCameraHeight(); + } + + /** + * Get the actual camera frame-rate. + * Returns 0 if actual frame-rate is not yet known. + */ + public int getCameraFrameRate() { + return mCameraRunner.getCameraFrameRate(); + } + + /** + * Returns true if the camera can be started at this point. + */ + public boolean canStart() { + return mCameraRunner.canStart(); + } + + /** + * Returns true if the camera is currently running. + */ + public boolean isRunning() { + return mCameraRunner.isRunning(); + } + + /** + * Starts the camera. + */ + public void start() { + mCameraRunner.pushEvent(Event.START, true); + } + + /** + * Stops the camera. + */ + public void stop() { + mCameraRunner.pushEvent(Event.STOP, true); + } + + /** + * Stops the camera and waits until it is completely closed. Generally, this should not be + * called in the UI thread, but may be necessary if you need the camera to be closed before + * performing subsequent steps. + */ + public void stopAndWait() { + mCameraRunner.pushEvent(Event.STOP, true); + try { + if (!mCameraLock.tryLock(MAX_CAMERA_WAIT_TIME, TimeUnit.SECONDS)) { + Log.w("CameraStreamer", "Time-out waiting for camera to close!"); + } + } catch (InterruptedException e) { + Log.w("CameraStreamer", "Interrupted while waiting for camera to close!"); + } + mCameraLock.unlock(); + } + + /** + * Registers a listener to handle camera state changes. + */ + public void addListener(CameraListener listener) { + mCameraRunner.addListener(listener); + } + + /** + * Unregisters a listener to handle camera state changes. + */ + public void removeListener(CameraListener listener) { + mCameraRunner.removeListener(listener); + } + + /** + * Registers the frame-client with the camera. + * This MUST be called from the client thread! + */ + public void registerClient(FrameClient client) { + mCameraRunner.getCamFrameHandler().registerClient(client); + } + + /** + * Unregisters the frame-client with the camera. + * This MUST be called from the client thread! + */ + public void unregisterClient(FrameClient client) { + mCameraRunner.getCamFrameHandler().unregisterClient(client); + } + + /** + * Gets the latest camera frame for the client. + * + * This must be called from the same thread as the {@link #registerClient(FrameClient)} call! + * The frame passed in will be resized by the camera streamer to fit the camera frame. + * Returns false if the frame could not be grabbed. This may happen if the camera has been + * closed in the meantime, and its resources let go. + * + * @return true, if the frame was grabbed successfully. + */ + public boolean getLatestFrame(FrameImage2D targetFrame) { + return mCameraRunner.grabFrame(targetFrame); + } + + /** + * Expose the underlying android.hardware.Camera object. + * Use the returned object with care: some camera functions may break the functionality + * of CameraStreamer. + * @return the Camera object. + */ + @Deprecated + public Camera getCamera() { + return mCameraRunner.getCamera(); + } + + /** + * Obtain access to the underlying android.hardware.Camera object. + * This grants temporary access to the internal Camera handle. Once you are done using the + * handle you must call {@link #unlockCamera(Object)}. While you are holding the Camera, + * it will not be modified or released by the CameraStreamer. The Camera object return is + * guaranteed to have the preview running. + * + * The CameraStreamer does not account for changes you make to the Camera. That is, if you + * change the Camera unexpectedly this may cause unintended behavior by the streamer. + * + * Note that the returned object may be null. This can happen when the CameraStreamer is not + * running, or is just transitioning to another Camera, such as during a switch from front to + * back Camera. + * @param context an object used as a context for locking and unlocking. lockCamera and + * unlockCamera should use the same context object. + * @return The Camera object. + */ + public Camera lockCamera(Object context) { + return mCameraRunner.lockCamera(context); + } + + /** + * Release the acquire Camera object. + * @param context the context object that used when lockCamera is called. + */ + public void unlockCamera(Object context) { + mCameraRunner.unlockCamera(context); + } + + /** + * Creates an instance of MediaRecorder to be used for the streamer. + * User should call the functions in the following sequence:<p> + * {@link #createRecorder}<p> + * {@link #startRecording}<p> + * {@link #stopRecording}<p> + * {@link #releaseRecorder}<p> + * @param path the output video path for the recorder + * @param profile the recording {@link CamcorderProfile} which has parameters indicating + * the resolution, quality etc. + */ + public void createRecorder(String path, CamcorderProfile profile) { + mCameraRunner.createRecorder(path, profile); + } + + public void releaseRecorder() { + mCameraRunner.releaseRecorder(); + } + + public void startRecording() { + mCameraRunner.startRecording(); + } + + public void stopRecording() { + mCameraRunner.stopRecording(); + } + + /** + * Retrieve the ID of the currently used camera. + * @return the ID of the currently used camera. + */ + public int getCameraId() { + return mCameraRunner.getCurrentCameraId(); + } + + /** + * @return The number of cameras available for streaming on this device. + */ + public static int getNumberOfCameras() { + // Currently, this is just the number of cameras that are available on the device. + return Camera.getNumberOfCameras(); + } + + CameraStreamer(MffContext context) { + mCameraRunner = new CameraRunnable(context); + } + + /** Halt is like stop, but may be resumed using restart(). */ + void halt() { + mCameraRunner.pushEvent(Event.HALT, true); + } + + /** Restart starts the camera only if previously halted. */ + void restart() { + mCameraRunner.pushEvent(Event.RESTART, true); + } + + static boolean requireDummySurfaceView() { + return VERSION.SDK_INT < 15; + } + + void tearDown() { + mCameraRunner.pushEvent(Event.TEARDOWN, true); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorSpace.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorSpace.java new file mode 100644 index 0000000..f2bfe08 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorSpace.java @@ -0,0 +1,137 @@ +/* + * Copyright 2013 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 androidx.media.filterfw; + +import java.nio.ByteBuffer; + +/** + * Utility functions to convert between color-spaces. + * + * Currently these methods are all CPU based native methods. These could be updated in the future + * to provide other implementations. + */ +public class ColorSpace { + + /** + * Convert YUV420-Planer data to RGBA8888. + * + * The input data is expected to be laid out in 3 planes. The width x height Y plane, followed + * by the U and V planes, where each chroma value corresponds to a 2x2 luminance value block. + * YUV to RGB conversion is done using the ITU-R BT.601 transformation. The output buffer must + * be large enough to hold the data, and the dimensions must be multiples of 2. + * + * @param input data encoded in YUV420-Planar. + * @param output buffer to hold RGBA8888 data. + * @param width the width of the image (must be a multiple of 2) + * @param height the height of the image (must be a multiple of 2) + */ + public static void convertYuv420pToRgba8888( + ByteBuffer input, ByteBuffer output, int width, int height) { + expectInputSize(input, (3 * width * height) / 2); + expectOutputSize(output, width * height * 4); + nativeYuv420pToRgba8888(input, output, width, height); + } + + /** + * Convert ARGB8888 to RGBA8888. + * + * The input data is expected to be encoded in 8-bit interleaved ARGB channels. The output + * buffer must be large enough to hold the data. The output buffer may be the same as the + * input buffer. + * + * @param input data encoded in ARGB8888. + * @param output buffer to hold RGBA8888 data. + * @param width the width of the image + * @param height the height of the image + */ + public static void convertArgb8888ToRgba8888( + ByteBuffer input, ByteBuffer output, int width, int height) { + expectInputSize(input, width * height * 4); + expectOutputSize(output, width * height * 4); + nativeArgb8888ToRgba8888(input, output, width, height); + } + + /** + * Convert RGBA8888 to HSVA8888. + * + * The input data is expected to be encoded in 8-bit interleaved RGBA channels. The output + * buffer must be large enough to hold the data. The output buffer may be the same as the + * input buffer. + * + * @param input data encoded in RGBA8888. + * @param output buffer to hold HSVA8888 data. + * @param width the width of the image + * @param height the height of the image + */ + public static void convertRgba8888ToHsva8888( + ByteBuffer input, ByteBuffer output, int width, int height) { + expectInputSize(input, width * height * 4); + expectOutputSize(output, width * height * 4); + nativeRgba8888ToHsva8888(input, output, width, height); + } + + /** + * Convert RGBA8888 to YCbCrA8888. + * + * The input data is expected to be encoded in 8-bit interleaved RGBA channels. The output + * buffer must be large enough to hold the data. The output buffer may be the same as the + * input buffer. + * + * @param input data encoded in RGBA8888. + * @param output buffer to hold YCbCrA8888 data. + * @param width the width of the image + * @param height the height of the image + */ + public static void convertRgba8888ToYcbcra8888( + ByteBuffer input, ByteBuffer output, int width, int height) { + expectInputSize(input, width * height * 4); + expectOutputSize(output, width * height * 4); + nativeRgba8888ToYcbcra8888(input, output, width, height); + } + + private static void expectInputSize(ByteBuffer input, int expectedSize) { + if (input.remaining() < expectedSize) { + throw new IllegalArgumentException("Input buffer's size does not fit given width " + + "and height! Expected: " + expectedSize + ", Got: " + input.remaining() + + "."); + } + } + + private static void expectOutputSize(ByteBuffer output, int expectedSize) { + if (output.remaining() < expectedSize) { + throw new IllegalArgumentException("Output buffer's size does not fit given width " + + "and height! Expected: " + expectedSize + ", Got: " + output.remaining() + + "."); + } + } + + private static native void nativeYuv420pToRgba8888( + ByteBuffer input, ByteBuffer output, int width, int height); + + private static native void nativeArgb8888ToRgba8888( + ByteBuffer input, ByteBuffer output, int width, int height); + + private static native void nativeRgba8888ToHsva8888( + ByteBuffer input, ByteBuffer output, int width, int height); + + private static native void nativeRgba8888ToYcbcra8888( + ByteBuffer input, ByteBuffer output, int width, int height); + + static { + System.loadLibrary("smartcamera_jni"); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorfulnessFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorfulnessFilter.java new file mode 100644 index 0000000..5bdf4af --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ColorfulnessFilter.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2012 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. + */ + +// Extract histogram from image. + +package androidx.media.filterpacks.colorspace; + +import androidx.media.filterfw.FrameValue; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameBuffer2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * ColorfulnessFilter takes in a particular Chroma histogram generated by NewChromaHistogramFilter + * and compute the colorfulness based on the entropy in Hue space. + */ +public final class ColorfulnessFilter extends Filter { + + public ColorfulnessFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType dataIn = FrameType.buffer2D(FrameType.ELEMENT_FLOAT32); + return new Signature() + .addInputPort("histogram", Signature.PORT_REQUIRED, dataIn) + .addOutputPort("score", Signature.PORT_REQUIRED, FrameType.single(float.class)) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + FrameBuffer2D histogramFrame = + getConnectedInputPort("histogram").pullFrame().asFrameBuffer2D(); + ByteBuffer byteBuffer = histogramFrame.lockBytes(Frame.MODE_READ); + byteBuffer.order(ByteOrder.nativeOrder()); + FloatBuffer histogramBuffer = byteBuffer.asFloatBuffer(); + histogramBuffer.rewind(); + + // Create a hue histogram from hue-saturation histogram + int hueBins = histogramFrame.getWidth(); + int saturationBins = histogramFrame.getHeight() - 1; + float[] hueHistogram = new float[hueBins]; + float total = 0; + for (int r = 0; r < saturationBins; ++r) { + float weight = (float) Math.pow(2, r); + for (int c = 0; c < hueBins; c++) { + float value = histogramBuffer.get() * weight; + hueHistogram[c] += value; + total += value; + } + } + float colorful = 0f; + for (int c = 0; c < hueBins; ++c) { + float value = hueHistogram[c] / total; + if (value > 0f) { + colorful -= value * ((float) Math.log(value)); + } + } + + colorful /= Math.log(2); + + histogramFrame.unlock(); + OutputPort outPort = getConnectedOutputPort("score"); + FrameValue frameValue = outPort.fetchAvailableFrame(null).asFrameValue(); + frameValue.setValue(colorful); + outPort.pushFrame(frameValue); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CropFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CropFilter.java new file mode 100644 index 0000000..91fe21c --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/CropFilter.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.transform; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.util.FloatMath; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; +import androidx.media.filterfw.geometry.Quad; + +public class CropFilter extends Filter { + + private Quad mCropRect = Quad.fromRect(0f, 0f, 1f, 1f); + private int mOutputWidth = 0; + private int mOutputHeight = 0; + private ImageShader mShader; + private boolean mUseMipmaps = false; + private FrameImage2D mPow2Frame = null; + + public CropFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addInputPort("cropRect", Signature.PORT_REQUIRED, FrameType.single(Quad.class)) + .addInputPort("outputWidth", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addInputPort("outputHeight", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addInputPort("useMipmaps", Signature.PORT_OPTIONAL, FrameType.single(boolean.class)) + .addOutputPort("image", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("cropRect")) { + port.bindToFieldNamed("mCropRect"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("outputWidth")) { + port.bindToFieldNamed("mOutputWidth"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("outputHeight")) { + port.bindToFieldNamed("mOutputHeight"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("useMipmaps")) { + port.bindToFieldNamed("mUseMipmaps"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected void onPrepare() { + if (isOpenGLSupported()) { + mShader = ImageShader.createIdentity(); + } + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("image"); + + // Pull input frame + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + int[] inDims = inputImage.getDimensions(); + int[] croppedDims = { (int)FloatMath.ceil(mCropRect.xEdge().length() * inDims[0]), + (int)FloatMath.ceil(mCropRect.yEdge().length() * inDims[1]) }; + int[] outDims = { getOutputWidth(croppedDims[0], croppedDims[1]), + getOutputHeight(croppedDims[0], croppedDims[1]) }; + FrameImage2D outputImage = outPort.fetchAvailableFrame(outDims).asFrameImage2D(); + + if (isOpenGLSupported()) { + FrameImage2D sourceFrame; + Quad sourceQuad = null; + boolean scaleDown = (outDims[0] < croppedDims[0]) || (outDims[1] < croppedDims[1]); + if (scaleDown && mUseMipmaps) { + mPow2Frame = TransformUtils.makeMipMappedFrame(mPow2Frame, croppedDims); + int[] extDims = mPow2Frame.getDimensions(); + float targetWidth = croppedDims[0] / (float)extDims[0]; + float targetHeight = croppedDims[1] / (float)extDims[1]; + Quad targetQuad = Quad.fromRect(0f, 0f, targetWidth, targetHeight); + mShader.setSourceQuad(mCropRect); + mShader.setTargetQuad(targetQuad); + mShader.process(inputImage, mPow2Frame); + TransformUtils.generateMipMaps(mPow2Frame); + sourceFrame = mPow2Frame; + sourceQuad = targetQuad; + } else { + sourceFrame = inputImage; + sourceQuad = mCropRect; + } + + mShader.setSourceQuad(sourceQuad); + mShader.setTargetRect(0f, 0f, 1f, 1f); + mShader.process(sourceFrame, outputImage); + } else { + // Convert quads to canvas coordinate space + Quad sourceQuad = mCropRect.scale2(inDims[0], inDims[1]); + Quad targetQuad = Quad.fromRect(0f, 0f, inDims[0], inDims[1]); + + // Calculate transform for crop + Matrix transform = Quad.getTransform(sourceQuad, targetQuad); + transform.postScale(outDims[0] / (float)inDims[0], outDims[1] / (float)inDims[1]); + + // Create target canvas + Bitmap.Config config = Bitmap.Config.ARGB_8888; + Bitmap cropped = Bitmap.createBitmap(outDims[0], outDims[1], config); + Canvas canvas = new Canvas(cropped); + + // Draw source bitmap into target canvas + Paint paint = new Paint(); + paint.setFilterBitmap(true); + Bitmap sourceBitmap = inputImage.toBitmap(); + canvas.drawBitmap(sourceBitmap, transform, paint); + + // Assign bitmap to output frame + outputImage.setBitmap(cropped); + } + + outPort.pushFrame(outputImage); + } + + @Override + protected void onClose() { + if (mPow2Frame != null){ + mPow2Frame.release(); + mPow2Frame = null; + } + } + + protected int getOutputWidth(int inWidth, int inHeight) { + return mOutputWidth <= 0 ? inWidth : mOutputWidth; + } + + protected int getOutputHeight(int inWidth, int inHeight) { + return mOutputHeight <= 0 ? inHeight : mOutputHeight; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Filter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Filter.java new file mode 100644 index 0000000..9e2eb92 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Filter.java @@ -0,0 +1,766 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.os.SystemClock; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Filters are the processing nodes of the filter graphs. + * + * Filters may have any number of input and output ports, through which the data frames flow. + * TODO: More documentation on filter life-cycle, port and type checking, GL and RenderScript, ... + */ +public abstract class Filter { + + private static class State { + private static final int STATE_UNPREPARED = 1; + private static final int STATE_PREPARED = 2; + private static final int STATE_OPEN = 3; + private static final int STATE_CLOSED = 4; + private static final int STATE_DESTROYED = 5; + + public int current = STATE_UNPREPARED; + + public synchronized boolean check(int state) { + return current == state; + } + + } + + private final int REQUEST_FLAG_NONE = 0; + private final int REQUEST_FLAG_CLOSE = 1; + + private String mName; + private MffContext mContext; + private FilterGraph mFilterGraph; + + private State mState = new State(); + private int mRequests = REQUEST_FLAG_NONE; + + private int mMinimumAvailableInputs = 1; + private int mMinimumAvailableOutputs = 1; + + private int mScheduleCount = 0; + private long mLastScheduleTime = 0; + + private boolean mIsActive = true; + private AtomicBoolean mIsSleeping = new AtomicBoolean(false); + + private long mCurrentTimestamp = Frame.TIMESTAMP_NOT_SET; + + private HashMap<String, InputPort> mConnectedInputPorts = new HashMap<String, InputPort>(); + private HashMap<String, OutputPort> mConnectedOutputPorts = new HashMap<String, OutputPort>(); + + private InputPort[] mConnectedInputPortArray = null; + private OutputPort[] mConnectedOutputPortArray = null; + + private ArrayList<Frame> mAutoReleaseFrames = new ArrayList<Frame>(); + + + /** + * Constructs a new filter. + * A filter is bound to a specific MffContext. Its name can be any String value, but it must + * be unique within the filter graph. + * + * Note that names starting with "$" are reserved for internal use, and should not be used. + * + * @param context The MffContext in which the filter will live. + * @param name The name of the filter. + */ + protected Filter(MffContext context, String name) { + mName = name; + mContext = context; + } + + /** + * Checks whether the filter class is available on this platform. + * Some filters may not be installed on all platforms and can therefore not be instantiated. + * Before instantiating a filter, check if it is available by using this method. + * + * This method uses the shared FilterFactory to check whether the filter class is available. + * + * @param filterClassName The fully qualified class name of the Filter class. + * @return true, if filters of the specified class name are available. + */ + public static final boolean isAvailable(String filterClassName) { + return FilterFactory.sharedFactory().isFilterAvailable(filterClassName); + } + + /** + * Returns the name of this filter. + * + * @return the name of the filter (specified during construction). + */ + public String getName() { + return mName; + } + + /** + * Returns the signature of this filter. + * + * Subclasses should override this and return their filter signature. The default + * implementation returns a generic signature with no constraints. + * + * This method may be called at any time. + * + * @return the Signature instance for this filter. + */ + public Signature getSignature() { + return new Signature(); + } + + /** + * Returns the MffContext that the filter resides in. + * + * @return the MffContext of the filter. + */ + public MffContext getContext() { + return mContext; + } + + /** + * Returns true, if the filter is active. + * TODO: thread safety? + * + * @return true, if the filter is active. + */ + public boolean isActive() { + return mIsActive; + } + + /** + * Activates the current filter. + * Only active filters can be scheduled for execution. This method can only be called if the + * GraphRunner that is executing the filter is stopped or paused. + */ + public void activate() { + assertIsPaused(); + if (!mIsActive) { + mIsActive = true; + } + } + + /** + * Deactivates the current filter. + * Only active filters can be scheduled for execution. This method can only be called if the + * GraphRunner that is executing the filter is stopped or paused. + */ + public void deactivate() { + // TODO: Support close-on-deactivate (must happen in processing thread). + assertIsPaused(); + if (mIsActive) { + mIsActive = false; + } + } + + /** + * Returns the filter's set of input ports. + * Note that this contains only the *connected* input ports. To retrieve all + * input ports that this filter accepts, one has to go via the filter's Signature. + * + * @return An array containing all connected input ports. + */ + public final InputPort[] getConnectedInputPorts() { + return mConnectedInputPortArray; + } + + /** + * Returns the filter's set of output ports. + * Note that this contains only the *connected* output ports. To retrieve all + * output ports that this filter provides, one has to go via the filter's Signature. + * + * @return An array containing all connected output ports. + */ + public final OutputPort[] getConnectedOutputPorts() { + return mConnectedOutputPortArray; + } + + /** + * Returns the input port with the given name. + * Note that this can only access the *connected* input ports. To retrieve all + * input ports that this filter accepts, one has to go via the filter's Signature. + * + * @return the input port with the specified name, or null if no connected input port + * with this name exists. + */ + public final InputPort getConnectedInputPort(String name) { + return mConnectedInputPorts.get(name); + } + + /** + * Returns the output port with the given name. + * Note that this can only access the *connected* output ports. To retrieve all + * output ports that this filter provides, one has to go via the filter's Signature. + * + * @return the output port with the specified name, or null if no connected output port + * with this name exists. + */ + public final OutputPort getConnectedOutputPort(String name) { + return mConnectedOutputPorts.get(name); + } + + /** + * Called when an input port has been attached in the graph. + * Override this method, in case you want to be informed of any connected input ports, or make + * modifications to them. Note that you may not assume that any other ports have been attached + * already. If you have dependencies on other ports, override + * {@link #onInputPortOpen(InputPort)}. The default implementation does nothing. + * + * @param port The InputPort instance that was attached. + */ + protected void onInputPortAttached(InputPort port) { + } + + /** + * Called when an output port has been attached in the graph. + * Override this method, in case you want to be informed of any connected output ports, or make + * modifications to them. Note that you may not assume that any other ports have been attached + * already. If you have dependencies on other ports, override + * {@link #onOutputPortOpen(OutputPort)}. The default implementation does nothing. + * + * @param port The OutputPort instance that was attached. + */ + protected void onOutputPortAttached(OutputPort port) { + } + + /** + * Called when an input port is opened on this filter. + * Input ports are opened by the data produce, that is the filter that is connected to an + * input port. Override this if you need to make modifications to the port before processing + * begins. Note, that this is only called if the connected filter is scheduled. You may assume + * that all ports are attached when this is called. + * + * @param port The InputPort instance that was opened. + */ + protected void onInputPortOpen(InputPort port) { + } + + /** + * Called when an output port is opened on this filter. + * Output ports are opened when the filter they are attached to is opened. Override this if you + * need to make modifications to the port before processing begins. Note, that this is only + * called if the filter is scheduled. You may assume that all ports are attached when this is + * called. + * + * @param port The OutputPort instance that was opened. + */ + protected void onOutputPortOpen(OutputPort port) { + } + + /** + * Returns true, if the filter is currently open. + * @return true, if the filter is currently open. + */ + public final boolean isOpen() { + return mState.check(State.STATE_OPEN); + } + + @Override + public String toString() { + return mName + " (" + getClass().getSimpleName() + ")"; + } + + /** + * Called when filter is prepared. + * Subclasses can override this to prepare the filter for processing. This method gets called + * once only just before the filter is scheduled for processing the first time. + * + * @see #onTearDown() + */ + protected void onPrepare() { + } + + /** + * Called when the filter is opened. + * Subclasses can override this to perform any kind of initialization just before processing + * starts. This method may be called any number of times, but is always balanced with an + * {@link #onClose()} call. + * + * @see #onClose() + */ + protected void onOpen() { + } + + /** + * Called to perform processing on Frame data. + * This is the only method subclasses must override. It is called every time the filter is + * ready for processing. Typically this is when there is input data to process and available + * output ports, but may differ depending on the port configuration. + */ + protected abstract void onProcess(); + + /** + * Called when the filter is closed. + * Subclasses can override this to perform any kind of post-processing steps. Processing will + * not resume until {@link #onOpen()} is called again. This method is only called if the filter + * is open. + * + * @see #onOpen() + */ + protected void onClose() { + } + + /** + * Called when the filter is torn down. + * Subclasses can override this to perform clean-up tasks just before the filter is disposed of. + * It is called when the filter graph that the filter belongs to is disposed. + * + * @see #onPrepare() + */ + protected void onTearDown() { + } + + /** + * Check if the input conditions are met in order to schedule this filter. + * + * This is used by {@link #canSchedule()} to determine if the input-port conditions given by + * the filter are met. Subclasses that override scheduling behavior can make use of this + * function. + * + * @return true, if the filter's input conditions are met. + */ + protected boolean inputConditionsMet() { + if (mConnectedInputPortArray.length > 0) { + int inputFrames = 0; + // [Non-iterator looping] + for (int i = 0; i < mConnectedInputPortArray.length; ++i) { + if (!mConnectedInputPortArray[i].conditionsMet()) { + return false; + } else if (mConnectedInputPortArray[i].hasFrame()) { + ++inputFrames; + } + } + if (inputFrames < mMinimumAvailableInputs) { + return false; + } + } + return true; + } + + /** + * Check if the output conditions are met in order to schedule this filter. + * + * This is used by {@link #canSchedule()} to determine if the output-port conditions given by + * the filter are met. Subclasses that override scheduling behavior can make use of this + * function. + * + * @return true, if the filter's output conditions are met. + */ + protected boolean outputConditionsMet() { + if (mConnectedOutputPortArray.length > 0) { + int availableOutputs = 0; + for (int i = 0; i < mConnectedOutputPortArray.length; ++i) { + if (!mConnectedOutputPortArray[i].conditionsMet()) { + return false; + } else if (mConnectedOutputPortArray[i].isAvailable()) { + ++availableOutputs; + } + } + if (availableOutputs < mMinimumAvailableOutputs) { + return false; + } + } + return true; + } + + /** + * Check if the Filter is in a state so that it can be scheduled. + * + * When overriding the filter's {@link #canSchedule()} method, you should never allow + * scheduling a filter that is not in a schedulable state. This will result in undefined + * behavior. + * + * @return true, if the filter is in a schedulable state. + */ + protected boolean inSchedulableState() { + return (mIsActive && !mState.check(State.STATE_CLOSED)); + } + + /** + * Returns true if the filter can be currently scheduled. + * + * Filters may override this method if they depend on custom factors that determine whether + * they can be scheduled or not. The scheduler calls this method to determine whether or not + * a filter can be scheduled for execution. It does not guarantee that it will be executed. + * It is strongly recommended to call super's implementation to make sure your filter can be + * scheduled based on its state, input and output ports. + * + * @return true, if the filter can be scheduled. + */ + protected boolean canSchedule() { + return inSchedulableState() && inputConditionsMet() && outputConditionsMet(); + } + + /** + * Returns the current FrameManager instance. + * @return the current FrameManager instance or null if there is no FrameManager set up yet. + */ + protected final FrameManager getFrameManager() { + return mFilterGraph.mRunner != null ? mFilterGraph.mRunner.getFrameManager() : null; + } + + /** + * Returns whether the GraphRunner for this filter is running. + * + * Generally, this method should not be used for performing operations that need to be carried + * out before running begins. Use {@link #performPreparation(Runnable)} for this. + * + * @return true, if the GraphRunner for this filter is running. + */ + protected final boolean isRunning() { + return mFilterGraph != null && mFilterGraph.mRunner != null + && mFilterGraph.mRunner.isRunning(); + } + + /** + * Performs operations before the filter is running. + * + * Use this method when your filter requires to perform operations while the graph is not + * running. The filter will not be scheduled for execution until your method has completed + * execution. + */ + protected final boolean performPreparation(Runnable runnable) { + synchronized (mState) { + if (mState.current == State.STATE_OPEN) { + return false; + } else { + runnable.run(); + return true; + } + } + } + + /** + * Request that this filter be closed after the current processing step. + * + * Implementations may call this within their {@link #onProcess()} calls to indicate that the + * filter is done processing and wishes to be closed. After such a request the filter will be + * closed and no longer receive {@link #onProcess()} calls. + * + * @see #onClose() + * @see #onProcess() + */ + protected final void requestClose() { + mRequests |= REQUEST_FLAG_CLOSE; + } + + /** + * Sets the minimum number of input frames required to process. + * A filter will not be scheduled unless at least a certain number of input frames are available + * on the input ports. This is only relevant if the filter has input ports and is not waiting on + * all ports. + * The default value is 1. + * + * @param count the minimum number of frames required to process. + * @see #getMinimumAvailableInputs() + * @see #setMinimumAvailableOutputs(int) + * @see InputPort#setWaitsForFrame(boolean) + */ + protected final void setMinimumAvailableInputs(int count) { + mMinimumAvailableInputs = count; + } + + /** + * Returns the minimum number of input frames required to process this filter. + * The default value is 1. + * + * @return the minimum number of input frames required to process. + * @see #setMinimumAvailableInputs(int) + */ + protected final int getMinimumAvailableInputs() { + return mMinimumAvailableInputs; + } + + /** + * Sets the minimum number of available output ports required to process. + * A filter will not be scheduled unless atleast a certain number of output ports are available. + * This is only relevant if the filter has output ports and is not waiting on all ports. The + * default value is 1. + * + * @param count the minimum number of frames required to process. + * @see #getMinimumAvailableOutputs() + * @see #setMinimumAvailableInputs(int) + * @see OutputPort#setWaitsUntilAvailable(boolean) + */ + protected final void setMinimumAvailableOutputs(int count) { + mMinimumAvailableOutputs = count; + } + + /** + * Returns the minimum number of available outputs required to process this filter. + * The default value is 1. + * + * @return the minimum number of available outputs required to process. + * @see #setMinimumAvailableOutputs(int) + */ + protected final int getMinimumAvailableOutputs() { + return mMinimumAvailableOutputs; + } + + /** + * Puts the filter to sleep so that it is no longer scheduled. + * To resume scheduling the filter another thread must call wakeUp() on this filter. + */ + protected final void enterSleepState() { + mIsSleeping.set(true); + } + + /** + * Wakes the filter and resumes scheduling. + * This is generally called from another thread to signal that this filter should resume + * processing. Does nothing if filter is not sleeping. + */ + protected final void wakeUp() { + if (mIsSleeping.getAndSet(false)) { + if (isRunning()) { + mFilterGraph.mRunner.signalWakeUp(); + } + } + } + + /** + * Returns whether this Filter is allowed to use OpenGL. + * + * Filters may use OpenGL if the MffContext supports OpenGL and its GraphRunner allows it. + * + * @return true, if this Filter is allowed to use OpenGL. + */ + protected final boolean isOpenGLSupported() { + return mFilterGraph.mRunner.isOpenGLSupported(); + } + + /** + * Connect an output port to an input port of another filter. + * Connects the output port with the specified name to the input port with the specified name + * of the specified filter. If the input or output ports do not exist already, they are + * automatically created and added to the respective filter. + */ + final void connect(String outputName, Filter targetFilter, String inputName) { + // Make sure not connected already + if (getConnectedOutputPort(outputName) != null) { + throw new RuntimeException("Attempting to connect already connected output port '" + + outputName + "' of filter " + this + "'!"); + } else if (targetFilter.getConnectedInputPort(inputName) != null) { + throw new RuntimeException("Attempting to connect already connected input port '" + + inputName + "' of filter " + targetFilter + "'!"); + } + + // Establish connection + InputPort inputPort = targetFilter.newInputPort(inputName); + OutputPort outputPort = newOutputPort(outputName); + outputPort.setTarget(inputPort); + + // Fire attachment callbacks + targetFilter.onInputPortAttached(inputPort); + onOutputPortAttached(outputPort); + + // Update array of ports (which is maintained for more efficient access) + updatePortArrays(); + } + + final Map<String, InputPort> getConnectedInputPortMap() { + return mConnectedInputPorts; + } + + final Map<String, OutputPort> getConnectedOutputPortMap() { + return mConnectedOutputPorts; + } + + final void execute() { + synchronized (mState) { + autoPullInputs(); + mLastScheduleTime = SystemClock.elapsedRealtime(); + if (mState.current == State.STATE_UNPREPARED) { + onPrepare(); + mState.current = State.STATE_PREPARED; + } + if (mState.current == State.STATE_PREPARED) { + openPorts(); + onOpen(); + mState.current = State.STATE_OPEN; + } + if (mState.current == State.STATE_OPEN) { + onProcess(); + if (mRequests != REQUEST_FLAG_NONE) { + processRequests(); + } + } + } + autoReleaseFrames(); + ++mScheduleCount; + } + + final void performClose() { + synchronized (mState) { + if (mState.current == State.STATE_OPEN) { + onClose(); + mIsSleeping.set(false); + mState.current = State.STATE_CLOSED; + mCurrentTimestamp = Frame.TIMESTAMP_NOT_SET; + } + } + } + + final void softReset() { + synchronized (mState) { + performClose(); + if (mState.current == State.STATE_CLOSED) { + mState.current = State.STATE_PREPARED; + } + } + } + + final void performTearDown() { + synchronized (mState) { + if (mState.current == State.STATE_OPEN) { + throw new RuntimeException("Attempting to tear-down filter " + this + " which is " + + "in an open state!"); + } else if (mState.current != State.STATE_DESTROYED + && mState.current != State.STATE_UNPREPARED) { + onTearDown(); + mState.current = State.STATE_DESTROYED; + } + } + } + + final void insertIntoFilterGraph(FilterGraph graph) { + mFilterGraph = graph; + updatePortArrays(); + } + + final int getScheduleCount() { + return mScheduleCount; + } + + final void resetScheduleCount() { + mScheduleCount = 0; + } + + final void openPorts() { + // Opening the output ports will open the connected input ports + for (OutputPort outputPort : mConnectedOutputPorts.values()) { + openOutputPort(outputPort); + } + } + + final void addAutoReleaseFrame(Frame frame) { + mAutoReleaseFrames.add(frame); + } + + final long getCurrentTimestamp() { + return mCurrentTimestamp; + } + + final void onPulledFrameWithTimestamp(long timestamp) { + if (timestamp > mCurrentTimestamp || mCurrentTimestamp == Frame.TIMESTAMP_NOT_SET) { + mCurrentTimestamp = timestamp; + } + } + + final void openOutputPort(OutputPort outPort) { + if (outPort.getQueue() == null) { + try { + FrameQueue.Builder builder = new FrameQueue.Builder(); + InputPort inPort = outPort.getTarget(); + outPort.onOpen(builder); + inPort.onOpen(builder); + Filter targetFilter = inPort.getFilter(); + String queueName = mName + "[" + outPort.getName() + "] -> " + targetFilter.mName + + "[" + inPort.getName() + "]"; + FrameQueue queue = builder.build(queueName); + outPort.setQueue(queue); + inPort.setQueue(queue); + } catch (RuntimeException e) { + throw new RuntimeException("Could not open output port " + outPort + "!", e); + } + } + } + + final boolean isSleeping() { + return mIsSleeping.get(); + } + + final long getLastScheduleTime() { + return mLastScheduleTime ; + } + + private final void autoPullInputs() { + // [Non-iterator looping] + for (int i = 0; i < mConnectedInputPortArray.length; ++i) { + InputPort port = mConnectedInputPortArray[i]; + if (port.hasFrame() && port.isAutoPullEnabled()) { + mConnectedInputPortArray[i].pullFrame(); + } + } + } + + private final void autoReleaseFrames() { + // [Non-iterator looping] + for (int i = 0; i < mAutoReleaseFrames.size(); ++i) { + mAutoReleaseFrames.get(i).release(); + } + mAutoReleaseFrames.clear(); + } + + private final InputPort newInputPort(String name) { + InputPort result = mConnectedInputPorts.get(name); + if (result == null) { + Signature.PortInfo info = getSignature().getInputPortInfo(name); + result = new InputPort(this, name, info); + mConnectedInputPorts.put(name, result); + } + return result; + } + + private final OutputPort newOutputPort(String name) { + OutputPort result = mConnectedOutputPorts.get(name); + if (result == null) { + Signature.PortInfo info = getSignature().getOutputPortInfo(name); + result = new OutputPort(this, name, info); + mConnectedOutputPorts.put(name, result); + } + return result; + } + + private final void processRequests() { + if ((mRequests & REQUEST_FLAG_CLOSE) != 0) { + performClose(); + mRequests = REQUEST_FLAG_NONE; + } + } + + private void assertIsPaused() { + GraphRunner runner = GraphRunner.current(); + if (runner != null && !runner.isPaused() && !runner.isStopped()) { + throw new RuntimeException("Attempting to modify filter state while runner is " + + "executing. Please pause or stop the runner first!"); + } + } + + private final void updatePortArrays() { + // Copy our port-maps to arrays for faster non-iterator access + mConnectedInputPortArray = mConnectedInputPorts.values().toArray(new InputPort[0]); + mConnectedOutputPortArray = mConnectedOutputPorts.values().toArray(new OutputPort[0]); + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterFactory.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterFactory.java new file mode 100644 index 0000000..2c67c79 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterFactory.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.util.Log; + +import dalvik.system.PathClassLoader; + +import java.lang.reflect.Constructor; +import java.util.HashSet; + +public class FilterFactory { + + private static FilterFactory mSharedFactory; + private HashSet<String> mPackages = new HashSet<String>(); + + private static ClassLoader mCurrentClassLoader; + private static HashSet<String> mLibraries; + private static Object mClassLoaderGuard; + + static { + mCurrentClassLoader = Thread.currentThread().getContextClassLoader(); + mLibraries = new HashSet<String>(); + mClassLoaderGuard = new Object(); + } + + private static final String TAG = "FilterFactory"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + public static FilterFactory sharedFactory() { + if (mSharedFactory == null) { + mSharedFactory = new FilterFactory(); + } + return mSharedFactory; + } + + /** + * Adds a new Java library to the list to be scanned for filters. + * libraryPath must be an absolute path of the jar file. This needs to be + * static because only one classloader per process can open a shared native + * library, which a filter may well have. + */ + public static void addFilterLibrary(String libraryPath) { + if (mLogVerbose) Log.v(TAG, "Adding filter library " + libraryPath); + synchronized(mClassLoaderGuard) { + if (mLibraries.contains(libraryPath)) { + if (mLogVerbose) Log.v(TAG, "Library already added"); + return; + } + mLibraries.add(libraryPath); + // Chain another path loader to the current chain + mCurrentClassLoader = new PathClassLoader(libraryPath, mCurrentClassLoader); + } + } + + public void addPackage(String packageName) { + if (mLogVerbose) Log.v(TAG, "Adding package " + packageName); + /* TODO: This should use a getPackage call in the caller's context, but no such method + exists. + Package pkg = Package.getPackage(packageName); + if (pkg == null) { + throw new IllegalArgumentException("Unknown filter package '" + packageName + "'!"); + } + */ + mPackages.add(packageName); + } + + public boolean isFilterAvailable(String className) { + return getFilterClass(className) != null; + } + + public Filter createFilterByClassName(String className, String filterName, MffContext context) { + if (mLogVerbose) Log.v(TAG, "Looking up class " + className); + Class<? extends Filter> filterClass = getFilterClass(className); + if (filterClass == null) { + throw new IllegalArgumentException("Unknown filter class '" + className + "'!"); + } + return createFilterByClass(filterClass, filterName, context); + } + + public Filter createFilterByClass(Class<? extends Filter> filterClass, + String filterName, MffContext context) { + // Look for the correct constructor + Constructor<? extends Filter> filterConstructor = null; + try { + filterConstructor = filterClass.getConstructor(MffContext.class, String.class); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("The filter class '" + filterClass + + "' does not have a constructor of the form <init>(MffContext, String)!"); + } + + // Construct the filter + Filter filter = null; + try { + filter = filterConstructor.newInstance(context, filterName); + } catch (Throwable t) { + throw new RuntimeException("Error creating filter " + filterName + "!", t); + } + + if (filter == null) { + throw new IllegalArgumentException("Could not construct the filter '" + + filterName + "'!"); + } + return filter; + } + + private Class<? extends Filter> getFilterClass(String name) { + Class<?> filterClass = null; + + // Look for the class in the imported packages + for (String packageName : mPackages) { + try { + if (mLogVerbose) Log.v(TAG, "Trying "+ packageName + "." + name); + synchronized(mClassLoaderGuard) { + filterClass = mCurrentClassLoader.loadClass(packageName + "." + name); + } + } catch (ClassNotFoundException e) { + continue; + } + // Exit loop if class was found. + if (filterClass != null) { + break; + } + } + Class<? extends Filter> result = null; + try { + if (filterClass != null) { + result = filterClass.asSubclass(Filter.class); + } + } catch (ClassCastException e) { + // Leave result == null + } + return result; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterGraph.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterGraph.java new file mode 100644 index 0000000..7d5ed9f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FilterGraph.java @@ -0,0 +1,567 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.util.Log; +import android.view.View; +import androidx.media.filterpacks.base.BranchFilter; +import androidx.media.filterpacks.base.FrameSlotSource; +import androidx.media.filterpacks.base.FrameSlotTarget; +import androidx.media.filterpacks.base.GraphInputSource; +import androidx.media.filterpacks.base.GraphOutputTarget; +import androidx.media.filterpacks.base.ValueTarget; +import androidx.media.filterpacks.base.ValueTarget.ValueListener; +import androidx.media.filterpacks.base.VariableSource; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; + +/** + * A graph of Filter nodes. + * + * A FilterGraph instance contains a set of Filter instances connected by their output and input + * ports. Every filter belongs to exactly one graph and cannot be moved to another graph. + * + * FilterGraphs may contain sub-graphs that are dependent on the parent graph. These are typically + * used when inserting sub-graphs into MetaFilters. When a parent graph is torn down so are its + * sub-graphs. The same applies to flushing frames of a graph. + */ +public class FilterGraph { + + private final static boolean DEBUG = false; + + /** The context that this graph lives in */ + private MffContext mContext; + + /** Map from name of filter to the filter instance */ + private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>(); + + /** Allows quick access to array of all filters. */ + private Filter[] mAllFilters = null; + + /** The GraphRunner currently attached to this graph */ + GraphRunner mRunner; + + /** The set of sub-graphs of this graph */ + HashSet<FilterGraph> mSubGraphs = new HashSet<FilterGraph>(); + + /** The parent graph of this graph, or null it this graph is a root graph. */ + private FilterGraph mParentGraph; + + public static class Builder { + + /** The context that this builder lives in */ + private MffContext mContext; + + /** Map from name of filter to the filter instance */ + private HashMap<String, Filter> mFilterMap = new HashMap<String, Filter>(); + + /** + * Creates a new builder for specifying a graph structure. + * @param context The context the graph will live in. + */ + public Builder(MffContext context) { + mContext = context; + } + + /** + * Add a filter to the graph. + * + * Adds the specified filter to the set of filters of this graph. The filter must not be in + * the graph already, and the filter's name must be unique within the graph. + * + * @param filter the filter to add to the graph. + * @throws IllegalArgumentException if the filter is in the graph already, or its name is + * is already taken. + */ + public void addFilter(Filter filter) { + if (mFilterMap.values().contains(filter)) { + throw new IllegalArgumentException("Attempting to add filter " + filter + " that " + + "is in the graph already!"); + } else if (mFilterMap.containsKey(filter.getName())) { + throw new IllegalArgumentException("Graph contains filter with name '" + + filter.getName() + "' already!"); + } else { + mFilterMap.put(filter.getName(), filter); + } + } + + /** + * Adds a variable to the graph. + * + * TODO: More documentation. + * + * @param name the name of the variable. + * @param value the value of the variable or null if no value is to be set yet. + * @return the VariableSource filter that holds the value of this variable. + */ + public VariableSource addVariable(String name, Object value) { + if (getFilter(name) != null) { + throw new IllegalArgumentException("Filter named '" + name + "' exists already!"); + } + VariableSource valueSource = new VariableSource(mContext, name); + addFilter(valueSource); + if (value != null) { + valueSource.setValue(value); + } + return valueSource; + } + + public FrameSlotSource addFrameSlotSource(String name, String slotName) { + FrameSlotSource filter = new FrameSlotSource(mContext, name, slotName); + addFilter(filter); + return filter; + } + + public FrameSlotTarget addFrameSlotTarget(String name, String slotName) { + FrameSlotTarget filter = new FrameSlotTarget(mContext, name, slotName); + addFilter(filter); + return filter; + } + + /** + * Connect two filters by their ports. + * The filters specified must have been previously added to the graph builder. + * + * @param sourceFilterName The name of the source filter. + * @param sourcePort The name of the source port. + * @param targetFilterName The name of the target filter. + * @param targetPort The name of the target port. + */ + public void connect(String sourceFilterName, String sourcePort, + String targetFilterName, String targetPort) { + Filter sourceFilter = getFilter(sourceFilterName); + Filter targetFilter = getFilter(targetFilterName); + if (sourceFilter == null) { + throw new IllegalArgumentException("Unknown filter '" + sourceFilterName + "'!"); + } else if (targetFilter == null) { + throw new IllegalArgumentException("Unknown filter '" + targetFilterName + "'!"); + } + connect(sourceFilter, sourcePort, targetFilter, targetPort); + } + + /** + * Connect two filters by their ports. + * The filters specified must have been previously added to the graph builder. + * + * @param sourceFilter The source filter. + * @param sourcePort The name of the source port. + * @param targetFilter The target filter. + * @param targetPort The name of the target port. + */ + public void connect(Filter sourceFilter, String sourcePort, + Filter targetFilter, String targetPort) { + sourceFilter.connect(sourcePort, targetFilter, targetPort); + } + + /** + * Returns the filter with the specified name. + * + * @return the filter with the specified name, or null if no such filter exists. + */ + public Filter getFilter(String name) { + return mFilterMap.get(name); + } + + /** + * Builds the graph and checks signatures. + * + * @return The new graph instance. + */ + public FilterGraph build() { + checkSignatures(); + return buildWithParent(null); + } + + /** + * Builds the sub-graph and checks signatures. + * + * @param parentGraph the parent graph of the built sub-graph. + * @return The new graph instance. + */ + public FilterGraph buildSubGraph(FilterGraph parentGraph) { + if (parentGraph == null) { + throw new NullPointerException("Parent graph must be non-null!"); + } + checkSignatures(); + return buildWithParent(parentGraph); + } + + VariableSource assignValueToFilterInput(Object value, String filterName, String inputName) { + // Get filter to connect to + Filter filter = getFilter(filterName); + if (filter == null) { + throw new IllegalArgumentException("Unknown filter '" + filterName + "'!"); + } + + // Construct a name for our value source and make sure it does not exist already + String valueSourceName = filterName + "." + inputName; + if (getFilter(valueSourceName) != null) { + throw new IllegalArgumentException("VariableSource for '" + filterName + "' and " + + "input '" + inputName + "' exists already!"); + } + + // Create new VariableSource and connect it to the target filter and port + VariableSource valueSource = new VariableSource(mContext, valueSourceName); + addFilter(valueSource); + try { + ((Filter)valueSource).connect("value", filter, inputName); + } catch (RuntimeException e) { + throw new RuntimeException("Could not connect VariableSource to input '" + inputName + + "' of filter '" + filterName + "'!", e); + } + + // Assign the value to the VariableSource + if (value != null) { + valueSource.setValue(value); + } + + return valueSource; + } + + VariableSource assignVariableToFilterInput(String varName, + String filterName, + String inputName) { + // Get filter to connect to + Filter filter = getFilter(filterName); + if (filter == null) { + throw new IllegalArgumentException("Unknown filter '" + filterName + "'!"); + } + + // Get variable + Filter variable = getFilter(varName); + if (variable == null || !(variable instanceof VariableSource)) { + throw new IllegalArgumentException("Unknown variable '" + varName + "'!"); + } + + // Connect variable (and possibly branch) variable to filter + try { + connectAndBranch(variable, "value", filter, inputName); + } catch (RuntimeException e) { + throw new RuntimeException("Could not connect VariableSource to input '" + inputName + + "' of filter '" + filterName + "'!", e); + } + + return (VariableSource)variable; + } + + /** + * Builds the graph without checking signatures. + * If parent is non-null, build a sub-graph of the specified parent. + * + * @return The new graph instance. + */ + private FilterGraph buildWithParent(FilterGraph parent) { + FilterGraph graph = new FilterGraph(mContext, parent); + graph.mFilterMap = mFilterMap; + graph.mAllFilters = mFilterMap.values().toArray(new Filter[0]); + for (Entry<String, Filter> filterEntry : mFilterMap.entrySet()) { + filterEntry.getValue().insertIntoFilterGraph(graph); + } + return graph; + } + + private void checkSignatures() { + checkSignaturesForFilters(mFilterMap.values()); + } + + // TODO: Currently this always branches even if the connection is a 1:1 connection. Later + // we may optimize to pass through directly in the 1:1 case (may require disconnecting + // ports). + private void connectAndBranch(Filter sourceFilter, + String sourcePort, + Filter targetFilter, + String targetPort) { + String branchName = "__" + sourceFilter.getName() + "_" + sourcePort + "Branch"; + Filter branch = getFilter(branchName); + if (branch == null) { + branch = new BranchFilter(mContext, branchName, false); + addFilter(branch); + sourceFilter.connect(sourcePort, branch, "input"); + } + String portName = "to" + targetFilter.getName() + "_" + targetPort; + branch.connect(portName, targetFilter, targetPort); + } + + } + + /** + * Attach the graph and its subgraphs to a custom GraphRunner. + * + * Call this if you want the graph to be executed by a specific GraphRunner. You must call + * this before any other runner is set. Note that calls to {@code getRunner()} and + * {@code run()} auto-create a GraphRunner. + * + * @param runner The GraphRunner instance that should execute this graph. + * @see #getRunner() + * @see #run() + */ + public void attachToRunner(GraphRunner runner) { + if (mRunner == null) { + for (FilterGraph subGraph : mSubGraphs) { + subGraph.attachToRunner(runner); + } + runner.attachGraph(this); + mRunner = runner; + } else if (mRunner != runner) { + throw new RuntimeException("Cannot attach FilterGraph to GraphRunner that is already " + + "attached to another GraphRunner!"); + } + } + + /** + * Forcibly tear down a filter graph. + * + * Call this to release any resources associated with the filter graph, its filters and any of + * its sub-graphs. This method must not be called if the graph (or any sub-graph) is running. + * + * You may no longer access this graph instance or any of its subgraphs after calling this + * method. + * + * Tearing down of sub-graphs is not supported. You must tear down the root graph, which will + * tear down all of its sub-graphs. + * + * @throws IllegalStateException if the graph is still running. + * @throws RuntimeException if you attempt to tear down a sub-graph. + */ + public void tearDown() { + assertNotRunning(); + if (mParentGraph != null) { + throw new RuntimeException("Attempting to tear down sub-graph!"); + } + if (mRunner != null) { + mRunner.tearDownGraph(this); + } + for (FilterGraph subGraph : mSubGraphs) { + subGraph.mParentGraph = null; + subGraph.tearDown(); + } + mSubGraphs.clear(); + } + + /** + * Returns the context of the graph. + * + * @return the MffContext instance that this graph is bound to. + */ + public MffContext getContext() { + return mContext; + } + + /** + * Returns the filter with the specified name. + * + * @return the filter with the specified name, or null if no such filter exists. + */ + public Filter getFilter(String name) { + return mFilterMap.get(name); + } + + /** + * Returns the VariableSource for the specified variable. + * + * TODO: More documentation. + * TODO: More specialized error handling. + * + * @param name The name of the VariableSource. + * @return The VariableSource filter instance with the specified name. + */ + public VariableSource getVariable(String name) { + Filter result = mFilterMap.get(name); + if (result != null && result instanceof VariableSource) { + return (VariableSource)result; + } else { + throw new IllegalArgumentException("Unknown variable '" + name + "' specified!"); + } + } + + /** + * Returns the GraphOutputTarget with the specified name. + * + * @param name The name of the target. + * @return The GraphOutputTarget instance with the specified name. + */ + public GraphOutputTarget getGraphOutput(String name) { + Filter result = mFilterMap.get(name); + if (result != null && result instanceof GraphOutputTarget) { + return (GraphOutputTarget)result; + } else { + throw new IllegalArgumentException("Unknown target '" + name + "' specified!"); + } + } + + /** + * Returns the GraphInputSource with the specified name. + * + * @param name The name of the source. + * @return The GraphInputSource instance with the specified name. + */ + public GraphInputSource getGraphInput(String name) { + Filter result = mFilterMap.get(name); + if (result != null && result instanceof GraphInputSource) { + return (GraphInputSource)result; + } else { + throw new IllegalArgumentException("Unknown source '" + name + "' specified!"); + } + } + + /** + * Binds a filter to a view. + * + * ViewFilter instances support visualizing their data to a view. See the specific filter + * documentation for details. Views may be bound only if the graph is not running. + * + * @param filterName the name of the filter to bind. + * @param view the view to bind to. + * @throws IllegalStateException if the filter is in an illegal state. + * @throws IllegalArgumentException if no such view-filter exists. + */ + public void bindFilterToView(String filterName, View view) { + Filter filter = mFilterMap.get(filterName); + if (filter != null && filter instanceof ViewFilter) { + ((ViewFilter)filter).bindToView(view); + } else { + throw new IllegalArgumentException("Unknown view filter '" + filterName + "'!"); + } + } + + /** + * TODO: Documentation. + */ + public void bindValueTarget(String filterName, ValueListener listener, boolean onCallerThread) { + Filter filter = mFilterMap.get(filterName); + if (filter != null && filter instanceof ValueTarget) { + ((ValueTarget)filter).setListener(listener, onCallerThread); + } else { + throw new IllegalArgumentException("Unknown ValueTarget filter '" + filterName + "'!"); + } + } + + // Running Graphs ////////////////////////////////////////////////////////////////////////////// + /** + * Convenience method to run the graph. + * + * Creates a new runner for this graph in the specified mode and executes it. Returns the + * runner to allow control of execution. + * + * @throws IllegalStateException if the graph is already running. + * @return the GraphRunner instance that was used for execution. + */ + public GraphRunner run() { + GraphRunner runner = getRunner(); + runner.setIsVerbose(false); + runner.start(this); + return runner; + } + + /** + * Returns the GraphRunner for this graph. + * + * Every FilterGraph instance has a GraphRunner instance associated with it for executing the + * graph. + * + * @return the GraphRunner instance for this graph. + */ + public GraphRunner getRunner() { + if (mRunner == null) { + GraphRunner runner = new GraphRunner(mContext); + attachToRunner(runner); + } + return mRunner; + } + + /** + * Returns whether the graph is currently running. + * + * @return true if the graph is currently running. + */ + public boolean isRunning() { + return mRunner != null && mRunner.isRunning(); + } + + /** + * Check each filter's signatures if all requirements are fulfilled. + * + * This will throw a RuntimeException if any unfulfilled requirements are found. + * Note that FilterGraph.Builder also has a function checkSignatures(), which allows + * to do the same /before/ the FilterGraph is built. + */ + public void checkSignatures() { + checkSignaturesForFilters(mFilterMap.values()); + } + + // MFF Internal Methods //////////////////////////////////////////////////////////////////////// + Filter[] getAllFilters() { + return mAllFilters; + } + + static void checkSignaturesForFilters(Collection<Filter> filters) { + for (Filter filter : filters) { + if (DEBUG) { + Log.d("FilterGraph", "Checking filter " + filter.getName() + "..."); + } + Signature signature = filter.getSignature(); + signature.checkInputPortsConform(filter); + signature.checkOutputPortsConform(filter); + } + } + + /** + * Wipes the filter references in this graph, so that they may be collected. + * + * This must be called only after a tearDown as this will make the FilterGraph invalid. + */ + void wipe() { + mAllFilters = null; + mFilterMap = null; + } + + void flushFrames() { + for (Filter filter : mFilterMap.values()) { + for (InputPort inputPort : filter.getConnectedInputPorts()) { + inputPort.clear(); + } + for (OutputPort outputPort : filter.getConnectedOutputPorts()) { + outputPort.clear(); + } + } + } + + Set<FilterGraph> getSubGraphs() { + return mSubGraphs; + } + + // Internal Methods //////////////////////////////////////////////////////////////////////////// + private FilterGraph(MffContext context, FilterGraph parentGraph) { + mContext = context; + mContext.addGraph(this); + if (parentGraph != null) { + mParentGraph = parentGraph; + mParentGraph.mSubGraphs.add(this); + } + } + + private void assertNotRunning() { + if (isRunning()) { + throw new IllegalStateException("Attempting to modify running graph!"); + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Frame.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Frame.java new file mode 100644 index 0000000..67907d3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Frame.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import java.util.Arrays; + +/** + * Frames are the data containers that are transported between Filters. + * + * Frames may be used only within a Filter during filter graph execution. Accessing Frames outside + * of graph execution may cause unexpected results. + * + * There are two ways to obtain new Frame instances. You can call + * {@link OutputPort#fetchAvailableFrame(int[])} on an OutputPort to obtain a Frame to pass to an + * output. You can also call {@link #create(FrameType, int[])} to obtain + * a detached Frame instance that you may hold onto in your filter. If you need to hold on to a + * Frame that is owned by an input or output queue, you must call + * {@link #retain()} on it. + * + * When you are done using a detached Frame, you must release it yourself. + * + * To access frame data, call any of the {@code lock}-methods. This will give you access to the + * frame data in the desired format. You must pass in a {@code mode} indicating whether you wish + * to read or write to the data. Writing to a read-locked Frame may produce unexpected results and + * interfere with other filters. When you are done reading or writing to the data, you must call + * {@link #unlock()}. Note, that a Frame must be unlocked before you push it into an output queue. + * + * Generally, any type of access format to a Frame's data will be granted. However, it is strongly + * recommended to specify the access format that you intend to use in your filter's signature or + * in the access flags passed to {@code newFrame()}. This will allow the Frame to allocate + * the most efficient backings for the intended type of access. + * + * A frame can be be pushed to an OutputPort by calling the {@link OutputPort#pushFrame(Frame)} + * method. Frames that have been pushed become read-only, and can no longer be modified. + * + * On the other end, a Filter can pull in an input Frame by calling {@link InputPort#pullFrame()} + * on the desired InputPort. Such frames are always read-only. + */ +public class Frame { + + /** Special timestamp value indicating that no time-stamp was set. */ + public static final long TIMESTAMP_NOT_SET = -1; + + /** Frame data access mode: Read */ + public static final int MODE_READ = 1; + /** Frame data access mode: Write */ + public static final int MODE_WRITE = 2; + + BackingStore mBackingStore; + boolean mReadOnly = false; + + // Public API ////////////////////////////////////////////////////////////////////////////////// + /** + * Returns the frame's type. + * @return A FrameType instance describing the frame data-type. + */ + public final FrameType getType() { + return mBackingStore.getFrameType(); + } + + public final int getElementCount() { + return mBackingStore.getElementCount(); + } + + /** + * Set the frame's timestamp in nanoseconds. + * + * @param timestamp the timestamp of this frame in nanoseconds. + */ + public final void setTimestamp(long timestamp) { + mBackingStore.setTimestamp(timestamp); + } + + /** + * @return the frame's timestamp in nanoseconds. + */ + public final long getTimestamp() { + return mBackingStore.getTimestamp(); + } + + /** + * @return the frame's timestamp in milliseconds. + */ + public final long getTimestampMillis() { + return mBackingStore.getTimestamp() / 1000000L; + } + + public final boolean isReadOnly() { + return mReadOnly; + } + + public final FrameValue asFrameValue() { + return FrameValue.create(mBackingStore); + } + + public final FrameValues asFrameValues() { + return FrameValues.create(mBackingStore); + } + + public final FrameBuffer1D asFrameBuffer1D() { + return FrameBuffer1D.create(mBackingStore); + } + + public final FrameBuffer2D asFrameBuffer2D() { + return FrameBuffer2D.create(mBackingStore); + } + + public final FrameImage2D asFrameImage2D() { + return FrameImage2D.create(mBackingStore); + } + + @Override + public String toString() { + return "Frame[" + getType().toString() + "]: " + mBackingStore; + } + + @Override + public boolean equals(Object object) { + return object instanceof Frame && ((Frame)object).mBackingStore == mBackingStore; + } + + public static Frame create(FrameType type, int[] dimensions) { + FrameManager manager = FrameManager.current(); + if (manager == null) { + throw new IllegalStateException("Attempting to create new Frame outside of " + + "FrameManager context!"); + } + return new Frame(type, dimensions, manager); + } + + public final Frame release() { + mBackingStore = mBackingStore.release(); + return mBackingStore != null ? this : null; + } + + public final Frame retain() { + mBackingStore = mBackingStore.retain(); + return this; + } + + public void unlock() { + if (!mBackingStore.unlock()) { + throw new RuntimeException("Attempting to unlock frame that is not locked!"); + } + } + + public int[] getDimensions() { + int[] dim = mBackingStore.getDimensions(); + return dim != null ? Arrays.copyOf(dim, dim.length) : null; + } + + Frame(FrameType type, int[] dimensions, FrameManager manager) { + mBackingStore = new BackingStore(type, dimensions, manager); + } + + Frame(BackingStore backingStore) { + mBackingStore = backingStore; + } + + final void assertAccessible(int mode) { + // Make sure frame is in write-mode + if (mReadOnly && mode == MODE_WRITE) { + throw new RuntimeException("Attempting to write to read-only frame " + this + "!"); + } + } + + final void setReadOnly(boolean readOnly) { + mReadOnly = readOnly; + } + + void resize(int[] newDims) { + int[] oldDims = mBackingStore.getDimensions(); + int oldCount = oldDims == null ? 0 : oldDims.length; + int newCount = newDims == null ? 0 : newDims.length; + if (oldCount != newCount) { + throw new IllegalArgumentException("Cannot resize " + oldCount + "-dimensional " + + "Frame to " + newCount + "-dimensional Frame!"); + } else if (newDims != null && !Arrays.equals(oldDims, newDims)) { + mBackingStore.resize(newDims); + } + } + + Frame makeCpuCopy(FrameManager frameManager) { + Frame frame = new Frame(getType(), getDimensions(), frameManager); + frame.mBackingStore.importStore(mBackingStore); + return frame; + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer1D.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer1D.java new file mode 100644 index 0000000..0e24f5b --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer1D.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.annotation.TargetApi; +import android.renderscript.Allocation; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class FrameBuffer1D extends Frame { + + private int mLength = 0; + + /** + * Access frame's data using a {@link ByteBuffer}. + * This is a convenience method and is equivalent to calling {@code lockData} with an + * {@code accessFormat} of {@code ACCESS_BYTES}. + * When writing to the {@link ByteBuffer}, the byte order should be always set to + * {@link ByteOrder#nativeOrder()}. + * + * @return The byte buffer instance holding the Frame's data. + */ + public ByteBuffer lockBytes(int mode) { + assertAccessible(mode); + return (ByteBuffer)mBackingStore.lockData(mode, BackingStore.ACCESS_BYTES); + } + + /** + * Access frame's data using a RenderScript {@link Allocation}. + * This is a convenience method and is equivalent to calling {@code lockData} with an + * {@code accessFormat} of {@code ACCESS_ALLOCATION}. + * + * @return The Allocation instance holding the Frame's data. + */ + @TargetApi(11) + public Allocation lockAllocation(int mode) { + assertAccessible(mode); + return (Allocation) mBackingStore.lockData(mode, BackingStore.ACCESS_ALLOCATION); + } + + public int getLength() { + return mLength; + } + + @Override + public int[] getDimensions() { + return super.getDimensions(); + } + + /** + * TODO: Documentation. Note that frame contents are invalidated. + */ + @Override + public void resize(int[] newDimensions) { + super.resize(newDimensions); + } + + static FrameBuffer1D create(BackingStore backingStore) { + assertCanCreate(backingStore); + return new FrameBuffer1D(backingStore); + } + + FrameBuffer1D(BackingStore backingStore) { + super(backingStore); + updateLength(backingStore.getDimensions()); + } + + static void assertCanCreate(BackingStore backingStore) { + FrameType type = backingStore.getFrameType(); + if (type.getElementSize() == 0) { + throw new RuntimeException("Cannot access Frame of type " + type + " as a FrameBuffer " + + "instance!"); + } + int[] dims = backingStore.getDimensions(); + if (dims == null || dims.length == 0) { + throw new RuntimeException("Cannot access Frame with no dimensions as a FrameBuffer " + + "instance!"); + } + } + + void updateLength(int[] dimensions) { + mLength = 1; + for (int dim : dimensions) { + mLength *= dim; + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer2D.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer2D.java new file mode 100644 index 0000000..6a7f12a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameBuffer2D.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +public class FrameBuffer2D extends FrameBuffer1D { + + public int getWidth() { + return mBackingStore.getDimensions()[0]; + } + + public int getHeight() { + return mBackingStore.getDimensions()[1]; + } + + static FrameBuffer2D create(BackingStore backingStore) { + assertCanCreate(backingStore); + return new FrameBuffer2D(backingStore); + } + + FrameBuffer2D(BackingStore backingStore) { + super(backingStore); + } + + static void assertCanCreate(BackingStore backingStore) { + FrameBuffer1D.assertCanCreate(backingStore); + int[] dimensions = backingStore.getDimensions(); + int dimCount = dimensions != null ? dimensions.length : 0; + if (dimCount != 2) { + throw new RuntimeException("Cannot access " + dimCount + "-dimensional Frame as a " + + "FrameBuffer2D instance!"); + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameImage2D.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameImage2D.java new file mode 100644 index 0000000..bca94f7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameImage2D.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import androidx.media.filterfw.BackingStore.Backing; + +public class FrameImage2D extends FrameBuffer2D { + + /** + * Access frame's data using a TextureSource. + * This is a convenience method and is equivalent to calling {@code lockData} with an + * {@code accessFormat} of {@code ACCESS_TEXTURE}. + * + * @return The TextureSource instance holding the Frame's data. + */ + public TextureSource lockTextureSource() { + return (TextureSource)mBackingStore.lockData(MODE_READ, BackingStore.ACCESS_TEXTURE); + } + + /** + * Access frame's data using a RenderTarget. + * This is a convenience method and is equivalent to calling {@code lockData} with an + * {@code accessFormat} of {@code ACCESS_RENDERTARGET}. + * + * @return The RenderTarget instance holding the Frame's data. + */ + public RenderTarget lockRenderTarget() { + return (RenderTarget)mBackingStore.lockData(MODE_WRITE, BackingStore.ACCESS_RENDERTARGET); + } + + /** + * Assigns the pixel data of the specified bitmap. + * + * The RGBA pixel data will be extracted from the bitmap and assigned to the frame data. Note, + * that the colors are premultiplied with the alpha channel. If you wish to have + * non-premultiplied colors, you must pass the Frame through an + * {@code UnpremultiplyAlphaFilter}. + * + * @param bitmap The bitmap pixels to assign. + */ + public void setBitmap(Bitmap bitmap) { + bitmap = convertToFrameType(bitmap, mBackingStore.getFrameType()); + validateBitmapSize(bitmap, mBackingStore.getDimensions()); + Backing backing = mBackingStore.lockBacking(MODE_WRITE, BackingStore.ACCESS_BITMAP); + backing.setData(bitmap); + mBackingStore.unlock(); + } + + /** + * Returns the RGBA image contents as a Bitmap instance. + * + * @return a Bitmap instance holding the RGBA Frame image content. + */ + public Bitmap toBitmap() { + Bitmap result = (Bitmap)mBackingStore.lockData(MODE_READ, BackingStore.ACCESS_BITMAP); + mBackingStore.unlock(); + return result; + } + + /** + * Copies the image data from one frame to another. + * + * The source and target rectangles must be given in normalized coordinates, where 0,0 is the + * top-left of the image and 1,1 is the bottom-right. + * + * If the target rectangle is smaller than the target frame, the pixel values outside of the + * target rectangle are undefined. + * + * This method must be called within a Filter during execution. It supports both GL-enabled + * and GL-disabled run contexts. + * + * @param target The target frame to copy to. + * @param sourceRect The source rectangle in normalized coordinates. + * @param targetRect The target rectangle in normalized coordinates. + */ + public void copyToFrame(FrameImage2D target, RectF sourceRect, RectF targetRect) { + if (GraphRunner.current().isOpenGLSupported()) { + gpuImageCopy(this, target, sourceRect, targetRect); + } else { + cpuImageCopy(this, target, sourceRect, targetRect); + } + } + + static FrameImage2D create(BackingStore backingStore) { + assertCanCreate(backingStore); + return new FrameImage2D(backingStore); + } + + FrameImage2D(BackingStore backingStore) { + super(backingStore); + } + + static void assertCanCreate(BackingStore backingStore) { + FrameBuffer2D.assertCanCreate(backingStore); + } + + private static Bitmap convertToFrameType(Bitmap bitmap, FrameType type) { + Bitmap.Config config = bitmap.getConfig(); + Bitmap result = bitmap; + switch(type.getElementId()) { + case FrameType.ELEMENT_RGBA8888: + if (config != Bitmap.Config.ARGB_8888) { + result = bitmap.copy(Bitmap.Config.ARGB_8888, false); + if (result == null) { + throw new RuntimeException("Could not convert bitmap to frame-type " + + "RGBA8888!"); + } + } + break; + default: + throw new IllegalArgumentException("Unsupported frame type '" + type + "' for " + + "bitmap assignment!"); + } + return result; + } + + private void validateBitmapSize(Bitmap bitmap, int[] dimensions) { + if (bitmap.getWidth() != dimensions[0] || bitmap.getHeight() != dimensions[1]) { + throw new IllegalArgumentException("Cannot assign bitmap of size " + bitmap.getWidth() + + "x" + bitmap.getHeight() + " to frame of size " + dimensions[0] + "x" + + dimensions[1] + "!"); + } + } + + private static void gpuImageCopy( + FrameImage2D srcImage, FrameImage2D dstImage, RectF srcRect, RectF dstRect) { + ImageShader idShader = RenderTarget.currentTarget().getIdentityShader(); + // We briefly modify the shader + // TODO: Implement a safer way to save and restore a shared shader. + idShader.setSourceRect(srcRect); + idShader.setTargetRect(dstRect); + idShader.process(srcImage, dstImage); + // And reset it as others may use it as well + idShader.setSourceRect(0f, 0f, 1f, 1f); + idShader.setTargetRect(0f, 0f, 1f, 1f); + } + + private static void cpuImageCopy( + FrameImage2D srcImage, FrameImage2D dstImage, RectF srcRect, RectF dstRect) { + // Convert rectangles to integer rectangles in image dimensions + Rect srcIRect = new Rect((int) srcRect.left * srcImage.getWidth(), + (int) srcRect.top * srcImage.getHeight(), + (int) srcRect.right * srcImage.getWidth(), + (int) srcRect.bottom * srcImage.getHeight()); + Rect dstIRect = new Rect((int) dstRect.left * srcImage.getWidth(), + (int) dstRect.top * srcImage.getHeight(), + (int) dstRect.right * srcImage.getWidth(), + (int) dstRect.bottom * srcImage.getHeight()); + + // Create target canvas + Bitmap.Config config = Bitmap.Config.ARGB_8888; + Bitmap dstBitmap = Bitmap.createBitmap(dstImage.getWidth(), dstImage.getHeight(), config); + Canvas canvas = new Canvas(dstBitmap); + + // Draw source bitmap into target canvas + Paint paint = new Paint(); + paint.setFilterBitmap(true); + Bitmap srcBitmap = srcImage.toBitmap(); + canvas.drawBitmap(srcBitmap, srcIRect, dstIRect, paint); + + // Assign bitmap to output frame + dstImage.setBitmap(dstBitmap); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameManager.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameManager.java new file mode 100644 index 0000000..55ed277 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameManager.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import androidx.media.filterfw.BackingStore.Backing; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Set; + +/** + * The FrameManager tracks, caches, allocates and deallocates frame data. + * All Frame instances are managed by a FrameManager, and belong to exactly one of these. Frames + * cannot be shared across FrameManager instances, however multiple MffContexts may use the same + * FrameManager. + * + * Additionally, frame managers allow attaching Frames under a specified key. This allows decoupling + * filter-graphs by instructing one node to attach a frame under a specific key, and another to + * fetch the frame under the same key. + */ +public class FrameManager { + + /** The default max cache size is set to 12 MB */ + public final static int DEFAULT_MAX_CACHE_SIZE = 12 * 1024 * 1024; + + /** Frame caching policy: No caching */ + public final static int FRAME_CACHE_NONE = 0; + /** Frame caching policy: Drop least recently used frame buffers */ + public final static int FRAME_CACHE_LRU = 1; + /** Frame caching policy: Drop least frequently used frame buffers */ + public final static int FRAME_CACHE_LFU = 2; + + /** Slot Flag: No flags set */ + public final static int SLOT_FLAGS_NONE = 0x00; + /** Slot Flag: Sticky flag set: Frame will remain in slot after fetch. */ + public final static int SLOT_FLAG_STICKY = 0x01; + + private GraphRunner mRunner; + private Set<Backing> mBackings = new HashSet<Backing>(); + private BackingCache mCache; + + private Map<String, FrameSlot> mFrameSlots = new HashMap<String, FrameSlot>(); + + static class FrameSlot { + private FrameType mType; + private int mFlags; + private Frame mFrame = null; + + public FrameSlot(FrameType type, int flags) { + mType = type; + mFlags = flags; + } + + public FrameType getType() { + return mType; + } + + public boolean hasFrame() { + return mFrame != null; + } + + public void releaseFrame() { + if (mFrame != null) { + mFrame.release(); + mFrame = null; + } + } + + // TODO: Type check + public void assignFrame(Frame frame) { + Frame oldFrame = mFrame; + mFrame = frame.retain(); + if (oldFrame != null) { + oldFrame.release(); + } + } + + public Frame getFrame() { + Frame result = mFrame.retain(); + if ((mFlags & SLOT_FLAG_STICKY) == 0) { + releaseFrame(); + } + return result; + } + + public void markWritable() { + if (mFrame != null) { + mFrame.setReadOnly(false); + } + } + } + + private static abstract class BackingCache { + + protected int mCacheMaxSize = DEFAULT_MAX_CACHE_SIZE; + + public abstract Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize); + + public abstract boolean cacheBacking(Backing backing); + + public abstract void clear(); + + public abstract int getSizeLeft(); + + public void setSize(int size) { + mCacheMaxSize = size; + } + + public int getSize() { + return mCacheMaxSize; + } + } + + private static class BackingCacheNone extends BackingCache { + + @Override + public Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) { + return null; + } + + @Override + public boolean cacheBacking(Backing backing) { + return false; + } + + @Override + public void clear() { + } + + @Override + public int getSize() { + return 0; + } + + @Override + public int getSizeLeft() { + return 0; + } + } + + private static abstract class PriorityBackingCache extends BackingCache { + private int mSize = 0; + private PriorityQueue<Backing> mQueue; + + public PriorityBackingCache() { + mQueue = new PriorityQueue<Backing>(4, new Comparator<Backing>() { + @Override + public int compare(Backing left, Backing right) { + return left.cachePriority - right.cachePriority; + } + }); + } + + @Override + public Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) { + for (Backing backing : mQueue) { + int backingAccess = (mode == Frame.MODE_WRITE) + ? backing.writeAccess() + : backing.readAccess(); + if ((backingAccess & access) == access + && dimensionsCompatible(backing.getDimensions(), dimensions) + && (elemSize == backing.getElementSize())) { + mQueue.remove(backing); + mSize -= backing.getSize(); + onFetchBacking(backing); + return backing; + } + } + //Log.w("FrameManager", "Could not find backing for dimensions " + Arrays.toString(dimensions)); + return null; + } + + @Override + public boolean cacheBacking(Backing backing) { + if (reserve(backing.getSize())) { + onCacheBacking(backing); + mQueue.add(backing); + return true; + } + return false; + } + + @Override + public void clear() { + mQueue.clear(); + mSize = 0; + } + + @Override + public int getSizeLeft() { + return mCacheMaxSize - mSize; + } + + protected abstract void onCacheBacking(Backing backing); + + protected abstract void onFetchBacking(Backing backing); + + private boolean reserve(int size) { + //Log.i("FM", "Reserving " + size + " bytes (max: " + mCacheMaxSize + " bytes)."); + //Log.i("FM", "Current size " + mSize); + if (size > mCacheMaxSize) { + return false; + } + mSize += size; + while (mSize > mCacheMaxSize) { + Backing dropped = mQueue.poll(); + mSize -= dropped.getSize(); + //Log.i("FM", "Dropping " + dropped + " with priority " + // + dropped.cachePriority + ". New size: " + mSize + "!"); + dropped.destroy(); + } + return true; + } + + + } + + private static class BackingCacheLru extends PriorityBackingCache { + private int mTimestamp = 0; + + @Override + protected void onCacheBacking(Backing backing) { + backing.cachePriority = 0; + } + + @Override + protected void onFetchBacking(Backing backing) { + ++mTimestamp; + backing.cachePriority = mTimestamp; + } + } + + private static class BackingCacheLfu extends PriorityBackingCache { + @Override + protected void onCacheBacking(Backing backing) { + backing.cachePriority = 0; + } + + @Override + protected void onFetchBacking(Backing backing) { + ++backing.cachePriority; + } + } + + public static FrameManager current() { + GraphRunner runner = GraphRunner.current(); + return runner != null ? runner.getFrameManager() : null; + } + + /** + * Returns the context that the FrameManager is bound to. + * + * @return the MffContext instance that the FrameManager is bound to. + */ + public MffContext getContext() { + return mRunner.getContext(); + } + + /** + * Returns the GraphRunner that the FrameManager is bound to. + * + * @return the GraphRunner instance that the FrameManager is bound to. + */ + public GraphRunner getRunner() { + return mRunner; + } + + /** + * Sets the size of the cache. + * + * Resizes the cache to the specified size in bytes. + * + * @param bytes the new size in bytes. + */ + public void setCacheSize(int bytes) { + mCache.setSize(bytes); + } + + /** + * Returns the size of the cache. + * + * @return the size of the cache in bytes. + */ + public int getCacheSize() { + return mCache.getSize(); + } + + /** + * Imports a frame from another FrameManager. + * + * This will return a frame with the contents of the given frame for use in this FrameManager. + * Note, that there is a substantial cost involved in moving a Frame from one FrameManager to + * another. This may be called from any thread. After the frame has been imported, it may be + * used in the runner that uses this FrameManager. As the new frame may share data with the + * provided frame, that frame must be read-only. + * + * @param frame The frame to import + */ + public Frame importFrame(Frame frame) { + if (!frame.isReadOnly()) { + throw new IllegalArgumentException("Frame " + frame + " must be read-only to import " + + "into another FrameManager!"); + } + return frame.makeCpuCopy(this); + } + + /** + * Adds a new frame slot to the frame manager. + * Filters can reference frame slots to pass frames between graphs or runs. If the name + * specified here is already taken the frame slot is overwritten. You can only + * modify frame-slots while no graph of the frame manager is running. + * + * @param name The name of the slot. + * @param type The type of Frame that will be assigned to this slot. + * @param flags A mask of {@code SLOT} flags. + */ + public void addFrameSlot(String name, FrameType type, int flags) { + assertNotRunning(); + FrameSlot oldSlot = mFrameSlots.get(name); + if (oldSlot != null) { + removeFrameSlot(name); + } + FrameSlot slot = new FrameSlot(type, flags); + mFrameSlots.put(name, slot); + } + + /** + * Removes a frame slot from the frame manager. + * Any frame within the slot is released. You can only modify frame-slots while no graph + * of the frame manager is running. + * + * @param name The name of the slot + * @throws IllegalArgumentException if no such slot exists. + */ + public void removeFrameSlot(String name) { + assertNotRunning(); + FrameSlot slot = getSlot(name); + slot.releaseFrame(); + mFrameSlots.remove(slot); + } + + /** + * TODO: Document! + */ + public void storeFrame(Frame frame, String slotName) { + assertInGraphRun(); + getSlot(slotName).assignFrame(frame); + } + + /** + * TODO: Document! + */ + public Frame fetchFrame(String slotName) { + assertInGraphRun(); + return getSlot(slotName).getFrame(); + } + + /** + * Clears the Frame cache. + */ + public void clearCache() { + mCache.clear(); + } + + /** + * Create a new FrameManager instance. + * + * Creates a new FrameManager instance in the specified context and employing a cache with the + * specified cache type (see the cache type constants defined by the FrameManager class). + * + * @param runner the GraphRunner to bind the FrameManager to. + * @param cacheType the type of cache to use. + */ + FrameManager(GraphRunner runner, int cacheType) { + mRunner = runner; + switch (cacheType) { + case FRAME_CACHE_NONE: + mCache = new BackingCacheNone(); + break; + case FRAME_CACHE_LRU: + mCache = new BackingCacheLru(); + break; + case FRAME_CACHE_LFU: + mCache = new BackingCacheLfu(); + break; + default: + throw new IllegalArgumentException("Unknown cache-type " + cacheType + "!"); + } + } + + Backing fetchBacking(int mode, int access, int[] dimensions, int elemSize) { + return mCache.fetchBacking(mode, access, dimensions, elemSize); + } + + void onBackingCreated(Backing backing) { + if (backing != null) { + mBackings.add(backing); + // Log.i("FrameManager", "RM: Now have " + mBackings.size() + " backings"); + } + } + + void onBackingAvailable(Backing backing) { + if (!backing.shouldCache() || !mCache.cacheBacking(backing)) { + backing.destroy(); + mBackings.remove(backing); + //Log.i("FrameManager", "RM: Now have " + mBackings.size() + " backings (" + mCache.getSizeLeft() + ")"); + } + } + + /** + * Destroying all references makes any Frames that contain them invalid. + */ + void destroyBackings() { + for (Backing backing : mBackings) { + backing.destroy(); + } + mBackings.clear(); + mCache.clear(); + } + + FrameSlot getSlot(String name) { + FrameSlot slot = mFrameSlots.get(name); + if (slot == null) { + throw new IllegalArgumentException("Unknown frame slot '" + name + "'!"); + } + return slot; + } + + void onBeginRun() { + for (FrameSlot slot : mFrameSlots.values()) { + slot.markWritable(); + } + } + + // Internals /////////////////////////////////////////////////////////////////////////////////// + private static boolean dimensionsCompatible(int[] dimA, int[] dimB) { + return dimA == null || dimB == null || Arrays.equals(dimA, dimB); + } + + private void assertNotRunning() { + if (mRunner.isRunning()) { + throw new IllegalStateException("Attempting to modify FrameManager while graph is " + + "running!"); + } + } + + private void assertInGraphRun() { + if (!mRunner.isRunning() || GraphRunner.current() != mRunner) { + throw new IllegalStateException("Attempting to access FrameManager Frame data " + + "outside of graph run-loop!"); + } + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameQueue.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameQueue.java new file mode 100644 index 0000000..c26f937 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameQueue.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import java.util.Vector; + +class FrameQueue { + + public static class Builder { + + private FrameType mReadType = null; + private FrameType mWriteType = null; + + private Vector<FrameQueue> mAttachedQueues = new Vector<FrameQueue>(); + + public Builder() {} + + public void setWriteType(FrameType type) { + mWriteType = type; + } + + public void setReadType(FrameType type) { + mReadType = type; + } + + public void attachQueue(FrameQueue queue) { + mAttachedQueues.add(queue); + } + + public FrameQueue build(String name) { + FrameType type = buildType(); + // TODO: This currently does not work correctly (Try camera -> branch -> target-slot) + //validateType(type, name); + FrameQueue result = new FrameQueue(type, name); + buildQueueImpl(result); + return result; + } + + private void buildQueueImpl(FrameQueue queue) { + QueueImpl queueImpl = queue.new SingleFrameQueueImpl(); + queue.mQueueImpl = queueImpl; + } + + private FrameType buildType() { + FrameType result = FrameType.merge(mWriteType, mReadType); + for (FrameQueue queue : mAttachedQueues) { + result = FrameType.merge(result, queue.mType); + } + return result; + } + + /* + private void validateType(FrameType type, String queueName) { + if (!type.isSpecified()) { + throw new RuntimeException("Cannot build connection queue '" + queueName + "' as " + + "its type (" + type + ") is underspecified!"); + } + } + */ + } + + private interface QueueImpl { + public boolean canPull(); + + public boolean canPush(); + + public Frame pullFrame(); + + public Frame fetchAvailableFrame(int[] dimensions); + + public Frame peek(); + + public void pushFrame(Frame frame); + + public void clear(); + } + + private class SingleFrameQueueImpl implements QueueImpl { + private Frame mFrame = null; + + @Override + public boolean canPull() { + return mFrame != null; + } + + @Override + public boolean canPush() { + return mFrame == null; + } + + @Override + public Frame pullFrame() { + Frame result = mFrame; + mFrame = null; + return result; + } + + @Override + public Frame peek() { + return mFrame; + } + + @Override + public Frame fetchAvailableFrame(int[] dimensions) { + // Note that we cannot use a cached frame here, as we do not know where that cached + // instance would end up. + FrameManager manager = FrameManager.current(); + return new Frame(mType, dimensions, manager); + } + + @Override + public void pushFrame(Frame frame) { + mFrame = frame.retain(); + mFrame.setReadOnly(true); + } + + @Override + public void clear() { + if (mFrame != null) { + mFrame.release(); + mFrame = null; + } + } + } + + private QueueImpl mQueueImpl; + private FrameType mType; + private String mName; + + public FrameType getType() { + return mType; + } + + public boolean canPull() { + return mQueueImpl.canPull(); + } + + public boolean canPush() { + return mQueueImpl.canPush(); + } + + public Frame pullFrame() { + return mQueueImpl.pullFrame(); + } + + public Frame fetchAvailableFrame(int[] dimensions) { + return mQueueImpl.fetchAvailableFrame(dimensions); + } + + public void pushFrame(Frame frame) { + mQueueImpl.pushFrame(frame); + } + + public Frame peek() { + return mQueueImpl.peek(); + } + + @Override + public String toString() { + return mName; + } + + public void clear() { + mQueueImpl.clear(); + } + + private FrameQueue(FrameType type, String name) { + mType = type; + mName = name; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotSource.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotSource.java new file mode 100644 index 0000000..0a093f9 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotSource.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.base; + +import androidx.media.filterfw.*; + +public final class FrameSlotSource extends SlotFilter { + + public FrameSlotSource(MffContext context, String name, String slotName) { + super(context, name, slotName); + } + + @Override + public Signature getSignature() { + // TODO: It would be nice if we could return the slot type here. Not currently possible + // as getSignature() is typically called before a FrameManager and its slots are setup. + return new Signature() + .addOutputPort("frame", Signature.PORT_REQUIRED, FrameType.any()) + .disallowOtherPorts(); + } + + @Override + protected boolean canSchedule() { + return super.canSchedule() && slotHasFrame(); + } + + @Override + protected void onProcess() { + Frame frame = getFrameManager().fetchFrame(mSlotName); + getConnectedOutputPort("frame").pushFrame(frame); + frame.release(); + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotTarget.java new file mode 100644 index 0000000..55648c6 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameSlotTarget.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.base; + +import androidx.media.filterfw.*; + +public final class FrameSlotTarget extends SlotFilter { + + public FrameSlotTarget(MffContext context, String name, String slotName) { + super(context, name, slotName); + } + + @Override + public Signature getSignature() { + // TODO: It would be nice if we could return the slot type here. Not currently possible + // as getSignature() is typically called before a FrameManager and its slots are setup. + return new Signature() + .addInputPort("frame", Signature.PORT_REQUIRED, FrameType.any()) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + Frame frame = getConnectedInputPort("frame").pullFrame(); + getFrameManager().storeFrame(frame, mSlotName); + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameType.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameType.java new file mode 100644 index 0000000..bfa4018 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameType.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + + +/** + * A FrameType instance specifies the data format of a Frame. + * + * FrameTypes are used mainly by Filters to specify the data type they intend to consume or produce. + * When filters are connected, their FrameType information is analyzed and checked for + * compatibility. This allows Filter writers to assume a certain data input type. It also helps + * filter-graph designers determine which filters can be hooked up to one another. + * + * A FrameType generally consists of an element type and number of dimensions. The currently + * supported element types are: + * + * <ul> + * <li>int8, int16, int32, in64</li> + * <li>float32, float64</li> + * <li>rgba8888</li> + * <li>object</li> + * <li>don't-care</li> + * </ul> + * + * If the object element type is used, class information may be appended to the FrameType to + * indicate what class of objects are expected. When constructing an object based FrameType, you + * have the option of either specifying a type that represents a single object of that class, or + * an array of objects (see the {@link #single()} and {@link #array()} constructors). A single + * object has a dimensionality of 0, while an array has a dimensionality of 1. + * + * When constructing a non-object type, you have the option of creating a 1D or 2D buffer, or + * a 2D image (see the {@link #buffer1D(int)}, {@link #buffer2D(int)}, and + * {@link #image2D(int, int)} constructors). To optimize access, provide access hints when making + * an image type. + * + * Finally, it is possible to create a wild-card type with the {@link #any()} constructor. This + * type matches any other type. Note, that this is a more general type than a {@code single(Object)} + * type that matches only object-base types (of any Object subclass). You may also specify the + * leave the element of any type unspecified by using the {@code ELEMENT_DONTCARE} constant. + * + * When a graph is connected the types between outputs and inputs are merged to a queue-type. All + * Frames in this queue will be of that type. In order for a merge to succeed the following + * conditions must hold: + * + * <ul> + * <li>The element types must be identical.</li> + * <li>The dimensions must match (except for singles and arrays, see below).</li> + * <li>For object-based types: The classes must be compatible.</li> + * <li>If one of the types is a wild-card, both types are always compatible.</li> + * </ul> + * + * Class compatibility is determined in an optimistic fashion, i.e. one class must be the subclass + * of the other. It does not matter which of the types is the subclass of the other. For instance, + * if one Filter outputs a type of class {@code Object}, and the consumer expects a Filter of type + * {@code Bitmap}, the connection is considered compatible. (Of course if at runtime a non-Bitmap + * object is produced, this will cause a runtime exception to be thrown). + * + * For convenience, single and array object-based types are compatible with one another. This + * in turn means that Frames with a single object can be accessed as an array with a single entry, + * and array based Frames can be accessed as a single object of the array class. For this reason + * you should prefer consuming objects as array types (if it makes sense for that specific port), + * as this will allow your Filter to handle multiple objects in one Frame while not giving up the + * possibility to deal with singles. + * TODO: This needs to be reworked. An array(int) should not be interchangeable with a single(int), + * but rather with a single(int[]). Use ArraySelectFilter for the former! + * + * After the types are merged, the queue-type must be a fully specified type. This means that the + * type must have its element and dimensions specified. This ensures that filters that need to + * query their input or output types receive meaningful information. + */ +public final class FrameType { + + public final static int ELEMENT_DONTCARE = 0; + public final static int ELEMENT_OBJECT = 1; + + public final static int ELEMENT_INT8 = 100; + public final static int ELEMENT_INT16 = 101; + public final static int ELEMENT_INT32 = 102; + public final static int ELEMENT_INT64 = 103; + + public final static int ELEMENT_FLOAT32 = 200; + public final static int ELEMENT_FLOAT64 = 201; + + public final static int ELEMENT_RGBA8888 = 301; + + public final static int READ_CPU = 0x01; + public final static int READ_GPU = 0x02; + public final static int READ_ALLOCATION = 0x04; + public final static int WRITE_CPU = 0x08; + public final static int WRITE_GPU = 0x10; + public final static int WRITE_ALLOCATION = 0x20; + + private final static int ACCESS_UNKNOWN = 0x00; + + private final int mElementId; + private final int mDimensions; + private final int mAccessHints; + private final Class<?> mClass; + + private static SimpleCache<String, FrameType> mTypeCache = + new SimpleCache<String, FrameType>(64); + + /** + * Constructs a wild-card FrameType that matches any other FrameType. + * @return The wild-card FrameType instance. + */ + public static FrameType any() { + return FrameType.fetchType(ELEMENT_DONTCARE, -1, ACCESS_UNKNOWN); + } + + /** + * Constructs an object-based single FrameType that matches object-based FrameTypes of any + * class. + * @return A single object-based FrameType instance. + */ + public static FrameType single() { + return FrameType.fetchType(null, 0); + } + + /** + * Constructs an object-based single FrameType of the specified class. + * @param clazz The class of the FrameType. + * @return A single object-base FrameType instance of the specified class. + */ + public static FrameType single(Class<?> clazz) { + return FrameType.fetchType(clazz, 0); + } + + /** + * Constructs an object-based array FrameType that matches object-based FrameTypes of any class. + * @return An array object-based FrameType instance. + */ + public static FrameType array() { + return FrameType.fetchType(null, 1); + } + + /** + * Constructs an object-based array FrameType with elements of the specified class. + * @param clazz The class of the array elements (not the array type). + * @return An array object-based FrameType instance of the specified class. + */ + public static FrameType array(Class<?> clazz) { + return FrameType.fetchType(clazz, 1); + } + + /** + * Constructs a one-dimensional buffer type of the specified element. + * @param elementType One of the {@code ELEMENT} constants. + * @return A 1D buffer FrameType instance. + */ + public static FrameType buffer1D(int elementType) { + return FrameType.fetchType(elementType, 1, ACCESS_UNKNOWN); + } + + /** + * Constructs a two-dimensional buffer type of the specified element. + * @param elementType One of the {@code ELEMENT} constants. + * @return A 2D buffer FrameType instance. + */ + public static FrameType buffer2D(int elementType) { + return FrameType.fetchType(elementType, 2, ACCESS_UNKNOWN); + } + + /** + * Constructs a two-dimensional image type of the specified element. + * @param elementType One of the {@code ELEMENT} constants. + * @param accessHint A bit-mask of access flags (see {@code READ} and {@code WRITE} constants). + * @return A 2D image FrameType instance. + */ + public static FrameType image2D(int elementType, int accessHint) { + return FrameType.fetchType(elementType, 2, accessHint); + } + + /** + * Converts the current array type to a single type. + * The type must be an object-based type. If the type is already a single type, this does + * nothing. + * @return type as a single type. + */ + public FrameType asSingle() { + if (mElementId != ELEMENT_OBJECT) { + throw new RuntimeException("Calling asSingle() on non-object type!"); + } + return FrameType.fetchType(mClass, 0); + } + + /** + * Converts the current single type to an array type. + * The type must be an object-based type. If the type is already an array type, this does + * nothing. + * @return type as an array type. + */ + public FrameType asArray() { + if (mElementId != ELEMENT_OBJECT) { + throw new RuntimeException("Calling asArray() on non-object type!"); + } + return FrameType.fetchType(mClass, 1); + } + + /** + * Returns the FrameType's class specifier, or null if no class was set or the receiver is not + * an object-based type. + * @return The FrameType's class specifier or null. + */ + public Class<?> getContentClass() { + return mClass; + } + + /** + * Returns the FrameType's element id. + * @return The element id constant. + */ + public int getElementId() { + return mElementId; + } + + /** + * Returns the number of bytes of the FrameType's element, or 0 if no such size can be + * determined. + * @return The number of bytes of the FrameType's element. + */ + public int getElementSize() { + switch (mElementId) { + case ELEMENT_INT8: + return 1; + case ELEMENT_INT16: + return 2; + case ELEMENT_INT32: + case ELEMENT_FLOAT32: + case ELEMENT_RGBA8888: + return 4; + case ELEMENT_INT64: + case ELEMENT_FLOAT64: + return 4; + default: + return 0; + } + } + + /** + * Returns the access hints bit-mask of the FrameType. + * @return The access hints bit-mask of the FrameType. + */ + public int getAccessHints() { + return mAccessHints; + } + + /** + * Returns the number of dimensions of the FrameType or -1 if no dimensions were set. + * @return The number of dimensions of the FrameType. + */ + public int getNumberOfDimensions() { + return mDimensions; + } + + /** + * Returns true, if the FrameType is fully specified. + * + * A FrameType is fully specified if its element and dimensions are specified. + * + * @return true, if the FrameType is fully specified. + */ + public boolean isSpecified() { + return mElementId != ELEMENT_DONTCARE && mDimensions >= 0; + } + + @Override + public boolean equals(Object object) { + if (object instanceof FrameType) { + FrameType type = (FrameType) object; + return mElementId == type.mElementId && mDimensions == type.mDimensions + && mAccessHints == type.mAccessHints && mClass == type.mClass; + } + return false; + } + + @Override + public int hashCode() { + return mElementId ^ mDimensions ^ mAccessHints ^ mClass.hashCode(); + } + + @Override + public String toString() { + String result = elementToString(mElementId, mClass) + "[" + mDimensions + "]"; + if ((mAccessHints & READ_CPU) != 0) { + result += "(rcpu)"; + } + if ((mAccessHints & READ_GPU) != 0) { + result += "(rgpu)"; + } + if ((mAccessHints & READ_ALLOCATION) != 0) { + result += "(ralloc)"; + } + if ((mAccessHints & WRITE_CPU) != 0) { + result += "(wcpu)"; + } + if ((mAccessHints & WRITE_GPU) != 0) { + result += "(wgpu)"; + } + if ((mAccessHints & WRITE_ALLOCATION) != 0) { + result += "(walloc)"; + } + return result; + } + + String keyString() { + return keyValueForType(mElementId, mDimensions, mAccessHints, mClass); + } + + static FrameType tryMerge(FrameType writer, FrameType reader) { + if (writer.mElementId == ELEMENT_DONTCARE) { + return reader; + } else if (reader.mElementId == ELEMENT_DONTCARE) { + return writer; + } else if (writer.mElementId == ELEMENT_OBJECT && reader.mElementId == ELEMENT_OBJECT) { + return tryMergeObjectTypes(writer, reader); + } else if (writer.mDimensions > 0 && writer.mElementId == reader.mElementId) { + return tryMergeBuffers(writer, reader); + } else { + return null; + } + } + + static FrameType tryMergeObjectTypes(FrameType writer, FrameType reader) { + int dimensions = Math.max(writer.mDimensions, reader.mDimensions); + Class<?> mergedClass = mergeClasses(writer.mClass, reader.mClass); + boolean success = mergedClass != null || writer.mClass == null; + return success ? FrameType.fetchType(mergedClass, dimensions) : null; + } + + static FrameType tryMergeBuffers(FrameType writer, FrameType reader) { + if (writer.mDimensions == reader.mDimensions) { + int accessHints = writer.mAccessHints | reader.mAccessHints; + return FrameType.fetchType(writer.mElementId, writer.mDimensions, accessHints); + } + return null; + } + + static FrameType merge(FrameType writer, FrameType reader) { + FrameType result = tryMerge(writer, reader); + if (result == null) { + throw new RuntimeException( + "Incompatible types in connection: " + writer + " vs. " + reader + "!"); + } + return result; + } + + private static String keyValueForType(int elemId, int dims, int hints, Class<?> clazz) { + return elemId + ":" + dims + ":" + hints + ":" + (clazz != null ? clazz.getName() : "0"); + } + + private static String elementToString(int elemId, Class<?> clazz) { + switch (elemId) { + case ELEMENT_INT8: + return "int8"; + case ELEMENT_INT16: + return "int16"; + case ELEMENT_INT32: + return "int32"; + case ELEMENT_INT64: + return "int64"; + case ELEMENT_FLOAT32: + return "float32"; + case ELEMENT_FLOAT64: + return "float64"; + case ELEMENT_RGBA8888: + return "rgba8888"; + case ELEMENT_OBJECT: + return "<" + (clazz == null ? "*" : clazz.getSimpleName()) + ">"; + case ELEMENT_DONTCARE: + return "*"; + default: + return "?"; + } + } + + private static Class<?> mergeClasses(Class<?> classA, Class<?> classB) { + // Return the most specialized class. + if (classA == null) { + return classB; + } else if (classB == null) { + return classA; + } else if (classA.isAssignableFrom(classB)) { + return classB; + } else if (classB.isAssignableFrom(classA)) { + return classA; + } else { + return null; + } + } + + private static FrameType fetchType(int elementId, int dimensions, int accessHints) { + return fetchType(elementId, dimensions, accessHints, null); + } + + private static FrameType fetchType(Class<?> clazz, int dimensions) { + return fetchType(ELEMENT_OBJECT, dimensions, ACCESS_UNKNOWN, clazz); + } + + private static FrameType fetchType( + int elementId, int dimensions, int accessHints, Class<?> clazz) { + String typeKey = FrameType.keyValueForType(elementId, dimensions, accessHints, clazz); + FrameType type = mTypeCache.get(typeKey); + if (type == null) { + type = new FrameType(elementId, dimensions, accessHints, clazz); + mTypeCache.put(typeKey, type); + } + return type; + } + + private FrameType(int elementId, int dimensions, int accessHints, Class<?> clazz) { + mElementId = elementId; + mDimensions = dimensions; + mClass = clazz; + mAccessHints = accessHints; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValue.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValue.java new file mode 100644 index 0000000..fb007e2 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValue.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import androidx.media.filterfw.BackingStore.Backing; + +public class FrameValue extends Frame { + + public Object getValue() { + Object result = mBackingStore.lockData(MODE_READ, BackingStore.ACCESS_OBJECT); + mBackingStore.unlock(); + return result; + } + + public void setValue(Object value) { + Backing backing = mBackingStore.lockBacking(MODE_WRITE, BackingStore.ACCESS_OBJECT); + backing.setData(value); + mBackingStore.unlock(); + } + + static FrameValue create(BackingStore backingStore) { + assertObjectBased(backingStore.getFrameType()); + return new FrameValue(backingStore); + } + + FrameValue(BackingStore backingStore) { + super(backingStore); + } + + static void assertObjectBased(FrameType type) { + if (type.getElementId() != FrameType.ELEMENT_OBJECT) { + throw new RuntimeException("Cannot access non-object based Frame as FrameValue!"); + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValues.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValues.java new file mode 100644 index 0000000..fbddcb1 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/FrameValues.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import java.lang.reflect.Array; + +public class FrameValues extends FrameValue { + + /** + * Returns the number of values in the Frame. + * + * This returns 1, if the Frame value is null, or if the value is not an array. + * + * @return The number of values in the Frame. + */ + public int getCount() { + Object value = super.getValue(); + if (value == null || !value.getClass().isArray()) { + return 1; + } else { + return Array.getLength(super.getValue()); + } + } + + /** + * Returns the values in the Frame as an array. + * + * Note, that this may be called on Frames that have a non-array object assigned to them. In + * that case, this method will wrap the object in an array and return that. This way, filters + * can treat any object based frame as arrays. + * + * @return The array of values in this frame. + */ + public Object getValues() { + Object value = super.getValue(); + if (value == null || value.getClass().isArray()) { + return super.getValue(); + } else { + // Allow reading a single as an array. + Object[] array = (Object[])Array.newInstance(value.getClass(), 1); + array[0] = value; + return array; + } + } + + /** + * Returns the value at the specified index. + * + * In case the value is null or not an array, the index must be 0, and the value itself is + * returned. + * + * @param index The index to access. + * @return The value at that index. + */ + public Object getValueAtIndex(int index) { + Object value = super.getValue(); + if (value == null || !value.getClass().isArray()) { + if (index != 0) { + throw new ArrayIndexOutOfBoundsException(index); + } else { + return value; + } + } else { + return Array.get(value, index); + } + } + + /** + * Returns the value as a FrameValue at the specified index. + * + * Use this if you want to access elements as FrameValues. You must release the result when + * you are done using it. + * + * @param index The index to access. + * @return The value as a FrameValue at that index (must release). + */ + public FrameValue getFrameValueAtIndex(int index) { + Object value = getValueAtIndex(index); + FrameValue result = Frame.create(getType().asSingle(), new int[0]).asFrameValue(); + result.setValue(value); + return result; + } + + /** + * Assign the array of values to the frame. + * + * You may assign null or a non-array object, which are interpreted as a 1-length array. + * + * @param values The values to assign to the frame. + */ + public void setValues(Object values) { + super.setValue(values); + } + + /** + * Assign a value at the specified index. + * + * In case the held value is not an array, the index must be 0, and the object will be replaced + * by the new object. + * + * @param value The value to assign. + * @param index The index to assign to. + */ + public void setValueAtIndex(Object value, int index) { + super.assertAccessible(MODE_WRITE); + Object curValue = super.getValue(); + if (curValue == null || !curValue.getClass().isArray()) { + if (index != 0) { + throw new ArrayIndexOutOfBoundsException(index); + } else { + curValue = value; + } + } else { + Array.set(curValue, index, value); + } + } + + /** + * Assign a FrameValue's value at the specified index. + * + * This method unpacks the FrameValue and assigns the unpacked value to the specified index. + * This does not affect the retain-count of the passed Frame. + * + * @param frame The frame value to assign. + * @param index The index to assign to. + */ + public void setFrameValueAtIndex(FrameValue frame, int index) { + Object value = frame.getValue(); + setValueAtIndex(value, index); + } + + static FrameValues create(BackingStore backingStore) { + assertObjectBased(backingStore.getFrameType()); + return new FrameValues(backingStore); + } + + FrameValues(BackingStore backingStore) { + super(backingStore); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GLToolbox.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GLToolbox.java new file mode 100644 index 0000000..1c3c7e9 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GLToolbox.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.graphics.Bitmap; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.os.Looper; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +/** + * TODO: Make this package-private as RenderTarget and TextureSource should suffice as public + * facing OpenGL utilities. + * @hide + */ +public class GLToolbox { + + public static int textureNone() { + return 0; + } + + public static boolean isTexture(int texId) { + return GLES20.glIsTexture(texId); + } + + public static void deleteTexture(int texId) { + int[] textures = new int[] { texId }; + assertNonUiThread("glDeleteTextures"); + GLES20.glDeleteTextures(1, textures, 0); + checkGlError("glDeleteTextures"); + } + + public static void deleteFbo(int fboId) { + int[] fbos = new int[] { fboId }; + assertNonUiThread("glDeleteFramebuffers"); + GLES20.glDeleteFramebuffers(1, fbos, 0); + checkGlError("glDeleteFramebuffers"); + } + + public static int generateTexture() { + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + checkGlError("glGenTextures"); + return textures[0]; + } + + public static int generateFbo() { + int[] fbos = new int[1]; + GLES20.glGenFramebuffers(1, fbos, 0); + checkGlError("glGenFramebuffers"); + return fbos[0]; + } + + public static void readFbo(int fboId, ByteBuffer pixels, int width, int height) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId); + GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels); + checkGlError("glReadPixels"); + } + + public static void readTarget(RenderTarget target, ByteBuffer pixels, int width, int height) { + target.focus(); + GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels); + checkGlError("glReadPixels"); + } + + public static int attachedTexture(int fboId) { + int[] params = new int[1]; + GLES20.glGetFramebufferAttachmentParameteriv( + GLES20.GL_FRAMEBUFFER, + GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, + params, 0); + checkGlError("glGetFramebufferAttachmentParameteriv"); + return params[0]; + } + + public static void attachTextureToFbo(int texId, int fboId) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, + GLES20.GL_COLOR_ATTACHMENT0, + GLES20.GL_TEXTURE_2D, + texId, + 0); + checkGlError("glFramebufferTexture2D"); + } + + public static void allocateTexturePixels(int texId, int target, int width, int height) { + setTexturePixels(texId, target, (ByteBuffer)null, width, height); + } + + public static void setTexturePixels(int texId, int target, Bitmap bitmap) { + GLES20.glBindTexture(target, texId); + GLUtils.texImage2D(target, 0, bitmap, 0); + checkGlError("glTexImage2D"); + setDefaultTexParams(); + } + + public static void setTexturePixels(int texId, int target, ByteBuffer pixels, + int width, int height) { + GLES20.glBindTexture(target, texId); + + // For some devices, "pixels" being null causes system error. + if (pixels == null) { + pixels = ByteBuffer.allocateDirect(width * height * 4); + } + GLES20.glTexImage2D(target, 0, GLES20.GL_RGBA, width, height, 0, + GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels); + checkGlError("glTexImage2D"); + setDefaultTexParams(); + } + + public static void setDefaultTexParams() { + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, + GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + checkGlError("glTexParameteri"); + } + + public static int vboNone() { + return 0; + } + + public static int generateVbo() { + int[] vbos = new int[1]; + GLES20.glGenBuffers(1, vbos, 0); + checkGlError("glGenBuffers"); + return vbos[0]; + } + + public static void setVboData(int vboId, ByteBuffer data) { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vboId); + GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, data.remaining(), data, GLES20.GL_STATIC_DRAW); + checkGlError("glBufferData"); + } + + public static void setVboFloats(int vboId, float[] values) { + int len = values.length * 4; + ByteBuffer buffer = ByteBuffer.allocateDirect(len).order(ByteOrder.nativeOrder()); + setVboData(vboId, buffer); + } + + public static boolean isVbo(int vboId) { + return GLES20.glIsBuffer(vboId); + } + + public static void deleteVbo(int vboId) { + int[] buffers = new int[] { vboId }; + GLES20.glDeleteBuffers(1, buffers, 0); + checkGlError("glDeleteBuffers"); + } + + public static void checkGlError(String operation) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + throw new RuntimeException("GL Operation '" + operation + "' caused error " + + Integer.toHexString(error) + "!"); + } + } + + /** + * Make sure we are not operating in the UI thread. + * + * It is often tricky to track down bugs that happen when issuing GL commands in the UI thread. + * This is especially true when releasing GL resources. Often this will cause errors much later + * on. Therefore we make sure we do not do these dangerous operations on the UI thread. + */ + private static void assertNonUiThread(String operation) { + if (Looper.getMainLooper().getThread() == Thread.currentThread()) { + throw new RuntimeException("Attempting to perform GL operation '" + operation + + "' on UI thread!"); + } + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphExporter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphExporter.java new file mode 100644 index 0000000..0013965 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphExporter.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2012 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. + */ + +// This class provides functions to export a FilterGraph. + +package androidx.media.filterfw; + +import android.content.Context; + +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; + +/** + * This class provides functions to export a FilterGraph as a DOT file. + */ +public class GraphExporter { + + /** + * Exports the graph as DOT (see http://en.wikipedia.org/wiki/DOT_language). + * Using the exported file, the graph can be visualized e.g. with the command line tool dot. + * Optionally, one may /exclude/ unconnected optional ports (third parameter = false), + * since they can quickly clutter the visualization (and, depending on the purpose, may not + * be interesting). + * + * Example workflow: + * 1. run application on device, make sure it calls exportGraphAsDOT(...); + * 2. adb pull /data/data/<application name>/files/<graph filename>.gv graph.gv + * 3. dot -Tpng graph.gv -o graph.png + * 4. eog graph.png + */ + static public void exportAsDot(FilterGraph graph, String filename, + boolean includeUnconnectedOptionalPorts) + throws java.io.FileNotFoundException, java.io.IOException { + // Initialize, open file stream + Context myAppContext = graph.getContext().getApplicationContext(); + Filter[] filters = graph.getAllFilters(); + FileOutputStream fOut = myAppContext.openFileOutput(filename, Context.MODE_PRIVATE); + OutputStreamWriter dotFile = new OutputStreamWriter(fOut); + + // Write beginning of DOT file + dotFile.write("digraph graphname {\n"); + dotFile.write(" node [shape=record];\n"); + + // N.B. For specification and lots of examples of the DOT language, see + // http://www.graphviz.org/Documentation/dotguide.pdf + + // Iterate over all filters of the graph, write corresponding DOT node elements + + for(Filter filter : filters) { + dotFile.write(getDotName(" " + filter.getName()) + " [label=\"{"); + + // Write upper part of element (i.e., input ports) + Set<String> inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts); + if(inputPorts.size() > 0) { + dotFile.write(" { "); + int counter = 0; + for(String p : inputPorts) { + dotFile.write("<" + getDotName(p) + "_IN>" + p); + if(++counter != inputPorts.size()) dotFile.write(" | "); + } + dotFile.write(" } | "); + } + + // Write center part of element (i.e., element label) + dotFile.write(filter.getName()); + + // Write lower part of element (i.e., output ports) + Set<String> outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts); + if(outputPorts.size() > 0) { + dotFile.write(" | { "); + int counter = 0; + for(String p : outputPorts) { + dotFile.write("<" + getDotName(p) + "_OUT>" + p); + if(++counter != outputPorts.size()) dotFile.write(" | "); + } + dotFile.write(" } "); + } + + dotFile.write("}\"];\n"); + } + dotFile.write("\n"); + + // Iterate over all filters again to collect connections and find unconnected ports + + int dummyNodeCounter = 0; + for(Filter filter : filters) { + Set<String> outputPorts = getOutputPorts(filter, includeUnconnectedOptionalPorts); + for(String portName : outputPorts) { + OutputPort source = filter.getConnectedOutputPort(portName); + if(source != null) { + // Found a connection, draw it + InputPort target = source.getTarget(); + dotFile.write(" " + + getDotName(source.getFilter().getName()) + ":" + + getDotName(source.getName()) + "_OUT -> " + + getDotName(target.getFilter().getName()) + ":" + + getDotName(target.getName()) + "_IN;\n" ); + } else { + // Found a unconnected output port, add dummy node + String color = filter.getSignature().getOutputPortInfo(portName).isRequired() + ? "red" : "blue"; // red for unconnected, required ports + dotFile.write(" " + + "dummy" + (++dummyNodeCounter) + + " [shape=point,label=\"\",color=" + color + "];\n" + + " " + getDotName(filter.getName()) + ":" + + getDotName(portName) + "_OUT -> " + + "dummy" + dummyNodeCounter + " [color=" + color + "];\n"); + } + } + + Set<String> inputPorts = getInputPorts(filter, includeUnconnectedOptionalPorts); + for(String portName : inputPorts) { + InputPort target = filter.getConnectedInputPort(portName); + if(target != null) { + // Found a connection -- nothing to do, connections have been written out above + } else { + // Found a unconnected input port, add dummy node + String color = filter.getSignature().getInputPortInfo(portName).isRequired() + ? "red" : "blue"; // red for unconnected, required ports + dotFile.write(" " + + "dummy" + (++dummyNodeCounter) + + " [shape=point,label=\"\",color=" + color + "];\n" + + " dummy" + dummyNodeCounter + " -> " + + getDotName(filter.getName()) + ":" + + getDotName(portName) + "_IN [color=" + color + "];\n"); + } + } + } + + // Write end of DOT file, close file stream + dotFile.write("}\n"); + dotFile.flush(); + dotFile.close(); + } + + // Internal methods + + // From element's name in XML, create DOT-allowed element name + static private String getDotName(String raw) { + return raw.replaceAll("\\.", "___"); // DOT does not allow . in element names + } + + // Retrieve all input ports of a filter, including: + // unconnected ports (which can not be retrieved from the filter, only from the signature), and + // additional (connected) ports not listed in the signature (which is allowed by default, + // unless disallowOtherInputs is defined in signature). + // With second parameter = false, *omit* unconnected optional ports. + static private Set<String> getInputPorts(Filter filter, boolean includeUnconnectedOptional) { + // add (connected) ports from filter + Set<String> ports = new HashSet<String>(); + ports.addAll(filter.getConnectedInputPortMap().keySet()); + + // add (unconnected) ports from signature + HashMap<String, Signature.PortInfo> signaturePorts = filter.getSignature().getInputPorts(); + if(signaturePorts != null){ + for(Entry<String, Signature.PortInfo> e : signaturePorts.entrySet()) { + if(includeUnconnectedOptional || e.getValue().isRequired()) { + ports.add(e.getKey()); + } + } + } + return ports; + } + + // Retrieve all output ports of a filter (analogous to above function) + static private Set<String> getOutputPorts(Filter filter, boolean includeUnconnectedOptional) { + // add (connected) ports from filter + Set<String> ports = new HashSet<String>(); + ports.addAll(filter.getConnectedOutputPortMap().keySet()); + + // add (unconnected) ports from signature + HashMap<String, Signature.PortInfo> signaturePorts = filter.getSignature().getOutputPorts(); + if(signaturePorts != null){ + for(Entry<String, Signature.PortInfo> e : signaturePorts.entrySet()) { + if(includeUnconnectedOptional || e.getValue().isRequired()) { + ports.add(e.getKey()); + } + } + } + return ports; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphInputSource.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphInputSource.java new file mode 100644 index 0000000..03b3abe --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphInputSource.java @@ -0,0 +1,58 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package androidx.media.filterpacks.base; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.Signature; + +public class GraphInputSource extends Filter { + + private Frame mFrame = null; + + public GraphInputSource(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("frame", Signature.PORT_REQUIRED, FrameType.any()) + .disallowOtherInputs(); + } + + public void pushFrame(Frame frame) { + if (mFrame != null) { + mFrame.release(); + } + if (frame == null) { + throw new RuntimeException("Attempting to assign null-frame!"); + } + mFrame = frame.retain(); + } + + @Override + protected void onProcess() { + if (mFrame != null) { + getConnectedOutputPort("frame").pushFrame(mFrame); + mFrame.release(); + mFrame = null; + } + } + + @Override + protected void onTearDown() { + if (mFrame != null) { + mFrame.release(); + mFrame = null; + } + } + + @Override + protected boolean canSchedule() { + return super.canSchedule() && mFrame != null; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphOutputTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphOutputTarget.java new file mode 100644 index 0000000..1f3be10 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphOutputTarget.java @@ -0,0 +1,60 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package androidx.media.filterpacks.base; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.Signature; + +public class GraphOutputTarget extends Filter { + + private Frame mFrame = null; + private FrameType mType = FrameType.any(); + + public GraphOutputTarget(MffContext context, String name) { + super(context, name); + } + + // TODO: During initialization only? + public void setType(FrameType type) { + mType = type; + } + + public FrameType getType() { + return mType; + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("frame", Signature.PORT_REQUIRED, mType) + .disallowOtherInputs(); + } + + // Returns a retained frame! + public Frame pullFrame() { + Frame result = null; + if (mFrame != null) { + result = mFrame; + mFrame = null; + } + return result; + } + + @Override + protected void onProcess() { + Frame frame = getConnectedInputPort("frame").pullFrame(); + if (mFrame != null) { + mFrame.release(); + } + mFrame = frame.retain(); + } + + @Override + protected boolean canSchedule() { + return super.canSchedule() && mFrame == null; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphReader.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphReader.java new file mode 100644 index 0000000..ef885e3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphReader.java @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.text.TextUtils; + +import java.io.InputStream; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +/** + * A GraphReader allows obtaining filter graphs from XML graph files or strings. + */ +public class GraphReader { + + private static interface Command { + public void execute(CommandStack stack); + } + + private static class CommandStack { + private ArrayList<Command> mCommands = new ArrayList<Command>(); + private FilterGraph.Builder mBuilder; + private FilterFactory mFactory; + private MffContext mContext; + + public CommandStack(MffContext context) { + mContext = context; + mBuilder = new FilterGraph.Builder(mContext); + mFactory = new FilterFactory(); + } + + public void execute() { + for (Command command : mCommands) { + command.execute(this); + } + } + + public void append(Command command) { + mCommands.add(command); + } + + public FilterFactory getFactory() { + return mFactory; + } + + public MffContext getContext() { + return mContext; + } + + protected FilterGraph.Builder getBuilder() { + return mBuilder; + } + } + + private static class ImportPackageCommand implements Command { + private String mPackageName; + + public ImportPackageCommand(String packageName) { + mPackageName = packageName; + } + + @Override + public void execute(CommandStack stack) { + try { + stack.getFactory().addPackage(mPackageName); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e.getMessage()); + } + } + } + + private static class AddLibraryCommand implements Command { + private String mLibraryName; + + public AddLibraryCommand(String libraryName) { + mLibraryName = libraryName; + } + + @Override + public void execute(CommandStack stack) { + FilterFactory.addFilterLibrary(mLibraryName); + } + } + + private static class AllocateFilterCommand implements Command { + private String mClassName; + private String mFilterName; + + public AllocateFilterCommand(String className, String filterName) { + mClassName = className; + mFilterName = filterName; + } + + @Override + public void execute(CommandStack stack) { + Filter filter = null; + try { + filter = stack.getFactory().createFilterByClassName(mClassName, + mFilterName, + stack.getContext()); + } catch (IllegalArgumentException e) { + throw new RuntimeException("Error creating filter " + mFilterName + "!", e); + } + stack.getBuilder().addFilter(filter); + } + } + + private static class AddSourceSlotCommand implements Command { + private String mName; + private String mSlotName; + + public AddSourceSlotCommand(String name, String slotName) { + mName = name; + mSlotName = slotName; + } + + @Override + public void execute(CommandStack stack) { + stack.getBuilder().addFrameSlotSource(mName, mSlotName); + } + } + + private static class AddTargetSlotCommand implements Command { + private String mName; + private String mSlotName; + + public AddTargetSlotCommand(String name, String slotName) { + mName = name; + mSlotName = slotName; + } + + @Override + public void execute(CommandStack stack) { + stack.getBuilder().addFrameSlotTarget(mName, mSlotName); + } + } + + private static class AddVariableCommand implements Command { + private String mName; + private Object mValue; + + public AddVariableCommand(String name, Object value) { + mName = name; + mValue = value; + } + + @Override + public void execute(CommandStack stack) { + stack.getBuilder().addVariable(mName, mValue); + } + } + + private static class SetFilterInputCommand implements Command { + private String mFilterName; + private String mFilterInput; + private Object mValue; + + public SetFilterInputCommand(String filterName, String input, Object value) { + mFilterName = filterName; + mFilterInput = input; + mValue = value; + } + + @Override + public void execute(CommandStack stack) { + if (mValue instanceof Variable) { + String varName = ((Variable)mValue).name; + stack.getBuilder().assignVariableToFilterInput(varName, mFilterName, mFilterInput); + } else { + stack.getBuilder().assignValueToFilterInput(mValue, mFilterName, mFilterInput); + } + } + } + + private static class ConnectCommand implements Command { + private String mSourceFilter; + private String mSourcePort; + private String mTargetFilter; + private String mTargetPort; + + public ConnectCommand(String sourceFilter, + String sourcePort, + String targetFilter, + String targetPort) { + mSourceFilter = sourceFilter; + mSourcePort = sourcePort; + mTargetFilter = targetFilter; + mTargetPort = targetPort; + } + + @Override + public void execute(CommandStack stack) { + stack.getBuilder().connect(mSourceFilter, mSourcePort, mTargetFilter, mTargetPort); + } + } + + private static class Variable { + public String name; + + public Variable(String name) { + this.name = name; + } + } + + private static class XmlGraphReader { + + private SAXParserFactory mParserFactory; + + private static class GraphDataHandler extends DefaultHandler { + + private CommandStack mCommandStack; + private boolean mInGraph = false; + private String mCurFilterName = null; + + public GraphDataHandler(CommandStack commandStack) { + mCommandStack = commandStack; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attr) + throws SAXException { + if (localName.equals("graph")) { + beginGraph(); + } else { + assertInGraph(localName); + if (localName.equals("import")) { + addImportCommand(attr); + } else if (localName.equals("library")) { + addLibraryCommand(attr); + } else if (localName.equals("connect")) { + addConnectCommand(attr); + } else if (localName.equals("var")) { + addVarCommand(attr); + } else if (localName.equals("filter")) { + beginFilter(attr); + } else if (localName.equals("input")) { + addFilterInput(attr); + } else { + throw new SAXException("Unknown XML element '" + localName + "'!"); + } + } + } + + @Override + public void endElement (String uri, String localName, String qName) { + if (localName.equals("graph")) { + endGraph(); + } else if (localName.equals("filter")) { + endFilter(); + } + } + + private void addImportCommand(Attributes attributes) throws SAXException { + String packageName = getRequiredAttribute(attributes, "package"); + mCommandStack.append(new ImportPackageCommand(packageName)); + } + + private void addLibraryCommand(Attributes attributes) throws SAXException { + String libraryName = getRequiredAttribute(attributes, "name"); + mCommandStack.append(new AddLibraryCommand(libraryName)); + } + + private void addConnectCommand(Attributes attributes) { + String sourcePortName = null; + String sourceFilterName = null; + String targetPortName = null; + String targetFilterName = null; + + // check for shorthand: <connect source="filter:port" target="filter:port"/> + String sourceTag = attributes.getValue("source"); + if (sourceTag != null) { + String[] sourceParts = sourceTag.split(":"); + if (sourceParts.length == 2) { + sourceFilterName = sourceParts[0]; + sourcePortName = sourceParts[1]; + } else { + throw new RuntimeException( + "'source' tag needs to have format \"filter:port\"! " + + "Alternatively, you may use the form " + + "'sourceFilter=\"filter\" sourcePort=\"port\"'."); + } + } else { + sourceFilterName = attributes.getValue("sourceFilter"); + sourcePortName = attributes.getValue("sourcePort"); + } + + String targetTag = attributes.getValue("target"); + if (targetTag != null) { + String[] targetParts = targetTag.split(":"); + if (targetParts.length == 2) { + targetFilterName = targetParts[0]; + targetPortName = targetParts[1]; + } else { + throw new RuntimeException( + "'target' tag needs to have format \"filter:port\"! " + + "Alternatively, you may use the form " + + "'targetFilter=\"filter\" targetPort=\"port\"'."); + } + } else { + targetFilterName = attributes.getValue("targetFilter"); + targetPortName = attributes.getValue("targetPort"); + } + + String sourceSlotName = attributes.getValue("sourceSlot"); + String targetSlotName = attributes.getValue("targetSlot"); + if (sourceSlotName != null) { + sourceFilterName = "sourceSlot_" + sourceSlotName; + mCommandStack.append(new AddSourceSlotCommand(sourceFilterName, + sourceSlotName)); + sourcePortName = "frame"; + } + if (targetSlotName != null) { + targetFilterName = "targetSlot_" + targetSlotName; + mCommandStack.append(new AddTargetSlotCommand(targetFilterName, + targetSlotName)); + targetPortName = "frame"; + } + assertValueNotNull("sourceFilter", sourceFilterName); + assertValueNotNull("sourcePort", sourcePortName); + assertValueNotNull("targetFilter", targetFilterName); + assertValueNotNull("targetPort", targetPortName); + // TODO: Should slot connections auto-branch? + mCommandStack.append(new ConnectCommand(sourceFilterName, + sourcePortName, + targetFilterName, + targetPortName)); + } + + private void addVarCommand(Attributes attributes) throws SAXException { + String varName = getRequiredAttribute(attributes, "name"); + Object varValue = getAssignmentValue(attributes); + mCommandStack.append(new AddVariableCommand(varName, varValue)); + } + + private void beginGraph() throws SAXException { + if (mInGraph) { + throw new SAXException("Found more than one graph element in XML!"); + } + mInGraph = true; + } + + private void endGraph() { + mInGraph = false; + } + + private void beginFilter(Attributes attributes) throws SAXException { + String className = getRequiredAttribute(attributes, "class"); + mCurFilterName = getRequiredAttribute(attributes, "name"); + mCommandStack.append(new AllocateFilterCommand(className, mCurFilterName)); + } + + private void endFilter() { + mCurFilterName = null; + } + + private void addFilterInput(Attributes attributes) throws SAXException { + // Make sure we are in a filter element + if (mCurFilterName == null) { + throw new SAXException("Found 'input' element outside of 'filter' " + + "element!"); + } + + // Get input name and value + String inputName = getRequiredAttribute(attributes, "name"); + Object inputValue = getAssignmentValue(attributes); + if (inputValue == null) { + throw new SAXException("No value specified for input '" + inputName + "' " + + "of filter '" + mCurFilterName + "'!"); + } + + // Push commmand + mCommandStack.append(new SetFilterInputCommand(mCurFilterName, + inputName, + inputValue)); + } + + private void assertInGraph(String localName) throws SAXException { + if (!mInGraph) { + throw new SAXException("Encountered '" + localName + "' element outside of " + + "'graph' element!"); + } + } + + private static Object getAssignmentValue(Attributes attributes) { + String strValue = null; + if ((strValue = attributes.getValue("stringValue")) != null) { + return strValue; + } else if ((strValue = attributes.getValue("booleanValue")) != null) { + return Boolean.parseBoolean(strValue); + } else if ((strValue = attributes.getValue("intValue")) != null) { + return Integer.parseInt(strValue); + } else if ((strValue = attributes.getValue("floatValue")) != null) { + return Float.parseFloat(strValue); + } else if ((strValue = attributes.getValue("floatsValue")) != null) { + String[] floatStrings = TextUtils.split(strValue, ","); + float[] result = new float[floatStrings.length]; + for (int i = 0; i < floatStrings.length; ++i) { + result[i] = Float.parseFloat(floatStrings[i]); + } + return result; + } else if ((strValue = attributes.getValue("varValue")) != null) { + return new Variable(strValue); + } else { + return null; + } + } + + private static String getRequiredAttribute(Attributes attributes, String name) + throws SAXException { + String result = attributes.getValue(name); + if (result == null) { + throw new SAXException("Required attribute '" + name + "' not found!"); + } + return result; + } + + private static void assertValueNotNull(String valueName, Object value) { + if (value == null) { + throw new NullPointerException("Required value '" + value + "' not specified!"); + } + } + + } + + public XmlGraphReader() { + mParserFactory = SAXParserFactory.newInstance(); + } + + public void parseString(String graphString, CommandStack commandStack) throws IOException { + try { + XMLReader reader = getReaderForCommandStack(commandStack); + reader.parse(new InputSource(new StringReader(graphString))); + } catch (SAXException e) { + throw new IOException("XML parse error during graph parsing!", e); + } + } + + public void parseInput(InputStream inputStream, CommandStack commandStack) + throws IOException { + try { + XMLReader reader = getReaderForCommandStack(commandStack); + reader.parse(new InputSource(inputStream)); + } catch (SAXException e) { + throw new IOException("XML parse error during graph parsing!", e); + } + } + + private XMLReader getReaderForCommandStack(CommandStack commandStack) throws IOException { + try { + SAXParser parser = mParserFactory.newSAXParser(); + XMLReader reader = parser.getXMLReader(); + GraphDataHandler graphHandler = new GraphDataHandler(commandStack); + reader.setContentHandler(graphHandler); + return reader; + } catch (ParserConfigurationException e) { + throw new IOException("Error creating SAXParser for graph parsing!", e); + } catch (SAXException e) { + throw new IOException("Error creating XMLReader for graph parsing!", e); + } + } + } + + /** + * Read an XML graph from a String. + * + * This function automatically checks each filters' signatures and throws a Runtime Exception + * if required ports are unconnected. Use the 3-parameter version to avoid this behavior. + * + * @param context the MffContext into which to load the graph. + * @param xmlSource the graph specified in XML. + * @return the FilterGraph instance for the XML source. + * @throws IOException if there was an error parsing the source. + */ + public static FilterGraph readXmlGraph(MffContext context, String xmlSource) + throws IOException { + FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource); + return builder.build(); + } + + /** + * Read an XML sub-graph from a String. + * + * @param context the MffContext into which to load the graph. + * @param xmlSource the graph specified in XML. + * @param parentGraph the parent graph. + * @return the FilterGraph instance for the XML source. + * @throws IOException if there was an error parsing the source. + */ + public static FilterGraph readXmlSubGraph( + MffContext context, String xmlSource, FilterGraph parentGraph) + throws IOException { + FilterGraph.Builder builder = getBuilderForXmlString(context, xmlSource); + return builder.buildSubGraph(parentGraph); + } + + /** + * Read an XML graph from a resource. + * + * This function automatically checks each filters' signatures and throws a Runtime Exception + * if required ports are unconnected. Use the 3-parameter version to avoid this behavior. + * + * @param context the MffContext into which to load the graph. + * @param resourceId the XML resource ID. + * @return the FilterGraph instance for the XML source. + * @throws IOException if there was an error reading or parsing the resource. + */ + public static FilterGraph readXmlGraphResource(MffContext context, int resourceId) + throws IOException { + FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId); + return builder.build(); + } + + /** + * Read an XML graph from a resource. + * + * This function automatically checks each filters' signatures and throws a Runtime Exception + * if required ports are unconnected. Use the 3-parameter version to avoid this behavior. + * + * @param context the MffContext into which to load the graph. + * @param resourceId the XML resource ID. + * @return the FilterGraph instance for the XML source. + * @throws IOException if there was an error reading or parsing the resource. + */ + public static FilterGraph readXmlSubGraphResource( + MffContext context, int resourceId, FilterGraph parentGraph) + throws IOException { + FilterGraph.Builder builder = getBuilderForXmlResource(context, resourceId); + return builder.buildSubGraph(parentGraph); + } + + private static FilterGraph.Builder getBuilderForXmlString(MffContext context, String source) + throws IOException { + XmlGraphReader reader = new XmlGraphReader(); + CommandStack commands = new CommandStack(context); + reader.parseString(source, commands); + commands.execute(); + return commands.getBuilder(); + } + + private static FilterGraph.Builder getBuilderForXmlResource(MffContext context, int resourceId) + throws IOException { + InputStream inputStream = context.getApplicationContext().getResources() + .openRawResource(resourceId); + XmlGraphReader reader = new XmlGraphReader(); + CommandStack commands = new CommandStack(context); + reader.parseInput(inputStream, commands); + commands.execute(); + return commands.getBuilder(); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphRunner.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphRunner.java new file mode 100644 index 0000000..36aed63 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/GraphRunner.java @@ -0,0 +1,1023 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.os.ConditionVariable; +import android.os.SystemClock; +import android.util.Log; + +import java.util.HashSet; +import java.util.Set; +import java.util.Stack; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * A GraphRunner schedules and executes the filter nodes of a graph. + * + * Typically, you create a GraphRunner given a FilterGraph instance, and execute it by calling + * {@link #start(FilterGraph)}. + * + * The scheduling strategy determines how the filter nodes are selected + * for scheduling. More precisely, given the set of nodes that can be scheduled, the scheduling + * strategy determines which node of this set to select for execution. For instance, an LFU + * scheduler (the default) chooses the node that has been executed the least amount of times. + */ +public final class GraphRunner { + + private static int PRIORITY_SLEEP = -1; + private static int PRIORITY_STOP = -2; + + private static final Event BEGIN_EVENT = new Event(Event.BEGIN, null); + private static final Event FLUSH_EVENT = new Event(Event.FLUSH, null); + private static final Event HALT_EVENT = new Event(Event.HALT, null); + private static final Event KILL_EVENT = new Event(Event.KILL, null); + private static final Event PAUSE_EVENT = new Event(Event.PAUSE, null); + private static final Event RELEASE_FRAMES_EVENT = new Event(Event.RELEASE_FRAMES, null); + private static final Event RESTART_EVENT = new Event(Event.RESTART, null); + private static final Event RESUME_EVENT = new Event(Event.RESUME, null); + private static final Event STEP_EVENT = new Event(Event.STEP, null); + private static final Event STOP_EVENT = new Event(Event.STOP, null); + + private static class State { + public static final int STOPPED = 1; + public static final int PREPARING = 2; + public static final int RUNNING = 4; + public static final int PAUSED = 8; + public static final int HALTED = 16; + + private int mCurrent = STOPPED; + + public synchronized void setState(int newState) { + mCurrent = newState; + } + + public synchronized boolean check(int state) { + return ((mCurrent & state) == state); + } + + public synchronized boolean addState(int state) { + if ((mCurrent & state) != state) { + mCurrent |= state; + return true; + } + return false; + } + + public synchronized boolean removeState(int state) { + boolean result = (mCurrent & state) == state; + mCurrent &= (~state); + return result; + } + + public synchronized int current() { + return mCurrent; + } + } + + private static class Event { + public static final int PREPARE = 1; + public static final int BEGIN = 2; + public static final int STEP = 3; + public static final int STOP = 4; + public static final int PAUSE = 6; + public static final int HALT = 7; + public static final int RESUME = 8; + public static final int RESTART = 9; + public static final int FLUSH = 10; + public static final int TEARDOWN = 11; + public static final int KILL = 12; + public static final int RELEASE_FRAMES = 13; + + public int code; + public Object object; + + public Event(int code, Object object) { + this.code = code; + this.object = object; + } + } + + private final class GraphRunLoop implements Runnable { + + private State mState = new State(); + private final boolean mAllowOpenGL; + private RenderTarget mRenderTarget = null; + private LinkedBlockingQueue<Event> mEventQueue = new LinkedBlockingQueue<Event>(); + private Exception mCaughtException = null; + private boolean mClosedSuccessfully = true; + private Stack<Filter[]> mFilters = new Stack<Filter[]>(); + private Stack<SubListener> mSubListeners = new Stack<SubListener>(); + private Set<FilterGraph> mOpenedGraphs = new HashSet<FilterGraph>(); + public ConditionVariable mStopCondition = new ConditionVariable(true); + + private void loop() { + boolean killed = false; + while (!killed) { + try { + Event event = nextEvent(); + if (event == null) continue; + switch (event.code) { + case Event.PREPARE: + onPrepare((FilterGraph)event.object); + break; + case Event.BEGIN: + onBegin(); + break; + case Event.STEP: + onStep(); + break; + case Event.STOP: + onStop(); + break; + case Event.PAUSE: + onPause(); + break; + case Event.HALT: + onHalt(); + break; + case Event.RESUME: + onResume(); + break; + case Event.RESTART: + onRestart(); + break; + case Event.FLUSH: + onFlush(); + break; + case Event.TEARDOWN: + onTearDown((FilterGraph)event.object); + break; + case Event.KILL: + killed = true; + break; + case Event.RELEASE_FRAMES: + onReleaseFrames(); + break; + } + } catch (Exception e) { + if (mCaughtException == null) { + mCaughtException = e; + mClosedSuccessfully = true; + e.printStackTrace(); + pushEvent(STOP_EVENT); + } else { + // Exception during exception recovery? Abort all processing. Do not + // overwrite the original exception. + mClosedSuccessfully = false; + mEventQueue.clear(); + cleanUp(); + } + } + } + } + + public GraphRunLoop(boolean allowOpenGL) { + mAllowOpenGL = allowOpenGL; + } + + @Override + public void run() { + onInit(); + loop(); + onDestroy(); + } + + public void enterSubGraph(FilterGraph graph, SubListener listener) { + if (mState.check(State.RUNNING)) { + onOpenGraph(graph); + mSubListeners.push(listener); + } + } + + public void pushWakeEvent(Event event) { + // This is of course not race-condition proof. The worst case is that the event + // is pushed even though the queue was not empty, which is acceptible for our cases. + if (mEventQueue.isEmpty()) { + pushEvent(event); + } + } + + public void pushEvent(Event event) { + mEventQueue.offer(event); + } + + public void pushEvent(int eventId, Object object) { + mEventQueue.offer(new Event(eventId, object)); + } + + public boolean checkState(int state) { + return mState.check(state); + } + + public ConditionVariable getStopCondition() { + return mStopCondition; + } + + public boolean isOpenGLAllowed() { + // Does not need synchronization as mAllowOpenGL flag is final. + return mAllowOpenGL; + } + + private Event nextEvent() { + try { + return mEventQueue.take(); + } catch (InterruptedException e) { + // Ignore and keep going. + Log.w("GraphRunner", "Event queue processing was interrupted."); + return null; + } + } + + private void onPause() { + mState.addState(State.PAUSED); + } + + private void onResume() { + if (mState.removeState(State.PAUSED)) { + if (mState.current() == State.RUNNING) { + pushEvent(STEP_EVENT); + } + } + } + + private void onHalt() { + if (mState.addState(State.HALTED) && mState.check(State.RUNNING)) { + closeAllFilters(); + } + } + + private void onRestart() { + if (mState.removeState(State.HALTED)) { + if (mState.current() == State.RUNNING) { + pushEvent(STEP_EVENT); + } + } + } + + private void onDestroy() { + mFrameManager.destroyBackings(); + if (mRenderTarget != null) { + mRenderTarget.release(); + mRenderTarget = null; + } + } + + private void onReleaseFrames() { + mFrameManager.destroyBackings(); + } + + private void onInit() { + mThreadRunner.set(GraphRunner.this); + if (getContext().isOpenGLSupported()) { + mRenderTarget = RenderTarget.newTarget(1, 1); + mRenderTarget.focus(); + } + } + + private void onPrepare(FilterGraph graph) { + if (mState.current() == State.STOPPED) { + mState.setState(State.PREPARING); + mCaughtException = null; + onOpenGraph(graph); + } + } + + private void onOpenGraph(FilterGraph graph) { + loadFilters(graph); + mOpenedGraphs.add(graph); + mScheduler.prepare(currentFilters()); + pushEvent(BEGIN_EVENT); + } + + private void onBegin() { + if (mState.current() == State.PREPARING) { + mState.setState(State.RUNNING); + pushEvent(STEP_EVENT); + } + } + + private void onStarve() { + mFilters.pop(); + if (mFilters.empty()) { + onStop(); + } else { + SubListener listener = mSubListeners.pop(); + if (listener != null) { + listener.onSubGraphRunEnded(GraphRunner.this); + } + mScheduler.prepare(currentFilters()); + pushEvent(STEP_EVENT); + } + } + + private void onStop() { + if (mState.check(State.RUNNING)) { + // Close filters if not already halted (and already closed) + if (!mState.check(State.HALTED)) { + closeAllFilters(); + } + cleanUp(); + } + } + + private void cleanUp() { + mState.setState(State.STOPPED); + if (flushOnClose()) { + onFlush(); + } + mOpenedGraphs.clear(); + mFilters.clear(); + onRunnerStopped(mCaughtException, mClosedSuccessfully); + mStopCondition.open(); + } + + private void onStep() { + if (mState.current() == State.RUNNING) { + Filter bestFilter = null; + long maxPriority = PRIORITY_STOP; + mScheduler.beginStep(); + Filter[] filters = currentFilters(); + for (int i = 0; i < filters.length; ++i) { + Filter filter = filters[i]; + long priority = mScheduler.priorityForFilter(filter); + if (priority > maxPriority) { + maxPriority = priority; + bestFilter = filter; + } + } + if (maxPriority == PRIORITY_SLEEP) { + // NOOP: When going into sleep mode, we simply do not schedule another node. + // If some other event (such as a resume()) does schedule, then we may schedule + // during sleeping. This is an edge case an irrelevant. (On the other hand, + // going into a dedicated "sleep state" requires highly complex synchronization + // to not "miss" a wake-up event. Thus we choose the more defensive approach + // here). + } else if (maxPriority == PRIORITY_STOP) { + onStarve(); + } else { + scheduleFilter(bestFilter); + pushEvent(STEP_EVENT); + } + } else { + Log.w("GraphRunner", "State is not running! (" + mState.current() + ")"); + } + } + + private void onFlush() { + if (mState.check(State.HALTED) || mState.check(State.STOPPED)) { + for (FilterGraph graph : mOpenedGraphs) { + graph.flushFrames(); + } + } + } + + private void onTearDown(FilterGraph graph) { + for (Filter filter : graph.getAllFilters()) { + filter.performTearDown(); + } + graph.wipe(); + } + + private void loadFilters(FilterGraph graph) { + Filter[] filters = graph.getAllFilters(); + mFilters.push(filters); + } + + private void closeAllFilters() { + for (FilterGraph graph : mOpenedGraphs) { + closeFilters(graph); + } + } + + private void closeFilters(FilterGraph graph) { + // [Non-iterator looping] + Log.v("GraphRunner", "CLOSING FILTERS"); + Filter[] filters = graph.getAllFilters(); + boolean isVerbose = isVerbose(); + for (int i = 0; i < filters.length; ++i) { + if (isVerbose) { + Log.i("GraphRunner", "Closing Filter " + filters[i] + "!"); + } + filters[i].softReset(); + } + } + + private Filter[] currentFilters() { + return mFilters.peek(); + } + + private void scheduleFilter(Filter filter) { + long scheduleTime = 0; + if (isVerbose()) { + scheduleTime = SystemClock.elapsedRealtime(); + Log.i("GraphRunner", scheduleTime + ": Scheduling " + filter + "!"); + } + filter.execute(); + if (isVerbose()) { + long nowTime = SystemClock.elapsedRealtime(); + Log.i("GraphRunner", + "-> Schedule time (" + filter + ") = " + (nowTime - scheduleTime) + " ms."); + } + } + + } + + // GraphRunner.Scheduler classes /////////////////////////////////////////////////////////////// + private interface Scheduler { + public void prepare(Filter[] filters); + + public int getStrategy(); + + public void beginStep(); + + public long priorityForFilter(Filter filter); + + } + + private class LruScheduler implements Scheduler { + + private long mNow; + + @Override + public void prepare(Filter[] filters) { + } + + @Override + public int getStrategy() { + return STRATEGY_LRU; + } + + @Override + public void beginStep() { + // TODO(renn): We could probably do this with a simple GraphRunner counter that would + // represent GraphRunner local time. This would allow us to use integers instead of + // longs, and save us calls to the system clock. + mNow = SystemClock.elapsedRealtime(); + } + + @Override + public long priorityForFilter(Filter filter) { + if (filter.isSleeping()) { + return PRIORITY_SLEEP; + } else if (filter.canSchedule()) { + return mNow - filter.getLastScheduleTime(); + } else { + return PRIORITY_STOP; + } + } + + } + + private class LfuScheduler implements Scheduler { + + private final int MAX_PRIORITY = Integer.MAX_VALUE; + + @Override + public void prepare(Filter[] filters) { + // [Non-iterator looping] + for (int i = 0; i < filters.length; ++i) { + filters[i].resetScheduleCount(); + } + } + + @Override + public int getStrategy() { + return STRATEGY_LFU; + } + + @Override + public void beginStep() { + } + + @Override + public long priorityForFilter(Filter filter) { + return filter.isSleeping() ? PRIORITY_SLEEP + : (filter.canSchedule() ? (MAX_PRIORITY - filter.getScheduleCount()) + : PRIORITY_STOP); + } + + } + + private class OneShotScheduler extends LfuScheduler { + private int mCurCount = 1; + + @Override + public void prepare(Filter[] filters) { + // [Non-iterator looping] + for (int i = 0; i < filters.length; ++i) { + filters[i].resetScheduleCount(); + } + } + + @Override + public int getStrategy() { + return STRATEGY_ONESHOT; + } + + @Override + public void beginStep() { + } + + @Override + public long priorityForFilter(Filter filter) { + return filter.getScheduleCount() < mCurCount ? super.priorityForFilter(filter) + : PRIORITY_STOP; + } + + } + + // GraphRunner.Listener callback class ///////////////////////////////////////////////////////// + public interface Listener { + /** + * Callback method that is called when the runner completes a run. This method is called + * only if the graph completed without an error. + */ + public void onGraphRunnerStopped(GraphRunner runner); + + /** + * Callback method that is called when runner encounters an error. + * + * Any exceptions thrown in the GraphRunner's thread will cause the run to abort. The + * thrown exception is passed to the listener in this method. If no listener is set, the + * exception message is logged to the error stream. You will not receive an + * {@link #onGraphRunnerStopped(GraphRunner)} callback in case of an error. + * + * @param exception the exception that was thrown. + * @param closedSuccessfully true, if the graph was closed successfully after the error. + */ + public void onGraphRunnerError(Exception exception, boolean closedSuccessfully); + } + + public interface SubListener { + public void onSubGraphRunEnded(GraphRunner runner); + } + + /** + * Config class to setup a GraphRunner with a custom configuration. + * + * The configuration object is passed to the constructor. Any changes to it will not affect + * the created GraphRunner instance. + */ + public static class Config { + /** The runner's thread priority. */ + public int threadPriority = Thread.NORM_PRIORITY; + /** Whether to allow filters to use OpenGL or not. */ + public boolean allowOpenGL = true; + } + + /** Parameters shared between run-thread and GraphRunner frontend. */ + private class RunParameters { + public Listener listener = null; + public boolean isVerbose = false; + public boolean flushOnClose = true; + } + + // GraphRunner implementation ////////////////////////////////////////////////////////////////// + /** Schedule strategy: From set of candidates, pick a random one. */ + public static final int STRATEGY_RANDOM = 1; + /** Schedule strategy: From set of candidates, pick node executed least recently executed. */ + public static final int STRATEGY_LRU = 2; + /** Schedule strategy: From set of candidates, pick node executed least number of times. */ + public static final int STRATEGY_LFU = 3; + /** Schedule strategy: Schedules no node more than once. */ + public static final int STRATEGY_ONESHOT = 4; + + private final MffContext mContext; + + private FilterGraph mRunningGraph = null; + private Set<FilterGraph> mGraphs = new HashSet<FilterGraph>(); + + private Scheduler mScheduler; + + private GraphRunLoop mRunLoop; + + private Thread mRunThread = null; + + private FrameManager mFrameManager = null; + + private static ThreadLocal<GraphRunner> mThreadRunner = new ThreadLocal<GraphRunner>(); + + private RunParameters mParams = new RunParameters(); + + /** + * Creates a new GraphRunner with the default configuration. You must attach FilterGraph + * instances to this runner before you can execute any of these graphs. + * + * @param context The MffContext instance for this runner. + */ + public GraphRunner(MffContext context) { + mContext = context; + init(new Config()); + } + + /** + * Creates a new GraphRunner with the specified configuration. You must attach FilterGraph + * instances to this runner before you can execute any of these graphs. + * + * @param context The MffContext instance for this runner. + * @param config A Config instance with the configuration of this runner. + */ + public GraphRunner(MffContext context, Config config) { + mContext = context; + init(config); + } + + /** + * Returns the currently running graph-runner. + * @return The currently running graph-runner. + */ + public static GraphRunner current() { + return mThreadRunner.get(); + } + + /** + * Returns the graph that this runner is currently executing. Returns null if no graph is + * currently being executed by this runner. + * + * @return the FilterGraph instance that this GraphRunner is executing. + */ + public synchronized FilterGraph getRunningGraph() { + return mRunningGraph; + } + + /** + * Returns the context that this runner is bound to. + * + * @return the MffContext instance that this runner is bound to. + */ + public MffContext getContext() { + return mContext; + } + + /** + * Begins graph execution. The graph filters are scheduled and executed until processing + * finishes or is stopped. + */ + public synchronized void start(FilterGraph graph) { + if (graph.mRunner != this) { + throw new IllegalArgumentException("Graph must be attached to runner!"); + } + mRunningGraph = graph; + mRunLoop.getStopCondition().close(); + mRunLoop.pushEvent(Event.PREPARE, graph); + } + + /** + * Begin executing a sub-graph. This only succeeds if the current runner is already + * executing. + */ + public void enterSubGraph(FilterGraph graph, SubListener listener) { + if (Thread.currentThread() != mRunThread) { + throw new RuntimeException("enterSubGraph must be called from the runner's thread!"); + } + mRunLoop.enterSubGraph(graph, listener); + } + + /** + * Waits until graph execution has finished or stopped with an error. + * Care must be taken when using this method to not block the UI thread. This is typically + * used when a graph is run in one-shot mode to compute a result. + */ + public void waitUntilStop() { + mRunLoop.getStopCondition().block(); + } + + /** + * Pauses graph execution. + */ + public void pause() { + mRunLoop.pushEvent(PAUSE_EVENT); + } + + /** + * Resumes graph execution after pausing. + */ + public void resume() { + mRunLoop.pushEvent(RESUME_EVENT); + } + + /** + * Stops graph execution. + */ + public void stop() { + mRunLoop.pushEvent(STOP_EVENT); + } + + /** + * Returns whether the graph is currently being executed. A graph is considered to be running, + * even if it is paused or in the process of being stopped. + * + * @return true, if the graph is currently being executed. + */ + public boolean isRunning() { + return !mRunLoop.checkState(State.STOPPED); + } + + /** + * Returns whether the graph is currently paused. + * + * @return true, if the graph is currently paused. + */ + public boolean isPaused() { + return mRunLoop.checkState(State.PAUSED); + } + + /** + * Returns whether the graph is currently stopped. + * + * @return true, if the graph is currently stopped. + */ + public boolean isStopped() { + return mRunLoop.checkState(State.STOPPED); + } + + /** + * Sets the filter scheduling strategy. This method can not be called when the GraphRunner is + * running. + * + * @param strategy a constant specifying which scheduler strategy to use. + * @throws RuntimeException if the GraphRunner is running. + * @throws IllegalArgumentException if invalid strategy is specified. + * @see #getSchedulerStrategy() + */ + public void setSchedulerStrategy(int strategy) { + if (isRunning()) { + throw new RuntimeException( + "Attempting to change scheduling strategy on running " + "GraphRunner!"); + } + createScheduler(strategy); + } + + /** + * Returns the current scheduling strategy. + * + * @return the scheduling strategy used by this GraphRunner. + * @see #setSchedulerStrategy(int) + */ + public int getSchedulerStrategy() { + return mScheduler.getStrategy(); + } + + /** + * Set whether or not the runner is verbose. When set to true, the runner will output individual + * scheduling steps that may help identify and debug problems in the graph structure. The + * default is false. + * + * @param isVerbose true, if the GraphRunner should log scheduling details. + * @see #isVerbose() + */ + public void setIsVerbose(boolean isVerbose) { + synchronized (mParams) { + mParams.isVerbose = isVerbose; + } + } + + /** + * Returns whether the GraphRunner is verbose. + * + * @return true, if the GraphRunner logs scheduling details. + * @see #setIsVerbose(boolean) + */ + public boolean isVerbose() { + synchronized (mParams) { + return mParams.isVerbose; + } + } + + /** + * Returns whether Filters of this GraphRunner can use OpenGL. + * + * Filters may use OpenGL if the MffContext supports OpenGL and the GraphRunner allows it. + * + * @return true, if Filters are allowed to use OpenGL. + */ + public boolean isOpenGLSupported() { + return mRunLoop.isOpenGLAllowed() && mContext.isOpenGLSupported(); + } + + /** + * Enable flushing all frames from the graph when running completes. + * + * If this is set to false, then frames may remain in the pipeline even after running completes. + * The default value is true. + * + * @param flush true, if the GraphRunner should flush the graph when running completes. + * @see #flushOnClose() + */ + public void setFlushOnClose(boolean flush) { + synchronized (mParams) { + mParams.flushOnClose = flush; + } + } + + /** + * Returns whether the GraphRunner flushes frames when running completes. + * + * @return true, if the GraphRunner flushes frames when running completes. + * @see #setFlushOnClose(boolean) + */ + public boolean flushOnClose() { + synchronized (mParams) { + return mParams.flushOnClose; + } + } + + /** + * Sets the listener for receiving runtime events. A GraphRunner.Listener instance can be used + * to determine when certain events occur during graph execution (and react on them). See the + * {@link GraphRunner.Listener} class for details. + * + * @param listener the GraphRunner.Listener instance to set. + * @see #getListener() + */ + public void setListener(Listener listener) { + synchronized (mParams) { + mParams.listener = listener; + } + } + + /** + * Returns the currently assigned GraphRunner.Listener. + * + * @return the currently assigned GraphRunner.Listener instance. + * @see #setListener(Listener) + */ + public Listener getListener() { + synchronized (mParams) { + return mParams.listener; + } + } + + /** + * Returns the FrameManager that manages the runner's frames. + * + * @return the FrameManager instance that manages the runner's frames. + */ + public FrameManager getFrameManager() { + return mFrameManager; + } + + /** + * Tear down a GraphRunner and all its resources. + * <p> + * You must make sure that before calling this, no more graphs are attached to this runner. + * Typically, graphs are removed from runners when they are torn down. + * + * @throws IllegalStateException if there are still graphs attached to this runner. + */ + public void tearDown() { + synchronized (mGraphs) { + if (!mGraphs.isEmpty()) { + throw new IllegalStateException("Attempting to tear down runner with " + + mGraphs.size() + " graphs still attached!"); + } + } + mRunLoop.pushEvent(KILL_EVENT); + // Wait for thread to complete, so that everything is torn down by the time we return. + try { + mRunThread.join(); + } catch (InterruptedException e) { + Log.e("GraphRunner", "Error waiting for runner thread to finish!"); + } + } + + /** + * Release all frames managed by this runner. + * <p> + * Note, that you must make sure no graphs are attached to this runner before calling this + * method, as otherwise Filters in the graph may reference frames that are now released. + * + * TODO: Eventually, this method should be removed. Instead we should have better analysis + * that catches leaking frames from filters. + * + * @throws IllegalStateException if there are still graphs attached to this runner. + */ + public void releaseFrames() { + synchronized (mGraphs) { + if (!mGraphs.isEmpty()) { + throw new IllegalStateException("Attempting to release frames with " + + mGraphs.size() + " graphs still attached!"); + } + } + mRunLoop.pushEvent(RELEASE_FRAMES_EVENT); + } + + // Core internal methods /////////////////////////////////////////////////////////////////////// + void attachGraph(FilterGraph graph) { + synchronized (mGraphs) { + mGraphs.add(graph); + } + } + + void signalWakeUp() { + mRunLoop.pushWakeEvent(STEP_EVENT); + } + + void begin() { + mRunLoop.pushEvent(BEGIN_EVENT); + } + + /** Like pause(), but closes all filters. Can be resumed using restart(). */ + void halt() { + mRunLoop.pushEvent(HALT_EVENT); + } + + /** Resumes a previously halted runner, and restores it to its non-halted state. */ + void restart() { + mRunLoop.pushEvent(RESTART_EVENT); + } + + /** + * Tears down the specified graph. + * + * The graph must be attached to this runner. + */ + void tearDownGraph(FilterGraph graph) { + if (graph.getRunner() != this) { + throw new IllegalArgumentException("Attempting to tear down graph with foreign " + + "GraphRunner!"); + } + mRunLoop.pushEvent(Event.TEARDOWN, graph); + synchronized (mGraphs) { + mGraphs.remove(graph); + } + } + + /** + * Remove all frames that are waiting to be processed. + * + * Removes and releases frames that are waiting in the graph connections of the currently + * halted graphs, i.e. frames that are waiting to be processed. This does not include frames + * that may be held or cached by filters themselves. + * + * TODO: With the new sub-graph architecture, this can now be simplified and made public. + * It can then no longer rely on opened graphs, and instead flush a graph and all its + * sub-graphs. + */ + void flushFrames() { + mRunLoop.pushEvent(FLUSH_EVENT); + } + + // Private methods ///////////////////////////////////////////////////////////////////////////// + private void init(Config config) { + mFrameManager = new FrameManager(this, FrameManager.FRAME_CACHE_LRU); + createScheduler(STRATEGY_LRU); + mRunLoop = new GraphRunLoop(config.allowOpenGL); + mRunThread = new Thread(mRunLoop); + mRunThread.setPriority(config.threadPriority); + mRunThread.start(); + mContext.addRunner(this); + } + + private void createScheduler(int strategy) { + switch (strategy) { + case STRATEGY_LRU: + mScheduler = new LruScheduler(); + break; + case STRATEGY_LFU: + mScheduler = new LfuScheduler(); + break; + case STRATEGY_ONESHOT: + mScheduler = new OneShotScheduler(); + break; + default: + throw new IllegalArgumentException( + "Unknown schedule-strategy constant " + strategy + "!"); + } + } + + // Called within the runner's thread + private void onRunnerStopped(final Exception exception, final boolean closed) { + mRunningGraph = null; + synchronized (mParams) { + if (mParams.listener != null) { + getContext().postRunnable(new Runnable() { + @Override + public void run() { + if (exception == null) { + mParams.listener.onGraphRunnerStopped(GraphRunner.this); + } else { + mParams.listener.onGraphRunnerError(exception, closed); + } + } + }); + } else if (exception != null) { + Log.e("GraphRunner", + "Uncaught exception during graph execution! Stack Trace: "); + exception.printStackTrace(); + } + } + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ImageShader.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ImageShader.java new file mode 100644 index 0000000..0ec50a3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ImageShader.java @@ -0,0 +1,793 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw; + +import android.graphics.RectF; +import android.opengl.GLES20; +import android.util.Log; + +import androidx.media.filterfw.geometry.Quad; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.Arrays; +import java.util.HashMap; + +/** + * Convenience class to perform GL shader operations on image data. + * <p> + * The ImageShader class greatly simplifies the task of running GL shader language kernels over + * Frame data buffers that contain RGBA image data. + * </p><p> + * TODO: More documentation + * </p> + */ +public class ImageShader { + + private int mProgram = 0; + private boolean mClearsOutput = false; + private float[] mClearColor = { 0f, 0f, 0f, 0f }; + private boolean mBlendEnabled = false; + private int mSFactor = GLES20.GL_SRC_ALPHA; + private int mDFactor = GLES20.GL_ONE_MINUS_SRC_ALPHA; + private int mDrawMode = GLES20.GL_TRIANGLE_STRIP; + private int mVertexCount = 4; + private int mBaseTexUnit = GLES20.GL_TEXTURE0; + private int mClearBuffers = GLES20.GL_COLOR_BUFFER_BIT; + private float[] mSourceCoords = new float[] { 0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f }; + private float[] mTargetCoords = new float[] { -1f, -1f, 1f, -1f, -1f, 1f, 1f, 1f }; + + private HashMap<String, ProgramUniform> mUniforms; + private HashMap<String, VertexAttribute> mAttributes = new HashMap<String, VertexAttribute>(); + + private final static int FLOAT_SIZE = 4; + + private final static String mDefaultVertexShader = + "attribute vec4 a_position;\n" + + "attribute vec2 a_texcoord;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_Position = a_position;\n" + + " v_texcoord = a_texcoord;\n" + + "}\n"; + + private final static String mIdentityShader = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler_0;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" + + "}\n"; + + private static class VertexAttribute { + private String mName; + private boolean mIsConst; + private int mIndex; + private boolean mShouldNormalize; + private int mOffset; + private int mStride; + private int mComponents; + private int mType; + private int mVbo; + private int mLength; + private FloatBuffer mValues; + + public VertexAttribute(String name, int index) { + mName = name; + mIndex = index; + mLength = -1; + } + + public void set(boolean normalize, int stride, int components, int type, float[] values) { + mIsConst = false; + mShouldNormalize = normalize; + mStride = stride; + mComponents = components; + mType = type; + mVbo = 0; + if (mLength != values.length){ + initBuffer(values); + mLength = values.length; + } + copyValues(values); + } + + public void set(boolean normalize, int offset, int stride, int components, int type, + int vbo){ + mIsConst = false; + mShouldNormalize = normalize; + mOffset = offset; + mStride = stride; + mComponents = components; + mType = type; + mVbo = vbo; + mValues = null; + } + + public boolean push() { + if (mIsConst) { + switch (mComponents) { + case 1: + GLES20.glVertexAttrib1fv(mIndex, mValues); + break; + case 2: + GLES20.glVertexAttrib2fv(mIndex, mValues); + break; + case 3: + GLES20.glVertexAttrib3fv(mIndex, mValues); + break; + case 4: + GLES20.glVertexAttrib4fv(mIndex, mValues); + break; + default: + return false; + } + GLES20.glDisableVertexAttribArray(mIndex); + } else { + if (mValues != null) { + // Note that we cannot do any size checking here, as the correct component + // count depends on the drawing step. GL should catch such errors then, and + // we will report them to the user. + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0); + GLES20.glVertexAttribPointer(mIndex, + mComponents, + mType, + mShouldNormalize, + mStride, + mValues); + } else { + GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVbo); + GLES20.glVertexAttribPointer(mIndex, + mComponents, + mType, + mShouldNormalize, + mStride, + mOffset); + } + GLES20.glEnableVertexAttribArray(mIndex); + } + GLToolbox.checkGlError("Set vertex-attribute values"); + return true; + } + + @Override + public String toString() { + return mName; + } + + private void initBuffer(float[] values) { + mValues = ByteBuffer.allocateDirect(values.length * FLOAT_SIZE) + .order(ByteOrder.nativeOrder()).asFloatBuffer(); + } + + private void copyValues(float[] values) { + mValues.put(values).position(0); + } + + } + + private static final class ProgramUniform { + private String mName; + private int mLocation; + private int mType; + private int mSize; + + public ProgramUniform(int program, int index) { + int[] len = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, len, 0); + + int[] type = new int[1]; + int[] size = new int[1]; + byte[] name = new byte[len[0]]; + int[] ignore = new int[1]; + + GLES20.glGetActiveUniform(program, index, len[0], ignore, 0, size, 0, type, 0, name, 0); + mName = new String(name, 0, strlen(name)); + mLocation = GLES20.glGetUniformLocation(program, mName); + mType = type[0]; + mSize = size[0]; + GLToolbox.checkGlError("Initializing uniform"); + } + + public String getName() { + return mName; + } + + public int getType() { + return mType; + } + + public int getLocation() { + return mLocation; + } + + public int getSize() { + return mSize; + } + } + + public ImageShader(String fragmentShader) { + mProgram = createProgram(mDefaultVertexShader, fragmentShader); + scanUniforms(); + } + + public ImageShader(String vertexShader, String fragmentShader) { + mProgram = createProgram(vertexShader, fragmentShader); + scanUniforms(); + } + + public static ImageShader createIdentity() { + return new ImageShader(mIdentityShader); + } + + public static ImageShader createIdentity(String vertexShader) { + return new ImageShader(vertexShader, mIdentityShader); + } + + public static void renderTextureToTarget(TextureSource texture, + RenderTarget target, + int width, + int height) { + ImageShader shader = RenderTarget.currentTarget().getIdentityShader(); + shader.process(texture, target, width, height); + } + + public void process(FrameImage2D input, FrameImage2D output) { + TextureSource texSource = input.lockTextureSource(); + RenderTarget renderTarget = output.lockRenderTarget(); + processMulti(new TextureSource[] { texSource }, + renderTarget, + output.getWidth(), + output.getHeight()); + input.unlock(); + output.unlock(); + } + + public void processMulti(FrameImage2D[] inputs, FrameImage2D output) { + TextureSource[] texSources = new TextureSource[inputs.length]; + for (int i = 0; i < inputs.length; ++i) { + texSources[i] = inputs[i].lockTextureSource(); + } + RenderTarget renderTarget = output.lockRenderTarget(); + processMulti(texSources, + renderTarget, + output.getWidth(), + output.getHeight()); + for (FrameImage2D input : inputs) { + input.unlock(); + } + output.unlock(); + } + + public void process(TextureSource texture, RenderTarget target, int width, int height) { + processMulti(new TextureSource[] { texture }, target, width, height); + } + + public void processMulti(TextureSource[] sources, RenderTarget target, int width, int height) { + GLToolbox.checkGlError("Unknown Operation"); + checkExecutable(); + checkTexCount(sources.length); + focusTarget(target, width, height); + pushShaderState(); + bindInputTextures(sources); + render(); + } + + public void processNoInput(FrameImage2D output) { + RenderTarget renderTarget = output.lockRenderTarget(); + processNoInput(renderTarget, output.getWidth(), output.getHeight()); + output.unlock(); + } + + public void processNoInput(RenderTarget target, int width, int height) { + processMulti(new TextureSource[] {}, target, width, height); + } + + public int getUniformLocation(String name) { + return getProgramUniform(name, true).getLocation(); + } + + public int getAttributeLocation(String name) { + if (name.equals(positionAttributeName()) || name.equals(texCoordAttributeName())) { + Log.w("ImageShader", "Attempting to access internal attribute '" + name + + "' directly!"); + } + int loc = GLES20.glGetAttribLocation(mProgram, name); + if (loc < 0) { + throw new RuntimeException("Unknown attribute '" + name + "' in shader program!"); + } + return loc; + } + + public void setUniformValue(String uniformName, int value) { + useProgram(); + int uniformHandle = getUniformLocation(uniformName); + GLES20.glUniform1i(uniformHandle, value); + GLToolbox.checkGlError("Set uniform value (" + uniformName + ")"); + } + + public void setUniformValue(String uniformName, float value) { + useProgram(); + int uniformHandle = getUniformLocation(uniformName); + GLES20.glUniform1f(uniformHandle, value); + GLToolbox.checkGlError("Set uniform value (" + uniformName + ")"); + } + + public void setUniformValue(String uniformName, int[] values) { + ProgramUniform uniform = getProgramUniform(uniformName, true); + useProgram(); + int len = values.length; + switch (uniform.getType()) { + case GLES20.GL_INT: + checkUniformAssignment(uniform, len, 1); + GLES20.glUniform1iv(uniform.getLocation(), len, values, 0); + break; + case GLES20.GL_INT_VEC2: + checkUniformAssignment(uniform, len, 2); + GLES20.glUniform2iv(uniform.getLocation(), len / 2, values, 0); + break; + case GLES20.GL_INT_VEC3: + checkUniformAssignment(uniform, len, 3); + GLES20.glUniform2iv(uniform.getLocation(), len / 3, values, 0); + break; + case GLES20.GL_INT_VEC4: + checkUniformAssignment(uniform, len, 4); + GLES20.glUniform2iv(uniform.getLocation(), len / 4, values, 0); + break; + default: + throw new RuntimeException("Cannot assign int-array to incompatible uniform type " + + "for uniform '" + uniformName + "'!"); + } + GLToolbox.checkGlError("Set uniform value (" + uniformName + ")"); + } + + + public void setUniformValue(String uniformName, float[] values) { + ProgramUniform uniform = getProgramUniform(uniformName, true); + useProgram(); + int len = values.length; + switch (uniform.getType()) { + case GLES20.GL_FLOAT: + checkUniformAssignment(uniform, len, 1); + GLES20.glUniform1fv(uniform.getLocation(), len, values, 0); + break; + case GLES20.GL_FLOAT_VEC2: + checkUniformAssignment(uniform, len, 2); + GLES20.glUniform2fv(uniform.getLocation(), len / 2, values, 0); + break; + case GLES20.GL_FLOAT_VEC3: + checkUniformAssignment(uniform, len, 3); + GLES20.glUniform3fv(uniform.getLocation(), len / 3, values, 0); + break; + case GLES20.GL_FLOAT_VEC4: + checkUniformAssignment(uniform, len, 4); + GLES20.glUniform4fv(uniform.getLocation(), len / 4, values, 0); + break; + case GLES20.GL_FLOAT_MAT2: + checkUniformAssignment(uniform, len, 4); + GLES20.glUniformMatrix2fv(uniform.getLocation(), len / 4, false, values, 0); + break; + case GLES20.GL_FLOAT_MAT3: + checkUniformAssignment(uniform, len, 9); + GLES20.glUniformMatrix3fv(uniform.getLocation(), len / 9, false, values, 0); + break; + case GLES20.GL_FLOAT_MAT4: + checkUniformAssignment(uniform, len, 16); + GLES20.glUniformMatrix4fv(uniform.getLocation(), len / 16, false, values, 0); + break; + default: + throw new RuntimeException("Cannot assign float-array to incompatible uniform type " + + "for uniform '" + uniformName + "'!"); + } + GLToolbox.checkGlError("Set uniform value (" + uniformName + ")"); + } + + public void setAttributeValues(String attributeName, float[] data, int components) { + VertexAttribute attr = getProgramAttribute(attributeName, true); + attr.set(false, FLOAT_SIZE * components, components, GLES20.GL_FLOAT, data); + } + + public void setAttributeValues(String attributeName, int vbo, int type, int components, + int stride, int offset, boolean normalize) { + VertexAttribute attr = getProgramAttribute(attributeName, true); + attr.set(normalize, offset, stride, components, type, vbo); + } + + public void setSourceRect(float x, float y, float width, float height) { + setSourceCoords(new float[] { x, y, x + width, y, x, y + height, x + width, y + height }); + } + + public void setSourceRect(RectF rect) { + setSourceRect(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + } + + public void setSourceQuad(Quad quad) { + setSourceCoords(new float[] { quad.topLeft().x, quad.topLeft().y, + quad.topRight().x, quad.topRight().y, + quad.bottomLeft().x, quad.bottomLeft().y, + quad.bottomRight().x, quad.bottomRight().y }); + } + + public void setSourceCoords(float[] coords) { + if (coords.length != 8) { + throw new IllegalArgumentException("Expected 8 coordinates as source coordinates but " + + "got " + coords.length + " coordinates!"); + } + mSourceCoords = Arrays.copyOf(coords, 8); + } + + public void setSourceTransform(float[] matrix) { + if (matrix.length != 16) { + throw new IllegalArgumentException("Expected 4x4 matrix for source transform!"); + } + setSourceCoords(new float[] { + matrix[12], + matrix[13], + + matrix[0] + matrix[12], + matrix[1] + matrix[13], + + matrix[4] + matrix[12], + matrix[5] + matrix[13], + + matrix[0] + matrix[4] + matrix[12], + matrix[1] + matrix[5] + matrix[13], + }); + } + + public void setTargetRect(float x, float y, float width, float height) { + setTargetCoords(new float[] { x, y, + x + width, y, + x, y + height, + x + width, y + height }); + } + + public void setTargetRect(RectF rect) { + setTargetCoords(new float[] { rect.left, rect.top, + rect.right, rect.top, + rect.left, rect.bottom, + rect.right, rect.bottom }); + } + + public void setTargetQuad(Quad quad) { + setTargetCoords(new float[] { quad.topLeft().x, quad.topLeft().y, + quad.topRight().x, quad.topRight().y, + quad.bottomLeft().x, quad.bottomLeft().y, + quad.bottomRight().x, quad.bottomRight().y }); + } + + public void setTargetCoords(float[] coords) { + if (coords.length != 8) { + throw new IllegalArgumentException("Expected 8 coordinates as target coordinates but " + + "got " + coords.length + " coordinates!"); + } + mTargetCoords = new float[8]; + for (int i = 0; i < 8; ++i) { + mTargetCoords[i] = coords[i] * 2f - 1f; + } + } + + public void setTargetTransform(float[] matrix) { + if (matrix.length != 16) { + throw new IllegalArgumentException("Expected 4x4 matrix for target transform!"); + } + setTargetCoords(new float[] { + matrix[12], + matrix[13], + + matrix[0] + matrix[12], + matrix[1] + matrix[13], + + matrix[4] + matrix[12], + matrix[5] + matrix[13], + + matrix[0] + matrix[4] + matrix[12], + matrix[1] + matrix[5] + matrix[13], + }); + } + + public void setClearsOutput(boolean clears) { + mClearsOutput = clears; + } + + public boolean getClearsOutput() { + return mClearsOutput; + } + + public void setClearColor(float[] rgba) { + mClearColor = rgba; + } + + public float[] getClearColor() { + return mClearColor; + } + + public void setClearBufferMask(int bufferMask) { + mClearBuffers = bufferMask; + } + + public int getClearBufferMask() { + return mClearBuffers; + } + + public void setBlendEnabled(boolean enable) { + mBlendEnabled = enable; + } + + public boolean getBlendEnabled() { + return mBlendEnabled; + } + + public void setBlendFunc(int sFactor, int dFactor) { + mSFactor = sFactor; + mDFactor = dFactor; + } + + public void setDrawMode(int drawMode) { + mDrawMode = drawMode; + } + + public int getDrawMode() { + return mDrawMode; + } + + public void setVertexCount(int count) { + mVertexCount = count; + } + + public int getVertexCount() { + return mVertexCount; + } + + public void setBaseTextureUnit(int baseTexUnit) { + mBaseTexUnit = baseTexUnit; + } + + public int baseTextureUnit() { + return mBaseTexUnit; + } + + public String texCoordAttributeName() { + return "a_texcoord"; + } + + public String positionAttributeName() { + return "a_position"; + } + + public String inputTextureUniformName(int index) { + return "tex_sampler_" + index; + } + + public static int maxTextureUnits() { + return GLES20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS; + } + + @Override + protected void finalize() throws Throwable { + GLES20.glDeleteProgram(mProgram); + } + + protected void pushShaderState() { + useProgram(); + updateSourceCoordAttribute(); + updateTargetCoordAttribute(); + pushAttributes(); + if (mClearsOutput) { + GLES20.glClearColor(mClearColor[0], mClearColor[1], mClearColor[2], mClearColor[3]); + GLES20.glClear(mClearBuffers); + } + if (mBlendEnabled) { + GLES20.glEnable(GLES20.GL_BLEND); + GLES20.glBlendFunc(mSFactor, mDFactor); + } else { + GLES20.glDisable(GLES20.GL_BLEND); + } + GLToolbox.checkGlError("Set render variables"); + } + + private void focusTarget(RenderTarget target, int width, int height) { + target.focus(); + GLES20.glViewport(0, 0, width, height); + GLToolbox.checkGlError("glViewport"); + } + + private void bindInputTextures(TextureSource[] sources) { + for (int i = 0; i < sources.length; ++i) { + // Activate texture unit i + GLES20.glActiveTexture(baseTextureUnit() + i); + + // Bind texture + sources[i].bind(); + + // Assign the texture uniform in the shader to unit i + int texUniform = GLES20.glGetUniformLocation(mProgram, inputTextureUniformName(i)); + if (texUniform >= 0) { + GLES20.glUniform1i(texUniform, i); + } else { + throw new RuntimeException("Shader does not seem to support " + sources.length + + " number of input textures! Missing uniform " + inputTextureUniformName(i) + + "!"); + } + GLToolbox.checkGlError("Binding input texture " + i); + } + } + + private void pushAttributes() { + for (VertexAttribute attr : mAttributes.values()) { + if (!attr.push()) { + throw new RuntimeException("Unable to assign attribute value '" + attr + "'!"); + } + } + GLToolbox.checkGlError("Push Attributes"); + } + + private void updateSourceCoordAttribute() { + // If attribute does not exist, simply do nothing (may be custom shader). + VertexAttribute attr = getProgramAttribute(texCoordAttributeName(), false); + // A non-null value of mSourceCoords indicates new values to be set. + if (mSourceCoords != null && attr != null) { + // Upload new source coordinates to GPU + attr.set(false, FLOAT_SIZE * 2, 2, GLES20.GL_FLOAT, mSourceCoords); + } + // Do not set again (even if failed, to not cause endless attempts) + mSourceCoords = null; + } + + private void updateTargetCoordAttribute() { + // If attribute does not exist, simply do nothing (may be custom shader). + VertexAttribute attr = getProgramAttribute(positionAttributeName(), false); + // A non-null value of mTargetCoords indicates new values to be set. + if (mTargetCoords != null && attr != null) { + // Upload new target coordinates to GPU + attr.set(false, FLOAT_SIZE * 2, 2, GLES20.GL_FLOAT, mTargetCoords); + } + // Do not set again (even if failed, to not cause endless attempts) + mTargetCoords = null; + } + + private void render() { + GLES20.glDrawArrays(mDrawMode, 0, mVertexCount); + GLToolbox.checkGlError("glDrawArrays"); + } + + private void checkExecutable() { + if (mProgram == 0) { + throw new RuntimeException("Attempting to execute invalid shader-program!"); + } + } + + private void useProgram() { + GLES20.glUseProgram(mProgram); + GLToolbox.checkGlError("glUseProgram"); + } + + private static void checkTexCount(int count) { + if (count > maxTextureUnits()) { + throw new RuntimeException("Number of textures passed (" + count + ") exceeds the " + + "maximum number of allowed texture units (" + maxTextureUnits() + ")!"); + } + } + + private static int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + if (shader != 0) { + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + String info = GLES20.glGetShaderInfoLog(shader); + GLES20.glDeleteShader(shader); + shader = 0; + throw new RuntimeException("Could not compile shader " + shaderType + ":" + info); + } + } + return shader; + } + + private static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + throw new RuntimeException("Could not create shader-program as vertex shader " + + "could not be compiled!"); + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + throw new RuntimeException("Could not create shader-program as fragment shader " + + "could not be compiled!"); + } + + int program = GLES20.glCreateProgram(); + if (program != 0) { + GLES20.glAttachShader(program, vertexShader); + GLToolbox.checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + GLToolbox.checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + String info = GLES20.glGetProgramInfoLog(program); + GLES20.glDeleteProgram(program); + program = 0; + throw new RuntimeException("Could not link program: " + info); + } + } + + GLES20.glDeleteShader(vertexShader); + GLES20.glDeleteShader(pixelShader); + + return program; + } + + private void scanUniforms() { + int uniformCount[] = new int [1]; + GLES20.glGetProgramiv(mProgram, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, 0); + if (uniformCount[0] > 0) { + mUniforms = new HashMap<String, ProgramUniform>(uniformCount[0]); + for (int i = 0; i < uniformCount[0]; ++i) { + ProgramUniform uniform = new ProgramUniform(mProgram, i); + mUniforms.put(uniform.getName(), uniform); + } + } + } + + private ProgramUniform getProgramUniform(String name, boolean required) { + ProgramUniform result = mUniforms.get(name); + if (result == null && required) { + throw new IllegalArgumentException("Unknown uniform '" + name + "'!"); + } + return result; + } + + private VertexAttribute getProgramAttribute(String name, boolean required) { + VertexAttribute result = mAttributes.get(name); + if (result == null) { + int handle = GLES20.glGetAttribLocation(mProgram, name); + if (handle >= 0) { + result = new VertexAttribute(name, handle); + mAttributes.put(name, result); + } else if (required) { + throw new IllegalArgumentException("Unknown attribute '" + name + "'!"); + } + } + return result; + } + + private void checkUniformAssignment(ProgramUniform uniform, int values, int components) { + if (values % components != 0) { + throw new RuntimeException("Size mismatch: Attempting to assign values of size " + + values + " to uniform '" + uniform.getName() + "' (must be multiple of " + + components + ")!"); + } else if (uniform.getSize() != values / components) { + throw new RuntimeException("Size mismatch: Cannot assign " + values + " values to " + + "uniform '" + uniform.getName() + "'!"); + } + } + + private static int strlen(byte[] strVal) { + for (int i = 0; i < strVal.length; ++i) { + if (strVal[i] == '\0') { + return i; + } + } + return strVal.length; + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/InputPort.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/InputPort.java new file mode 100644 index 0000000..82749c5 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/InputPort.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import java.lang.reflect.Field; + +/** + * Input ports are the receiving ports of frames in a filter. + * <p> + * InputPort instances receive Frame data from connected OutputPort instances of a previous filter. + * Frames flow from output ports to input ports. Filters can process frame data by calling + * {@link #pullFrame()} on an input port. If the input port is set to wait for an input frame + * (see {@link #setWaitsForFrame(boolean)}), there is guaranteed to be Frame on the port before + * {@code onProcess()} is called. This is the default setting. Otherwise, calling + * {@link #pullFrame()} may return a value of {@code null}. + * <p/><p> + * InputPorts may be bound to fields of the Filter. When an input port is bound to a field, Frame + * values will be assigned to the field once a Frame is received on that port. The Frame value must + * be of a type that is compatible with the field type. + * </p> + */ +public final class InputPort { + + private Filter mFilter; + private String mName; + private Signature.PortInfo mInfo; + private FrameListener mListener = null; + private FrameQueue.Builder mQueueBuilder = null; + private FrameQueue mQueue = null; + private boolean mWaitForFrame = true; + private boolean mAutoPullEnabled = false; + + public interface FrameListener { + public void onFrameReceived(InputPort port, Frame frame); + } + + private class FieldBinding implements FrameListener { + private Field mField; + + public FieldBinding(Field field) { + mField = field; + } + + @Override + public void onFrameReceived(InputPort port, Frame frame) { + try { + if(port.mInfo.type.getNumberOfDimensions() > 0) { + FrameValues frameValues = frame.asFrameValues(); + mField.set(mFilter, frameValues.getValues()); + } else { + FrameValue frameValue = frame.asFrameValue(); + mField.set(mFilter, frameValue.getValue()); + } + } catch (Exception e) { + throw new RuntimeException("Assigning frame " + frame + " to field " + + mField + " of filter " + mFilter + " caused exception!", e); + } + } + } + + /** + * Attach this input port to an output port for frame passing. + * + * Use this method whenever you plan on passing a Frame through from an input port to an + * output port. This must be called from inside + * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}. + * + * @param outputPort the output port that Frames will be pushed to. + */ + public void attachToOutputPort(OutputPort outputPort) { + assertInAttachmentStage(); + mFilter.openOutputPort(outputPort); + mQueueBuilder.attachQueue(outputPort.getQueue()); + } + + /** + * Bind this input port to the specified listener. + * + * Use this when you wish to be notified of incoming frames. The listener method + * {@link FrameListener#onFrameReceived(InputPort, Frame)} will be called once a Frame is pulled + * on this port. Typically this is called from inside + * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}, and used in + * conjunction with {@link #setAutoPullEnabled(boolean)}. Overrides any previous bindings. + * + * @param listener the listener to handle incoming Frames. + */ + public void bindToListener(FrameListener listener) { + assertInAttachmentStage(); + mListener = listener; + } + + /** + * Bind this input port to the specified field. + * + * Use this when you wish to pull frames directly into a field of the filter. This requires + * that the input frames can be interpreted as object-based frames of the field's class. + * Overrides any previous bindings. + * + * This is typically called from inside + * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}, and used in + * conjunction with {@link #setAutoPullEnabled(boolean)}. + * + * @param field the field to pull frame data into. + * @see #bindToFieldNamed(String) + * @see #setAutoPullEnabled(boolean) + */ + public void bindToField(Field field) { + assertInAttachmentStage(); + mListener = new FieldBinding(field); + } + + /** + * Bind this input port to the field with the specified name. + * + * Use this when you wish to pull frames directly into a field of the filter. This requires + * that the input frames can be interpreted as object-based frames of the field's class. + * Overrides any previous bindings. + * + * This is typically called from inside + * {@link Filter#onInputPortAttached(InputPort) onInputPortAttached}, and used in + * conjunction with {@link #setAutoPullEnabled(boolean)}. + * + * @param fieldName the field to pull frame data into. + * @see #bindToField(Field) + * @see #setAutoPullEnabled(boolean) + */ + public void bindToFieldNamed(String fieldName) { + Field field = findFieldNamed(fieldName, mFilter.getClass()); + if (field == null) { + throw new IllegalArgumentException("Attempting to bind to unknown field '" + + fieldName + "'!"); + } + bindToField(field); + } + + /** + * Set whether the InputPort automatically pulls frames. + * This is typically only used when the port is bound to another target. + * @param enabled true, if frames should be automatically pulled on this port. + */ + public void setAutoPullEnabled(boolean enabled) { + mAutoPullEnabled = enabled; + } + + /** + * Returns whether the InputPort automatically pulls frames. + * @return true, if frames are automatically pulled on this port. + */ + public boolean isAutoPullEnabled() { + return mAutoPullEnabled; + } + + /** + * Pull a waiting a frame from the port. + * + * Call this to pull a frame from the input port for processing. If no frame is waiting on the + * input port, returns null. After this call the port will have no Frame waiting (empty port). + * Note, that this returns a frame owned by the input queue. You must detach the frame if you + * wish to hold on to it. + * + * @return Frame instance, or null if no frame is available for pulling. + */ + public synchronized Frame pullFrame() { + if (mQueue == null) { + throw new IllegalStateException("Cannot pull frame from closed input port!"); + } + Frame frame = mQueue.pullFrame(); + if (frame != null) { + if (mListener != null) { + mListener.onFrameReceived(this, frame); + } + //Log.i("InputPort", "Adding frame " + frame + " to auto-release pool"); + mFilter.addAutoReleaseFrame(frame); + long timestamp = frame.getTimestamp(); + if (timestamp != Frame.TIMESTAMP_NOT_SET) { + mFilter.onPulledFrameWithTimestamp(frame.getTimestamp()); + } + } + return frame; + } + + public synchronized Frame peek() { + if (mQueue == null) { + throw new IllegalStateException("Cannot pull frame from closed input port!"); + } + return mQueue.peek(); + } + + /** + * Returns true, if the port is connected. + * @return true, if there is an output port that connects to this port. + */ + public boolean isConnected() { + return mQueue != null; + } + + /** + * Returns true, if there is a frame waiting on this port. + * @return true, if there is a frame waiting on this port. + */ + public synchronized boolean hasFrame() { + return mQueue != null && mQueue.canPull(); + } + + /** + * Sets whether to wait for a frame on this port before processing. + * When set to true, the Filter will not be scheduled for processing unless there is a Frame + * waiting on this port. The default value is true. + * + * @param wait true, if the Filter should wait for a Frame before processing. + * @see #waitsForFrame() + */ + public void setWaitsForFrame(boolean wait) { + mWaitForFrame = wait; + } + + /** + * Returns whether the filter waits for a frame on this port before processing. + * @return true, if the filter waits for a frame on this port before processing. + * @see #setWaitsForFrame(boolean) + */ + public boolean waitsForFrame() { + return mWaitForFrame; + } + + /** + * Returns the input port's name. + * This is the name that was specified when the input port was connected. + * + * @return the input port's name. + */ + public String getName() { + return mName; + } + + /** + * Returns the FrameType of this port. + * This is the type that was specified when the input port was declared. + * + * @return the input port's FrameType. + */ + public FrameType getType() { + return getQueue().getType(); + } + + /** + * Return the filter object that this port belongs to. + * + * @return the input port's filter. + */ + public Filter getFilter() { + return mFilter; + } + + @Override + public String toString() { + return mFilter.getName() + ":" + mName; + } + + // Internal only /////////////////////////////////////////////////////////////////////////////// + InputPort(Filter filter, String name, Signature.PortInfo info) { + mFilter = filter; + mName = name; + mInfo = info; + } + + boolean conditionsMet() { + return !mWaitForFrame || hasFrame(); + } + + void onOpen(FrameQueue.Builder builder) { + mQueueBuilder = builder; + mQueueBuilder.setReadType(mInfo.type); + mFilter.onInputPortOpen(this); + } + + void setQueue(FrameQueue queue) { + mQueue = queue; + mQueueBuilder = null; + } + + FrameQueue getQueue() { + return mQueue; + } + + void clear() { + if (mQueue != null) { + mQueue.clear(); + } + } + + private void assertInAttachmentStage() { + if (mQueueBuilder == null) { + throw new IllegalStateException("Attempting to attach port while not in attachment " + + "stage!"); + } + } + + private Field findFieldNamed(String fieldName, Class<?> clazz) { + Field field = null; + try { + field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + } catch (NoSuchFieldException e) { + Class<?> superClass = clazz.getSuperclass(); + if (superClass != null) { + field = findFieldNamed(fieldName, superClass); + } + } + return field; + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MffContext.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MffContext.java new file mode 100644 index 0000000..b7212f9 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MffContext.java @@ -0,0 +1,470 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ConfigurationInfo; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.renderscript.RenderScript; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.ViewGroup; + +import java.util.HashSet; +import java.util.Set; + +/** + * The MffContext holds the state and resources of a Mobile Filter Framework processing instance. + * Though it is possible to create multiple MffContext instances, typical applications will rely on + * a single MffContext to perform all processing within the Mobile Filter Framework. + * + * The MffContext class declares two methods {@link #onPause()} and {@link #onResume()}, that are + * typically called when the application activity is paused and resumed. This will take care of + * halting any processing in the context, and releasing resources while the activity is paused. + */ +public class MffContext { + + /** + * Class to hold configuration information for MffContexts. + */ + public static class Config { + /** + * Set to true, if this context will make use of the camera. + * If your application does not require the camera, the context does not guarantee that + * a camera is available for streaming. That is, you may only use a CameraStreamer if + * the context's {@link #isCameraStreamingSupported()} returns true. + */ + public boolean requireCamera = true; + + /** + * Set to true, if this context requires OpenGL. + * If your application does not require OpenGL, the context does not guarantee that OpenGL + * is available. That is, you may only use OpenGL (within filters running in this context) + * if the context's {@link #isOpenGLSupported()} method returns true. + */ + public boolean requireOpenGL = true; + + /** + * On older Android versions the Camera may need a SurfaceView to render into in order to + * function. You may specify a dummy SurfaceView here if you do not want the context to + * create its own view. Note, that your view may or may not be used. You cannot rely on + * your dummy view to be used by the Camera. If you pass null, no dummy view will be used. + * In this case your application may not run correctly on older devices if you use the + * camera. This flag has no effect if you do not require the camera. + */ + public SurfaceView dummySurface = null; + + /** Force MFF to not use OpenGL in its processing. */ + public boolean forceNoGL = false; + } + + static private class State { + public static final int STATE_RUNNING = 1; + public static final int STATE_PAUSED = 2; + public static final int STATE_DESTROYED = 3; + + public int current = STATE_RUNNING; + } + + /** The application context. */ + private Context mApplicationContext = null; + + /** The set of filter graphs within this context */ + private Set<FilterGraph> mGraphs = new HashSet<FilterGraph>(); + + /** The set of graph runners within this context */ + private Set<GraphRunner> mRunners = new HashSet<GraphRunner>(); + + /** True, if the context preserves frames when paused. */ + private boolean mPreserveFramesOnPause = false; + + /** The shared CameraStreamer that streams camera frames to CameraSource filters. */ + private CameraStreamer mCameraStreamer = null; + + /** The current context state. */ + private State mState = new State(); + + /** A dummy SurfaceView that is required for Camera operation on older devices. */ + private SurfaceView mDummySurfaceView = null; + + /** Handler to execute code in the context's thread, such as issuing callbacks. */ + private Handler mHandler = null; + + /** Flag whether OpenGL ES 2 is supported in this context. */ + private boolean mGLSupport; + + /** Flag whether camera streaming is supported in this context. */ + private boolean mCameraStreamingSupport; + + /** RenderScript base master class. */ + private RenderScript mRenderScript; + + /** + * Creates a new MffContext with the default configuration. + * + * An MffContext must be attached to a Context object of an application. You may create + * multiple MffContexts, however data between them cannot be shared. The context must be + * created in a thread with a Looper (such as the main/UI thread). + * + * On older versions of Android, the MffContext may create a visible dummy view for the + * camera to render into. This is a 1x1 SurfaceView that is placed into the top-left corner. + * + * @param context The application context to attach the MffContext to. + */ + public MffContext(Context context) { + init(context, new Config()); + } + + /** + * Creates a new MffContext with the specified configuration. + * + * An MffContext must be attached to a Context object of an application. You may create + * multiple MffContexts, however data between them cannot be shared. The context must be + * created in a thread with a Looper (such as the main/UI thread). + * + * On older versions of Android, the MffContext may create a visible dummy view for the + * camera to render into. This is a 1x1 SurfaceView that is placed into the top-left corner. + * You may alternatively specify your own SurfaceView in the configuration. + * + * @param context The application context to attach the MffContext to. + * @param config The configuration to use. + * + * @throws RuntimeException If no context for the requested configuration could be created. + */ + public MffContext(Context context, Config config) { + init(context, config); + } + + /** + * Put all processing in the context on hold. + * This is typically called from your application's <code>onPause()</code> method, and will + * stop all running graphs (closing their filters). If the context does not preserve frames on + * pause (see {@link #setPreserveFramesOnPause(boolean)}) all frames attached to this context + * are released. + */ + public void onPause() { + synchronized (mState) { + if (mState.current == State.STATE_RUNNING) { + if (mCameraStreamer != null) { + mCameraStreamer.halt(); + } + stopRunners(true); + mState.current = State.STATE_PAUSED; + } + } + } + + /** + * Resumes the processing in this context. + * This is typically called from the application's <code>onResume()</code> method, and will + * resume processing any of the previously stopped filter graphs. + */ + public void onResume() { + synchronized (mState) { + if (mState.current == State.STATE_PAUSED) { + resumeRunners(); + resumeCamera(); + mState.current = State.STATE_RUNNING; + } + } + } + + /** + * Release all resources associated with this context. + * This will also stop any running graphs. + */ + public void release() { + synchronized (mState) { + if (mState.current != State.STATE_DESTROYED) { + if (mCameraStreamer != null) { + mCameraStreamer.stop(); + mCameraStreamer.tearDown(); + } + if (Build.VERSION.SDK_INT >= 11) { + maybeDestroyRenderScript(); + } + stopRunners(false); + waitUntilStopped(); + tearDown(); + mState.current = State.STATE_DESTROYED; + } + } + } + + /** + * Set whether frames are preserved when the context is paused. + * When passing false, all Frames associated with this context are released. The default + * value is true. + * + * @param preserve true, to preserve frames when the context is paused. + * + * @see #getPreserveFramesOnPause() + */ + public void setPreserveFramesOnPause(boolean preserve) { + mPreserveFramesOnPause = preserve; + } + + /** + * Returns whether frames are preserved when the context is paused. + * + * @return true, if frames are preserved when the context is paused. + * + * @see #setPreserveFramesOnPause(boolean) + */ + public boolean getPreserveFramesOnPause() { + return mPreserveFramesOnPause; + } + + /** + * Returns the application context that the MffContext is attached to. + * + * @return The application context for this context. + */ + public Context getApplicationContext() { + return mApplicationContext; + } + + /** + * Returns the context's shared CameraStreamer. + * Use the CameraStreamer to control the Camera. Frames from the Camera are typically streamed + * to CameraSource filters. + * + * @return The context's CameraStreamer instance. + */ + public CameraStreamer getCameraStreamer() { + if (mCameraStreamer == null) { + mCameraStreamer = new CameraStreamer(this); + } + return mCameraStreamer; + } + + /** + * Set the default EGL config chooser. + * + * When an EGL context is required by the MFF, the channel sizes specified here are used. The + * default sizes are 8 bits per R,G,B,A channel and 0 bits for depth and stencil channels. + * + * @param redSize The size of the red channel in bits. + * @param greenSize The size of the green channel in bits. + * @param blueSize The size of the blue channel in bits. + * @param alphaSize The size of the alpha channel in bits. + * @param depthSize The size of the depth channel in bits. + * @param stencilSize The size of the stencil channel in bits. + */ + public static void setEGLConfigChooser(int redSize, + int greenSize, + int blueSize, + int alphaSize, + int depthSize, + int stencilSize) { + RenderTarget.setEGLConfigChooser(redSize, + greenSize, + blueSize, + alphaSize, + depthSize, + stencilSize); + } + + /** + * Returns true, if this context supports using OpenGL. + * @return true, if this context supports using OpenGL. + */ + public final boolean isOpenGLSupported() { + return mGLSupport; + } + + /** + * Returns true, if this context supports camera streaming. + * @return true, if this context supports camera streaming. + */ + public final boolean isCameraStreamingSupported() { + return mCameraStreamingSupport; + } + + @TargetApi(11) + public final RenderScript getRenderScript() { + if (mRenderScript == null) { + mRenderScript = RenderScript.create(mApplicationContext); + } + return mRenderScript; + } + + final void assertOpenGLSupported() { + if (!isOpenGLSupported()) { + throw new RuntimeException("Attempting to use OpenGL ES 2 in a context that does not " + + "support it!"); + } + } + + void addGraph(FilterGraph graph) { + synchronized (mGraphs) { + mGraphs.add(graph); + } + } + + void addRunner(GraphRunner runner) { + synchronized (mRunners) { + mRunners.add(runner); + } + } + + SurfaceView getDummySurfaceView() { + return mDummySurfaceView; + } + + void postRunnable(Runnable runnable) { + mHandler.post(runnable); + } + + private void init(Context context, Config config) { + determineGLSupport(context, config); + determineCameraSupport(config); + createHandler(); + mApplicationContext = context.getApplicationContext(); + fetchDummySurfaceView(context, config); + } + + private void fetchDummySurfaceView(Context context, Config config) { + if (config.requireCamera && CameraStreamer.requireDummySurfaceView()) { + mDummySurfaceView = config.dummySurface != null + ? config.dummySurface + : createDummySurfaceView(context); + } + } + + private void determineGLSupport(Context context, Config config) { + if (config.forceNoGL) { + mGLSupport = false; + } else { + mGLSupport = getPlatformSupportsGLES2(context); + if (config.requireOpenGL && !mGLSupport) { + throw new RuntimeException("Cannot create context that requires GL support on " + + "this platform!"); + } + } + } + + private void determineCameraSupport(Config config) { + mCameraStreamingSupport = (CameraStreamer.getNumberOfCameras() > 0); + if (config.requireCamera && !mCameraStreamingSupport) { + throw new RuntimeException("Cannot create context that requires a camera on " + + "this platform!"); + } + } + + private static boolean getPlatformSupportsGLES2(Context context) { + ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + ConfigurationInfo configurationInfo = am.getDeviceConfigurationInfo(); + return configurationInfo.reqGlEsVersion >= 0x20000; + } + + private void createHandler() { + if (Looper.myLooper() == null) { + throw new RuntimeException("MffContext must be created in a thread with a Looper!"); + } + mHandler = new Handler(); + } + + private void stopRunners(boolean haltOnly) { + synchronized (mRunners) { + // Halt all runners (does nothing if not running) + for (GraphRunner runner : mRunners) { + if (haltOnly) { + runner.halt(); + } else { + runner.stop(); + } + } + // Flush all graphs if requested (this is queued up after the call to halt) + if (!mPreserveFramesOnPause) { + for (GraphRunner runner : mRunners) { + runner.flushFrames(); + } + } + } + } + + private void resumeRunners() { + synchronized (mRunners) { + for (GraphRunner runner : mRunners) { + runner.restart(); + } + } + } + + private void resumeCamera() { + // Restart only affects previously halted cameras that were running. + if (mCameraStreamer != null) { + mCameraStreamer.restart(); + } + } + + private void waitUntilStopped() { + for (GraphRunner runner : mRunners) { + runner.waitUntilStop(); + } + } + + private void tearDown() { + // Tear down graphs + for (FilterGraph graph : mGraphs) { + graph.tearDown(); + } + + // Tear down runners + for (GraphRunner runner : mRunners) { + runner.tearDown(); + } + } + + @SuppressWarnings("deprecation") + private SurfaceView createDummySurfaceView(Context context) { + // This is only called on Gingerbread devices, so deprecation warning is unnecessary. + SurfaceView dummySurfaceView = new SurfaceView(context); + dummySurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + // If we have an activity for this context we'll add the SurfaceView to it (as a 1x1 view + // in the top-left corner). If not, we warn the user that they may need to add one manually. + Activity activity = findActivityForContext(context); + if (activity != null) { + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(1, 1); + activity.addContentView(dummySurfaceView, params); + } else { + Log.w("MffContext", "Could not find activity for dummy surface! Consider specifying " + + "your own SurfaceView!"); + } + return dummySurfaceView; + } + + private Activity findActivityForContext(Context context) { + return (context instanceof Activity) ? (Activity) context : null; + } + + @TargetApi(11) + private void maybeDestroyRenderScript() { + if (mRenderScript != null) { + mRenderScript.destroy(); + mRenderScript = null; + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MotionSensor.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MotionSensor.java new file mode 100644 index 0000000..95558f2 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/MotionSensor.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 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. + */ + +// Make values from a motion sensor (e.g., accelerometer) available as filter outputs. + +package androidx.media.filterpacks.sensors; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValues; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public final class MotionSensor extends Filter implements SensorEventListener { + + private SensorManager mSensorManager = null; + private Sensor mSensor = null; + + private float[] mValues = new float[3]; + + public MotionSensor(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("values", Signature.PORT_REQUIRED, FrameType.array(float.class)) + .disallowOtherPorts(); + } + + @Override + protected void onPrepare() { + mSensorManager = (SensorManager)getContext().getApplicationContext() + .getSystemService(Context.SENSOR_SERVICE); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION); + // TODO: currently, the type of sensor is hardcoded. Should be able to set the sensor + // type as filter input! + mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_UI); + } + + @Override + protected void onTearDown() { + mSensorManager.unregisterListener(this); + } + + @Override + public final void onAccuracyChanged(Sensor sensor, int accuracy) { + // (Do we need to do something when sensor accuracy changes?) + } + + @Override + public final void onSensorChanged(SensorEvent event) { + synchronized(mValues) { + mValues[0] = event.values[0]; + mValues[1] = event.values[1]; + mValues[2] = event.values[2]; + } + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("values"); + FrameValues outFrame = outPort.fetchAvailableFrame(null).asFrameValues(); + synchronized(mValues) { + outFrame.setValues(mValues); + } + outFrame.setTimestamp(System.currentTimeMillis() * 1000000L); + outPort.pushFrame(outFrame); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NewChromaHistogramFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NewChromaHistogramFilter.java new file mode 100644 index 0000000..f524be3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NewChromaHistogramFilter.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2012 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. + */ + +// Extract histogram from image. + +package androidx.media.filterpacks.histogram; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameBuffer2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +/** + * ChromaHistogramFilter takes in an image in HSVA format and computes a 2-D histogram with a + * 2 dimensional chroma histogram based on hue (column) and saturation (row) at the top and + * a 1-D value histogram in the last row. The number of bin in the value histogram equals to + * the number of bins in hue. + */ +public final class NewChromaHistogramFilter extends Filter { + + private int mHueBins = 6; + private int mSaturationBins = 3; + private int mValueBins; + + private int mSaturationThreshold = 26; // 255 * 0.1 + private int mValueThreshold = 51; // 255 * 0.2 + + public NewChromaHistogramFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU); + FrameType dataOut = FrameType.buffer2D(FrameType.ELEMENT_FLOAT32); + + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addInputPort("huebins", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addInputPort("saturationbins", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addInputPort("saturationthreshold", Signature.PORT_OPTIONAL, + FrameType.single(int.class)) + .addInputPort("valuethreshold", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addOutputPort("histogram", Signature.PORT_REQUIRED, dataOut) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("huebins")) { + port.bindToFieldNamed("mHueBins"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("saturationbins")) { + port.bindToFieldNamed("mSaturationBins"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("saturationthreshold")) { + port.bindToFieldNamed("mSaturationThreshold"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("valuethreshold")) { + port.bindToFieldNamed("mValueThreshold"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected void onProcess() { + FrameBuffer2D imageFrame = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + OutputPort outPort = getConnectedOutputPort("histogram"); + + mValueBins = mHueBins; + int[] outDims = new int[] {mHueBins, mSaturationBins + 1}; + FrameBuffer2D histogramFrame = outPort.fetchAvailableFrame(outDims).asFrameBuffer2D(); + + ByteBuffer imageBuffer = imageFrame.lockBytes(Frame.MODE_READ); + ByteBuffer histogramBuffer = histogramFrame.lockBytes(Frame.MODE_READ); + histogramBuffer.order(ByteOrder.nativeOrder()); + FloatBuffer floatHistogram = histogramBuffer.asFloatBuffer(); + + // Run native method + extractChromaHistogram(imageBuffer, floatHistogram, mHueBins, mSaturationBins, mValueBins, + mSaturationThreshold, mValueThreshold); + + imageFrame.unlock(); + histogramFrame.unlock(); + + outPort.pushFrame(histogramFrame); + } + + private static native void extractChromaHistogram(ByteBuffer imageBuffer, + FloatBuffer histogramBuffer, int hueBins, int saturationBins, int valueBins, + int saturationThreshold, int valueThreshold); + + static { + System.loadLibrary("smartcamera_jni"); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NormFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NormFilter.java new file mode 100644 index 0000000..e816110 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/NormFilter.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterpacks.numeric; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +/** + * Filter to calculate the 2-norm of the inputs. i.e. sqrt(x^2 + y^2) + * TODO: Add support for more norms in the future. + */ +public final class NormFilter extends Filter { + private static final String TAG = "NormFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + public NormFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType floatT = FrameType.single(float.class); + return new Signature() + .addInputPort("x", Signature.PORT_REQUIRED, floatT) + .addInputPort("y", Signature.PORT_REQUIRED, floatT) + .addOutputPort("norm", Signature.PORT_REQUIRED, floatT) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + FrameValue xFrameValue = getConnectedInputPort("x").pullFrame().asFrameValue(); + float xValue = ((Float)xFrameValue.getValue()).floatValue(); + FrameValue yFrameValue = getConnectedInputPort("y").pullFrame().asFrameValue(); + float yValue = ((Float)yFrameValue.getValue()).floatValue(); + + float norm = (float) Math.hypot(xValue, yValue); + if (mLogVerbose) Log.v(TAG, "Norm = " + norm); + OutputPort outPort = getConnectedOutputPort("norm"); + FrameValue outFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + outFrame.setValue(norm); + outPort.pushFrame(outFrame); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/OutputPort.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/OutputPort.java new file mode 100644 index 0000000..06117c3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/OutputPort.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +/** + * Output ports are the data emitting ports of filters. + * <p> + * Filters push data frames onto output-ports, which in turn push them onto their connected input + * ports. Output ports must be connected to an input port before data can be pushed onto them. + * Input and output ports share their Frame slot, meaning that when a frame is waiting on an output + * port, it is also waiting on the connected input port. + * </p><p> + * Only one frame can be pushed onto an output port at a time. In other words, a Frame must first + * be consumed by the target filter before a new frame can be pushed on the output port. If the + * output port is set to wait until it becomes free (see {@link #setWaitsUntilAvailable(boolean)}), + * it is guaranteed to be available when {@code onProcess()} is called. This is the default setting. + * </p> + */ +public final class OutputPort { + + private Filter mFilter; + private String mName; + private Signature.PortInfo mInfo; + private FrameQueue.Builder mQueueBuilder = null; + private FrameQueue mQueue = null; + private boolean mWaitsUntilAvailable = true; + private InputPort mTarget = null; + + /** + * Returns true, if this port is connected to a target port. + * @return true, if this port is connected to a target port. + */ + public boolean isConnected() { + return mTarget != null; + } + + /** + * Returns true, if there is no frame waiting on this port. + * @return true, if no Frame instance is waiting on this port. + */ + public boolean isAvailable() { + return mQueue == null || mQueue.canPush(); + } + + /** + * Returns a frame for writing. + * + * Call this method to fetch a new frame to write into. When you have finished writing the + * frame data, you can push it into the output queue using {@link #pushFrame(Frame)}. Note, + * that the Frame returned is owned by the queue. If you wish to hold on to the frame, you + * must detach it. + * + * @param dimensions the size of the Frame you wish to obtain. + * @return a writable Frame instance. + */ + public Frame fetchAvailableFrame(int[] dimensions) { + Frame frame = getQueue().fetchAvailableFrame(dimensions); + if (frame != null) { + //Log.i("OutputPort", "Adding frame " + frame + " to auto-release pool"); + mFilter.addAutoReleaseFrame(frame); + } + return frame; + } + + /** + * Pushes a frame onto this output port. + * + * This is typically a Frame instance you obtained by previously calling + * {@link #fetchAvailableFrame(int[])}, but may come from other sources such as an input port + * that is attached to this output port. + * + * Once you have pushed a frame to an output, you may no longer modify it as it may be shared + * among other filters. + * + * @param frame the frame to push to the output queue. + */ + public void pushFrame(Frame frame) { + // Some queues allow pushing without fetching, so we need to make sure queue is open + // before pushing! + long timestamp = frame.getTimestamp(); + if (timestamp == Frame.TIMESTAMP_NOT_SET) + frame.setTimestamp(mFilter.getCurrentTimestamp()); + getQueue().pushFrame(frame); + } + + /** + * Sets whether to wait until this port becomes available before processing. + * When set to true, the Filter will not be scheduled for processing unless there is no Frame + * waiting on this port. The default value is true. + * + * @param wait true, if filter should wait for the port to become available before processing. + * @see #waitsUntilAvailable() + */ + public void setWaitsUntilAvailable(boolean wait) { + mWaitsUntilAvailable = wait; + } + + /** + * Returns whether the filter waits until this port is available before processing. + * @return true, if the filter waits until this port is available before processing. + * @see #setWaitsUntilAvailable(boolean) + */ + public boolean waitsUntilAvailable() { + return mWaitsUntilAvailable; + } + + /** + * Returns the output port's name. + * This is the name that was specified when the output port was connected. + * + * @return the output port's name. + */ + public String getName() { + return mName; + } + + /** + * Return the filter object that this port belongs to. + * + * @return the output port's filter. + */ + public Filter getFilter() { + return mFilter; + } + + @Override + public String toString() { + return mFilter.getName() + ":" + mName; + } + + OutputPort(Filter filter, String name, Signature.PortInfo info) { + mFilter = filter; + mName = name; + mInfo = info; + } + + void setTarget(InputPort target) { + mTarget = target; + } + + /** + * Return the (input) port that this output port is connected to. + * + * @return the connected port, null if not connected. + */ + public InputPort getTarget() { + return mTarget; + } + + FrameQueue getQueue() { + return mQueue; + } + + void setQueue(FrameQueue queue) { + mQueue = queue; + mQueueBuilder = null; + } + + void onOpen(FrameQueue.Builder builder) { + mQueueBuilder = builder; + mQueueBuilder.setWriteType(mInfo.type); + mFilter.onOutputPortOpen(this); + } + + boolean isOpen() { + return mQueue != null; + } + + final boolean conditionsMet() { + return !mWaitsUntilAvailable || isAvailable(); + } + + void clear() { + if (mQueue != null) { + mQueue.clear(); + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/PixelUtils.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/PixelUtils.java new file mode 100644 index 0000000..88538d4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/PixelUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 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 androidx.media.filterfw; + +import java.nio.ByteBuffer; + +/** + * A collection of utilities to deal with pixel operations on ByteBuffers. + */ +public class PixelUtils { + + /** + * Copy pixels from one buffer to another, applying a transformation. + * + * <p>The transformation is specified by specifying the initial offset in the output buffer, the + * stride (in pixels) between each pixel, and the stride (in pixels) between each row. The row + * stride is measured as the number of pixels between the start of each row.</p> + * + * <p>Note that this method is native for efficiency reasons. It does NOT do any bounds checking + * other than making sure the buffers are of sufficient size. This means that you can corrupt + * memory if specifying incorrect stride values!</p> + * + * @param input The input buffer containing pixel data. + * @param output The output buffer to hold the transformed pixel data. + * @param width The width of the input image. + * @param height The height of the input image. + * @param offset The start offset in the output (in pixels) + * @param pixStride The stride between each pixel (in pixels) + * @param rowStride The stride between the start of each row (in pixels) + */ + public static void copyPixels(ByteBuffer input, + ByteBuffer output, + int width, + int height, + int offset, + int pixStride, + int rowStride) { + if (input.remaining() != output.remaining()) { + throw new IllegalArgumentException("Input and output buffers must have the same size!"); + } else if (input.remaining() % 4 != 0) { + throw new IllegalArgumentException("Input buffer size must be a multiple of 4!"); + } else if (output.remaining() % 4 != 0) { + throw new IllegalArgumentException("Output buffer size must be a multiple of 4!"); + } else if ((width * height * 4) != input.remaining()) { + throw new IllegalArgumentException( + "Input buffer size does not match given dimensions!"); + } else if ((width * height * 4) != output.remaining()) { + throw new IllegalArgumentException( + "Output buffer size does not match given dimensions!"); + } + nativeCopyPixels(input, output, width, height, offset, pixStride, rowStride); + } + + private static native void nativeCopyPixels(ByteBuffer input, + ByteBuffer output, + int width, + int height, + int offset, + int pixStride, + int rowStride); + + static { + System.loadLibrary("smartcamera_jni"); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RenderTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RenderTarget.java new file mode 100644 index 0000000..ab0546d --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RenderTarget.java @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.media.MediaRecorder; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.os.Build.VERSION; +import android.util.Log; +import android.view.Surface; +import android.view.SurfaceHolder; + +import java.nio.ByteBuffer; +import java.util.HashMap; + +import javax.microedition.khronos.egl.EGL10; +import javax.microedition.khronos.egl.EGLConfig; +import javax.microedition.khronos.egl.EGLContext; +import javax.microedition.khronos.egl.EGLDisplay; +import javax.microedition.khronos.egl.EGLSurface; + +public final class RenderTarget { + + private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; + private static final int EGL_OPENGL_ES2_BIT = 4; + + // Pre-HC devices do not necessarily support multiple display surfaces. + private static boolean mSupportsMultipleDisplaySurfaces = (VERSION.SDK_INT >= 11); + + /** A Map that tracks which objects are wrapped by EGLSurfaces */ + private static HashMap<Object, EGLSurface> mSurfaceSources = new HashMap<Object, EGLSurface>(); + + /** A Map for performing reference counting over shared objects across RenderTargets */ + private static HashMap<Object, Integer> mRefCounts = new HashMap<Object, Integer>(); + + /** Stores the RenderTarget that is focused on the current thread. */ + private static ThreadLocal<RenderTarget> mCurrentTarget = new ThreadLocal<RenderTarget>(); + + /** The source for the surface used in this target (if any) */ + private Object mSurfaceSource = null; + + /** The cached EGLConfig instance. */ + private static EGLConfig mEglConfig = null; + + /** The display for which the EGLConfig was chosen. We expect only one. */ + private static EGLDisplay mConfiguredDisplay; + + private EGL10 mEgl; + private EGLDisplay mDisplay; + private EGLContext mContext; + private EGLSurface mSurface; + private int mFbo; + + private boolean mOwnsContext; + private boolean mOwnsSurface; + + private static HashMap<EGLContext, ImageShader> mIdShaders + = new HashMap<EGLContext, ImageShader>(); + + private static HashMap<EGLContext, EGLSurface> mDisplaySurfaces + = new HashMap<EGLContext, EGLSurface>(); + + private static int sRedSize = 8; + private static int sGreenSize = 8; + private static int sBlueSize = 8; + private static int sAlphaSize = 8; + private static int sDepthSize = 0; + private static int sStencilSize = 0; + + public static RenderTarget newTarget(int width, int height) { + EGL10 egl = (EGL10) EGLContext.getEGL(); + EGLDisplay eglDisplay = createDefaultDisplay(egl); + EGLConfig eglConfig = chooseEglConfig(egl, eglDisplay); + EGLContext eglContext = createContext(egl, eglDisplay, eglConfig); + EGLSurface eglSurface = createSurface(egl, eglDisplay, width, height); + RenderTarget result = new RenderTarget(eglDisplay, eglContext, eglSurface, 0, true, true); + result.addReferenceTo(eglSurface); + return result; + } + + public static RenderTarget currentTarget() { + // As RenderTargets are immutable, we can safely return the last focused instance on this + // thread, as we know it cannot have changed, and therefore must be current. + return mCurrentTarget.get(); + } + + public RenderTarget forTexture(TextureSource texture, int width, int height) { + // NOTE: We do not need to lookup any previous bindings of this texture to an FBO, as + // multiple FBOs to a single texture is valid. + int fbo = GLToolbox.generateFbo(); + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo); + GLToolbox.checkGlError("glBindFramebuffer"); + GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, + GLES20.GL_COLOR_ATTACHMENT0, + texture.getTarget(), + texture.getTextureId(), + 0); + GLToolbox.checkGlError("glFramebufferTexture2D"); + return new RenderTarget(mDisplay, mContext, surface(), fbo, false, false); + } + + public RenderTarget forSurfaceHolder(SurfaceHolder surfaceHolder) { + EGLConfig eglConfig = chooseEglConfig(mEgl, mDisplay); + EGLSurface eglSurf = null; + synchronized (mSurfaceSources) { + eglSurf = mSurfaceSources.get(surfaceHolder); + if (eglSurf == null) { + eglSurf = mEgl.eglCreateWindowSurface(mDisplay, eglConfig, surfaceHolder, null); + mSurfaceSources.put(surfaceHolder, eglSurf); + } + } + checkEglError(mEgl, "eglCreateWindowSurface"); + checkSurface(mEgl, eglSurf); + RenderTarget result = new RenderTarget(mDisplay, mContext, eglSurf, 0, false, true); + result.addReferenceTo(eglSurf); + result.setSurfaceSource(surfaceHolder); + return result; + } + + @TargetApi(11) + public RenderTarget forSurfaceTexture(SurfaceTexture surfaceTexture) { + EGLConfig eglConfig = chooseEglConfig(mEgl, mDisplay); + EGLSurface eglSurf = null; + synchronized (mSurfaceSources) { + eglSurf = mSurfaceSources.get(surfaceTexture); + if (eglSurf == null) { + eglSurf = mEgl.eglCreateWindowSurface(mDisplay, eglConfig, surfaceTexture, null); + mSurfaceSources.put(surfaceTexture, eglSurf); + } + } + checkEglError(mEgl, "eglCreateWindowSurface"); + checkSurface(mEgl, eglSurf); + RenderTarget result = new RenderTarget(mDisplay, mContext, eglSurf, 0, false, true); + result.setSurfaceSource(surfaceTexture); + result.addReferenceTo(eglSurf); + return result; + } + + @TargetApi(11) + public RenderTarget forSurface(Surface surface) { + EGLConfig eglConfig = chooseEglConfig(mEgl, mDisplay); + EGLSurface eglSurf = null; + synchronized (mSurfaceSources) { + eglSurf = mSurfaceSources.get(surface); + if (eglSurf == null) { + eglSurf = mEgl.eglCreateWindowSurface(mDisplay, eglConfig, surface, null); + mSurfaceSources.put(surface, eglSurf); + } + } + checkEglError(mEgl, "eglCreateWindowSurface"); + checkSurface(mEgl, eglSurf); + RenderTarget result = new RenderTarget(mDisplay, mContext, eglSurf, 0, false, true); + result.setSurfaceSource(surface); + result.addReferenceTo(eglSurf); + return result; + } + + public static RenderTarget forMediaRecorder(MediaRecorder mediaRecorder) { + throw new RuntimeException("Not yet implemented MediaRecorder -> RenderTarget!"); + } + + public static void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, + int depthSize, int stencilSize) { + sRedSize = redSize; + sGreenSize = greenSize; + sBlueSize = blueSize; + sAlphaSize = alphaSize; + sDepthSize = depthSize; + sStencilSize = stencilSize; + } + + public void registerAsDisplaySurface() { + if (!mSupportsMultipleDisplaySurfaces) { + // Note that while this does in effect change RenderTarget instances (by modifying + // their returned EGLSurface), breaking the immutability requirement, it does not modify + // the current target. This is important so that the instance returned in + // currentTarget() remains accurate. + EGLSurface currentSurface = mDisplaySurfaces.get(mContext); + if (currentSurface != null && !currentSurface.equals(mSurface)) { + throw new RuntimeException("This device supports only a single display surface!"); + } else { + mDisplaySurfaces.put(mContext, mSurface); + } + } + } + + public void unregisterAsDisplaySurface() { + if (!mSupportsMultipleDisplaySurfaces) { + mDisplaySurfaces.put(mContext, null); + } + } + + public void focus() { + RenderTarget current = mCurrentTarget.get(); + // We assume RenderTargets are immutable, so that we do not need to focus if the current + // RenderTarget has not changed. + if (current != this) { + mEgl.eglMakeCurrent(mDisplay, surface(), surface(), mContext); + mCurrentTarget.set(this); + } + if (getCurrentFbo() != mFbo) { + GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, mFbo); + GLToolbox.checkGlError("glBindFramebuffer"); + } + } + + public static void focusNone() { + EGL10 egl = (EGL10) EGLContext.getEGL(); + egl.eglMakeCurrent(egl.eglGetCurrentDisplay(), + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_SURFACE, + EGL10.EGL_NO_CONTEXT); + mCurrentTarget.set(null); + checkEglError(egl, "eglMakeCurrent"); + } + + public void swapBuffers() { + mEgl.eglSwapBuffers(mDisplay, surface()); + } + + public EGLContext getContext() { + return mContext; + } + + public static EGLContext currentContext() { + RenderTarget current = RenderTarget.currentTarget(); + return current != null ? current.getContext() : EGL10.EGL_NO_CONTEXT; + } + + public void release() { + if (mOwnsContext) { + mEgl.eglDestroyContext(mDisplay, mContext); + mContext = EGL10.EGL_NO_CONTEXT; + } + if (mOwnsSurface) { + synchronized (mSurfaceSources) { + if (removeReferenceTo(mSurface)) { + mEgl.eglDestroySurface(mDisplay, mSurface); + mSurface = EGL10.EGL_NO_SURFACE; + mSurfaceSources.remove(mSurfaceSource); + } + } + } + if (mFbo != 0) { + GLToolbox.deleteFbo(mFbo); + } + } + + public void readPixelData(ByteBuffer pixels, int width, int height) { + GLToolbox.readTarget(this, pixels, width, height); + } + + public ByteBuffer getPixelData(int width, int height) { + ByteBuffer pixels = ByteBuffer.allocateDirect(width * height * 4); + GLToolbox.readTarget(this, pixels, width, height); + return pixels; + } + + /** + * Returns an identity shader for this context. + * You must not modify this shader. Use {@link ImageShader#createIdentity()} if you need to + * modify an identity shader. + */ + public ImageShader getIdentityShader() { + ImageShader idShader = mIdShaders.get(mContext); + if (idShader == null) { + idShader = ImageShader.createIdentity(); + mIdShaders.put(mContext, idShader); + } + return idShader; + } + + @Override + public String toString() { + return "RenderTarget(" + mDisplay + ", " + mContext + ", " + mSurface + ", " + mFbo + ")"; + } + + private void setSurfaceSource(Object source) { + mSurfaceSource = source; + } + + private void addReferenceTo(Object object) { + Integer refCount = mRefCounts.get(object); + if (refCount != null) { + mRefCounts.put(object, refCount + 1); + } else { + mRefCounts.put(object, 1); + } + } + + private boolean removeReferenceTo(Object object) { + Integer refCount = mRefCounts.get(object); + if (refCount != null && refCount > 0) { + --refCount; + mRefCounts.put(object, refCount); + return refCount == 0; + } else { + Log.e("RenderTarget", "Removing reference of already released: " + object + "!"); + return false; + } + } + + private static EGLConfig chooseEglConfig(EGL10 egl, EGLDisplay display) { + if (mEglConfig == null || !display.equals(mConfiguredDisplay)) { + int[] configsCount = new int[1]; + EGLConfig[] configs = new EGLConfig[1]; + int[] configSpec = getDesiredConfig(); + if (!egl.eglChooseConfig(display, configSpec, configs, 1, configsCount)) { + throw new IllegalArgumentException("EGL Error: eglChooseConfig failed " + + getEGLErrorString(egl, egl.eglGetError())); + } else if (configsCount[0] > 0) { + mEglConfig = configs[0]; + mConfiguredDisplay = display; + } + } + return mEglConfig; + } + + private static int[] getDesiredConfig() { + return new int[] { + EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL10.EGL_RED_SIZE, sRedSize, + EGL10.EGL_GREEN_SIZE, sGreenSize, + EGL10.EGL_BLUE_SIZE, sBlueSize, + EGL10.EGL_ALPHA_SIZE, sAlphaSize, + EGL10.EGL_DEPTH_SIZE, sDepthSize, + EGL10.EGL_STENCIL_SIZE, sStencilSize, + EGL10.EGL_NONE + }; + } + + private RenderTarget(EGLDisplay display, EGLContext context, EGLSurface surface, int fbo, + boolean ownsContext, boolean ownsSurface) { + mEgl = (EGL10) EGLContext.getEGL(); + mDisplay = display; + mContext = context; + mSurface = surface; + mFbo = fbo; + mOwnsContext = ownsContext; + mOwnsSurface = ownsSurface; + } + + private EGLSurface surface() { + if (mSupportsMultipleDisplaySurfaces) { + return mSurface; + } else { + EGLSurface displaySurface = mDisplaySurfaces.get(mContext); + return displaySurface != null ? displaySurface : mSurface; + } + } + + private static void initEgl(EGL10 egl, EGLDisplay display) { + int[] version = new int[2]; + if (!egl.eglInitialize(display, version)) { + throw new RuntimeException("EGL Error: eglInitialize failed " + + getEGLErrorString(egl, egl.eglGetError())); + } + } + + private static EGLDisplay createDefaultDisplay(EGL10 egl) { + EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + checkDisplay(egl, display); + initEgl(egl, display); + return display; + } + + private static EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) { + int[] attrib_list = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE }; + EGLContext ctxt = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attrib_list); + checkContext(egl, ctxt); + return ctxt; + } + + private static EGLSurface createSurface(EGL10 egl, EGLDisplay display, int width, int height) { + EGLConfig eglConfig = chooseEglConfig(egl, display); + int[] attribs = { EGL10.EGL_WIDTH, width, EGL10.EGL_HEIGHT, height, EGL10.EGL_NONE }; + return egl.eglCreatePbufferSurface(display, eglConfig, attribs); + } + + private static int getCurrentFbo() { + int[] result = new int[1]; + GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, result, 0); + return result[0]; + } + + private static void checkDisplay(EGL10 egl, EGLDisplay display) { + if (display == EGL10.EGL_NO_DISPLAY) { + throw new RuntimeException("EGL Error: Bad display: " + + getEGLErrorString(egl, egl.eglGetError())); + } + } + + private static void checkContext(EGL10 egl, EGLContext context) { + if (context == EGL10.EGL_NO_CONTEXT) { + throw new RuntimeException("EGL Error: Bad context: " + + getEGLErrorString(egl, egl.eglGetError())); + } + } + + private static void checkSurface(EGL10 egl, EGLSurface surface) { + if (surface == EGL10.EGL_NO_SURFACE) { + throw new RuntimeException("EGL Error: Bad surface: " + + getEGLErrorString(egl, egl.eglGetError())); + } + } + + private static void checkEglError(EGL10 egl, String command) { + int error = egl.eglGetError(); + if (error != EGL10.EGL_SUCCESS) { + throw new RuntimeException("Error executing " + command + "! EGL error = 0x" + + Integer.toHexString(error)); + } + } + + private static String getEGLErrorString(EGL10 egl, int eglError) { + if (VERSION.SDK_INT >= 14) { + return getEGLErrorStringICS(egl, eglError); + } else { + return "EGL Error 0x" + Integer.toHexString(eglError); + } + } + + @TargetApi(14) + private static String getEGLErrorStringICS(EGL10 egl, int eglError) { + return GLUtils.getEGLErrorString(egl.eglGetError()); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ResizeFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ResizeFilter.java new file mode 100644 index 0000000..c334c91 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ResizeFilter.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.transform; + +import androidx.media.filterfw.*; + +// TODO: In the future this could be done with a meta-filter that simply "hard-codes" the crop +// parameters. +public class ResizeFilter extends CropFilter { + + public ResizeFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addInputPort("outputWidth", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addInputPort("outputHeight", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .addInputPort("useMipmaps", Signature.PORT_OPTIONAL, FrameType.single(boolean.class)) + .addOutputPort("image", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RotateFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RotateFilter.java new file mode 100644 index 0000000..5db20a4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/RotateFilter.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.transform; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; +import androidx.media.filterfw.geometry.Quad; + +public class RotateFilter extends Filter { + + private Quad mSourceRect = Quad.fromRect(0f, 0f, 1f, 1f); + private float mRotateAngle = 0; + private ImageShader mShader; + + public RotateFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addInputPort("rotateAngle", Signature.PORT_REQUIRED, FrameType.single(float.class)) + .addInputPort("sourceRect", Signature.PORT_OPTIONAL, FrameType.single(Quad.class)) + .addOutputPort("image", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("rotateAngle")) { + port.bindToFieldNamed("mRotateAngle"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("sourceRect")) { + port.bindToFieldNamed("mSourceRect"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected void onPrepare() { + mShader = ImageShader.createIdentity(); + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("image"); + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + int[] inDims = inputImage.getDimensions(); + + FrameImage2D outputImage = outPort.fetchAvailableFrame(inDims).asFrameImage2D(); + mShader.setSourceQuad(mSourceRect); + Quad targetQuad = mSourceRect.rotated((float) (mRotateAngle / 180 * Math.PI)); + mShader.setTargetQuad(targetQuad); + mShader.process(inputImage, outputImage); + outPort.pushFrame(outputImage); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ScaleFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ScaleFilter.java new file mode 100644 index 0000000..1c3f328 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ScaleFilter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.transform; + +// TODO: scale filter needs to be able to specify output width and height +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.Signature; + +public class ScaleFilter extends ResizeFilter { + + private float mScale = 1.0f; + + public ScaleFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addInputPort("scale", Signature.PORT_OPTIONAL, FrameType.single(float.class)) + .addInputPort("useMipmaps", Signature.PORT_OPTIONAL, FrameType.single(boolean.class)) + .addOutputPort("image", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("scale")) { + port.bindToFieldNamed("mScale"); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("useMipmaps")) { + port.bindToFieldNamed("mUseMipmaps"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected int getOutputWidth(int inWidth, int inHeight) { + return (int)(inWidth * mScale); + } + + @Override + protected int getOutputHeight(int inWidth, int inHeight) { + return (int)(inHeight * mScale); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Signature.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Signature.java new file mode 100644 index 0000000..2c2916f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Signature.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; + +/** + * A Signature holds the specification for a filter's input and output ports. + * + * A Signature instance must be returned by the filter's {@link Filter#getSignature()} method. It + * specifies the number and names of the filter's input and output ports, whether or not they + * are required, how data for those ports are accessed, and more. A Signature does not change over + * time. This makes Signatures useful for understanding how a filter can be integrated into a + * graph. + * + * There are a number of flags that can be specified for each input and output port. The flag + * {@code PORT_REQUIRED} indicates that the user must connect the specified port. On the other hand, + * {@code PORT_OPTIONAL} indicates that a port may be connected by the user. + * + * If ports other than the ones in the Signature are allowed, they default to the most generic + * format, that allows passing in any type of Frame. Thus, if more granular access is needed to + * a frame's data, it must be specified in the Signature. + */ +public class Signature { + + private HashMap<String, PortInfo> mInputPorts = null; + private HashMap<String, PortInfo> mOutputPorts = null; + private boolean mAllowOtherInputs = true; + private boolean mAllowOtherOutputs = true; + + static class PortInfo { + public int flags; + public FrameType type; + + public PortInfo() { + flags = 0; + type = FrameType.any(); + } + + public PortInfo(int flags, FrameType type) { + this.flags = flags; + this.type = type; + } + + public boolean isRequired() { + return (flags & PORT_REQUIRED) != 0; + } + + public String toString(String ioMode, String name) { + String ioName = ioMode + " " + name; + String modeName = isRequired() ? "required" : "optional"; + return modeName + " " + ioName + ": " + type.toString(); + } + } + + /** Indicates that the port must be connected in the graph. */ + public static final int PORT_REQUIRED = 0x02; + /** Indicates that the port may be connected in the graph . */ + public static final int PORT_OPTIONAL = 0x01; + + /** + * Creates a new empty Signature. + */ + public Signature() { + } + + /** + * Adds an input port to the Signature. + * + * @param name the name of the input port. Must be unique among input port names. + * @param flags a combination of port flags. + * @param type the type of the input frame. + * @return this Signature instance. + */ + public Signature addInputPort(String name, int flags, FrameType type) { + addInputPort(name, new PortInfo(flags, type)); + return this; + } + + /** + * Adds an output port to the Signature. + * + * @param name the name of the output port. Must be unique among output port names. + * @param flags a combination of port flags. + * @param type the type of the output frame. + * @return this Signature instance. + */ + public Signature addOutputPort(String name, int flags, FrameType type) { + addOutputPort(name, new PortInfo(flags, type)); + return this; + } + + /** + * Disallows the user from adding any other input ports. + * Adding any input port not explicitly specified in this Signature will cause an error. + * @return this Signature instance. + */ + public Signature disallowOtherInputs() { + mAllowOtherInputs = false; + return this; + } + + /** + * Disallows the user from adding any other output ports. + * Adding any output port not explicitly specified in this Signature will cause an error. + * @return this Signature instance. + */ + public Signature disallowOtherOutputs() { + mAllowOtherOutputs = false; + return this; + } + + /** + * Disallows the user from adding any other ports. + * Adding any input or output port not explicitly specified in this Signature will cause an + * error. + * @return this Signature instance. + */ + public Signature disallowOtherPorts() { + mAllowOtherInputs = false; + mAllowOtherOutputs = false; + return this; + } + + @Override + public String toString() { + StringBuffer stringBuffer = new StringBuffer(); + for (Entry<String, PortInfo> entry : mInputPorts.entrySet()) { + stringBuffer.append(entry.getValue().toString("input", entry.getKey()) + "\n"); + } + for (Entry<String, PortInfo> entry : mOutputPorts.entrySet()) { + stringBuffer.append(entry.getValue().toString("output", entry.getKey()) + "\n"); + } + if (!mAllowOtherInputs) { + stringBuffer.append("disallow other inputs\n"); + } + if (!mAllowOtherOutputs) { + stringBuffer.append("disallow other outputs\n"); + } + return stringBuffer.toString(); + } + + PortInfo getInputPortInfo(String name) { + PortInfo result = mInputPorts != null ? mInputPorts.get(name) : null; + return result != null ? result : new PortInfo(); + } + + PortInfo getOutputPortInfo(String name) { + PortInfo result = mOutputPorts != null ? mOutputPorts.get(name) : null; + return result != null ? result : new PortInfo(); + } + + void checkInputPortsConform(Filter filter) { + Set<String> filterInputs = new HashSet<String>(); + filterInputs.addAll(filter.getConnectedInputPortMap().keySet()); + if (mInputPorts != null) { + for (Entry<String, PortInfo> entry : mInputPorts.entrySet()) { + String portName = entry.getKey(); + PortInfo portInfo = entry.getValue(); + InputPort inputPort = filter.getConnectedInputPort(portName); + if (inputPort == null && portInfo.isRequired()) { + throw new RuntimeException("Filter " + filter + " does not have required " + + "input port '" + portName + "'!"); + } + filterInputs.remove(portName); + } + } + if (!mAllowOtherInputs && !filterInputs.isEmpty()) { + throw new RuntimeException("Filter " + filter + " has invalid input ports: " + + filterInputs + "!"); + } + } + + void checkOutputPortsConform(Filter filter) { + Set<String> filterOutputs = new HashSet<String>(); + filterOutputs.addAll(filter.getConnectedOutputPortMap().keySet()); + if (mOutputPorts != null) { + for (Entry<String, PortInfo> entry : mOutputPorts.entrySet()) { + String portName = entry.getKey(); + PortInfo portInfo = entry.getValue(); + OutputPort outputPort = filter.getConnectedOutputPort(portName); + if (outputPort == null && portInfo.isRequired()) { + throw new RuntimeException("Filter " + filter + " does not have required " + + "output port '" + portName + "'!"); + } + filterOutputs.remove(portName); + } + } + if (!mAllowOtherOutputs && !filterOutputs.isEmpty()) { + throw new RuntimeException("Filter " + filter + " has invalid output ports: " + + filterOutputs + "!"); + } + } + + HashMap<String, PortInfo> getInputPorts() { + return mInputPorts; + } + + HashMap<String, PortInfo> getOutputPorts() { + return mOutputPorts; + } + + private void addInputPort(String name, PortInfo portInfo) { + if (mInputPorts == null) { + mInputPorts = new HashMap<String, PortInfo>(); + } + if (mInputPorts.containsKey(name)) { + throw new RuntimeException("Attempting to add duplicate input port '" + name + "'!"); + } + mInputPorts.put(name, portInfo); + } + + private void addOutputPort(String name, PortInfo portInfo) { + if (mOutputPorts == null) { + mOutputPorts = new HashMap<String, PortInfo>(); + } + if (mOutputPorts.containsKey(name)) { + throw new RuntimeException("Attempting to add duplicate output port '" + name + "'!"); + } + mOutputPorts.put(name, portInfo); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SimpleCache.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SimpleCache.java new file mode 100644 index 0000000..f54621f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SimpleCache.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * This is a simple LRU cache that is used internally for managing repetitive objects. + */ +class SimpleCache<K, V> extends LinkedHashMap<K, V> { + + private int mMaxEntries; + + public SimpleCache(final int maxEntries) { + super(maxEntries + 1, 1f, true); + mMaxEntries = maxEntries; + } + + @Override + protected boolean removeEldestEntry(final Map.Entry<K, V> eldest) { + return super.size() > mMaxEntries; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SlotFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SlotFilter.java new file mode 100644 index 0000000..aaa87c2 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SlotFilter.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +public abstract class SlotFilter extends Filter { + + protected final String mSlotName; + + protected SlotFilter(MffContext context, String name, String slotName) { + super(context, name); + mSlotName = slotName; + } + + protected final FrameType getSlotType() { + return getFrameManager().getSlot(mSlotName).getType(); + } + + protected final boolean slotHasFrame() { + return getFrameManager().getSlot(mSlotName).hasFrame(); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SobelFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SobelFilter.java new file mode 100644 index 0000000..a4c39a1 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SobelFilter.java @@ -0,0 +1,174 @@ +/* + * Copyright 2013 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 androidx.media.filterpacks.image; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; + +public class SobelFilter extends Filter { + + private static final String mGradientXSource = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler_0;\n" + + "uniform vec2 pix;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " vec4 a1 = -1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(-pix.x, -pix.y));\n" + + " vec4 a2 = -2.0 * texture2D(tex_sampler_0, v_texcoord + vec2(-pix.x, 0.0));\n" + + " vec4 a3 = -1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(-pix.x, +pix.y));\n" + + " vec4 b1 = +1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(+pix.x, -pix.y));\n" + + " vec4 b2 = +2.0 * texture2D(tex_sampler_0, v_texcoord + vec2(+pix.x, 0.0));\n" + + " vec4 b3 = +1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(+pix.x, +pix.y));\n" + + " gl_FragColor = 0.5 + (a1 + a2 + a3 + b1 + b2 + b3) / 8.0;\n" + + "}\n"; + + private static final String mGradientYSource = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler_0;\n" + + "uniform vec2 pix;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " vec4 a1 = -1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(-pix.x, -pix.y));\n" + + " vec4 a2 = -2.0 * texture2D(tex_sampler_0, v_texcoord + vec2(0.0, -pix.y));\n" + + " vec4 a3 = -1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(+pix.x, -pix.y));\n" + + " vec4 b1 = +1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(-pix.x, +pix.y));\n" + + " vec4 b2 = +2.0 * texture2D(tex_sampler_0, v_texcoord + vec2(0.0, +pix.y));\n" + + " vec4 b3 = +1.0 * texture2D(tex_sampler_0, v_texcoord + vec2(+pix.x, +pix.y));\n" + + " gl_FragColor = 0.5 + (a1 + a2 + a3 + b1 + b2 + b3) / 8.0;\n" + + "}\n"; + + private static final String mMagnitudeSource = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler_0;\n" + + "uniform sampler2D tex_sampler_1;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " vec4 gx = 2.0 * texture2D(tex_sampler_0, v_texcoord) - 1.0;\n" + + " vec4 gy = 2.0 * texture2D(tex_sampler_1, v_texcoord) - 1.0;\n" + + " gl_FragColor = vec4(sqrt(gx.rgb * gx.rgb + gy.rgb * gy.rgb), 1.0);\n" + + "}\n"; + + private static final String mDirectionSource = + "precision mediump float;\n" + + "uniform sampler2D tex_sampler_0;\n" + + "uniform sampler2D tex_sampler_1;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " vec4 gy = 2.0 * texture2D(tex_sampler_1, v_texcoord) - 1.0;\n" + + " vec4 gx = 2.0 * texture2D(tex_sampler_0, v_texcoord) - 1.0;\n" + + " gl_FragColor = vec4((atan(gy.rgb, gx.rgb) + 3.14) / (2.0 * 3.14), 1.0);\n" + + "}\n"; + + private ImageShader mGradientXShader; + private ImageShader mGradientYShader; + private ImageShader mMagnitudeShader; + private ImageShader mDirectionShader; + + private FrameType mImageType; + + public SobelFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + // TODO: we will address the issue of READ_GPU / WRITE_GPU when using CPU filters later. + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + return new Signature().addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addOutputPort("direction", Signature.PORT_OPTIONAL, imageOut) + .addOutputPort("magnitude", Signature.PORT_OPTIONAL, imageOut).disallowOtherPorts(); + } + + @Override + protected void onPrepare() { + if (isOpenGLSupported()) { + mGradientXShader = new ImageShader(mGradientXSource); + mGradientYShader = new ImageShader(mGradientYSource); + mMagnitudeShader = new ImageShader(mMagnitudeSource); + mDirectionShader = new ImageShader(mDirectionSource); + mImageType = FrameType.image2D( + FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU | FrameType.WRITE_GPU); + } + } + + @Override + protected void onProcess() { + OutputPort magnitudePort = getConnectedOutputPort("magnitude"); + OutputPort directionPort = getConnectedOutputPort("direction"); + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + int[] inputDims = inputImage.getDimensions(); + + FrameImage2D magImage = (magnitudePort != null) ? + magnitudePort.fetchAvailableFrame(inputDims).asFrameImage2D() : null; + FrameImage2D dirImage = (directionPort != null) ? + directionPort.fetchAvailableFrame(inputDims).asFrameImage2D() : null; + if (isOpenGLSupported()) { + FrameImage2D gxFrame = Frame.create(mImageType, inputDims).asFrameImage2D(); + FrameImage2D gyFrame = Frame.create(mImageType, inputDims).asFrameImage2D(); + mGradientXShader.setUniformValue("pix", new float[] {1f/inputDims[0], 1f/inputDims[1]}); + mGradientYShader.setUniformValue("pix", new float[] {1f/inputDims[0], 1f/inputDims[1]}); + mGradientXShader.process(inputImage, gxFrame); + mGradientYShader.process(inputImage, gyFrame); + FrameImage2D[] gradientFrames = new FrameImage2D[] { gxFrame, gyFrame }; + if (magnitudePort != null) { + mMagnitudeShader.processMulti(gradientFrames, magImage); + } + if (directionPort != null) { + mDirectionShader.processMulti(gradientFrames, dirImage); + } + gxFrame.release(); + gyFrame.release(); + } else { + ByteBuffer inputBuffer = inputImage.lockBytes(Frame.MODE_READ); + ByteBuffer magBuffer = (magImage != null) ? + magImage.lockBytes(Frame.MODE_WRITE) : null; + ByteBuffer dirBuffer = (dirImage != null) ? + dirImage.lockBytes(Frame.MODE_WRITE) : null; + sobelOperator(inputImage.getWidth(), inputImage.getHeight(), + inputBuffer, magBuffer, dirBuffer); + inputImage.unlock(); + if (magImage != null) { + magImage.unlock(); + } + if (dirImage != null) { + dirImage.unlock(); + } + } + if (magImage != null) { + magnitudePort.pushFrame(magImage); + } + if (dirImage != null) { + directionPort.pushFrame(dirImage); + } + } + + private static native boolean sobelOperator(int width, int height, + ByteBuffer imageBuffer, ByteBuffer magBuffer, ByteBuffer dirBudder); + + static { + System.loadLibrary("smartcamera_jni"); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/StatsFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/StatsFilter.java new file mode 100644 index 0000000..94030c3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/StatsFilter.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2012 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. + */ + +// Calculates the mean and standard deviation of the values in the input image. +// It takes in an RGBA image, but assumes that r, g, b, a are all the same values. + +package androidx.media.filterpacks.numeric; + +import android.util.Log; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameBuffer2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; +import androidx.media.filterfw.geometry.Quad; + +import java.nio.ByteBuffer; + +/** + * Get the sample mean and variance of a 2-D buffer of bytes over a given rectangle. + * TODO: Add more statistics as needed. + * TODO: Check if crop rectangle is necessary to be included in this filter. + */ +public class StatsFilter extends Filter { + + private static final int MEAN_INDEX = 0; + private static final int STDEV_INDEX = 1; + + private final float[] mStats = new float[2]; + + private Quad mCropRect = Quad.fromRect(0f, 0f, 1f, 1f); + private static final String TAG = "StatsFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + /** + * @param context + * @param name + */ + public StatsFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType inputFrame = FrameType.buffer2D(FrameType.ELEMENT_INT8); + FrameType floatT = FrameType.single(float.class); + return new Signature() + .addInputPort("buffer", Signature.PORT_REQUIRED, inputFrame) + .addInputPort("cropRect", Signature.PORT_OPTIONAL, FrameType.single(Quad.class)) + .addOutputPort("mean", Signature.PORT_REQUIRED, floatT) + .addOutputPort("stdev", Signature.PORT_REQUIRED, floatT) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("cropRect")) { + port.bindToFieldNamed("mCropRect"); + port.setAutoPullEnabled(true); + } + } + + private void calcMeanAndStd(ByteBuffer pixelBuffer, int width, int height, Quad quad) { + // Native + pixelBuffer.rewind(); + regionscore(pixelBuffer, width, height, quad.topLeft().x, quad.topLeft().y, + quad.bottomRight().x, quad.bottomRight().y, mStats); + if (mLogVerbose) { + Log.v(TAG, "Native calc stats: Mean = " + mStats[MEAN_INDEX] + ", Stdev = " + + mStats[STDEV_INDEX]); + } + } + + /** + * @see androidx.media.filterfw.Filter#onProcess() + */ + @Override + protected void onProcess() { + FrameBuffer2D inputFrame = getConnectedInputPort("buffer").pullFrame().asFrameImage2D(); + ByteBuffer pixelBuffer = inputFrame.lockBytes(Frame.MODE_READ); + + calcMeanAndStd(pixelBuffer, inputFrame.getWidth(), inputFrame.getHeight(), mCropRect); + inputFrame.unlock(); + + OutputPort outPort = getConnectedOutputPort("mean"); + FrameValue outFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + outFrame.setValue(mStats[MEAN_INDEX]); + outPort.pushFrame(outFrame); + + OutputPort outPortStdev = getConnectedOutputPort("stdev"); + FrameValue outFrameStdev = outPortStdev.fetchAvailableFrame(null).asFrameValue(); + outFrameStdev.setValue(mStats[STDEV_INDEX]); + outPortStdev.pushFrame(outFrameStdev); + } + + private native void regionscore(ByteBuffer imageBuffer, int width, int height, float left, + float top, float right, float bottom, float[] statsArray); + + static { + System.loadLibrary("smartcamera_jni"); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SurfaceHolderTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SurfaceHolderTarget.java new file mode 100644 index 0000000..dac723b --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/SurfaceHolderTarget.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.image; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.RenderTarget; +import androidx.media.filterfw.Signature; +import androidx.media.filterfw.ViewFilter; + +public class SurfaceHolderTarget extends ViewFilter { + + private SurfaceHolder mSurfaceHolder = null; + private RenderTarget mRenderTarget = null; + private ImageShader mShader = null; + private boolean mHasSurface = false; + + private SurfaceHolder.Callback mSurfaceHolderListener = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // This just makes sure the holder is still the one we expect. + onSurfaceCreated(holder); + } + + @Override + public void surfaceCreated (SurfaceHolder holder) { + onSurfaceCreated(holder); + } + + @Override + public void surfaceDestroyed (SurfaceHolder holder) { + onDestroySurface(); + } + }; + + public SurfaceHolderTarget(MffContext context, String name) { + super(context, name); + } + + @Override + public void onBindToView(View view) { + if (view instanceof SurfaceView) { + SurfaceHolder holder = ((SurfaceView)view).getHolder(); + if (holder == null) { + throw new RuntimeException("Could not get SurfaceHolder from SurfaceView " + + view + "!"); + } + setSurfaceHolder(holder); + } else { + throw new IllegalArgumentException("View must be a SurfaceView!"); + } + } + + public void setSurfaceHolder(SurfaceHolder holder) { + if (isRunning()) { + throw new IllegalStateException("Cannot set SurfaceHolder while running!"); + } + mSurfaceHolder = holder; + } + + public synchronized void onDestroySurface() { + if (mRenderTarget != null) { + mRenderTarget.release(); + mRenderTarget = null; + } + mHasSurface = false; + } + + @Override + public Signature getSignature() { + FrameType imageType = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + return super.getSignature() + .addInputPort("image", Signature.PORT_REQUIRED, imageType) + .disallowOtherPorts(); + } + + @Override + protected void onInputPortOpen(InputPort port) { + super.connectViewInputs(port); + } + + @Override + protected synchronized void onPrepare() { + if (isOpenGLSupported()) { + mShader = ImageShader.createIdentity(); + } + } + + @Override + protected synchronized void onOpen() { + mSurfaceHolder.addCallback(mSurfaceHolderListener); + Surface surface = mSurfaceHolder.getSurface(); + mHasSurface = (surface != null) && surface.isValid(); + } + + @Override + protected synchronized void onProcess() { + FrameImage2D image = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + if (mHasSurface) { + // Synchronize the surface holder in case another filter is accessing this surface. + synchronized (mSurfaceHolder) { + if (isOpenGLSupported()) { + renderGL(image); + } else { + renderCanvas(image); + } + } + } + } + + /** + * Renders the given frame to the screen using GLES2. + * @param image the image to render + */ + private void renderGL(FrameImage2D image) { + if (mRenderTarget == null) { + mRenderTarget = RenderTarget.currentTarget().forSurfaceHolder(mSurfaceHolder); + mRenderTarget.registerAsDisplaySurface(); + } + Rect frameRect = new Rect(0, 0, image.getWidth(), image.getHeight()); + Rect surfRect = mSurfaceHolder.getSurfaceFrame(); + setupShader(mShader, frameRect, surfRect); + mShader.process(image.lockTextureSource(), + mRenderTarget, + surfRect.width(), + surfRect.height()); + image.unlock(); + mRenderTarget.swapBuffers(); + } + + /** + * Renders the given frame to the screen using a Canvas. + * @param image the image to render + */ + private void renderCanvas(FrameImage2D image) { + Canvas canvas = mSurfaceHolder.lockCanvas(); + Bitmap bitmap = image.toBitmap(); + Rect sourceRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + Rect surfaceRect = mSurfaceHolder.getSurfaceFrame(); + RectF targetRect = getTargetRect(sourceRect, surfaceRect); + canvas.drawColor(Color.BLACK); + if (targetRect.width() > 0 && targetRect.height() > 0) { + canvas.scale(surfaceRect.width(), surfaceRect.height()); + canvas.drawBitmap(bitmap, sourceRect, targetRect, new Paint()); + } + mSurfaceHolder.unlockCanvasAndPost(canvas); + } + + @Override + protected synchronized void onClose() { + if (mRenderTarget != null) { + mRenderTarget.unregisterAsDisplaySurface(); + mRenderTarget.release(); + mRenderTarget = null; + } + if (mSurfaceHolder != null) { + mSurfaceHolder.removeCallback(mSurfaceHolderListener); + } + } + + private synchronized void onSurfaceCreated(SurfaceHolder holder) { + if (mSurfaceHolder != holder) { + throw new RuntimeException("Unexpected Holder!"); + } + mHasSurface = true; + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TextViewTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TextViewTarget.java new file mode 100644 index 0000000..5aafced --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TextViewTarget.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.text; + +import android.view.View; +import android.widget.TextView; + +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.Signature; +import androidx.media.filterfw.ViewFilter; + +public class TextViewTarget extends ViewFilter { + + private TextView mTextView = null; + + public TextViewTarget(MffContext context, String name) { + super(context, name); + } + + @Override + public void onBindToView(View view) { + if (view instanceof TextView) { + mTextView = (TextView)view; + } else { + throw new IllegalArgumentException("View must be a TextView!"); + } + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("text", Signature.PORT_REQUIRED, FrameType.single(String.class)) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + FrameValue textFrame = getConnectedInputPort("text").pullFrame().asFrameValue(); + final String text = (String)textFrame.getValue(); + if (mTextView != null) { + mTextView.post(new Runnable() { + @Override + public void run() { + mTextView.setText(text); + } + }); + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TextureSource.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TextureSource.java new file mode 100644 index 0000000..30fda82 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TextureSource.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.graphics.Bitmap; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; + +import java.nio.ByteBuffer; + +public class TextureSource { + + private int mTexId; + private int mTarget; + private boolean mIsOwner; + private boolean mIsAllocated = false; + + public static TextureSource fromTexture(int texId, int target) { + return new TextureSource(texId, target, false); + } + + public static TextureSource fromTexture(int texId) { + return new TextureSource(texId, GLES20.GL_TEXTURE_2D, false); + } + + public static TextureSource newTexture() { + return new TextureSource(GLToolbox.generateTexture(), GLES20.GL_TEXTURE_2D, true); + } + + public static TextureSource newExternalTexture() { + return new TextureSource(GLToolbox.generateTexture(), + GLES11Ext.GL_TEXTURE_EXTERNAL_OES, + true); + } + + public int getTextureId() { + return mTexId; + } + + public int getTarget() { + return mTarget; + } + + public void bind() { + GLES20.glBindTexture(mTarget, mTexId); + GLToolbox.checkGlError("glBindTexture"); + } + + public void allocate(int width, int height) { + //Log.i("TextureSource", "Allocating empty texture " + mTexId + ": " + width + "x" + height + "."); + GLToolbox.allocateTexturePixels(mTexId, mTarget, width, height); + mIsAllocated = true; + } + + public void allocateWithPixels(ByteBuffer pixels, int width, int height) { + //Log.i("TextureSource", "Uploading pixels to texture " + mTexId + ": " + width + "x" + height + "."); + GLToolbox.setTexturePixels(mTexId, mTarget, pixels, width, height); + mIsAllocated = true; + } + + public void allocateWithBitmapPixels(Bitmap bitmap) { + //Log.i("TextureSource", "Uploading pixels to texture " + mTexId + "!"); + GLToolbox.setTexturePixels(mTexId, mTarget, bitmap); + mIsAllocated = true; + } + + public void generateMipmaps() { + GLES20.glBindTexture(mTarget, mTexId); + GLES20.glTexParameteri(mTarget, + GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_LINEAR_MIPMAP_LINEAR); + GLES20.glGenerateMipmap(mTarget); + GLES20.glBindTexture(mTarget, 0); + } + + public void setParameter(int parameter, int value) { + GLES20.glBindTexture(mTarget, mTexId); + GLES20.glTexParameteri(mTarget, parameter, value); + GLES20.glBindTexture(mTarget, 0); + } + + /** + * @hide + */ + public void release() { + if (GLToolbox.isTexture(mTexId) && mIsOwner) { + GLToolbox.deleteTexture(mTexId); + } + mTexId = GLToolbox.textureNone(); + } + + @Override + public String toString() { + return "TextureSource(id=" + mTexId + ", target=" + mTarget + ")"; + } + + boolean isAllocated() { + return mIsAllocated; + } + + private TextureSource(int texId, int target, boolean isOwner) { + mTexId = texId; + mTarget = target; + mIsOwner = isOwner; + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Throughput.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Throughput.java new file mode 100644 index 0000000..c16aae0 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/Throughput.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.performance; + +public class Throughput { + + private final int mTotalFrames; + private final int mPeriodFrames; + private final long mPeriodTime; + + public Throughput(int totalFrames, int periodFrames, long periodTime, int size) { + mTotalFrames = totalFrames; + mPeriodFrames = periodFrames; + mPeriodTime = periodTime; + } + + public int getTotalFrameCount() { + return mTotalFrames; + } + + public int getPeriodFrameCount() { + return mPeriodFrames; + } + + public long getPeriodTime() { + return mPeriodTime; + } + + public float getFramesPerSecond() { + return mPeriodFrames / (mPeriodTime / 1000.0f); + } + + @Override + public String toString() { + return Math.round(getFramesPerSecond()) + " FPS"; + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ThroughputFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ThroughputFilter.java new file mode 100644 index 0000000..25243a7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ThroughputFilter.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.performance; + +import android.util.Log; +import android.os.SystemClock; + +import androidx.media.filterfw.*; + +public class ThroughputFilter extends Filter { + + private int mPeriod = 3; + private long mLastTime = 0; + private int mTotalFrameCount = 0; + private int mPeriodFrameCount = 0; + + public ThroughputFilter(MffContext context, String name) { + super(context, name); + } + + + @Override + public Signature getSignature() { + FrameType throughputType = FrameType.single(Throughput.class); + return new Signature() + .addInputPort("frame", Signature.PORT_REQUIRED, FrameType.any()) + .addOutputPort("throughput", Signature.PORT_REQUIRED, throughputType) + .addOutputPort("frame", Signature.PORT_REQUIRED, FrameType.any()) + .addInputPort("period", Signature.PORT_OPTIONAL, FrameType.single(int.class)) + .disallowOtherPorts(); + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("period")) { + port.bindToFieldNamed("mPeriod"); + } else { + port.attachToOutputPort(getConnectedOutputPort("frame")); + } + } + + @Override + protected void onOpen() { + mTotalFrameCount = 0; + mPeriodFrameCount = 0; + mLastTime = 0; + } + + @Override + protected synchronized void onProcess() { + Frame inputFrame = getConnectedInputPort("frame").pullFrame(); + + // Update stats + ++mTotalFrameCount; + ++mPeriodFrameCount; + + // Check clock + if (mLastTime == 0) { + mLastTime = SystemClock.elapsedRealtime(); + } + long curTime = SystemClock.elapsedRealtime(); + + // Output throughput info if time period is up + if ((curTime - mLastTime) >= (mPeriod * 1000)) { + Log.i("Thru", "It is time!"); + OutputPort tpPort = getConnectedOutputPort("throughput"); + Throughput throughput = new Throughput(mTotalFrameCount, + mPeriodFrameCount, + curTime - mLastTime, + inputFrame.getElementCount()); + FrameValue throughputFrame = tpPort.fetchAvailableFrame(null).asFrameValue(); + throughputFrame.setValue(throughput); + tpPort.pushFrame(throughputFrame); + mLastTime = curTime; + mPeriodFrameCount = 0; + } + + getConnectedOutputPort("frame").pushFrame(inputFrame); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ToGrayValuesFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ToGrayValuesFilter.java new file mode 100644 index 0000000..8e0fd6c --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ToGrayValuesFilter.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013 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 androidx.media.filterpacks.image; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameBuffer2D; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.RenderTarget; +import androidx.media.filterfw.Signature; +import androidx.media.filterfw.geometry.Quad; + +import java.nio.ByteBuffer; + +public class ToGrayValuesFilter extends Filter { + + private final static String mGrayPackFragment = + "precision mediump float;\n" + + "const vec4 coeff_y = vec4(0.299, 0.587, 0.114, 0);\n" + + "uniform sampler2D tex_sampler_0;\n" + + "uniform float pix_stride;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " for (int i = 0; i < 4; i++) {\n" + + // Here is an example showing how this works: + // Assuming the input texture is 1x4 while the output texture is 1x1 + // the coordinates of the 4 input pixels will be: + // { (0.125, 0.5), (0.375, 0.5), (0.625, 0.5), (0.875, 0.5) } + // and the coordinates of the 1 output pixels will be: + // { (0.5, 0.5) } + // the equation below locates the 4 input pixels from the coordinate of the output pixel + " vec4 p = texture2D(tex_sampler_0,\n" + + " v_texcoord + vec2(pix_stride * (float(i) - 1.5), 0.0));\n" + + " gl_FragColor[i] = dot(p, coeff_y);\n" + + " }\n" + + "}\n"; + + private ImageShader mShader; + + private FrameType mImageInType; + + public ToGrayValuesFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + mImageInType = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.buffer2D(FrameType.ELEMENT_INT8); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, mImageInType) + .addOutputPort("image", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } + + @Override + protected void onPrepare() { + if (isOpenGLSupported()) { + mShader = new ImageShader(mGrayPackFragment); + } + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("image"); + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + int[] dim = inputImage.getDimensions(); + FrameBuffer2D outputFrame; + ByteBuffer grayBuffer; + + if (isOpenGLSupported()) { + // crop out the portion of inputImage that will be used to generate outputFrame. + int modular = dim[0] % 4; + int[] outDim = new int[] {dim[0] - modular, dim[1]}; + outputFrame = outPort.fetchAvailableFrame(outDim).asFrameBuffer2D(); + grayBuffer = outputFrame.lockBytes(Frame.MODE_WRITE); + + int[] targetDims = new int[] { outDim[0] / 4, outDim[1] }; + FrameImage2D targetFrame = Frame.create(mImageInType, targetDims).asFrameImage2D(); + mShader.setSourceQuad(Quad.fromRect(0f, 0f, ((float)outDim[0])/dim[0], 1f)); + mShader.setUniformValue("pix_stride", 1f / outDim[0]); + mShader.process(inputImage, targetFrame); + RenderTarget grayTarget = targetFrame.lockRenderTarget(); + grayTarget.readPixelData(grayBuffer, targetDims[0], targetDims[1]); + targetFrame.unlock(); + targetFrame.release(); + } else { + outputFrame = outPort.fetchAvailableFrame(dim).asFrameBuffer2D(); + grayBuffer = outputFrame.lockBytes(Frame.MODE_WRITE); + ByteBuffer inputBuffer = inputImage.lockBytes(Frame.MODE_READ); + if (!toGrayValues(inputBuffer, grayBuffer)) { + throw new RuntimeException( + "Native implementation encountered an error during processing!"); + } + inputImage.unlock(); + } + outputFrame.unlock(); + outPort.pushFrame(outputFrame); + } + + private static native boolean toGrayValues(ByteBuffer imageBuffer, ByteBuffer grayBuffer); + + static { + System.loadLibrary("smartcamera_jni"); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ToStringFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ToStringFilter.java new file mode 100644 index 0000000..7306b61 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ToStringFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.text; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public class ToStringFilter extends Filter { + + public ToStringFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("object", Signature.PORT_REQUIRED, FrameType.single()) + .addOutputPort("string", Signature.PORT_REQUIRED, FrameType.single(String.class)) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + FrameValue objectFrame = getConnectedInputPort("object").pullFrame().asFrameValue(); + String outStr = objectFrame.getValue().toString(); + OutputPort outPort = getConnectedOutputPort("string"); + FrameValue stringFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + stringFrame.setValue(outStr); + outPort.pushFrame(stringFrame); + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TransformUtils.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TransformUtils.java new file mode 100644 index 0000000..8dd1949 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/TransformUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.transform; + +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.TextureSource; + +import java.util.Arrays; + +/** Internal class that contains utility functions used by the transform filters. **/ +class TransformUtils { + + public static int powOf2(int x) { + --x; + // Fill with 1s + x |= x >> 1; + x |= x >> 2; + x |= x >> 4; + x |= x >> 8; + x |= x >> 16; + // Next int is now pow-of-2 + return x + 1; + } + + public static FrameImage2D makeMipMappedFrame(FrameImage2D current, int[] dimensions) { + // Note: Future versions of GLES will support NPOT mipmapping. When these become more + // widely used, we can add a check here to disable frame expansion on such devices. + int[] pow2Dims = new int[] { powOf2(dimensions[0]), powOf2(dimensions[1]) }; + if (current == null) { + FrameType imageType = FrameType.image2D(FrameType.ELEMENT_RGBA8888, + FrameType.READ_GPU | FrameType.WRITE_GPU); + current = Frame.create(imageType, pow2Dims).asFrameImage2D(); + } else if (!Arrays.equals(dimensions, current.getDimensions())) { + current.resize(pow2Dims); + } + return current; + } + + public static FrameImage2D makeTempFrame(FrameImage2D current, int[] dimensions) { + if (current == null) { + FrameType imageType = FrameType.image2D(FrameType.ELEMENT_RGBA8888, + FrameType.READ_GPU | FrameType.WRITE_GPU); + current = Frame.create(imageType, dimensions).asFrameImage2D(); + } else if (!Arrays.equals(dimensions, current.getDimensions())) { + current.resize(dimensions); + } + return current; + } + + public static void generateMipMaps(FrameImage2D frame) { + TextureSource texture = frame.lockTextureSource(); + texture.generateMipmaps(); + frame.unlock(); + } + + public static void setTextureParameter(FrameImage2D frame, int param, int value) { + TextureSource texture = frame.lockTextureSource(); + texture.setParameter(param, value); + frame.unlock(); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ValueTarget.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ValueTarget.java new file mode 100644 index 0000000..e2db8af --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ValueTarget.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.base; + +import android.os.Handler; +import android.os.Looper; + +import androidx.media.filterfw.*; + +public final class ValueTarget extends Filter { + + public static interface ValueListener { + public void onReceivedValue(Object value); + } + + private ValueListener mListener = null; + private Handler mHandler = null; + + public ValueTarget(MffContext context, String name) { + super(context, name); + } + + public void setListener(ValueListener listener, boolean onCallerThread) { + if (isRunning()) { + throw new IllegalStateException("Attempting to bind filter to callback while it is " + + "running!"); + } + mListener = listener; + if (onCallerThread) { + if (Looper.myLooper() == null) { + throw new IllegalArgumentException("Attempting to set callback on thread which " + + "has no looper!"); + } + mHandler = new Handler(); + } + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("value", Signature.PORT_REQUIRED, FrameType.single()) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + FrameValue valueFrame = getConnectedInputPort("value").pullFrame().asFrameValue(); + if (mListener != null) { + if (mHandler != null) { + postValueToUiThread(valueFrame.getValue()); + } else { + mListener.onReceivedValue(valueFrame.getValue()); + } + } + } + + private void postValueToUiThread(final Object value) { + mHandler.post(new Runnable() { + @Override + public void run() { + mListener.onReceivedValue(value); + } + }); + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/VariableSource.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/VariableSource.java new file mode 100644 index 0000000..69060cb --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/VariableSource.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterpacks.base; + +import androidx.media.filterfw.*; + +// TODO: Rename back to ValueSource? Seems to make more sense even if we use it as a Variable +// in some contexts. +public final class VariableSource extends Filter { + + private Object mValue = null; + private OutputPort mOutputPort = null; + + public VariableSource(MffContext context, String name) { + super(context, name); + } + + public synchronized void setValue(Object value) { + mValue = value; + } + + public synchronized Object getValue() { + return mValue; + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("value", Signature.PORT_REQUIRED, FrameType.single()) + .disallowOtherPorts(); + } + + @Override + protected void onPrepare() { + mOutputPort = getConnectedOutputPort("value"); + } + + @Override + protected synchronized void onProcess() { + FrameValue frame = mOutputPort.fetchAvailableFrame(null).asFrameValue(); + frame.setValue(mValue); + mOutputPort.pushFrame(frame); + } + +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ViewFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ViewFilter.java new file mode 100644 index 0000000..ddb7222 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/ViewFilter.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw; + +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.View; + +/** + * TODO: Move this to filterpacks/base? + */ +public abstract class ViewFilter extends Filter { + + public static final int SCALE_STRETCH = 1; + public static final int SCALE_FIT = 2; + public static final int SCALE_FILL = 3; + + protected int mScaleMode = SCALE_FIT; + protected float[] mClearColor = new float[] { 0f, 0f, 0f, 1f }; + protected boolean mFlipVertically = true; + + private String mRequestedScaleMode = null; + + protected ViewFilter(MffContext context, String name) { + super(context, name); + } + + /** + * Binds the filter to a view. + * View filters support visualizing data to a view. Check the specific filter documentation + * for details. The view may be bound only if the filter's graph is not running. + * + * @param view the view to bind to. + * @throws IllegalStateException if the method is called while the graph is running. + */ + public void bindToView(View view) { + if (isRunning()) { + throw new IllegalStateException("Attempting to bind filter to view while it is " + + "running!"); + } + onBindToView(view); + } + + public void setScaleMode(int scaleMode) { + if (isRunning()) { + throw new IllegalStateException("Attempting to change scale mode while filter is " + + "running!"); + } + mScaleMode = scaleMode; + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("scaleMode", Signature.PORT_OPTIONAL, FrameType.single(String.class)) + .addInputPort("flip", Signature.PORT_OPTIONAL, FrameType.single(boolean.class)); + } + + /** + * Subclasses must override this method to bind their filter to the specified view. + * + * When this method is called, Filter implementations may assume that the graph is not + * currently running. + */ + protected abstract void onBindToView(View view); + + /** + * TODO: Document. + */ + protected RectF getTargetRect(Rect frameRect, Rect bufferRect) { + RectF result = new RectF(); + if (bufferRect.width() > 0 && bufferRect.height() > 0) { + float frameAR = (float)frameRect.width() / frameRect.height(); + float bufferAR = (float)bufferRect.width() / bufferRect.height(); + float relativeAR = bufferAR / frameAR; + switch (mScaleMode) { + case SCALE_STRETCH: + result.set(0f, 0f, 1f, 1f); + break; + case SCALE_FIT: + if (relativeAR > 1.0f) { + float x = 0.5f - 0.5f / relativeAR; + float y = 0.0f; + result.set(x, y, x + 1.0f / relativeAR, y + 1.0f); + } else { + float x = 0.0f; + float y = 0.5f - 0.5f * relativeAR; + result.set(x, y, x + 1.0f, y + relativeAR); + } + break; + case SCALE_FILL: + if (relativeAR > 1.0f) { + float x = 0.0f; + float y = 0.5f - 0.5f * relativeAR; + result.set(x, y, x + 1.0f, y + relativeAR); + } else { + float x = 0.5f - 0.5f / relativeAR; + float y = 0.0f; + result.set(x, y, x + 1.0f / relativeAR, y + 1.0f); + } + break; + } + } + return result; + } + + protected void connectViewInputs(InputPort port) { + if (port.getName().equals("scaleMode")) { + port.bindToListener(mScaleModeListener); + port.setAutoPullEnabled(true); + } else if (port.getName().equals("flip")) { + port.bindToFieldNamed("mFlipVertically"); + port.setAutoPullEnabled(true); + } + } + + protected void setupShader(ImageShader shader, Rect frameRect, Rect outputRect) { + shader.setTargetRect(getTargetRect(frameRect, outputRect)); + shader.setClearsOutput(true); + shader.setClearColor(mClearColor); + if (mFlipVertically) { + shader.setSourceRect(0f, 1f, 1f, -1f); + } + } + + private InputPort.FrameListener mScaleModeListener = new InputPort.FrameListener() { + @Override + public void onFrameReceived(InputPort port, Frame frame) { + String scaleMode = (String)frame.asFrameValue().getValue(); + if (!scaleMode.equals(mRequestedScaleMode)) { + mRequestedScaleMode = scaleMode; + if (scaleMode.equals("stretch")) { + mScaleMode = SCALE_STRETCH; + } else if (scaleMode.equals("fit")) { + mScaleMode = SCALE_FIT; + } else if (scaleMode.equals("fill")) { + mScaleMode = SCALE_FILL; + } else { + throw new RuntimeException("Unknown scale-mode '" + scaleMode + "'!"); + } + } + } + }; +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/AudioSample.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/AudioSample.java new file mode 100644 index 0000000..c7eec26 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/AudioSample.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.decoder; + +public class AudioSample { + + public final int sampleRate; + public final int channelCount; + public final byte[] bytes; + + public AudioSample(int sampleRate, int channelCount, byte[] bytes) { + this.sampleRate = sampleRate; + this.channelCount = channelCount; + this.bytes = bytes; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/AudioTrackDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/AudioTrackDecoder.java new file mode 100644 index 0000000..0219fd7 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/AudioTrackDecoder.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaFormat; + +import androidx.media.filterfw.FrameValue; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * {@link TrackDecoder} for decoding audio tracks. + * + * TODO: find out if we always get 16 bits per channel and document. + */ +@TargetApi(16) +public class AudioTrackDecoder extends TrackDecoder { + + private final ByteArrayOutputStream mAudioByteStream; // Guarded by mAudioByteStreamLock. + private final Object mAudioByteStreamLock; + + private int mAudioSampleRate; + private int mAudioChannelCount; + private long mAudioPresentationTimeUs; + + public AudioTrackDecoder(int trackIndex, MediaFormat format, Listener listener) { + super(trackIndex, format, listener); + + if (!DecoderUtil.isAudioFormat(format)) { + throw new IllegalArgumentException( + "AudioTrackDecoder can only be used with audio formats"); + } + + mAudioByteStream = new ByteArrayOutputStream(); + mAudioByteStreamLock = new Object(); + + mAudioSampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); + mAudioChannelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); + } + + @Override + protected MediaCodec initMediaCodec(MediaFormat format) { + MediaCodec mediaCodec = MediaCodec.createDecoderByType( + format.getString(MediaFormat.KEY_MIME)); + mediaCodec.configure(format, null, null, 0); + return mediaCodec; + } + + @Override + protected boolean onDataAvailable( + MediaCodec codec, ByteBuffer[] buffers, int bufferIndex, BufferInfo info) { + ByteBuffer buffer = buffers[bufferIndex]; + byte[] data = new byte[info.size]; + buffer.position(info.offset); + buffer.get(data, 0, info.size); + + synchronized (mAudioByteStreamLock) { + try { + if (mAudioByteStream.size() == 0 && data.length > 0) { + mAudioPresentationTimeUs = info.presentationTimeUs; + } + + mAudioByteStream.write(data); + } catch (IOException e) { + // Just drop the audio sample. + } + } + + buffer.clear(); + codec.releaseOutputBuffer(bufferIndex, false); + notifyListener(); + return true; + } + + /** + * Fills the argument {@link FrameValue} with an audio sample containing the audio that was + * decoded since the last call of this method. The decoder's buffer is cleared as a result. + */ + public void grabSample(FrameValue audioFrame) { + synchronized (mAudioByteStreamLock) { + if (audioFrame != null) { + AudioSample sample = new AudioSample( + mAudioSampleRate, mAudioChannelCount, mAudioByteStream.toByteArray()); + audioFrame.setValue(sample); + audioFrame.setTimestamp(mAudioPresentationTimeUs * 1000); + } + clearBuffer(); + } + } + + /** + * Clears the decoder's buffer. + */ + public void clearBuffer() { + synchronized (mAudioByteStreamLock) { + mAudioByteStream.reset(); + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/CpuVideoTrackDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/CpuVideoTrackDecoder.java new file mode 100644 index 0000000..96f3059 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/CpuVideoTrackDecoder.java @@ -0,0 +1,243 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecList; +import android.media.MediaFormat; +import android.util.SparseIntArray; +import androidx.media.filterfw.ColorSpace; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.PixelUtils; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeMap; + +/** + * {@link TrackDecoder} that decodes a video track and renders the frames onto a + * {@link SurfaceTexture}. + * + * This implementation purely uses CPU based methods to decode and color-convert the frames. + */ +@TargetApi(16) +public class CpuVideoTrackDecoder extends VideoTrackDecoder { + + private static final int COLOR_FORMAT_UNSET = -1; + + private final int mWidth; + private final int mHeight; + + private int mColorFormat = COLOR_FORMAT_UNSET; + private long mCurrentPresentationTimeUs; + private ByteBuffer mDecodedBuffer; + private ByteBuffer mUnrotatedBytes; + + protected CpuVideoTrackDecoder(int trackIndex, MediaFormat format, Listener listener) { + super(trackIndex, format, listener); + + mWidth = format.getInteger(MediaFormat.KEY_WIDTH); + mHeight = format.getInteger(MediaFormat.KEY_HEIGHT); + } + + @Override + protected MediaCodec initMediaCodec(MediaFormat format) { + // Find a codec for our video that can output to one of our supported color-spaces + MediaCodec mediaCodec = findDecoderCodec(format, new int[] { + CodecCapabilities.COLOR_Format32bitARGB8888, + CodecCapabilities.COLOR_FormatYUV420Planar}); + if (mediaCodec == null) { + throw new RuntimeException( + "Could not find a suitable decoder for format: " + format + "!"); + } + mediaCodec.configure(format, null, null, 0); + return mediaCodec; + } + + @Override + protected boolean onDataAvailable( + MediaCodec codec, ByteBuffer[] buffers, int bufferIndex, BufferInfo info) { + + mCurrentPresentationTimeUs = info.presentationTimeUs; + mDecodedBuffer = buffers[bufferIndex]; + + if (mColorFormat == -1) { + mColorFormat = codec.getOutputFormat().getInteger(MediaFormat.KEY_COLOR_FORMAT); + } + + markFrameAvailable(); + notifyListener(); + + // Wait for the grab before we release this buffer. + waitForFrameGrab(); + + codec.releaseOutputBuffer(bufferIndex, false); + + return false; + } + + @Override + protected void copyFrameDataTo(FrameImage2D outputVideoFrame, int rotation) { + // Calculate output dimensions + int outputWidth = mWidth; + int outputHeight = mHeight; + if (needSwapDimension(rotation)) { + outputWidth = mHeight; + outputHeight = mWidth; + } + + // Create output frame + outputVideoFrame.resize(new int[] {outputWidth, outputHeight}); + outputVideoFrame.setTimestamp(mCurrentPresentationTimeUs * 1000); + ByteBuffer outBytes = outputVideoFrame.lockBytes(Frame.MODE_WRITE); + + // Set data + if (rotation == MediaDecoder.ROTATE_NONE) { + convertImage(mDecodedBuffer, outBytes, mColorFormat, mWidth, mHeight); + } else { + if (mUnrotatedBytes == null) { + mUnrotatedBytes = ByteBuffer.allocateDirect(mWidth * mHeight * 4); + } + // TODO: This could be optimized by including the rotation in the color conversion. + convertImage(mDecodedBuffer, mUnrotatedBytes, mColorFormat, mWidth, mHeight); + copyRotate(mUnrotatedBytes, outBytes, rotation); + } + outputVideoFrame.unlock(); + } + + /** + * Copy the input data to the output data applying the specified rotation. + * + * @param input The input image data + * @param output Buffer for the output image data + * @param rotation The rotation to apply + */ + private void copyRotate(ByteBuffer input, ByteBuffer output, int rotation) { + int offset; + int pixStride; + int rowStride; + switch (rotation) { + case MediaDecoder.ROTATE_NONE: + offset = 0; + pixStride = 1; + rowStride = mWidth; + break; + case MediaDecoder.ROTATE_90_LEFT: + offset = (mWidth - 1) * mHeight; + pixStride = -mHeight; + rowStride = 1; + break; + case MediaDecoder.ROTATE_90_RIGHT: + offset = mHeight - 1; + pixStride = mHeight; + rowStride = -1; + break; + case MediaDecoder.ROTATE_180: + offset = mWidth * mHeight - 1; + pixStride = -1; + rowStride = -mWidth; + break; + default: + throw new IllegalArgumentException("Unsupported rotation " + rotation + "!"); + } + PixelUtils.copyPixels(input, output, mWidth, mHeight, offset, pixStride, rowStride); + } + + /** + * Looks for a codec with the specified requirements. + * + * The set of codecs will be filtered down to those that meet the following requirements: + * <ol> + * <li>The codec is a decoder.</li> + * <li>The codec can decode a video of the specified format.</li> + * <li>The codec can decode to one of the specified color formats.</li> + * </ol> + * If multiple codecs are found, the one with the preferred color-format is taken. Color format + * preference is determined by the order of their appearance in the color format array. + * + * @param format The format the codec must decode. + * @param requiredColorFormats Array of target color spaces ordered by preference. + * @return A codec that meets the requirements, or null if no such codec was found. + */ + private static MediaCodec findDecoderCodec(MediaFormat format, int[] requiredColorFormats) { + TreeMap<Integer, String> candidateCodecs = new TreeMap<Integer, String>(); + SparseIntArray colorPriorities = intArrayToPriorityMap(requiredColorFormats); + for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) { + // Get next codec + MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); + + // Check that this is a decoder + if (info.isEncoder()) { + continue; + } + + // Check if this codec can decode the video in question + String requiredType = format.getString(MediaFormat.KEY_MIME); + String[] supportedTypes = info.getSupportedTypes(); + Set<String> typeSet = new HashSet<String>(Arrays.asList(supportedTypes)); + + // Check if it can decode to one of the required color formats + if (typeSet.contains(requiredType)) { + CodecCapabilities capabilities = info.getCapabilitiesForType(requiredType); + for (int supportedColorFormat : capabilities.colorFormats) { + if (colorPriorities.indexOfKey(supportedColorFormat) >= 0) { + int priority = colorPriorities.get(supportedColorFormat); + candidateCodecs.put(priority, info.getName()); + } + } + } + } + + // Pick the best codec (with the highest color priority) + if (candidateCodecs.isEmpty()) { + return null; + } else { + String bestCodec = candidateCodecs.firstEntry().getValue(); + return MediaCodec.createByCodecName(bestCodec); + } + } + + private static SparseIntArray intArrayToPriorityMap(int[] values) { + SparseIntArray result = new SparseIntArray(); + for (int priority = 0; priority < values.length; ++priority) { + result.append(values[priority], priority); + } + return result; + } + + private static void convertImage( + ByteBuffer input, ByteBuffer output, int colorFormat, int width, int height) { + switch (colorFormat) { + case CodecCapabilities.COLOR_Format32bitARGB8888: + ColorSpace.convertArgb8888ToRgba8888(input, output, width, height); + break; + case CodecCapabilities.COLOR_FormatYUV420Planar: + ColorSpace.convertYuv420pToRgba8888(input, output, width, height); + break; + default: + throw new RuntimeException("Unsupported color format: " + colorFormat + "!"); + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/DecoderUtil.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/DecoderUtil.java new file mode 100644 index 0000000..ec0ead0 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/DecoderUtil.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.media.MediaFormat; + +@TargetApi(16) +public class DecoderUtil { + + public static boolean isAudioFormat(MediaFormat format) { + return format.getString(MediaFormat.KEY_MIME).startsWith("audio/"); + } + + public static boolean isVideoFormat(MediaFormat format) { + return format.getString(MediaFormat.KEY_MIME).startsWith("video/"); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/GpuVideoTrackDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/GpuVideoTrackDecoder.java new file mode 100644 index 0000000..bbba9d8 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/GpuVideoTrackDecoder.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.graphics.SurfaceTexture.OnFrameAvailableListener; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaFormat; +import android.view.Surface; + +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.ImageShader; +import androidx.media.filterfw.TextureSource; + +import java.nio.ByteBuffer; + +/** + * {@link TrackDecoder} that decodes a video track and renders the frames onto a + * {@link SurfaceTexture}. + * + * This implementation uses the GPU for image operations such as copying + * and color-space conversion. + */ +@TargetApi(16) +public class GpuVideoTrackDecoder extends VideoTrackDecoder { + + /** + * Identity fragment shader for external textures. + */ + private static final String COPY_FRAGMENT_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "uniform samplerExternalOES tex_sampler_0;\n" + + "varying vec2 v_texcoord;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(tex_sampler_0, v_texcoord);\n" + + "}\n"; + + private final TextureSource mTextureSource; + private final SurfaceTexture mSurfaceTexture; // Access guarded by mFrameMonitor. + private final float[] mTransformMatrix; + + private final int mOutputWidth; + private final int mOutputHeight; + + private ImageShader mImageShader; + + private long mCurrentPresentationTimeUs; + + public GpuVideoTrackDecoder( + int trackIndex, MediaFormat format, Listener listener) { + super(trackIndex, format, listener); + + // Create a surface texture to be used by the video track decoder. + mTextureSource = TextureSource.newExternalTexture(); + mSurfaceTexture = new SurfaceTexture(mTextureSource.getTextureId()); + mSurfaceTexture.detachFromGLContext(); + mSurfaceTexture.setOnFrameAvailableListener(new OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + markFrameAvailable(); + } + }); + + mOutputWidth = format.getInteger(MediaFormat.KEY_WIDTH); + mOutputHeight = format.getInteger(MediaFormat.KEY_HEIGHT); + + mTransformMatrix = new float[16]; + } + + @Override + protected MediaCodec initMediaCodec(MediaFormat format) { + Surface surface = new Surface(mSurfaceTexture); + MediaCodec mediaCodec = MediaCodec.createDecoderByType( + format.getString(MediaFormat.KEY_MIME)); + mediaCodec.configure(format, surface, null, 0); + surface.release(); + return mediaCodec; + } + + @Override + protected boolean onDataAvailable( + MediaCodec codec, ByteBuffer[] buffers, int bufferIndex, BufferInfo info) { + boolean textureAvailable = waitForFrameGrab(); + + mCurrentPresentationTimeUs = info.presentationTimeUs; + + // Only render the next frame if we weren't interrupted. + codec.releaseOutputBuffer(bufferIndex, textureAvailable); + + if (textureAvailable) { + if (updateTexture()) { + notifyListener(); + } + } + + return false; + } + + /** + * Waits for the texture's {@link OnFrameAvailableListener} to be notified and then updates + * the internal {@link SurfaceTexture}. + */ + private boolean updateTexture() { + // Wait for the frame we just released to appear in the texture. + synchronized (mFrameMonitor) { + try { + while (!mFrameAvailable) { + mFrameMonitor.wait(); + } + mSurfaceTexture.attachToGLContext(mTextureSource.getTextureId()); + mSurfaceTexture.updateTexImage(); + mSurfaceTexture.detachFromGLContext(); + return true; + } catch (InterruptedException e) { + return false; + } + } + } + + @Override + protected void copyFrameDataTo(FrameImage2D outputVideoFrame, int rotation) { + TextureSource targetTexture = TextureSource.newExternalTexture(); + mSurfaceTexture.attachToGLContext(targetTexture.getTextureId()); + mSurfaceTexture.getTransformMatrix(mTransformMatrix); + + ImageShader imageShader = getImageShader(); + imageShader.setSourceTransform(mTransformMatrix); + + int outputWidth = mOutputWidth; + int outputHeight = mOutputHeight; + if (rotation != 0) { + float[] targetCoords = getRotationCoords(rotation); + imageShader.setTargetCoords(targetCoords); + if (needSwapDimension(rotation)) { + outputWidth = mOutputHeight; + outputHeight = mOutputWidth; + } + } + outputVideoFrame.resize(new int[] { outputWidth, outputHeight }); + imageShader.process( + targetTexture, + outputVideoFrame.lockRenderTarget(), + outputWidth, + outputHeight); + outputVideoFrame.setTimestamp(mCurrentPresentationTimeUs * 1000); + outputVideoFrame.unlock(); + targetTexture.release(); + + mSurfaceTexture.detachFromGLContext(); + } + + @Override + public void release() { + super.release(); + synchronized (mFrameMonitor) { + mTextureSource.release(); + mSurfaceTexture.release(); + } + } + + /* + * This method has to be called on the MFF processing thread. + */ + private ImageShader getImageShader() { + if (mImageShader == null) { + mImageShader = new ImageShader(COPY_FRAGMENT_SHADER); + mImageShader.setTargetRect(0f, 1f, 1f, -1f); + } + return mImageShader; + } + + /** + * Get the quad coords for rotation. + * @param rotation applied to the frame, value is one of + * {ROTATE_NONE, ROTATE_90_RIGHT, ROTATE_180, ROTATE_90_LEFT} + * @return coords the calculated quad coords for the given rotation + */ + private static float[] getRotationCoords(int rotation) { + switch(rotation) { + case MediaDecoder.ROTATE_90_RIGHT: + return new float[] { 0f, 0f, 0f, 1f, 1f, 0f, 1f, 1f }; + case MediaDecoder.ROTATE_180: + return new float[] { 1f, 0f, 0f, 0f, 1f, 1f, 0f, 1f }; + case MediaDecoder.ROTATE_90_LEFT: + return new float[] { 1f, 1f, 1f, 0f, 0f, 1f, 0f, 0f }; + case MediaDecoder.ROTATE_NONE: + return new float[] { 0f, 1f, 1f, 1f, 0f, 0f, 1f, 0f }; + default: + throw new IllegalArgumentException("Unsupported rotation angle."); + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java new file mode 100644 index 0000000..aa57394 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/MediaDecoder.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.content.Context; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Build; +import android.util.Log; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.RenderTarget; + +import java.util.concurrent.LinkedBlockingQueue; + +@TargetApi(16) +public class MediaDecoder implements + Runnable, + TrackDecoder.Listener { + + public interface Listener { + /** + * Notifies a listener when a decoded video frame is available. The listener should use + * {@link MediaDecoder#grabVideoFrame(FrameImage2D, int)} to grab the video data for this + * frame. + */ + void onVideoFrameAvailable(); + + /** + * Notifies a listener when one or more audio samples are available. The listener should use + * {@link MediaDecoder#grabAudioSamples(FrameValue)} to grab the audio samples. + */ + void onAudioSamplesAvailable(); + + /** + * Notifies a listener that decoding has started. This method is called on the decoder + * thread. + */ + void onDecodingStarted(); + + /** + * Notifies a listener that decoding has stopped. This method is called on the decoder + * thread. + */ + void onDecodingStopped(); + + /** + * Notifies a listener that an error occurred. If an error occurs, {@link MediaDecoder} is + * stopped and no more events are reported to this {@link Listener}'s callbacks. + * This method is called on the decoder thread. + */ + void onError(Exception e); + } + + public static final int ROTATE_NONE = 0; + public static final int ROTATE_90_RIGHT = 90; + public static final int ROTATE_180 = 180; + public static final int ROTATE_90_LEFT = 270; + + private static final String LOG_TAG = "MediaDecoder"; + private static final boolean DEBUG = false; + + private static final int MAX_EVENTS = 32; + private static final int EVENT_START = 0; + private static final int EVENT_STOP = 1; + private static final int EVENT_EOF = 2; + + private final Listener mListener; + private final Uri mUri; + private final Context mContext; + + private final LinkedBlockingQueue<Integer> mEventQueue; + + private final Thread mDecoderThread; + + private MediaExtractor mMediaExtractor; + + private RenderTarget mRenderTarget; + + private int mDefaultRotation; + private int mVideoTrackIndex; + private int mAudioTrackIndex; + + private VideoTrackDecoder mVideoTrackDecoder; + private AudioTrackDecoder mAudioTrackDecoder; + + private boolean mStarted; + + private long mStartMicros; + + private boolean mOpenGLEnabled = true; + + private boolean mSignaledEndOfInput; + private boolean mSeenEndOfAudioOutput; + private boolean mSeenEndOfVideoOutput; + + public MediaDecoder(Context context, Uri uri, Listener listener) { + this(context, uri, 0, listener); + } + + public MediaDecoder(Context context, Uri uri, long startMicros, Listener listener) { + if (context == null) { + throw new NullPointerException("context cannot be null"); + } + mContext = context; + + if (uri == null) { + throw new NullPointerException("uri cannot be null"); + } + mUri = uri; + + if (startMicros < 0) { + throw new IllegalArgumentException("startMicros cannot be negative"); + } + mStartMicros = startMicros; + + if (listener == null) { + throw new NullPointerException("listener cannot be null"); + } + mListener = listener; + + mEventQueue = new LinkedBlockingQueue<Integer>(MAX_EVENTS); + mDecoderThread = new Thread(this); + } + + /** + * Set whether decoder may use OpenGL for decoding. + * + * This must be called before {@link #start()}. + * + * @param enabled flag whether to enable OpenGL decoding (default is true). + */ + public void setOpenGLEnabled(boolean enabled) { + // If event-queue already has events, we have started already. + if (mEventQueue.isEmpty()) { + mOpenGLEnabled = enabled; + } else { + throw new IllegalStateException( + "Must call setOpenGLEnabled() before calling start()!"); + } + } + + /** + * Returns whether OpenGL is enabled for decoding. + * + * @return whether OpenGL is enabled for decoding. + */ + public boolean isOpenGLEnabled() { + return mOpenGLEnabled; + } + + public void start() { + mEventQueue.offer(EVENT_START); + mDecoderThread.start(); + } + + public void stop() { + stop(true); + } + + private void stop(boolean manual) { + if (manual) { + mEventQueue.offer(EVENT_STOP); + mDecoderThread.interrupt(); + } else { + mEventQueue.offer(EVENT_EOF); + } + } + + @Override + public void run() { + Integer event; + try { + while (true) { + event = mEventQueue.poll(); + boolean shouldStop = false; + if (event != null) { + switch (event) { + case EVENT_START: + onStart(); + break; + case EVENT_EOF: + if (mVideoTrackDecoder != null) { + mVideoTrackDecoder.waitForFrameGrab(); + } + // once the last frame has been grabbed, fall through and stop + case EVENT_STOP: + onStop(true); + shouldStop = true; + break; + } + } else if (mStarted) { + decode(); + } + if (shouldStop) { + break; + } + + } + } catch (Exception e) { + mListener.onError(e); + onStop(false); + } + } + + private void onStart() throws Exception { + if (mOpenGLEnabled) { + getRenderTarget().focus(); + } + + mMediaExtractor = new MediaExtractor(); + mMediaExtractor.setDataSource(mContext, mUri, null); + + mVideoTrackIndex = -1; + mAudioTrackIndex = -1; + + for (int i = 0; i < mMediaExtractor.getTrackCount(); i++) { + MediaFormat format = mMediaExtractor.getTrackFormat(i); + if (DEBUG) { + Log.i(LOG_TAG, "Uri " + mUri + ", track " + i + ": " + format); + } + if (DecoderUtil.isVideoFormat(format) && mVideoTrackIndex == -1) { + mVideoTrackIndex = i; + } else if (DecoderUtil.isAudioFormat(format) && mAudioTrackIndex == -1) { + mAudioTrackIndex = i; + } + } + + if (mVideoTrackIndex == -1 && mAudioTrackIndex == -1) { + throw new IllegalArgumentException( + "Couldn't find a video or audio track in the provided file"); + } + + if (mVideoTrackIndex != -1) { + MediaFormat videoFormat = mMediaExtractor.getTrackFormat(mVideoTrackIndex); + mVideoTrackDecoder = mOpenGLEnabled + ? new GpuVideoTrackDecoder(mVideoTrackIndex, videoFormat, this) + : new CpuVideoTrackDecoder(mVideoTrackIndex, videoFormat, this); + mVideoTrackDecoder.init(); + mMediaExtractor.selectTrack(mVideoTrackIndex); + if (Build.VERSION.SDK_INT >= 17) { + retrieveDefaultRotation(); + } + } + + if (mAudioTrackIndex != -1) { + MediaFormat audioFormat = mMediaExtractor.getTrackFormat(mAudioTrackIndex); + mAudioTrackDecoder = new AudioTrackDecoder(mAudioTrackIndex, audioFormat, this); + mAudioTrackDecoder.init(); + mMediaExtractor.selectTrack(mAudioTrackIndex); + } + + if (mStartMicros > 0) { + mMediaExtractor.seekTo(mStartMicros, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + } + + mStarted = true; + mListener.onDecodingStarted(); + } + + @TargetApi(17) + private void retrieveDefaultRotation() { + MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever(); + metadataRetriever.setDataSource(mContext, mUri); + String rotationString = metadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + mDefaultRotation = rotationString == null ? 0 : Integer.parseInt(rotationString); + } + + private void onStop(boolean notifyListener) { + mMediaExtractor.release(); + mMediaExtractor = null; + + if (mVideoTrackDecoder != null) { + mVideoTrackDecoder.release(); + mVideoTrackDecoder = null; + } + + if (mAudioTrackDecoder != null) { + mAudioTrackDecoder.release(); + mAudioTrackDecoder = null; + } + + if (mOpenGLEnabled) { + if (mRenderTarget != null) { + getRenderTarget().release(); + } + RenderTarget.focusNone(); + } + + mVideoTrackIndex = -1; + mAudioTrackIndex = -1; + + mEventQueue.clear(); + mStarted = false; + if (notifyListener) { + mListener.onDecodingStopped(); + } + } + + private void decode() { + int sampleTrackIndex = mMediaExtractor.getSampleTrackIndex(); + if (sampleTrackIndex >= 0) { + if (sampleTrackIndex == mVideoTrackIndex) { + mVideoTrackDecoder.feedInput(mMediaExtractor); + } else if (sampleTrackIndex == mAudioTrackIndex) { + mAudioTrackDecoder.feedInput(mMediaExtractor); + } + } else if (!mSignaledEndOfInput) { + if (mVideoTrackDecoder != null) { + mVideoTrackDecoder.signalEndOfInput(); + } + if (mAudioTrackDecoder != null) { + mAudioTrackDecoder.signalEndOfInput(); + } + mSignaledEndOfInput = true; + } + + if (mVideoTrackDecoder != null) { + mVideoTrackDecoder.drainOutputBuffer(); + } + if (mAudioTrackDecoder != null) { + mAudioTrackDecoder.drainOutputBuffer(); + } + } + + /** + * Fills the argument frame with the video data, using the rotation hint obtained from the + * file's metadata, if any. + * + * @see #grabVideoFrame(FrameImage2D, int) + */ + public void grabVideoFrame(FrameImage2D outputVideoFrame) { + grabVideoFrame(outputVideoFrame, mDefaultRotation); + } + + /** + * Fills the argument frame with the video data, the frame will be returned with the given + * rotation applied. + * + * @param outputVideoFrame the output video frame. + * @param videoRotation the rotation angle that is applied to the raw decoded frame. + * Value is one of {ROTATE_NONE, ROTATE_90_RIGHT, ROTATE_180, ROTATE_90_LEFT}. + */ + public void grabVideoFrame(FrameImage2D outputVideoFrame, int videoRotation) { + if (mVideoTrackDecoder != null && outputVideoFrame != null) { + mVideoTrackDecoder.grabFrame(outputVideoFrame, videoRotation); + } + } + + /** + * Fills the argument frame with the audio data. + * + * @param outputAudioFrame the output audio frame. + */ + public void grabAudioSamples(FrameValue outputAudioFrame) { + if (mAudioTrackDecoder != null) { + if (outputAudioFrame != null) { + mAudioTrackDecoder.grabSample(outputAudioFrame); + } else { + mAudioTrackDecoder.clearBuffer(); + } + } + } + + /** + * Gets the duration, in nanoseconds. + */ + public long getDuration() { + if (!mStarted) { + throw new IllegalStateException("MediaDecoder has not been started"); + } + + MediaFormat mediaFormat = mMediaExtractor.getTrackFormat( + mVideoTrackIndex != -1 ? mVideoTrackIndex : mAudioTrackIndex); + return mediaFormat.getLong(MediaFormat.KEY_DURATION) * 1000; + } + + private RenderTarget getRenderTarget() { + if (mRenderTarget == null) { + mRenderTarget = RenderTarget.newTarget(1, 1); + } + return mRenderTarget; + } + + @Override + public void onDecodedOutputAvailable(TrackDecoder decoder) { + if (decoder == mVideoTrackDecoder) { + mListener.onVideoFrameAvailable(); + } else if (decoder == mAudioTrackDecoder) { + mListener.onAudioSamplesAvailable(); + } + } + + @Override + public void onEndOfStream(TrackDecoder decoder) { + if (decoder == mAudioTrackDecoder) { + mSeenEndOfAudioOutput = true; + } else if (decoder == mVideoTrackDecoder) { + mSeenEndOfVideoOutput = true; + } + + if ((mAudioTrackDecoder == null || mSeenEndOfAudioOutput) + && (mVideoTrackDecoder == null || mSeenEndOfVideoOutput)) { + stop(false); + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/TrackDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/TrackDecoder.java new file mode 100644 index 0000000..c81e8b4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/TrackDecoder.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.media.MediaCodec; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.util.Log; + +import java.nio.ByteBuffer; + +@TargetApi(16) +abstract class TrackDecoder { + + interface Listener { + void onDecodedOutputAvailable(TrackDecoder decoder); + + void onEndOfStream(TrackDecoder decoder); + } + + private static final String LOG_TAG = "TrackDecoder"; + + private static final long TIMEOUT_US = 50; // Timeout for en-queueing and de-queueing buffers. + + private static final int NO_INPUT_BUFFER = -1; + + private final int mTrackIndex; + private final MediaFormat mMediaFormat; + private final Listener mListener; + + private MediaCodec mMediaCodec; + private MediaFormat mOutputFormat; + + private ByteBuffer[] mCodecInputBuffers; + private ByteBuffer[] mCodecOutputBuffers; + + private boolean mShouldEnqueueEndOfStream; + + /** + * @return a configured {@link MediaCodec}. + */ + protected abstract MediaCodec initMediaCodec(MediaFormat format); + + /** + * Called when decoded output is available. The implementer is responsible for releasing the + * assigned buffer. + * + * @return {@code true} if any further decoding should be attempted at the moment. + */ + protected abstract boolean onDataAvailable( + MediaCodec codec, ByteBuffer[] buffers, int bufferIndex, BufferInfo info); + + protected TrackDecoder(int trackIndex, MediaFormat mediaFormat, Listener listener) { + mTrackIndex = trackIndex; + + if (mediaFormat == null) { + throw new NullPointerException("mediaFormat cannot be null"); + } + mMediaFormat = mediaFormat; + + if (listener == null) { + throw new NullPointerException("listener cannot be null"); + } + mListener = listener; + } + + public void init() { + mMediaCodec = initMediaCodec(mMediaFormat); + mMediaCodec.start(); + mCodecInputBuffers = mMediaCodec.getInputBuffers(); + mCodecOutputBuffers = mMediaCodec.getOutputBuffers(); + } + + public void signalEndOfInput() { + mShouldEnqueueEndOfStream = true; + tryEnqueueEndOfStream(); + } + + public void release() { + if (mMediaCodec != null) { + mMediaCodec.stop(); + mMediaCodec.release(); + } + } + + protected MediaCodec getMediaCodec() { + return mMediaCodec; + } + + protected void notifyListener() { + mListener.onDecodedOutputAvailable(this); + } + + public boolean feedInput(MediaExtractor mediaExtractor) { + long presentationTimeUs = 0; + + int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_US); + if (inputBufferIndex != NO_INPUT_BUFFER) { + ByteBuffer destinationBuffer = mCodecInputBuffers[inputBufferIndex]; + int sampleSize = mediaExtractor.readSampleData(destinationBuffer, 0); + // We don't expect to get a sample without any data, so this should never happen. + if (sampleSize < 0) { + Log.w(LOG_TAG, "Media extractor had sample but no data."); + + // Signal the end of the track immediately anyway, using the buffer. + mMediaCodec.queueInputBuffer( + inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + return false; + } + + presentationTimeUs = mediaExtractor.getSampleTime(); + mMediaCodec.queueInputBuffer( + inputBufferIndex, + 0, + sampleSize, + presentationTimeUs, + 0); + + return mediaExtractor.advance() + && mediaExtractor.getSampleTrackIndex() == mTrackIndex; + } + return false; + } + + private void tryEnqueueEndOfStream() { + int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_US); + // We will always eventually have an input buffer, because we keep trying until the last + // decoded frame is output. + // The EoS does not need to be signaled if the application stops decoding. + if (inputBufferIndex != NO_INPUT_BUFFER) { + mMediaCodec.queueInputBuffer( + inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); + mShouldEnqueueEndOfStream = false; + } + } + + public boolean drainOutputBuffer() { + BufferInfo outputInfo = new BufferInfo(); + int outputBufferIndex = mMediaCodec.dequeueOutputBuffer(outputInfo, TIMEOUT_US); + + if ((outputInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { + mListener.onEndOfStream(this); + return false; + } + if (mShouldEnqueueEndOfStream) { + tryEnqueueEndOfStream(); + } + if (outputBufferIndex >= 0) { + return onDataAvailable( + mMediaCodec, mCodecOutputBuffers, outputBufferIndex, outputInfo); + } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { + mCodecOutputBuffers = mMediaCodec.getOutputBuffers(); + return true; + } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + mOutputFormat = mMediaCodec.getOutputFormat(); + Log.d(LOG_TAG, "Output format has changed to " + mOutputFormat); + return true; + } + return false; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/VideoTrackDecoder.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/VideoTrackDecoder.java new file mode 100644 index 0000000..06a4305 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/decoder/VideoTrackDecoder.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.decoder; + +import android.annotation.TargetApi; +import android.media.MediaFormat; +import android.util.Log; + +import androidx.media.filterfw.FrameImage2D; + +/** + * Base class for all {@link TrackDecoder} classes that decode video. + */ +@TargetApi(16) +public abstract class VideoTrackDecoder extends TrackDecoder { + + private static final String LOG_TAG = "VideoTrackDecoder"; + + protected final Object mFrameMonitor = new Object(); + protected volatile boolean mFrameAvailable; // Access guarded by mFrameMonitor. + + protected VideoTrackDecoder(int trackIndex, MediaFormat format, Listener listener) { + super(trackIndex, format, listener); + if (!DecoderUtil.isVideoFormat(format)) { + throw new IllegalArgumentException( + "VideoTrackDecoder can only be used with video formats"); + } + } + + public void grabFrame(FrameImage2D outputVideoFrame, int rotation) { + synchronized (mFrameMonitor) { + if (!mFrameAvailable) { + Log.w(LOG_TAG, "frame is not ready - the caller has to wait for a corresponding " + + "onDecodedFrameAvailable() call"); + return; + } + + copyFrameDataTo(outputVideoFrame, rotation); + + mFrameAvailable = false; + mFrameMonitor.notifyAll(); + } + } + + + /** + * Waits for the frame to be picked up by the MFF thread, i.e. blocks until the + * {@link #grabFrame(FrameImage2D, int)}) method is called. + */ + public boolean waitForFrameGrab() { + synchronized (mFrameMonitor) { + try { + while (mFrameAvailable) { + mFrameMonitor.wait(); + } + return true; + } catch (InterruptedException e) { + return false; + } + } + } + + protected final void markFrameAvailable() { + synchronized (mFrameMonitor) { + mFrameAvailable = true; + mFrameMonitor.notifyAll(); + } + } + + /** + * @return if the frame dimension needs to be swapped, + * i.e. (width,height) becomes (height, width) + */ + protected static boolean needSwapDimension(int rotation) { + switch(rotation) { + case MediaDecoder.ROTATE_90_RIGHT: + case MediaDecoder.ROTATE_90_LEFT: + return true; + case MediaDecoder.ROTATE_NONE: + case MediaDecoder.ROTATE_180: + return false; + default: + throw new IllegalArgumentException("Unsupported rotation angle."); + } + } + + /** + * Subclasses must implement this to copy the video frame data to an MFF frame. + * + * @param outputVideoFrame The destination frame + * @param rotation The desired rotation of the frame + */ + protected abstract void copyFrameDataTo(FrameImage2D outputVideoFrame, int rotation); + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/geometry/Quad.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/geometry/Quad.java new file mode 100644 index 0000000..4035f7f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/geometry/Quad.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2011 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 androidx.media.filterfw.geometry; + +import android.annotation.SuppressLint; +import android.graphics.Matrix; +import android.graphics.PointF; +import android.graphics.RectF; + +/** + * The Quad class specifies a (possibly affine transformed) rectangle. + * + * A Quad instance holds 4 points that define its shape. The points may represent any rectangle that + * has been transformed by an affine transformation. This means that Quads can represent translated, + * scaled, rotated and sheared/skewed rectangles. As such, Quads are restricted to the set of + * parallelograms. + * + * Each point in the Quad represents a specific corner of the Quad. These are top-left, top-right, + * bottom-left, and bottom-right. These labels allow mapping a transformed Quad back to an up-right + * Quad, with the point-to-point mapping well-defined. They do not necessarily indicate that e.g. + * the top-left corner is actually at the top-left of coordinate space. + */ +@SuppressLint("FloatMath") +public class Quad { + + private final PointF mTopLeft; + private final PointF mTopRight; + private final PointF mBottomLeft; + private final PointF mBottomRight; + + /** + * Returns the unit Quad. + * The unit Quad has its top-left point at (0, 0) and bottom-right point at (1, 1). + * @return the unit Quad. + */ + public static Quad unitQuad() { + return new Quad(0f, 0f, 1f, 0f, 0f, 1f, 1f, 1f); + } + + /** + * Return a Quad from the specified rectangle. + * + * @param rect a RectF instance. + * @return Quad that represents the passed rectangle. + */ + public static Quad fromRect(RectF rect) { + return new Quad(new PointF(rect.left, rect.top), + new PointF(rect.right, rect.top), + new PointF(rect.left, rect.bottom), + new PointF(rect.right, rect.bottom)); + } + + /** + * Return a Quad from the specified rectangle coordinates. + * + * @param x the top left x coordinate + * @param y the top left y coordinate + * @param width the width of the rectangle + * @param height the height of the rectangle + * @return Quad that represents the passed rectangle. + */ + public static Quad fromRect(float x, float y, float width, float height) { + return new Quad(new PointF(x, y), + new PointF(x + width, y), + new PointF(x, y + height), + new PointF(x + width, y + height)); + } + + /** + * Return a Quad that spans the specified points and height. + * + * The returned Quad has the specified top-left and top-right points, and the specified height + * while maintaining 90 degree angles on all 4 corners. + * + * @param topLeft the top-left of the quad + * @param topRight the top-right of the quad + * @param height the height of the quad + * @return Quad that spans the specified points and height. + */ + public static Quad fromLineAndHeight(PointF topLeft, PointF topRight, float height) { + PointF dp = new PointF(topRight.x - topLeft.x, topRight.y - topLeft.y); + float len = dp.length(); + PointF np = new PointF(height * (dp.y / len), height * (dp.x / len)); + PointF p2 = new PointF(topLeft.x - np.x, topLeft.y + np.y); + PointF p3 = new PointF(topRight.x - np.x, topRight.y + np.y); + return new Quad(topLeft, topRight, p2, p3); + } + + /** + * Return a Quad that represents the specified rotated rectangle. + * + * The Quad is rotated counter-clockwise around its centroid. + * + * @param rect the source rectangle + * @param angle the angle to rotate the source rectangle in radians + * @return the Quad representing the source rectangle rotated by the given angle. + */ + public static Quad fromRotatedRect(RectF rect, float angle) { + return Quad.fromRect(rect).rotated(angle); + } + + /** + * Return a Quad that represents the specified transformed rectangle. + * + * The transform is applied by multiplying each point (x, y, 1) by the matrix. + * + * @param rect the source rectangle + * @param matrix the transformation matrix + * @return the Quad representing the source rectangle transformed by the matrix + */ + public static Quad fromTransformedRect(RectF rect, Matrix matrix) { + return Quad.fromRect(rect).transformed(matrix); + } + + /** + * Returns the transformation matrix to transform the source Quad to the target Quad. + * + * @param source the source quad + * @param target the target quad + * @return the transformation matrix to map source to target. + */ + public static Matrix getTransform(Quad source, Quad target) { + // We only use the first 3 points as they sufficiently specify the transform + Matrix transform = new Matrix(); + transform.setPolyToPoly(source.asCoords(), 0, target.asCoords(), 0, 3); + return transform; + } + + /** + * The top-left point of the Quad. + * @return top-left point of the Quad. + */ + public PointF topLeft() { + return mTopLeft; + } + + /** + * The top-right point of the Quad. + * @return top-right point of the Quad. + */ + public PointF topRight() { + return mTopRight; + } + + /** + * The bottom-left point of the Quad. + * @return bottom-left point of the Quad. + */ + public PointF bottomLeft() { + return mBottomLeft; + } + + /** + * The bottom-right point of the Quad. + * @return bottom-right point of the Quad. + */ + public PointF bottomRight() { + return mBottomRight; + } + + /** + * Rotate the quad by the given angle. + * + * The Quad is rotated counter-clockwise around its centroid. + * + * @param angle the angle to rotate in radians + * @return the rotated Quad + */ + public Quad rotated(float angle) { + PointF center = center(); + float cosa = (float) Math.cos(angle); + float sina = (float) Math.sin(angle); + + PointF topLeft = rotatePoint(topLeft(), center, cosa, sina); + PointF topRight = rotatePoint(topRight(), center, cosa, sina); + PointF bottomLeft = rotatePoint(bottomLeft(), center, cosa, sina); + PointF bottomRight = rotatePoint(bottomRight(), center, cosa, sina); + + return new Quad(topLeft, topRight, bottomLeft, bottomRight); + } + + /** + * Transform the quad with the given transformation matrix. + * + * The transform is applied by multiplying each point (x, y, 1) by the matrix. + * + * @param matrix the transformation matrix + * @return the transformed Quad + */ + public Quad transformed(Matrix matrix) { + float[] points = asCoords(); + matrix.mapPoints(points); + return new Quad(points); + } + + /** + * Returns the centroid of the Quad. + * + * The centroid of the Quad is where the two inner diagonals connecting the opposite corners + * meet. + * + * @return the centroid of the Quad. + */ + public PointF center() { + // As the diagonals bisect each other, we can simply return the center of one of the + // diagonals. + return new PointF((mTopLeft.x + mBottomRight.x) / 2f, + (mTopLeft.y + mBottomRight.y) / 2f); + } + + /** + * Returns the quad as a float-array of coordinates. + * The order of coordinates is top-left, top-right, bottom-left, bottom-right. This is the + * default order of coordinates used in ImageShaders, so this method can be used to bind + * an attribute to the Quad. + */ + public float[] asCoords() { + return new float[] { mTopLeft.x, mTopLeft.y, + mTopRight.x, mTopRight.y, + mBottomLeft.x, mBottomLeft.y, + mBottomRight.x, mBottomRight.y }; + } + + /** + * Grow the Quad outwards by the specified factor. + * + * This method moves the corner points of the Quad outward along the diagonals that connect + * them to the centroid. A factor of 1.0 moves the quad outwards by the distance of the corners + * to the centroid. + * + * @param factor the growth factor + * @return the Quad grown by the specified amount + */ + public Quad grow(float factor) { + PointF pc = center(); + return new Quad(factor * (mTopLeft.x - pc.x) + pc.x, + factor * (mTopLeft.y - pc.y) + pc.y, + factor * (mTopRight.x - pc.x) + pc.x, + factor * (mTopRight.y - pc.y) + pc.y, + factor * (mBottomLeft.x - pc.x) + pc.x, + factor * (mBottomLeft.y - pc.y) + pc.y, + factor * (mBottomRight.x - pc.x) + pc.x, + factor * (mBottomRight.y - pc.y) + pc.y); + } + + /** + * Scale the Quad by the specified factor. + * + * @param factor the scaling factor + * @return the Quad instance scaled by the specified factor. + */ + public Quad scale(float factor) { + return new Quad(mTopLeft.x * factor, mTopLeft.y * factor, + mTopRight.x * factor, mTopRight.y * factor, + mBottomLeft.x * factor, mBottomLeft.y * factor, + mBottomRight.x * factor, mBottomRight.y * factor); + } + + /** + * Scale the Quad by the specified factors in the x and y factors. + * + * @param sx the x scaling factor + * @param sy the y scaling factor + * @return the Quad instance scaled by the specified factors. + */ + public Quad scale2(float sx, float sy) { + return new Quad(mTopLeft.x * sx, mTopLeft.y * sy, + mTopRight.x * sx, mTopRight.y * sy, + mBottomLeft.x * sx, mBottomLeft.y * sy, + mBottomRight.x * sx, mBottomRight.y * sy); + } + + /** + * Returns the Quad's left-to-right edge. + * + * Returns a vector that goes from the Quad's top-left to top-right (or bottom-left to + * bottom-right). + * + * @return the edge vector as a PointF. + */ + public PointF xEdge() { + return new PointF(mTopRight.x - mTopLeft.x, mTopRight.y - mTopLeft.y); + } + + /** + * Returns the Quad's top-to-bottom edge. + * + * Returns a vector that goes from the Quad's top-left to bottom-left (or top-right to + * bottom-right). + * + * @return the edge vector as a PointF. + */ + public PointF yEdge() { + return new PointF(mBottomLeft.x - mTopLeft.x, mBottomLeft.y - mTopLeft.y); + } + + @Override + public String toString() { + return "Quad(" + mTopLeft.x + ", " + mTopLeft.y + ", " + + mTopRight.x + ", " + mTopRight.y + ", " + + mBottomLeft.x + ", " + mBottomLeft.y + ", " + + mBottomRight.x + ", " + mBottomRight.y + ")"; + } + + private Quad(PointF topLeft, PointF topRight, PointF bottomLeft, PointF bottomRight) { + mTopLeft = topLeft; + mTopRight = topRight; + mBottomLeft = bottomLeft; + mBottomRight = bottomRight; + } + + private Quad(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3) { + mTopLeft = new PointF(x0, y0); + mTopRight = new PointF(x1, y1); + mBottomLeft = new PointF(x2, y2); + mBottomRight = new PointF(x3, y3); + } + + private Quad(float[] points) { + mTopLeft = new PointF(points[0], points[1]); + mTopRight = new PointF(points[2], points[3]); + mBottomLeft = new PointF(points[4], points[5]); + mBottomRight = new PointF(points[6], points[7]); + } + + private static PointF rotatePoint(PointF p, PointF c, float cosa, float sina) { + float x = (p.x - c.x) * cosa - (p.y - c.y) * sina + c.x; + float y = (p.x - c.x) * sina + (p.y - c.y) * cosa + c.y; + return new PointF(x,y); + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/AverageFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/AverageFilter.java new file mode 100644 index 0000000..d873e0a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/AverageFilter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 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. + */ +// Takes sharpness scores in RT and averages them over time + +package androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public class AverageFilter extends Filter { + + private static final String TAG = "AverageFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + private static final int NUM_FRAMES = 5; + private int counter = 0; + private float[] temp = new float[NUM_FRAMES]; + + /** + * @param context + * @param name + */ + public AverageFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType floatT = FrameType.single(float.class); + return new Signature() + .addInputPort("sharpness", Signature.PORT_REQUIRED, floatT) + .addOutputPort("avg", Signature.PORT_REQUIRED, floatT) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + FrameValue inFrameValue = getConnectedInputPort("sharpness").pullFrame().asFrameValue(); + if (counter < NUM_FRAMES && counter >= 0) { + temp[counter] = ((Float)inFrameValue.getValue()).floatValue(); + } + + counter = (counter + 1) % NUM_FRAMES; + + float output = (temp[0] + temp[1] + temp[2] + temp[3] + temp[4]) / NUM_FRAMES; + if (mLogVerbose) Log.v(TAG, "Avg= " + output + "temp1= " + temp[0] + "temp2= " + + temp[1] + "temp3= " + temp[2] + "temp4=" + temp[3] + "temp5=" + temp[4]); + + OutputPort outPort = getConnectedOutputPort("avg"); + FrameValue outFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + outFrame.setValue(output); + outPort.pushFrame(outFrame); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/AvgBrightnessFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/AvgBrightnessFilter.java new file mode 100644 index 0000000..88cd44a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/AvgBrightnessFilter.java @@ -0,0 +1,75 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; + +public class AvgBrightnessFilter extends Filter { + + private static final String TAG = "AvgBrightnessFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + public AvgBrightnessFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU); + FrameType floatT = FrameType.single(float.class); + return new Signature().addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addOutputPort("brightnessRating", Signature.PORT_OPTIONAL, floatT) + .disallowOtherPorts(); + + } + + @Override + protected void onProcess() { + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + + float brightness; + ByteBuffer inputBuffer = inputImage.lockBytes(Frame.MODE_READ); + + brightness = brightnessOperator(inputImage.getWidth(),inputImage.getHeight(), inputBuffer); + + inputImage.unlock(); + + if (mLogVerbose) Log.v(TAG, "contrastRatio: " + brightness); + + OutputPort brightnessPort = getConnectedOutputPort("brightnessRating"); + FrameValue brightnessOutFrame = brightnessPort.fetchAvailableFrame(null).asFrameValue(); + brightnessOutFrame.setValue(brightness); + brightnessPort.pushFrame(brightnessOutFrame); + } + + private static native float brightnessOperator(int width, int height, ByteBuffer imageBuffer); + + static { + System.loadLibrary("smartcamera_jni"); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/CSVWriterFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/CSVWriterFilter.java new file mode 100644 index 0000000..ca16c27 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/CSVWriterFilter.java @@ -0,0 +1,147 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameBuffer2D; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + + +public class CSVWriterFilter extends Filter { + + private static final String TAG = "CSVWriterFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + private boolean mFirstTime = true; + private final static int NUM_FRAMES = 3; + private final String mFileName = "/CSVFile.csv"; + + public CSVWriterFilter(MffContext context, String name) { + + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType floatT = FrameType.single(float.class); + FrameType stringT = FrameType.single(String.class); + FrameType floatArrayT = FrameType.array(float.class); + + return new Signature() + .addInputPort("sharpness", Signature.PORT_REQUIRED, floatT) + .addInputPort("overExposure", Signature.PORT_REQUIRED, floatT) + .addInputPort("underExposure", Signature.PORT_REQUIRED, floatT) + .addInputPort("colorfulness", Signature.PORT_REQUIRED, floatT) + .addInputPort("contrastRating", Signature.PORT_REQUIRED, floatT) + .addInputPort("brightness", Signature.PORT_REQUIRED, floatT) + .addInputPort("motionValues", Signature.PORT_REQUIRED, floatArrayT) + .addInputPort("imageFileName", Signature.PORT_REQUIRED, stringT) + .addInputPort("csvFilePath", Signature.PORT_REQUIRED, stringT) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + + + + Log.v(TAG,"in csv writer on process"); + FrameValue sharpnessValue = + getConnectedInputPort("sharpness").pullFrame().asFrameValue(); + float sharpness = ((Float)sharpnessValue.getValue()).floatValue(); + + FrameValue overExposureValue = + getConnectedInputPort("overExposure").pullFrame().asFrameValue(); + float overExposure = ((Float)overExposureValue.getValue()).floatValue(); + + FrameValue underExposureValue = + getConnectedInputPort("underExposure").pullFrame().asFrameValue(); + float underExposure = ((Float)underExposureValue.getValue()).floatValue(); + + FrameValue colorfulnessValue = + getConnectedInputPort("colorfulness").pullFrame().asFrameValue(); + float colorfulness = ((Float)colorfulnessValue.getValue()).floatValue(); + + FrameValue contrastValue = + getConnectedInputPort("contrastRating").pullFrame().asFrameValue(); + float contrast = ((Float)contrastValue.getValue()).floatValue(); + + FrameValue brightnessValue = + getConnectedInputPort("brightness").pullFrame().asFrameValue(); + float brightness = ((Float)brightnessValue.getValue()).floatValue(); + + FrameValue motionValuesFrameValue = + getConnectedInputPort("motionValues").pullFrame().asFrameValue(); + float[] motionValues = (float[]) motionValuesFrameValue.getValue(); + float vectorAccel = (float) Math.sqrt(Math.pow(motionValues[0], 2) + + Math.pow(motionValues[1], 2) + Math.pow(motionValues[2], 2)); + + FrameValue imageFileNameFrameValue = + getConnectedInputPort("imageFileName").pullFrame().asFrameValue(); + String imageFileName = ((String)imageFileNameFrameValue.getValue()); + + FrameValue csvFilePathFrameValue = + getConnectedInputPort("csvFilePath").pullFrame().asFrameValue(); + String csvFilePath = ((String)csvFilePathFrameValue.getValue()); + + + if(mFirstTime) { + try { + FileWriter fileWriter = new FileWriter(csvFilePath + "/CSVFile.csv"); + BufferedWriter csvWriter = new BufferedWriter(fileWriter); + + csvWriter.write("FileName,Sharpness,OverExposure,UnderExposure,Colorfulness," + + "ContrastRating,Brightness,Motion"); + csvWriter.newLine(); + csvWriter.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + mFirstTime = false; + } + + try { + Log.v(TAG,"about to write to file"); + FileWriter fileWriter = new FileWriter(csvFilePath + mFileName, true); + BufferedWriter csvWriter = new BufferedWriter(fileWriter); + + csvWriter.write(imageFileName + "," + sharpness + "," + overExposure + "," + + underExposure + "," + colorfulness + "," + contrast + "," + brightness + + "," + vectorAccel); + Log.v(TAG, "" + imageFileName + "," + sharpness + "," + overExposure + "," + + underExposure + "," + colorfulness + "," + contrast + "," + brightness + + "," + vectorAccel); + csvWriter.newLine(); + csvWriter.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + throw new RuntimeException(e); + } + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/Camera2Source.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/Camera2Source.java new file mode 100644 index 0000000..c553b6d --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/Camera2Source.java @@ -0,0 +1,255 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.ImageFormat; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraProperties; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.os.Handler; +import android.renderscript.Allocation; +import android.renderscript.Element; +import android.renderscript.RenderScript; +import android.renderscript.ScriptIntrinsicYuvToRGB; +import android.renderscript.Type; +import android.util.Log; +import android.view.Surface; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.util.ArrayList; +import java.util.List; + +public class Camera2Source extends Filter implements Allocation.OnBufferAvailableListener { + + private boolean mNewFrameAvailable = false; + private FrameType mOutputType; + private static final String TAG = "Camera2Source"; + private CameraManager mCameraManager; + private CameraDevice mCamera; + private RenderScript mRS; + private Surface mSurface; + private CameraProperties mProperties; + private CameraTestThread mLooperThread; + private int mHeight = 480; + private int mWidth = 640; + private Allocation mAllocationIn; + private ScriptIntrinsicYuvToRGB rgbConverter; + private Allocation mAllocationOut; + private Bitmap mBitmap; + + class MyCameraListener extends CameraManager.AvailabilityListener { + + @Override + public void onCameraAvailable(String cameraId) { + // TODO Auto-generated method stub + Log.v(TAG, "camera available to open"); + } + + @Override + public void onCameraUnavailable(String cameraId) { + // TODO Auto-generated method stub + Log.v(TAG, "camera unavailable to open"); + } + + } + + class MyCaptureListener extends CameraDevice.CaptureListener { + + @Override + public void onCaptureCompleted(CameraDevice camera, CaptureRequest request, + CaptureResult result) { + // TODO Auto-generated method stub + Log.v(TAG, "in onCaptureComplete"); + + } + + @Override + public void onCaptureFailed(CameraDevice camera, CaptureRequest request) { + // TODO Auto-generated method stub + Log.v(TAG, "onCaptureFailed is being called"); + } + + } + + public Camera2Source(MffContext context, String name) { + super(context, name); + mOutputType = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + + Context ctx = context.getApplicationContext(); + mCameraManager = (CameraManager) ctx.getSystemService(Context.CAMERA_SERVICE); + + mRS = RenderScript.create(context.getApplicationContext()); + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("timestamp", Signature.PORT_OPTIONAL, FrameType.single(long.class)) + .addOutputPort("video", Signature.PORT_REQUIRED, mOutputType) + .addOutputPort("orientation", Signature.PORT_REQUIRED, + FrameType.single(float.class)) + .disallowOtherPorts(); + } + + @Override + protected void onClose() { + Log.v(TAG, "onClose being called"); + try { + mCamera.close(); + mSurface.release(); + mLooperThread.close(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + protected void onOpen() { + try { + String backCameraId = "0"; + mCamera = mCameraManager.openCamera(backCameraId); + } catch (CameraAccessException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + Element ele = Element.createPixel(mRS, Element.DataType.UNSIGNED_8, + Element.DataKind.PIXEL_YUV); + + rgbConverter = ScriptIntrinsicYuvToRGB.create(mRS,ele); + Type.Builder yuvBuilder = new Type.Builder(mRS,ele); + + yuvBuilder.setYuvFormat(ImageFormat.YUV_420_888); + yuvBuilder.setX(mWidth); + yuvBuilder.setY(mHeight); + mAllocationIn = Allocation.createTyped(mRS, yuvBuilder.create(), + Allocation.USAGE_SCRIPT | Allocation.USAGE_IO_INPUT); + mSurface = mAllocationIn.getSurface(); + mAllocationIn.setOnBufferAvailableListener(this); + rgbConverter.setInput(mAllocationIn); + + mBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); + mAllocationOut = Allocation.createFromBitmap(mRS, mBitmap); + + + Log.v(TAG, "mcamera: " + mCamera); + + List<Surface> surfaces = new ArrayList<Surface>(); + surfaces.add(mSurface); + CaptureRequest.Builder mCaptureRequest = null; + try { + mCamera.configureOutputs(surfaces); + mCaptureRequest = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + mCaptureRequest.addTarget(mSurface); + } catch (CameraAccessException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + mLooperThread = new CameraTestThread(); + Handler mHandler; + try { + mHandler = mLooperThread.start(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + throw new RuntimeException(e); + } + try { + mCamera.setRepeatingRequest(mCaptureRequest.build(), new MyCaptureListener(), + mHandler); + } catch (CameraAccessException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + mProperties = null; + try { + mProperties = mCamera.getProperties(); + } catch (CameraAccessException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + + } + + @Override + protected void onProcess() { + Log.v(TAG, "on Process"); + if (nextFrame()) { + OutputPort outPort = getConnectedOutputPort("video"); + + // Create a 2D frame that will hold the output + int[] dims = new int[] { + mWidth, mHeight + }; + FrameImage2D outputFrame = Frame.create(mOutputType, dims).asFrameImage2D(); + rgbConverter.forEach(mAllocationOut); + mAllocationOut.copyTo(mBitmap); + outputFrame.setBitmap(mBitmap); + outPort.pushFrame(outputFrame); + outputFrame.release(); + + OutputPort orientationPort = getConnectedOutputPort("orientation"); + FrameValue orientationFrame = orientationPort.fetchAvailableFrame(null).asFrameValue(); + + // FIXME: Hardcoded value because ORIENTATION returns null, Qualcomm + // bug + Integer orientation = mProperties.get(CameraProperties.SENSOR_ORIENTATION); + float temp; + if (orientation != null) { + temp = orientation.floatValue(); + } else { + temp = 90.0f; + } + orientationFrame.setValue(temp); + orientationPort.pushFrame(orientationFrame); + } + } + + private synchronized boolean nextFrame() { + boolean frameAvailable = mNewFrameAvailable; + if (frameAvailable) { + mNewFrameAvailable = false; + } else { + enterSleepState(); + } + return frameAvailable; + } + + public void onBufferAvailable(Allocation a) { + Log.v(TAG, "on Buffer Available"); + a.ioReceive(); + synchronized (this) { + mNewFrameAvailable = true; + } + wakeUp(); + } + + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/CameraTestThread.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/CameraTestThread.java new file mode 100644 index 0000000..8a0fced --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/CameraTestThread.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import java.util.concurrent.TimeoutException; + +/** + * Camera test thread wrapper for handling camera callbacks + */ +public class CameraTestThread implements AutoCloseable { + private static final String TAG = "CameraTestThread"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + // Timeout for initializing looper and opening camera in Milliseconds. + private static final long WAIT_FOR_COMMAND_TO_COMPLETE = 5000; + private Looper mLooper = null; + private Handler mHandler = null; + + /** + * Create and start a looper thread, return the Handler + */ + public synchronized Handler start() throws Exception { + final ConditionVariable startDone = new ConditionVariable(); + if (mLooper != null || mHandler !=null) { + Log.w(TAG, "Looper thread already started"); + return mHandler; + } + + new Thread() { + @Override + public void run() { + if (VERBOSE) Log.v(TAG, "start loopRun"); + Looper.prepare(); + // Save the looper so that we can terminate this thread + // after we are done with it. + mLooper = Looper.myLooper(); + mHandler = new Handler(); + startDone.open(); + Looper.loop(); + if (VERBOSE) Log.v(TAG, "createLooperThread: finished"); + } + }.start(); + + if (VERBOSE) Log.v(TAG, "start waiting for looper"); + if (!startDone.block(WAIT_FOR_COMMAND_TO_COMPLETE)) { + throw new TimeoutException("createLooperThread: start timeout"); + } + return mHandler; + } + + /** + * Terminate the looper thread + */ + public synchronized void close() throws Exception { + if (mLooper == null || mHandler == null) { + Log.w(TAG, "Looper thread doesn't start yet"); + return; + } + + if (VERBOSE) Log.v(TAG, "Terminate looper thread"); + mLooper.quit(); + mLooper.getThread().join(); + mLooper = null; + mHandler = null; + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ContrastRatioFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ContrastRatioFilter.java new file mode 100644 index 0000000..d918437 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ContrastRatioFilter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; + +public class ContrastRatioFilter extends Filter { + + private static final String TAG = "ContrastRatioFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + public ContrastRatioFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU); + FrameType floatT = FrameType.single(float.class); + return new Signature().addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addOutputPort("contrastRatingToGoodness", Signature.PORT_REQUIRED, floatT) + .disallowOtherPorts(); + + } + + @Override + protected void onProcess() { + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + + float contrastRatio; + ByteBuffer inputBuffer = inputImage.lockBytes(Frame.MODE_READ); + + contrastRatio = contrastOperator(inputImage.getWidth(), inputImage.getHeight(), + inputBuffer); + + inputImage.unlock(); + + if (mLogVerbose) Log.v(TAG, "contrastRatio: " + contrastRatio); + + OutputPort contrastToGoodnessPort = getConnectedOutputPort("contrastRatingToGoodness"); + FrameValue contrastOutFrame2 = + contrastToGoodnessPort.fetchAvailableFrame(null).asFrameValue(); + contrastOutFrame2.setValue(contrastRatio); + contrastToGoodnessPort.pushFrame(contrastOutFrame2); + + + } + + private static native float contrastOperator(int width, int height, ByteBuffer imageBuffer); + + static { + System.loadLibrary("smartcamera_jni"); + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ExposureFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ExposureFilter.java new file mode 100644 index 0000000..6128718 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ExposureFilter.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; + +public class ExposureFilter extends Filter { + + private FrameType mImageType; + private static final String TAG = "ExposureFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + private static int OVER_EXPOSURE_TOLERANCE = 5; + + public ExposureFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType floatT = FrameType.single(float.class); + return new Signature().addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addOutputPort("overExposedNum", Signature.PORT_OPTIONAL, floatT) + .addOutputPort("overExposureRating", Signature.PORT_REQUIRED, floatT) + .addOutputPort("underExposedNum", Signature.PORT_OPTIONAL, floatT) + .addOutputPort("underExposureRating", Signature.PORT_REQUIRED, floatT) + .disallowOtherPorts(); + + } + + @Override + protected void onProcess() { + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + + float overExposedPixels, underExposedPixels; + ByteBuffer inputBuffer = inputImage.lockBytes(Frame.MODE_READ); + + overExposedPixels = overExposureOperator(inputImage.getWidth(), + inputImage.getHeight(), + inputBuffer); + underExposedPixels = underExposureOperator(inputImage.getWidth(), + inputImage.getHeight(), + inputBuffer); + inputImage.unlock(); + + + if (mLogVerbose) Log.v(TAG, "underExposedPixelCount: " + underExposedPixels); + + OutputPort underPort = getConnectedOutputPort("underExposedNum"); + if (underPort != null) { + FrameValue underOutFrame = underPort.fetchAvailableFrame(null).asFrameValue(); + underOutFrame.setValue(underExposedPixels*inputImage.getWidth()*inputImage.getHeight()); + underPort.pushFrame(underOutFrame); + } + + + OutputPort underPort2 = getConnectedOutputPort("underExposureRating"); + FrameValue underOutFrame2 = underPort2.fetchAvailableFrame(null).asFrameValue(); + underOutFrame2.setValue(underExposedPixels); + underPort2.pushFrame(underOutFrame2); + + if (mLogVerbose) Log.v(TAG, "overExposedPixelCount: " + overExposedPixels); + + OutputPort overPort = getConnectedOutputPort("overExposedNum"); + if (overPort != null) { + FrameValue overOutFrame = overPort.fetchAvailableFrame(null).asFrameValue(); + overOutFrame.setValue(overExposedPixels*inputImage.getWidth()*inputImage.getHeight()); + overPort.pushFrame(overOutFrame); + } + + + OutputPort overPort2 = getConnectedOutputPort("overExposureRating"); + FrameValue overOutFrame2 = overPort2.fetchAvailableFrame(null).asFrameValue(); + overOutFrame2.setValue(overExposedPixels); + overPort2.pushFrame(overOutFrame2); + + } + + private static native float overExposureOperator(int width, int height, + ByteBuffer imageBuffer); + private static native float underExposureOperator(int width, int height, + ByteBuffer imageBuffer); + + static { + System.loadLibrary("smartcamera_jni"); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FaceSquareFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FaceSquareFilter.java new file mode 100644 index 0000000..c4a39e8 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FaceSquareFilter.java @@ -0,0 +1,159 @@ +/* + * Copyright 2013 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. + */ +// Takes in an array, returns the size of the array + +package androidx.media.filterfw.samples.simplecamera; + +import android.graphics.Rect; +import android.hardware.Camera; +import android.hardware.Camera.Face; +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.Frame; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValues; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.nio.ByteBuffer; + +public class FaceSquareFilter extends Filter { + + private static final String TAG = "FaceSquareFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + private static int FACE_X_RANGE = 2000; + private static int WIDTH_OFFSET = 1000; + private static int HEIGHT_OFFSET = 1000; + + public FaceSquareFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageType = FrameType.buffer2D(FrameType.ELEMENT_RGBA8888); + FrameType facesType = FrameType.array(Camera.Face.class); + return new Signature() + .addInputPort("image", Signature.PORT_REQUIRED, imageType) + .addInputPort("faces", Signature.PORT_REQUIRED, facesType) + .addOutputPort("image", Signature.PORT_REQUIRED, imageType) + .disallowOtherPorts(); + } + + /** + * @see androidx.media.filterfw.Filter#onProcess() + */ + @Override + protected void onProcess() { + // Get inputs + FrameImage2D imageFrame = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + FrameValues facesFrame = getConnectedInputPort("faces").pullFrame().asFrameValues(); + Face[] faces = (Face[]) facesFrame.getValues(); + int[] dims = imageFrame.getDimensions(); + ByteBuffer buffer = imageFrame.lockBytes(Frame.MODE_WRITE); + byte[] pixels = buffer.array(); + + // For every face in faces, draw a white rect around the + // face following the rect member of the Face + drawBoxes(pixels, faces, dims); + + imageFrame.unlock(); + + OutputPort outPort = getConnectedOutputPort("image"); + outPort.pushFrame(imageFrame); + } + + public void drawBoxes(byte[] pixels, Face[] faces, int[] dims) { + for(int i = 0; i < faces.length; i++) { + Rect tempRect = faces[i].rect; + int top = (tempRect.top+HEIGHT_OFFSET)*dims[1]/FACE_X_RANGE; + int bottom = (tempRect.bottom+HEIGHT_OFFSET)*dims[1]/FACE_X_RANGE; + int left = (tempRect.left+WIDTH_OFFSET)*dims[0]/FACE_X_RANGE; + int right = (tempRect.right+WIDTH_OFFSET)*dims[0]/FACE_X_RANGE; + + if (top < 0) { + top = 0; + } else if (top > dims[1]) { + top = dims[1]; + } + if (left < 0) { + left = 0; + } else if (left > dims[0]) { + left = dims[0]; + } + if (bottom > dims[1]) { + bottom = dims[1]; + } else if (bottom < 0) { + bottom = 0; + } + if (right > dims[0]) { + right = dims[0]; + } else if (right < 0) { + right = 0; + } + + for (int j = 0; j < (bottom - top); j++) { + // Left edge + if (left > 0 && top > 0) { + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * (top + j) + left) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * (top + j) + left) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * (top + j) + left) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + } + + // Right edge + if (right > 0 && top > 0) { + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * (top + j) + right) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * (top + j) + right) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * (top + j) + right) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + } + + } + for (int k = 0; k < (right - left); k++) { + // Top edge + if (top < dims[1]) { + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * top + left + k) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * top + left + k) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * top + left + k) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + + } + // Bottom edge + if (bottom < dims[1]) { + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * bottom + left + k) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * bottom + left + k) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (dims[0] * bottom + left + k) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + } + + + } + + } + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToSizeFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToSizeFilter.java new file mode 100644 index 0000000..72452b3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToSizeFilter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013 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. + */ +// Takes in an array, returns the size of the array + +package androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.lang.reflect.Array; + +public class FloatArrayToSizeFilter extends Filter { + + private static final String TAG = "FloatArrayToSizeFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + /** + * @param context + * @param name + */ + public FloatArrayToSizeFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType intT = FrameType.single(int.class); + FrameType floatType = FrameType.array(float.class); + + return new Signature() + .addInputPort("array", Signature.PORT_REQUIRED, floatType) + .addOutputPort("size", Signature.PORT_REQUIRED, intT) + .disallowOtherPorts(); + } + + /** + * @see androidx.media.filterfw.Filter#onProcess() + */ + @Override + protected void onProcess() { + FrameValue arrayFrame = getConnectedInputPort("array").pullFrame().asFrameValues(); + Object array = arrayFrame.getValue(); + int size = Array.getLength(array); + + OutputPort outPort = getConnectedOutputPort("size"); + FrameValue sizeFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + sizeFrame.setValue(size); + outPort.pushFrame(sizeFrame); + + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToStrFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToStrFilter.java new file mode 100644 index 0000000..4606cfb --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToStrFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013 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. + */ +// Takes in an array, returns the size of the array + +package androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +import java.lang.reflect.Array; +import java.util.Arrays; + +public class FloatArrayToStrFilter extends Filter { + + private static final String TAG = "FloatArrayToStrFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + /** + * @param context + * @param name + */ + public FloatArrayToStrFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType floatType = FrameType.array(float.class); + + return new Signature() + .addInputPort("array", Signature.PORT_REQUIRED, floatType) + .addOutputPort("string", Signature.PORT_REQUIRED, FrameType.single(String.class)) + .disallowOtherPorts(); + } + + /** + * @see androidx.media.filterfw.Filter#onProcess() + */ + @Override + protected void onProcess() { + FrameValue arrayFrame = getConnectedInputPort("array").pullFrame().asFrameValues(); + float[] array = (float[]) arrayFrame.getValue(); + String outstr = Arrays.toString(array); + + OutputPort outPort = getConnectedOutputPort("string"); + FrameValue stringFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + stringFrame.setValue(outstr); + outPort.pushFrame(stringFrame); + + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/IfElseFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/IfElseFilter.java new file mode 100644 index 0000000..9553b75 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/IfElseFilter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.util.Log; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameBuffer2D; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + + +public class IfElseFilter extends Filter { + + private static final String TAG = "IfElseFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + public IfElseFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType videoIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + FrameType imageOut = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.WRITE_GPU); + + return new Signature().addInputPort("falseResult", Signature.PORT_REQUIRED, imageIn) + .addInputPort("trueResult", Signature.PORT_REQUIRED, videoIn) + .addInputPort("condition", Signature.PORT_REQUIRED, FrameType.single(boolean.class)) + .addOutputPort("output", Signature.PORT_REQUIRED, imageOut) + .disallowOtherPorts(); + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("output"); + FrameImage2D trueFrame = getConnectedInputPort("trueResult").pullFrame().asFrameImage2D(); + FrameImage2D falseFrame = getConnectedInputPort("falseResult").pullFrame().asFrameImage2D(); + FrameValue boolFrameValue = getConnectedInputPort("condition").pullFrame().asFrameValue(); + boolean condition = (Boolean) boolFrameValue.getValue(); + FrameBuffer2D outputFrame; + // If the condition is true, then we want to use the camera, else use the gallery + if (condition) { + outputFrame = trueFrame; + } else { + outputFrame = falseFrame; + } + outPort.pushFrame(outputFrame); + + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ImageConstants.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ImageConstants.java new file mode 100644 index 0000000..cfae8fc --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ImageConstants.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +public class ImageConstants { + + public static final int MAX_BYTE = 255; + public static final int RED_OFFSET = 0; + public static final int GREEN_OFFSET = 1; + public static final int BLUE_OFFSET = 2; + public static final int PIX_CHANNELS = 4; +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ImageGoodnessFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ImageGoodnessFilter.java new file mode 100644 index 0000000..14ec762 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/ImageGoodnessFilter.java @@ -0,0 +1,410 @@ +/* + * Copyright 2013 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. + */ +// Takes sharpness score, rates the image good if above 10, bad otherwise + +package androidx.media.filterfw.samples.simplecamera; + +import android.graphics.Bitmap; +import android.os.AsyncTask; +import android.util.Log; +import android.widget.ImageView; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public class ImageGoodnessFilter extends Filter { + + private static final String TAG = "ImageGoodnessFilter"; + private static boolean mLogVerbose = Log.isLoggable(TAG, Log.VERBOSE); + + private final static String GREAT = "Great Picture!"; + private final static String GOOD = "Good Picture!"; + private final static String OK = "Ok Picture"; + private final static String BAD = "Bad Picture"; + private final static String AWFUL = "Awful Picture"; + private final static float SMALL_SCORE_INC = 0.25f; + private final static float BIG_SCORE_INC = 0.5f; + private final static float LOW_VARIANCE = 0.1f; + private final static float MEDIUM_VARIANCE = 10; + private final static float HIGH_VARIANCE = 100; + private float sharpnessMean = 0; + private float sharpnessVar = 0; + private float underExposureMean = 0; + private float underExposureVar = 0; + private float overExposureMean = 0; + private float overExposureVar = 0; + private float contrastMean = 0; + private float contrastVar = 0; + private float colorfulnessMean = 0; + private float colorfulnessVar = 0; + private float brightnessMean = 0; + private float brightnessVar = 0; + + private float motionMean = 0; + private float scoreMean = 0; + private static final float DECAY = 0.03f; + /** + * @param context + * @param name + */ + public ImageGoodnessFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + FrameType floatT = FrameType.single(float.class); + FrameType imageIn = FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_GPU); + + return new Signature() + .addInputPort("sharpness", Signature.PORT_REQUIRED, floatT) + .addInputPort("overExposure", Signature.PORT_REQUIRED, floatT) + .addInputPort("underExposure", Signature.PORT_REQUIRED, floatT) + .addInputPort("colorfulness", Signature.PORT_REQUIRED, floatT) + .addInputPort("contrastRating", Signature.PORT_REQUIRED, floatT) + .addInputPort("motionValues", Signature.PORT_REQUIRED, FrameType.array(float.class)) + .addInputPort("brightness", Signature.PORT_REQUIRED, floatT) + .addInputPort("capturing", Signature.PORT_REQUIRED, FrameType.single(boolean.class)) + .addInputPort("image", Signature.PORT_REQUIRED, imageIn) + .addOutputPort("goodOrBadPic", Signature.PORT_REQUIRED, + FrameType.single(String.class)) + .addOutputPort("score", Signature.PORT_OPTIONAL, floatT) + .disallowOtherPorts(); + } + + /** + * @see androidx.media.filterfw.Filter#onProcess() + */ + @Override + protected void onProcess() { + FrameValue sharpnessFrameValue = + getConnectedInputPort("sharpness").pullFrame().asFrameValue(); + float sharpness = ((Float)sharpnessFrameValue.getValue()).floatValue(); + + FrameValue overExposureFrameValue = + getConnectedInputPort("overExposure").pullFrame().asFrameValue(); + float overExposure = ((Float)overExposureFrameValue.getValue()).floatValue(); + + FrameValue underExposureFrameValue = + getConnectedInputPort("underExposure").pullFrame().asFrameValue(); + float underExposure = ((Float)underExposureFrameValue.getValue()).floatValue(); + + FrameValue colorfulnessFrameValue = + getConnectedInputPort("colorfulness").pullFrame().asFrameValue(); + float colorfulness = ((Float)colorfulnessFrameValue.getValue()).floatValue(); + + FrameValue contrastRatingFrameValue = + getConnectedInputPort("contrastRating").pullFrame().asFrameValue(); + float contrastRating = ((Float)contrastRatingFrameValue.getValue()).floatValue(); + + FrameValue brightnessFrameValue = + getConnectedInputPort("brightness").pullFrame().asFrameValue(); + float brightness = ((Float)brightnessFrameValue.getValue()).floatValue(); + + FrameValue motionValuesFrameValue = + getConnectedInputPort("motionValues").pullFrame().asFrameValue(); + float[] motionValues = (float[]) motionValuesFrameValue.getValue(); + + + float vectorAccel = (float) Math.sqrt(Math.pow(motionValues[0], 2) + + Math.pow(motionValues[1], 2) + Math.pow(motionValues[2], 2)); + String outStr; + + FrameValue capturingFrameValue = + getConnectedInputPort("capturing").pullFrame().asFrameValue(); + boolean capturing = (Boolean) capturingFrameValue.getValue(); + + FrameImage2D inputImage = getConnectedInputPort("image").pullFrame().asFrameImage2D(); + + + // TODO: get rid of magic numbers + float score = 0.0f; + score = computePictureScore(vectorAccel, sharpness, underExposure, overExposure, + contrastRating, colorfulness, brightness); + if (scoreMean == 0) scoreMean = score; + else scoreMean = scoreMean * (1 - DECAY) + score * DECAY; + + if (motionMean == 0) motionMean = vectorAccel; + else motionMean = motionMean * (1 - DECAY) + vectorAccel * DECAY; + + float classifierScore = classifierComputeScore(vectorAccel, sharpness, underExposure, + colorfulness, contrastRating, score); + +// Log.v(TAG, "ClassifierScore:: " + classifierScore); + final float GREAT_SCORE = 3.5f; + final float GOOD_SCORE = 2.5f; + final float OK_SCORE = 1.5f; + final float BAD_SCORE = 0.5f; + + if (score >= GREAT_SCORE) { + outStr = GREAT; + } else if (score >= GOOD_SCORE) { + outStr = GOOD; + } else if (score >= OK_SCORE) { + outStr = OK; + } else if (score >= BAD_SCORE) { + outStr = BAD; + } else { + outStr = AWFUL; + } + + if(capturing) { + if (outStr.equals(GREAT)) { + // take a picture + Bitmap bitmap = inputImage.toBitmap(); + + new AsyncOperation().execute(bitmap); + final float RESET_FEATURES = 0.01f; + sharpnessMean = RESET_FEATURES; + underExposureMean = RESET_FEATURES; + overExposureMean = RESET_FEATURES; + contrastMean = RESET_FEATURES; + colorfulnessMean = RESET_FEATURES; + brightnessMean = RESET_FEATURES; + } + } + + OutputPort outPort = getConnectedOutputPort("goodOrBadPic"); + FrameValue stringFrame = outPort.fetchAvailableFrame(null).asFrameValue(); + stringFrame.setValue(outStr); + outPort.pushFrame(stringFrame); + + OutputPort scoreOutPort = getConnectedOutputPort("score"); + FrameValue scoreFrame = scoreOutPort.fetchAvailableFrame(null).asFrameValue(); + scoreFrame.setValue(score); + scoreOutPort.pushFrame(scoreFrame); + + } + + private class AsyncOperation extends AsyncTask<Bitmap, Void, String> { + private Bitmap b; + protected void onPostExecute(String result) { + ImageView view = SmartCamera.getImageView(); + view.setImageBitmap(b); + } + + @Override + protected String doInBackground(Bitmap... params) { + // TODO Auto-generated method stub + b = params[0]; + return null; + } + + } + // Returns a number between -1 and 1 + private float classifierComputeScore(float vectorAccel, float sharpness, float underExposure, + float colorfulness, float contrast, float score) { + float result = (-0.0223f * sharpness + -0.0563f * underExposure + 0.0137f * colorfulness + + 0.3102f * contrast + 0.0314f * vectorAccel + -0.0094f * score + 0.0227f * + sharpnessMean + 0.0459f * underExposureMean + -0.3934f * contrastMean + + -0.0697f * motionMean + 0.0091f * scoreMean + -0.0152f); + return result; + } + + // Returns a number between -1 and 4 representing the score for this picture + private float computePictureScore(float vector_accel, float sharpness, + float underExposure, float overExposure, float contrastRating, float colorfulness, + float brightness) { + final float ACCELERATION_THRESHOLD_VERY_STEADY = 0.1f; + final float ACCELERATION_THRESHOLD_STEADY = 0.3f; + final float ACCELERATION_THRESHOLD_MOTION = 2f; + + float score = 0.0f; + if (vector_accel > ACCELERATION_THRESHOLD_MOTION) { + score -= (BIG_SCORE_INC + BIG_SCORE_INC); // set score to -1, bad pic + } else if (vector_accel > ACCELERATION_THRESHOLD_STEADY) { + score -= BIG_SCORE_INC; + score = subComputeScore(sharpness, underExposure, overExposure, contrastRating, + colorfulness, brightness, score); + } else if (vector_accel < ACCELERATION_THRESHOLD_VERY_STEADY) { + score += BIG_SCORE_INC; + score = subComputeScore(sharpness, underExposure, overExposure, contrastRating, + colorfulness, brightness, score); + } else { + score = subComputeScore(sharpness, underExposure, overExposure, contrastRating, + colorfulness, brightness, score); + } + return score; + } + + // Changes the score by at most +/- 3.5 + private float subComputeScore(float sharpness, float underExposure, float overExposure, + float contrastRating, float colorfulness, float brightness, float score) { + // The score methods return values -0.5 to 0.5 + final float SHARPNESS_WEIGHT = 2; + score += SHARPNESS_WEIGHT * sharpnessScore(sharpness); + score += underExposureScore(underExposure); + score += overExposureScore(overExposure); + score += contrastScore(contrastRating); + score += colorfulnessScore(colorfulness); + score += brightnessScore(brightness); + return score; + } + + private float sharpnessScore(float sharpness) { + if (sharpnessMean == 0) { + sharpnessMean = sharpness; + sharpnessVar = 0; + return 0; + } else { + sharpnessMean = sharpnessMean * (1 - DECAY) + sharpness * DECAY; + sharpnessVar = sharpnessVar * (1 - DECAY) + (sharpness - sharpnessMean) * + (sharpness - sharpnessMean) * DECAY; + if (sharpnessVar < LOW_VARIANCE) { + return BIG_SCORE_INC; + } else if (sharpness < sharpnessMean && sharpnessVar > MEDIUM_VARIANCE) { + return -BIG_SCORE_INC; + } else if (sharpness < sharpnessMean) { + return -SMALL_SCORE_INC; + } else if (sharpness > sharpnessMean && sharpnessVar > HIGH_VARIANCE) { + return 0; + } else if (sharpness > sharpnessMean && sharpnessVar > MEDIUM_VARIANCE) { + return SMALL_SCORE_INC; + } else { + return BIG_SCORE_INC; // low variance, sharpness above the mean + } + } + } + + private float underExposureScore(float underExposure) { + if (underExposureMean == 0) { + underExposureMean = underExposure; + underExposureVar = 0; + return 0; + } else { + underExposureMean = underExposureMean * (1 - DECAY) + underExposure * DECAY; + underExposureVar = underExposureVar * (1 - DECAY) + (underExposure - underExposureMean) + * (underExposure - underExposureMean) * DECAY; + if (underExposureVar < LOW_VARIANCE) { + return BIG_SCORE_INC; + } else if (underExposure > underExposureMean && underExposureVar > MEDIUM_VARIANCE) { + return -BIG_SCORE_INC; + } else if (underExposure > underExposureMean) { + return -SMALL_SCORE_INC; + } else if (underExposure < underExposureMean && underExposureVar > HIGH_VARIANCE) { + return 0; + } else if (underExposure < underExposureMean && underExposureVar > MEDIUM_VARIANCE) { + return SMALL_SCORE_INC; + } else { + return BIG_SCORE_INC; // low variance, underExposure below the mean + } + } + } + + private float overExposureScore(float overExposure) { + if (overExposureMean == 0) { + overExposureMean = overExposure; + overExposureVar = 0; + return 0; + } else { + overExposureMean = overExposureMean * (1 - DECAY) + overExposure * DECAY; + overExposureVar = overExposureVar * (1 - DECAY) + (overExposure - overExposureMean) * + (overExposure - overExposureMean) * DECAY; + if (overExposureVar < LOW_VARIANCE) { + return BIG_SCORE_INC; + } else if (overExposure > overExposureMean && overExposureVar > MEDIUM_VARIANCE) { + return -BIG_SCORE_INC; + } else if (overExposure > overExposureMean) { + return -SMALL_SCORE_INC; + } else if (overExposure < overExposureMean && overExposureVar > HIGH_VARIANCE) { + return 0; + } else if (overExposure < overExposureMean && overExposureVar > MEDIUM_VARIANCE) { + return SMALL_SCORE_INC; + } else { + return BIG_SCORE_INC; // low variance, overExposure below the mean + } + } + } + + private float contrastScore(float contrast) { + if (contrastMean == 0) { + contrastMean = contrast; + contrastVar = 0; + return 0; + } else { + contrastMean = contrastMean * (1 - DECAY) + contrast * DECAY; + contrastVar = contrastVar * (1 - DECAY) + (contrast - contrastMean) * + (contrast - contrastMean) * DECAY; + if (contrastVar < LOW_VARIANCE) { + return BIG_SCORE_INC; + } else if (contrast < contrastMean && contrastVar > MEDIUM_VARIANCE) { + return -BIG_SCORE_INC; + } else if (contrast < contrastMean) { + return -SMALL_SCORE_INC; + } else if (contrast > contrastMean && contrastVar > 100) { + return 0; + } else if (contrast > contrastMean && contrastVar > MEDIUM_VARIANCE) { + return SMALL_SCORE_INC; + } else { + return BIG_SCORE_INC; // low variance, contrast above the mean + } + } + } + + private float colorfulnessScore(float colorfulness) { + if (colorfulnessMean == 0) { + colorfulnessMean = colorfulness; + colorfulnessVar = 0; + return 0; + } else { + colorfulnessMean = colorfulnessMean * (1 - DECAY) + colorfulness * DECAY; + colorfulnessVar = colorfulnessVar * (1 - DECAY) + (colorfulness - colorfulnessMean) * + (colorfulness - colorfulnessMean) * DECAY; + if (colorfulnessVar < LOW_VARIANCE) { + return BIG_SCORE_INC; + } else if (colorfulness < colorfulnessMean && colorfulnessVar > MEDIUM_VARIANCE) { + return -BIG_SCORE_INC; + } else if (colorfulness < colorfulnessMean) { + return -SMALL_SCORE_INC; + } else if (colorfulness > colorfulnessMean && colorfulnessVar > 100) { + return 0; + } else if (colorfulness > colorfulnessMean && colorfulnessVar > MEDIUM_VARIANCE) { + return SMALL_SCORE_INC; + } else { + return BIG_SCORE_INC; // low variance, colorfulness above the mean + } + } + } + + private float brightnessScore(float brightness) { + if (brightnessMean == 0) { + brightnessMean = brightness; + brightnessVar = 0; + return 0; + } else { + brightnessMean = brightnessMean * (1 - DECAY) + brightness * DECAY; + brightnessVar = brightnessVar * (1 - DECAY) + (brightness - brightnessMean) * + (brightness - brightnessMean) * DECAY; + if (brightnessVar < LOW_VARIANCE) { + return BIG_SCORE_INC; + } else if (brightness < brightnessMean && brightnessVar > MEDIUM_VARIANCE) { + return -BIG_SCORE_INC; + } else if (brightness < brightnessMean) { + return -SMALL_SCORE_INC; + } else if (brightness > brightnessMean && brightnessVar > 100) { + return 0; + } else if (brightness > brightnessMean && brightnessVar > MEDIUM_VARIANCE) { + return SMALL_SCORE_INC; + } else { + return BIG_SCORE_INC; // low variance, brightness above the mean + } + } + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/MotionSensorWTime.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/MotionSensorWTime.java new file mode 100644 index 0000000..64f3ef3 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/MotionSensorWTime.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2013 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. + */ + +// Make values from a motion sensor (e.g., accelerometer) available as filter outputs. + +package androidx.media.filterfw.samples.simplecamera; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.FrameValues; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +public final class MotionSensorWTime extends Filter implements SensorEventListener { + + private SensorManager mSensorManager = null; + private Sensor mSensor = null; + + private float[] mValues = new float[3]; + private float[][] mTemp = new float[3][3]; + private float[] mAvgValues = new float[3]; + private int mCounter = 0; + + public MotionSensorWTime(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("values", Signature.PORT_REQUIRED, FrameType.array(float.class)) + .addOutputPort("timestamp", Signature.PORT_OPTIONAL, FrameType.single(long.class)) + .disallowOtherPorts(); + } + + @Override + protected void onPrepare() { + mSensorManager = (SensorManager)getContext().getApplicationContext() + .getSystemService(Context.SENSOR_SERVICE); + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION); + // TODO: currently, the type of sensor is hardcoded. Should be able to set the sensor + // type as filter input! + mSensorManager.registerListener(this, mSensor, SensorManager.SENSOR_DELAY_UI); + } + + @Override + protected void onTearDown() { + mSensorManager.unregisterListener(this); + } + + @Override + public final void onAccuracyChanged(Sensor sensor, int accuracy) { + // (Do we need to do something when sensor accuracy changes?) + } + + @Override + public final void onSensorChanged(SensorEvent event) { + synchronized(mValues) { + mValues[0] = event.values[0]; + mValues[1] = event.values[1]; + mValues[2] = event.values[2]; + } + } + + @Override + protected void onProcess() { + OutputPort outPort = getConnectedOutputPort("values"); + FrameValues outFrame = outPort.fetchAvailableFrame(null).asFrameValues(); + synchronized(mValues) { + if (mCounter < 3 && mCounter >= 0) { + mTemp[0][mCounter] = mValues[0]; + mTemp[1][mCounter] = mValues[1]; + mTemp[2][mCounter] = mValues[2]; + } + + mCounter = (mCounter + 1) % 3; + + mAvgValues[0] = (mTemp[0][0] + mTemp[0][1] + mTemp[0][2]) / 3; + mAvgValues[1] = (mTemp[1][0] + mTemp[1][1] + mTemp[1][2]) / 3; + mAvgValues[2] = (mTemp[2][0] + mTemp[2][1] + mTemp[2][2]) / 3; + outFrame.setValues(mAvgValues); + } + outFrame.setTimestamp(System.currentTimeMillis() * 1000000L); + outPort.pushFrame(outFrame); + + OutputPort timeOutPort = getConnectedOutputPort("timestamp"); + if (timeOutPort != null) { + long timestamp = System.nanoTime(); + Log.v("MotionSensor", "Timestamp is: " + timestamp); + FrameValue timeStampFrame = timeOutPort.fetchAvailableFrame(null).asFrameValue(); + timeStampFrame.setValue(timestamp); + timeOutPort.pushFrame(timeStampFrame); + } + } +} + diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/SmartCamera.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/SmartCamera.java new file mode 100644 index 0000000..ba0333a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/SmartCamera.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Handler; +import android.provider.MediaStore; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.SurfaceView; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.Spinner; +import android.widget.TextView; +import androidx.media.filterfw.FilterGraph; +import androidx.media.filterfw.GraphReader; +import androidx.media.filterfw.GraphRunner; +import androidx.media.filterfw.MffContext; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; + + +public class SmartCamera extends Activity { + + private SurfaceView mCameraView; + private TextView mGoodBadTextView; + private TextView mFPSTextView; + private TextView mEyesTextView; + private TextView mSmilesTextView; + private TextView mScoreTextView; + private static ImageView mImageView1; + private static ImageView mImageView2; + private static ImageView mImageView3; + private static ImageView mImageView4; + private static ImageView mImageView5; + private Button mStartStopButton; + private TextView mImagesSavedTextView; + private Spinner mSpinner; + private LinearLayout mLinearLayout; + + private MffContext mContext; + private FilterGraph mGraph; + private GraphRunner mRunner; + private Handler mHandler = new Handler(); + + private static final String TAG = "SmartCamera"; + private static final boolean sUseFacialExpression = false; + private boolean isPendingRunGraph = false; + + private static ArrayList<ImageView> mImages; + private static int count = -1; + private static boolean countHasReachedMax = false; + private static int numImages = 0; + + // Function to return the correct image view to display the current bitmap + public static ImageView getImageView() { + if (count == numImages-1) countHasReachedMax = true; + count = (count+1) % numImages; + return mImages.get(count); + } + + // Function used to run images through the graph, mainly for CSV data generation + public void runGraphOnImage(String filePath, String fileName) { + if(fileName.endsWith(".jpg") == false) { + return; + } + mGraph.getVariable("gallerySource").setValue(filePath + "/" + fileName); + Log.v(TAG, "runGraphOnImage : : " + filePath + " name: " + fileName); + mGraph.getVariable("imageName").setValue(fileName); + mGraph.getVariable("filePath").setValue(filePath); // wrong + try { + Thread.sleep(400); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + // Function to clear the "Images Saved" text off the screen + private void clearImagesSavedTextView() { + mImagesSavedTextView.setText(""); + } + + // Function to capture the images in the current imageviews and save them to the gallery + private void captureImages() { + ((WaveTriggerFilter) mGraph.getFilter("snapEffect")).trigger(); + mGraph.getVariable("startCapture").setValue(false); + Bitmap bitmap = null; + Drawable res = getResources().getDrawable(R.drawable.black_screen); + Calendar cal = Calendar.getInstance(); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH:mm:ss"); + + Log.v(TAG, "numImages: " + numImages + " count: " + count + + " hasReachedMax: " + countHasReachedMax); + int maxI = countHasReachedMax ? numImages : count+1; + if(maxI != 0) { + if (maxI == 1) mImagesSavedTextView.setText("Image Saved"); + else { + mImagesSavedTextView.setText("" + maxI + " Images Saved"); + } + } + for (int i = 0; i < maxI; i++) { + bitmap = ((BitmapDrawable)mImages.get(i).getDrawable()).getBitmap(); + mImages.get(i).setImageDrawable(res); + MediaStore.Images.Media.insertImage(getContentResolver(), bitmap, + sdf.format(cal.getTime()) + "_image" + i + ".jpg", "image " + i); + } + mStartStopButton.setText("Start"); + count = -1; + countHasReachedMax = false; + mSpinner.setEnabled(true); + mHandler.postDelayed(new Runnable() { + public void run() { + clearImagesSavedTextView(); + } + }, 5000); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.simplecamera); + setTitle("Smart Camera"); + + mContext = new MffContext(this); + + mCameraView = (SurfaceView) findViewById(R.id.cameraView); + mGoodBadTextView = (TextView) findViewById(R.id.goodOrBadTextView); + mFPSTextView = (TextView) findViewById(R.id.fpsTextView); + mScoreTextView = (TextView) findViewById(R.id.scoreTextView); + mStartStopButton = (Button) findViewById(R.id.startButton); + mImagesSavedTextView = (TextView) findViewById(R.id.imagesSavedTextView); + mImagesSavedTextView.setText(""); + mSpinner = (Spinner) findViewById(R.id.spinner); + mLinearLayout = (LinearLayout) findViewById(R.id.scrollViewLinearLayout); + mImages = new ArrayList<ImageView>(); + + // Spinner is used to determine how many image views are displayed at the bottom + // of the screen. Based on the item position that is selected, we inflate that + // many imageviews into the bottom linear layout. + mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView<?> parentView, View selectedItemView, + int position, long id) { + mLinearLayout.removeViews(0,numImages); + numImages = position+1; + mImages.clear(); + LayoutInflater inflater = getLayoutInflater(); + for (int i = 0; i < numImages; i++) { + ImageView tmp = (ImageView) inflater.inflate(R.layout.imageview, null); + mImages.add(tmp); + mLinearLayout.addView(tmp); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parentView) { + } + }); + + numImages = mSpinner.getSelectedItemPosition()+1; + mImages.clear(); + LayoutInflater inflater = getLayoutInflater(); + for (int i = 0; i < numImages; i++) { + ImageView tmp = (ImageView) inflater.inflate(R.layout.imageview, null); + mImages.add(tmp); + mLinearLayout.addView(tmp); + + } + + // Button used to start and stop the capture of images when they are deemed great + mStartStopButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mStartStopButton.getText().equals("Start")) { + mGraph.getVariable("startCapture").setValue(true); + mStartStopButton.setText("Stop"); + mSpinner.setEnabled(false); + } else { + boolean tmp = (Boolean) mGraph.getVariable("startCapture").getValue(); + if (tmp == false) { + return; + } + if (count == numImages-1) countHasReachedMax = true; + captureImages(); + } + } + }); + + // Button to open the gallery to show the images in there + Button galleryOpen = (Button) findViewById(R.id.galleryOpenButton); + galleryOpen.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + Intent openGalleryIntent = new Intent(Intent.ACTION_MAIN); + openGalleryIntent.addCategory(Intent.CATEGORY_APP_GALLERY); + startActivity(openGalleryIntent); + } + }); + + loadGraph(); + mGraph.getVariable("startCapture").setValue(false); + runGraph(); + } + + @Override + public void onPause() { + super.onPause(); + Log.i(TAG, "onPause"); + if (mContext != null) { + mContext.onPause(); + } + } + + @Override + public void onResume() { + super.onResume(); + Log.i(TAG, "onResume"); + if (mContext != null) { + mContext.onResume(); + } + if (isPendingRunGraph) { + isPendingRunGraph = false; + runGraph(); + } + } + + @Override + public void onStop() { + super.onStop(); + Log.i(TAG, "onStop"); + } + + // Build the Filtergraph for Camera + private void loadGraph() { + try { + mGraph = GraphReader.readXmlGraphResource(mContext, R.raw.camera_graph); + mRunner = mGraph.getRunner(); + + // Connect views + mGraph.bindFilterToView("camViewTarget", mCameraView); + mGraph.bindFilterToView("goodOrBadTextView", mGoodBadTextView); + mGraph.bindFilterToView("fpsTextView", mFPSTextView); + mGraph.bindFilterToView("scoreTextView", mScoreTextView); + + // Used for Facial Expressions + if (sUseFacialExpression) { + mGraph.bindFilterToView("eyesTextView", mEyesTextView); + mGraph.bindFilterToView("smilesTextView", mSmilesTextView); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Asynchronously run the filtergraph + private void runGraph() { + mRunner.setIsVerbose(true); + mRunner.start(mGraph); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/WaveTriggerFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/WaveTriggerFilter.java new file mode 100644 index 0000000..9f72940 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/src/androidx/media/filterfw/samples/simplecamera/WaveTriggerFilter.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 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 androidx.media.filterfw.samples.simplecamera; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.InputPort; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.OutputPort; +import androidx.media.filterfw.Signature; + +// The Filter is used to generate the camera snap effect. +// The effect is to give the image a sudden white appearance. +public final class WaveTriggerFilter extends Filter { + + private boolean mTrigger = false; + private boolean mInWaveMode = false; + private float mTime = 0f; + + public WaveTriggerFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("value", Signature.PORT_REQUIRED, FrameType.single()) + .disallowOtherPorts(); + } + + public synchronized void trigger() { + mTrigger = true; + } + + @Override + public void onInputPortOpen(InputPort port) { + if (port.getName().equals("trigger")) { + port.bindToFieldNamed("mTrigger"); + port.setAutoPullEnabled(true); + } + } + + @Override + protected synchronized void onProcess() { + // Check if we were triggered + if (mTrigger) { + mInWaveMode = true; + mTrigger = false; + mTime = 0.5f; + } + + // Calculate output value + float value = 0.5f; + if (mInWaveMode) { + value = -Math.abs(mTime - 1f) + 1f; + mTime += 0.2f; + if (mTime >= 2f) { + mInWaveMode = false; + } + } + + // Push Value + OutputPort outPort = getConnectedOutputPort("value"); + FrameValue frame = outPort.fetchAvailableFrame(null).asFrameValue(); + frame.setValue(value); + outPort.pushFrame(frame); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/Android.mk b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/Android.mk new file mode 100644 index 0000000..3923549 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/Android.mk @@ -0,0 +1,39 @@ +# Copyright (C) 2013 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. +# + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_SDK_VERSION := current + +LOCAL_PACKAGE_NAME := SmartCamera-tests + +LOCAL_SRC_FILES += $(call all-java-files-under, src) + +LOCAL_JAVA_LIBRARIES := android.test.runner +#LOCAL_STATIC_JAVA_LIBRARIES := filterframework-test-lib +LOCAL_STATIC_JAVA_LIBRARIES += guava + +#LOCAL_JAVA_LIBRARIES := filterframework-test-lib +LOCAL_STATIC_JAVA_LIBRARIES := guava + +LOCAL_STATIC_JAVA_LIBRARIES += +LOCAL_PROGUARD_ENABLED := disabled + +LOCAL_INSTRUMENTATION_FOR := SmartCamera + +include $(BUILD_PACKAGE) diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/AndroidManifest.xml b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/AndroidManifest.xml new file mode 100644 index 0000000..3363af4 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/AndroidManifest.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2013 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="androidx.media.filterfw.samples.simplecamera.tests" + android:versionCode="1" + android:versionName="1.0" > + + <uses-sdk android:minSdkVersion="9" android:targetSdkVersion="17" /> + + <instrumentation + android:name="android.test.InstrumentationTestRunner" + android:targetPackage="androidx.media.filterfw.samples.simplecamera" /> + + <application> + <uses-library android:name="android.test.runner" /> + </application> + +</manifest> diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/project.properties b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/project.properties new file mode 100644 index 0000000..4653837a --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/project.properties @@ -0,0 +1,16 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library.reference.1=../filterfw-test-lib +android.library.reference.2=../filterfw diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/res/.README b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/res/.README new file mode 100644 index 0000000..c29cd87 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/res/.README @@ -0,0 +1,3 @@ +The res directory is needed for Eclipse to correctly build the project, but it +is not possible to check in a directory into git. This file guarantees the res +directory exists. diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/FrameSourceFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/FrameSourceFilter.java new file mode 100644 index 0000000..7daa03f --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/FrameSourceFilter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013 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 androidx.media.filterfw; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * A {@link Filter} that pushes out externally injected frames. + * <p> When a frame is injected using {@link #injectFrame(Frame)}, this source will push it on its + * output port and then sleep until another frame is injected. + * <p> Multiple frames may be injected before any frame is pushed out. In this case they will be + * queued and pushed in FIFO order. + */ +class FrameSourceFilter extends Filter { + + private final Queue<Frame> mFrames = new LinkedList<Frame>(); + + FrameSourceFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addOutputPort("output", Signature.PORT_REQUIRED, FrameType.any()) + .disallowOtherPorts(); + } + + private synchronized Frame obtainFrame() { + if (mFrames.isEmpty()) { + enterSleepState(); + return null; + } else { + return mFrames.poll(); + } + } + + /** + * Call this method to inject a frame that will be pushed in a future execution of the filter. + * <p> If multiple frames are injected then they will be pushed one per execution in FIFO order. + */ + public synchronized void injectFrame(Frame frame) { + mFrames.add(frame); + wakeUp(); + } + + @Override + protected void onProcess() { + Frame frame = obtainFrame(); + if (frame != null) { + getConnectedOutputPort("output").pushFrame(frame); + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/FrameTargetFilter.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/FrameTargetFilter.java new file mode 100644 index 0000000..1f0e267 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/FrameTargetFilter.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2013 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 androidx.media.filterfw; + +/** + * A {@link Filter} that consumes frames and allows to register a listener to observe when + * a new frame has been consumed. + */ +class FrameTargetFilter extends Filter { + + interface Listener { + /** + * Called each time this filter receives a new frame. The implementer of this method is + * responsible for releasing the frame. + */ + void onFramePushed(String filterName, Frame frame); + } + + private Listener mListener; + + FrameTargetFilter(MffContext context, String name) { + super(context, name); + } + + @Override + public Signature getSignature() { + return new Signature() + .addInputPort("input", Signature.PORT_REQUIRED, FrameType.any()) + .disallowOtherPorts(); + } + + public synchronized void setListener(Listener listener) { + mListener = listener; + } + + @Override + protected synchronized void onProcess() { + Frame frame = getConnectedInputPort("input").pullFrame(); + if (mListener != null) { + frame.retain(); + mListener.onFramePushed(getName(), frame); + } + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/MffFilterTestCase.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/MffFilterTestCase.java new file mode 100644 index 0000000..84efd28 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/MffFilterTestCase.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2013 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 androidx.media.filterfw; + +import androidx.media.filterfw.GraphRunner.Listener; +import androidx.media.filterfw.Signature.PortInfo; + +import com.google.common.util.concurrent.SettableFuture; + +import junit.framework.TestCase; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * A {@link TestCase} for testing single MFF filter runs. Implementers should extend this class and + * implement the {@link #createFilter(MffContext)} method to create the filter under test. Inside + * each test method, the implementer should supply one or more frames for all the filter inputs + * (calling {@link #injectInputFrame(String, Frame)}) and then invoke {@link #process()}. Once the + * processing finishes, one should call {@link #getOutputFrame(String)} to get and inspect the + * output frames. + * + * TODO: extend this to deal with filters that push multiple output frames. + * TODO: relax the requirement that all output ports should be pushed (the implementer should be + * able to tell which ports to wait for before process() returns). + * TODO: handle undeclared inputs and outputs. + */ +public abstract class MffFilterTestCase extends MffTestCase { + + private static final long DEFAULT_TIMEOUT_MS = 1000; + + private FilterGraph mGraph; + private GraphRunner mRunner; + private Map<String, Frame> mOutputFrames; + private Set<String> mEmptyOutputPorts; + + private SettableFuture<Void> mProcessResult; + + protected abstract Filter createFilter(MffContext mffContext); + + @Override + protected void setUp() throws Exception { + super.setUp(); + MffContext mffContext = getMffContext(); + FilterGraph.Builder graphBuilder = new FilterGraph.Builder(mffContext); + Filter filterUnderTest = createFilter(mffContext); + graphBuilder.addFilter(filterUnderTest); + + connectInputPorts(mffContext, graphBuilder, filterUnderTest); + connectOutputPorts(mffContext, graphBuilder, filterUnderTest); + + mGraph = graphBuilder.build(); + mRunner = mGraph.getRunner(); + mRunner.setListener(new Listener() { + @Override + public void onGraphRunnerStopped(GraphRunner runner) { + mProcessResult.set(null); + } + + @Override + public void onGraphRunnerError(Exception exception, boolean closedSuccessfully) { + mProcessResult.setException(exception); + } + }); + + mOutputFrames = new HashMap<String, Frame>(); + mProcessResult = SettableFuture.create(); + } + + @Override + protected void tearDown() throws Exception { + for (Frame frame : mOutputFrames.values()) { + frame.release(); + } + mOutputFrames = null; + + mRunner.stop(); + mRunner = null; + mGraph = null; + + mProcessResult = null; + super.tearDown(); + } + + protected void injectInputFrame(String portName, Frame frame) { + FrameSourceFilter filter = (FrameSourceFilter) mGraph.getFilter("in_" + portName); + filter.injectFrame(frame); + } + + /** + * Returns the frame pushed out by the filter under test. Should only be called after + * {@link #process(long)} has returned. + */ + protected Frame getOutputFrame(String outputPortName) { + return mOutputFrames.get("out_" + outputPortName); + } + + protected void process(long timeoutMs) + throws ExecutionException, TimeoutException, InterruptedException { + mRunner.start(mGraph); + mProcessResult.get(timeoutMs, TimeUnit.MILLISECONDS); + } + + protected void process() throws ExecutionException, TimeoutException, InterruptedException { + process(DEFAULT_TIMEOUT_MS); + } + + /** + * This method should be called to create the input frames inside the test cases (instead of + * {@link Frame#create(FrameType, int[])}). This is required to work around a requirement for + * the latter method to be called on the MFF thread. + */ + protected Frame createFrame(FrameType type, int[] dimensions) { + return new Frame(type, dimensions, mRunner.getFrameManager()); + } + + private void connectInputPorts( + MffContext mffContext, FilterGraph.Builder graphBuilder, Filter filter) { + Signature signature = filter.getSignature(); + for (Entry<String, PortInfo> inputPortEntry : signature.getInputPorts().entrySet()) { + Filter inputFilter = new FrameSourceFilter(mffContext, "in_" + inputPortEntry.getKey()); + graphBuilder.addFilter(inputFilter); + graphBuilder.connect(inputFilter, "output", filter, inputPortEntry.getKey()); + } + } + + private void connectOutputPorts( + MffContext mffContext, FilterGraph.Builder graphBuilder, Filter filter) { + Signature signature = filter.getSignature(); + mEmptyOutputPorts = new HashSet<String>(); + OutputFrameListener outputFrameListener = new OutputFrameListener(); + for (Entry<String, PortInfo> outputPortEntry : signature.getOutputPorts().entrySet()) { + FrameTargetFilter outputFilter = new FrameTargetFilter( + mffContext, "out_" + outputPortEntry.getKey()); + graphBuilder.addFilter(outputFilter); + graphBuilder.connect(filter, outputPortEntry.getKey(), outputFilter, "input"); + outputFilter.setListener(outputFrameListener); + mEmptyOutputPorts.add("out_" + outputPortEntry.getKey()); + } + } + + private class OutputFrameListener implements FrameTargetFilter.Listener { + + @Override + public void onFramePushed(String filterName, Frame frame) { + mOutputFrames.put(filterName, frame); + boolean alreadyPushed = !mEmptyOutputPorts.remove(filterName); + if (alreadyPushed) { + throw new IllegalStateException( + "A frame has been pushed twice to the same output port."); + } + if (mEmptyOutputPorts.isEmpty()) { + // All outputs have been pushed, stop the graph. + mRunner.stop(); + } + } + + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/MffTestCase.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/MffTestCase.java new file mode 100644 index 0000000..2f33a5c --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/MffTestCase.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2013 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 androidx.media.filterfw; + +import android.os.Handler; +import android.os.HandlerThread; +import android.test.AndroidTestCase; + +import junit.framework.TestCase; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +/** + * A {@link TestCase} for testing objects requiring {@link MffContext}. This test case can only be + * used to test the functionality that does not rely on GL support and camera. + */ +public class MffTestCase extends AndroidTestCase { + + private HandlerThread mMffContextHandlerThread; + private MffContext mMffContext; + + @Override + protected void setUp() throws Exception { + super.setUp(); + // MffContext needs to be created on a separate thread to allow MFF to post Runnable's. + mMffContextHandlerThread = new HandlerThread("MffContextThread"); + mMffContextHandlerThread.start(); + Handler handler = new Handler(mMffContextHandlerThread.getLooper()); + FutureTask<MffContext> task = new FutureTask<MffContext>(new Callable<MffContext>() { + @Override + public MffContext call() throws Exception { + MffContext.Config config = new MffContext.Config(); + config.requireCamera = false; + config.requireOpenGL = false; + config.forceNoGL = true; + return new MffContext(getContext(), config); + } + }); + handler.post(task); + // Wait for the context to be created on the handler thread. + mMffContext = task.get(); + } + + @Override + protected void tearDown() throws Exception { + mMffContextHandlerThread.getLooper().quit(); + mMffContextHandlerThread = null; + mMffContext.release(); + mMffContext = null; + super.tearDown(); + } + + protected MffContext getMffContext() { + return mMffContext; + } + +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/AverageFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/AverageFilterTest.java new file mode 100644 index 0000000..37b5eb8 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/AverageFilterTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2013 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 androidx.media.filterfw.samples.simplecamera; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + + +public class AverageFilterTest extends MffFilterTestCase { + + @Override + protected Filter createFilter(MffContext mffContext) { + return new AverageFilter(mffContext, "averageFilter"); + } + + public void testAverageFilter() throws Exception { + FrameValue frame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + frame.setValue(5f); + + injectInputFrame("sharpness", frame); + + process(); + assertEquals(1f, ((Float) getOutputFrame("avg").asFrameValue().getValue()).floatValue(), + 0.001f); + } + + public void testAverageFilter2() throws Exception{ + FrameValue frame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + frame.setValue(4f); + + injectInputFrame("sharpness", frame); + + process(); + assertEquals(0.8f, ((Float) getOutputFrame("avg").asFrameValue().getValue()).floatValue(), + 0.001f); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/AvgBrightnessFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/AvgBrightnessFilterTest.java new file mode 100644 index 0000000..3c8d127 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/AvgBrightnessFilterTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import android.net.Uri; +import android.provider.MediaStore; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + + +public class AvgBrightnessFilterTest extends MffFilterTestCase { + private AssetManager assetMgr = null; + @Override + protected Filter createFilter(MffContext mffContext) { + assetMgr = mffContext.getApplicationContext().getAssets(); + return new AvgBrightnessFilter(mffContext, "brightnessFilter"); + } + + public void testBrightnessFilter() throws Exception{ + final int INPUT_WIDTH = 480; + final int INPUT_HEIGHT = 640; + FrameImage2D image = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {INPUT_WIDTH,INPUT_HEIGHT}).asFrameImage2D(); + + Bitmap bitmap = BitmapFactory.decodeStream(assetMgr.open("0002_000390.jpg")); + image.setBitmap(bitmap); + + injectInputFrame("image", image); + + process(); + final float EXPECTED_RESULT = 0.35f; + assertEquals(EXPECTED_RESULT, ((Float) getOutputFrame("brightnessRating"). + asFrameValue().getValue()).floatValue(), 0.01f); + } +}
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ContrastRatioFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ContrastRatioFilterTest.java new file mode 100644 index 0000000..6072755 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ContrastRatioFilterTest.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; +import androidx.media.filterfw.samples.simplecamera.ContrastRatioFilter; + +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.MediaStore; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + + +public class ContrastRatioFilterTest extends MffFilterTestCase { + private AssetManager assetMgr = null; + + @Override + protected Filter createFilter(MffContext mffContext) { + assetMgr = mffContext.getApplicationContext().getAssets(); + return new ContrastRatioFilter(mffContext, "contrastFilter"); + } + + public void testContrastFilter() throws Exception { + + final int INPUT_WIDTH = 480; + final int INPUT_HEIGHT = 640; + FrameImage2D image = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {INPUT_WIDTH,INPUT_HEIGHT}).asFrameImage2D(); + + Bitmap bitmap = BitmapFactory.decodeStream(assetMgr.open("0002_000390.jpg")); + image.setBitmap(bitmap); + + injectInputFrame("image", image); + + process(); + final float EXPECTED_RESULT = 0.29901487f; + assertEquals(EXPECTED_RESULT, ((Float) getOutputFrame("contrastRating"). + asFrameValue().getValue()).floatValue(), 0.001f); + + + } +}
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ExposureFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ExposureFilterTest.java new file mode 100644 index 0000000..25ac212 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ExposureFilterTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.MediaStore; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + + +public class ExposureFilterTest extends MffFilterTestCase { + + private AssetManager assetMgr = null; + @Override + protected Filter createFilter(MffContext mffContext) { + assetMgr = mffContext.getApplicationContext().getAssets(); + return new ExposureFilter(mffContext, "exposureFilter"); + } + + public void testExposureFilter() throws Exception{ + final int INPUT_WIDTH = 480; + final int INPUT_HEIGHT = 640; + FrameImage2D image = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {INPUT_WIDTH,INPUT_HEIGHT}).asFrameImage2D(); + + Bitmap bitmap = BitmapFactory.decodeStream(assetMgr.open("0002_000390.jpg")); + image.setBitmap(bitmap); + + injectInputFrame("image", image); + process(); + final float EXPECTED_OVEREXPOSURE = 0.00757f; + assertEquals(EXPECTED_OVEREXPOSURE, ((Float) getOutputFrame("overExposureRating"). + asFrameValue().getValue()).floatValue(), 0.001f); + final float EXPECTED_UNDEREXPOSURE = 0.2077f; + assertEquals(EXPECTED_UNDEREXPOSURE, ((Float) getOutputFrame("underExposureRating"). + asFrameValue().getValue()).floatValue(), 0.001f); + } +}
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FaceSquareFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FaceSquareFilterTest.java new file mode 100644 index 0000000..02387fe --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FaceSquareFilterTest.java @@ -0,0 +1,172 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + + +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.MediaStore; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValues; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import android.hardware.Camera; +import android.hardware.Camera.Face; +import android.graphics.Rect; + + +public class FaceSquareFilterTest extends MffFilterTestCase { + + private AssetManager assetMgr = null; + @Override + protected Filter createFilter(MffContext mffContext) { + assetMgr = mffContext.getApplicationContext().getAssets(); + return new FaceSquareFilter(mffContext, "faceSquareFilter"); + } + + public void testFaceSquareFilter() throws Exception{ + final int INPUT_WIDTH = 1536; + final int INPUT_HEIGHT = 2048; + FrameImage2D image = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {INPUT_WIDTH,INPUT_HEIGHT}).asFrameImage2D(); + + FrameValues facesFrame = createFrame(FrameType.array(Camera.Face.class), new int[] {1,1}). + asFrameValues(); + + Bitmap bitmap = BitmapFactory.decodeStream(assetMgr.open("XZZ019.jpg")); + image.setBitmap(bitmap); + injectInputFrame("image", image); + + Face face = new Face(); + Rect faceRect = new Rect(); + // These are the values for image 141 with 1 face + faceRect.set(-533, -453, 369, 224); + face.rect = faceRect; + Face[] faces = new Face[1]; + faces[0] = face; + facesFrame.setValue(faces); + injectInputFrame("faces", facesFrame); + process(); + + // ensure the output image has the rectangle in the right place + FrameImage2D outputImage = getOutputFrame("image").asFrameImage2D(); + int[] pixels = new int[bitmap.getByteCount()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), + bitmap.getHeight()); + + final int FACE_X_RANGE = 2000; + final int WIDTH_OFFSET = 1000; + final int HEIGHT_OFFSET = 1000; + + int top = (faceRect.top+HEIGHT_OFFSET)*bitmap.getHeight()/FACE_X_RANGE; + int bottom = (faceRect.bottom+HEIGHT_OFFSET)*bitmap.getHeight()/FACE_X_RANGE; + int left = (faceRect.left+WIDTH_OFFSET)*bitmap.getWidth()/FACE_X_RANGE; + int right = (faceRect.right+WIDTH_OFFSET)*bitmap.getWidth()/FACE_X_RANGE; + + if (top < 0) { + top = 0; + } else if (top > bitmap.getHeight()) { + top = bitmap.getHeight(); + } + if (left < 0) { + left = 0; + } else if (left > bitmap.getWidth()) { + left = bitmap.getWidth(); + } + if (bottom > bitmap.getHeight()) { + bottom = bitmap.getHeight(); + } else if (bottom < 0) { + bottom = 0; + } + if (right > bitmap.getWidth()) { + right = bitmap.getWidth(); + } else if (right < 0) { + right = 0; + } + + for (int j = 0; j < (bottom - top); j++) { + // Left edge + if (left > 0 && top > 0) { + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * (top + j) + left) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * (top + j) + left) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * (top + j) + left) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + } + + // Right edge + if (right > 0 && top > 0) { + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * (top + j) + right) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * (top + j) + right) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * (top + j) + right) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + } + + } + for (int k = 0; k < (right - left); k++) { + // Top edge + if (top < bitmap.getHeight()) { + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * top + left + k) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * top + left + k) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * top + left + k) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + + } + // Bottom edge + if (bottom < bitmap.getHeight()) { + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * bottom + left + k) + + ImageConstants.RED_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * bottom + left + k) + + ImageConstants.GREEN_OFFSET] = (byte) ImageConstants.MAX_BYTE; + pixels[ImageConstants.PIX_CHANNELS * (bitmap.getWidth() * bottom + left + k) + + ImageConstants.BLUE_OFFSET] = (byte) ImageConstants.MAX_BYTE; + } + } + + Bitmap outputBitmap = outputImage.toBitmap(); + int[] outputPixels = new int[outputBitmap.getByteCount()]; + outputBitmap.getPixels(outputPixels, 0, outputBitmap.getWidth(), 0, 0, + outputBitmap.getWidth(), outputBitmap.getHeight()); + int equalCount = 0; + for ( int i = 0; i < outputBitmap.getByteCount(); i++) { + if (pixels[i] == outputPixels[i]) + equalCount++; + } + + if (equalCount + (0.05f*outputBitmap.getByteCount()) < outputBitmap.getByteCount()) { + // Assertion will fail if condition is true + assertEquals(equalCount, outputBitmap.getByteCount()); + } + } +}
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToSizeFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToSizeFilterTest.java new file mode 100644 index 0000000..0f4c6d0 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToSizeFilterTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import androidx.media.filterfw.samples.simplecamera.FloatArrayToSizeFilter; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + + +public class FloatArrayToSizeFilterTest extends MffFilterTestCase { + + @Override + protected Filter createFilter(MffContext mffContext) { + return new FloatArrayToSizeFilter(mffContext, "floatArrayToSizeFilter"); + } + + + public void testToSize() throws Exception { + FrameValue floatArray = createFrame(FrameType.array(float.class), new int[] { 1 }). + asFrameValue(); + float[] floatArr = { 10f, 15f, 25f }; + floatArray.setValue(floatArr); + + injectInputFrame("array", floatArray); + + process(); + assertEquals(3, ((Integer) getOutputFrame("size").asFrameValue().getValue()).intValue()); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToStrFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToStrFilterTest.java new file mode 100644 index 0000000..bf6a197 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/FloatArrayToStrFilterTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; +import androidx.media.filterfw.samples.simplecamera.FloatArrayToStrFilter; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + + +public class FloatArrayToStrFilterTest extends MffFilterTestCase { + + @Override + protected Filter createFilter(MffContext mffContext) { + return new FloatArrayToStrFilter(mffContext, "floatArrayToStrFilter"); + } + + public void testToStr() throws Exception { + FrameValue floatArray = createFrame(FrameType.array(float.class), new int[] { 1 }). + asFrameValue(); + float[] floatArr = { 10f, 15f, 25f }; + floatArray.setValue(floatArr); + + injectInputFrame("array", floatArray); + + process(); + + assertEquals("[10.0, 15.0, 25.0]", (String) getOutputFrame("string").asFrameValue(). + getValue()); + } +}
\ No newline at end of file diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/IfElseFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/IfElseFilterTest.java new file mode 100644 index 0000000..30835ea --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/IfElseFilterTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + + +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.provider.MediaStore; +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameImage2D; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +public class IfElseFilterTest extends MffFilterTestCase { + private final static int BIG_INPUT_WIDTH = 1536; + private final static int BIG_INPUT_HEIGHT = 2048; + private final static int SMALL_INPUT_WIDTH = 480; + private final static int SMALL_INPUT_HEIGHT = 640; + + private AssetManager assetMgr = null; + @Override + protected Filter createFilter(MffContext mffContext) { + assetMgr = mffContext.getApplicationContext().getAssets(); + return new IfElseFilter(mffContext, "ifElseFilter"); + } + + public void testIfElseFilterTrue() throws Exception { + FrameImage2D image = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {BIG_INPUT_WIDTH,BIG_INPUT_HEIGHT}).asFrameImage2D(); + FrameImage2D video = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {SMALL_INPUT_WIDTH,SMALL_INPUT_HEIGHT}).asFrameImage2D(); + + // Image of legs + Bitmap videoBitmap = BitmapFactory.decodeStream(assetMgr.open("0002_000390.jpg")); + // Image of a face + Bitmap imageBitmap = BitmapFactory.decodeStream(assetMgr.open("XZZ019.jpg")); + + image.setBitmap(imageBitmap); + injectInputFrame("falseResult", image); + video.setBitmap(videoBitmap); + injectInputFrame("trueResult", video); + + FrameValue conditionFrame = createFrame(FrameType.single(boolean.class), new int[] {1}). + asFrameValue(); + conditionFrame.setValue(true); + injectInputFrame("condition", conditionFrame); + + process(); + + // Ensure that for true, we use the video input + FrameImage2D outputImage = getOutputFrame("output").asFrameImage2D(); + assertEquals(outputImage, video); + } + + public void testIfElseFilterFalse() throws Exception { + + FrameImage2D image = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {BIG_INPUT_WIDTH,BIG_INPUT_HEIGHT}).asFrameImage2D(); + FrameImage2D video = + createFrame(FrameType.image2D(FrameType.ELEMENT_RGBA8888, FrameType.READ_CPU), + new int[] {SMALL_INPUT_WIDTH,SMALL_INPUT_HEIGHT}).asFrameImage2D(); + + // Image of legs + Bitmap videoBitmap = BitmapFactory.decodeStream(assetMgr.open("0002_000390.jpg")); + // Image of a face + Bitmap imageBitmap = BitmapFactory.decodeStream(assetMgr.open("XZZ019.jpg")); + + image.setBitmap(imageBitmap); + injectInputFrame("falseResult", image); + video.setBitmap(videoBitmap); + injectInputFrame("trueResult", video); + + + FrameValue conditionFrame = createFrame(FrameType.single(boolean.class), new int[] {1}). + asFrameValue(); + conditionFrame.setValue(false); + injectInputFrame("condition", conditionFrame); + + process(); + + // Ensure that for true, we use the video input + FrameImage2D outputImage = getOutputFrame("output").asFrameImage2D(); + assertEquals(outputImage, image); + } +} diff --git a/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ImageGoodnessFilterTest.java b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ImageGoodnessFilterTest.java new file mode 100644 index 0000000..43bc090 --- /dev/null +++ b/tests/Camera2Tests/SmartCamera/SimpleCamera/tests/src/androidx/media/filterfw/samples/simplecamera/ImageGoodnessFilterTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2013 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 androidx.media.filterfw.samples.simplecamera; + +import androidx.media.filterfw.Filter; +import androidx.media.filterfw.FrameType; +import androidx.media.filterfw.FrameValue; +import androidx.media.filterfw.MffContext; +import androidx.media.filterfw.MffFilterTestCase; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +public class ImageGoodnessFilterTest extends MffFilterTestCase { + + String AWFUL_STRING = "Awful Picture"; + String BAD_STRING = "Bad Picture"; + String OK_STRING = "Ok Picture"; + String GOOD_STRING = "Good Picture!"; + String GREAT_STRING = "Great Picture!"; + @Override + protected Filter createFilter(MffContext mffContext) { + return new ImageGoodnessFilter(mffContext, "goodnessFilter"); + } + + public void testAwfulPicture() throws Exception { + FrameValue sharpnessFrame = createFrame(FrameType.single(), new int[] { 1 }). + asFrameValue(); + sharpnessFrame.setValue(10f); + FrameValue oEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + oEFrame.setValue(0.39f); + FrameValue uEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + uEFrame.setValue(0.25f); + FrameValue colorFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + colorFrame.setValue(2.1f); + FrameValue contrastFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + contrastFrame.setValue(0.18f); + FrameValue motionFrame = createFrame(FrameType.array(), new int[] { 1 }).asFrameValue(); + float[] motionFloatArray = { 9.0f, 3.0f, 2.0f }; + motionFrame.setValue(motionFloatArray); + + injectInputFrame("sharpness", sharpnessFrame); + injectInputFrame("overExposure", oEFrame); + injectInputFrame("underExposure", uEFrame); + injectInputFrame("colorfulness", colorFrame); + injectInputFrame("contrastRating", contrastFrame); + injectInputFrame("motionValues", motionFrame); + + process(); + assertEquals("Awful Picture", (String) getOutputFrame("goodOrBadPic").asFrameValue(). + getValue()); + } + + public void testBadPicture() throws Exception { + FrameValue sharpnessFrame = createFrame(FrameType.single(), new int[] { 1 }). + asFrameValue(); + sharpnessFrame.setValue(10f); + FrameValue oEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + oEFrame.setValue(0.39f); + FrameValue uEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + uEFrame.setValue(0.25f); + FrameValue colorFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + colorFrame.setValue(2.1f); + FrameValue contrastFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + contrastFrame.setValue(0.18f); + FrameValue motionFrame = createFrame(FrameType.array(), new int[] { 1 }).asFrameValue(); + float[] motionFloatArray = { 0.0f, 0.0f, 0.0f }; + motionFrame.setValue(motionFloatArray); + + injectInputFrame("sharpness", sharpnessFrame); + injectInputFrame("overExposure", oEFrame); + injectInputFrame("underExposure", uEFrame); + injectInputFrame("colorfulness", colorFrame); + injectInputFrame("contrastRating", contrastFrame); + injectInputFrame("motionValues", motionFrame); + + process(); + assertEquals("Bad Picture", (String) getOutputFrame("goodOrBadPic").asFrameValue(). + getValue()); + } + + public void testOkPicture() throws Exception { + FrameValue sharpnessFrame = createFrame(FrameType.single(), new int[] { 1 }). + asFrameValue(); + sharpnessFrame.setValue(30f); + FrameValue oEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + oEFrame.setValue(0.39f); + FrameValue uEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + uEFrame.setValue(0.25f); + FrameValue colorFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + colorFrame.setValue(2.1f); + FrameValue contrastFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + contrastFrame.setValue(0.18f); + FrameValue motionFrame = createFrame(FrameType.array(), new int[] { 1 }).asFrameValue(); + float[] motionFloatArray = { 0.0f, 0.0f, 0.0f }; + motionFrame.setValue(motionFloatArray); + + injectInputFrame("sharpness", sharpnessFrame); + injectInputFrame("overExposure", oEFrame); + injectInputFrame("underExposure", uEFrame); + injectInputFrame("colorfulness", colorFrame); + injectInputFrame("contrastRating", contrastFrame); + injectInputFrame("motionValues", motionFrame); + + process(); + assertEquals("Ok Picture", (String) getOutputFrame("goodOrBadPic").asFrameValue(). + getValue()); + } + + public void testGoodPicture() throws Exception { + FrameValue sharpnessFrame = createFrame(FrameType.single(), new int[] { 1 }). + asFrameValue(); + sharpnessFrame.setValue(50f); + FrameValue oEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + oEFrame.setValue(0.01f); + FrameValue uEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + uEFrame.setValue(0.01f); + FrameValue colorFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + colorFrame.setValue(2.1f); + FrameValue contrastFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + contrastFrame.setValue(0.18f); + FrameValue motionFrame = createFrame(FrameType.array(), new int[] { 1 }).asFrameValue(); + float[] motionFloatArray = { 0.0f, 0.0f, 0.0f }; + motionFrame.setValue(motionFloatArray); + + injectInputFrame("sharpness", sharpnessFrame); + injectInputFrame("overExposure", oEFrame); + injectInputFrame("underExposure", uEFrame); + injectInputFrame("colorfulness", colorFrame); + injectInputFrame("contrastRating", contrastFrame); + injectInputFrame("motionValues", motionFrame); + + process(); + assertEquals("Good Picture!", (String) getOutputFrame("goodOrBadPic").asFrameValue(). + getValue()); + } + + public void testGreatPicture() throws Exception { + FrameValue sharpnessFrame = createFrame(FrameType.single(), new int[] { 1 }). + asFrameValue(); + sharpnessFrame.setValue(50f); + FrameValue oEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + oEFrame.setValue(0.01f); + FrameValue uEFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + uEFrame.setValue(0.02f); + FrameValue colorFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + colorFrame.setValue(2.1f); + FrameValue contrastFrame = createFrame(FrameType.single(), new int[] { 1 }).asFrameValue(); + contrastFrame.setValue(0.25f); + FrameValue motionFrame = createFrame(FrameType.array(), new int[] { 1 }).asFrameValue(); + float[] motionFloatArray = { 0.0f, 0.0f, 0.0f }; + motionFrame.setValue(motionFloatArray); + + injectInputFrame("sharpness", sharpnessFrame); + injectInputFrame("overExposure", oEFrame); + injectInputFrame("underExposure", uEFrame); + injectInputFrame("colorfulness", colorFrame); + injectInputFrame("contrastRating", contrastFrame); + injectInputFrame("motionValues", motionFrame); + + process(); + assertEquals("Great Picture!", (String) getOutputFrame("goodOrBadPic").asFrameValue(). + getValue()); + } +} |