From b4695e613f0bca451485f95572f23d464b56a95e Mon Sep 17 00:00:00 2001 From: erdgeist Date: Fri, 24 Apr 2026 16:42:18 +0200 Subject: Initial import --- .gitignore | 60 +++++ README.md | 10 + build.gradle | 64 +++++ gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 +++++++++++++ gradlew.bat | 84 ++++++ settings.gradle | 1 + tflite/.gitignore | 1 + tflite/build.gradle | 75 ++++++ tflite/src/main/AndroidManifest.xml | 51 ++++ tflite/src/main/ic_launcher-playstore.png | Bin 0 -> 1096 bytes .../android/camerax/tflite/CameraActivity.kt | 281 +++++++++++++++++++++ .../main/res/drawable/ic_launcher_background.xml | 89 +++++++ tflite/src/main/res/drawable/ic_shutter.xml | 21 ++ .../src/main/res/drawable/ic_shutter_focused.xml | 28 ++ tflite/src/main/res/drawable/ic_shutter_normal.xml | 28 ++ .../src/main/res/drawable/ic_shutter_pressed.xml | 28 ++ tflite/src/main/res/drawable/shape_rectangle.xml | 28 ++ .../src/main/res/layout-land/activity_camera.xml | 72 ++++++ tflite/src/main/res/layout/activity_camera.xml | 72 ++++++ .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 20 ++ .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 20 ++ tflite/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 1869 bytes .../res/mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 2708 bytes .../src/main/res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 3816 bytes tflite/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1312 bytes .../res/mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 1630 bytes .../src/main/res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2370 bytes tflite/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2578 bytes .../res/mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 3962 bytes .../main/res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5401 bytes tflite/src/main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4132 bytes .../res/mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 7404 bytes .../main/res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8712 bytes tflite/src/main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 5897 bytes .../res/mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 11849 bytes .../main/res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12878 bytes tflite/src/main/res/values/dimens.xml | 33 +++ tflite/src/main/res/values/strings.xml | 21 ++ tflite/src/main/res/values/styles.xml | 27 ++ utils/.gitignore | 2 + utils/README.md | 2 + utils/build.gradle | 75 ++++++ utils/src/main/AndroidManifest.xml | 17 ++ .../android/camera/utils/AutoFitSurfaceView.kt | 79 ++++++ .../example/android/camera/utils/CameraSizes.kt | 79 ++++++ .../com/example/android/camera/utils/ExifUtils.kt | 73 ++++++ .../android/camera/utils/GenericListAdapter.kt | 55 ++++ .../android/camera/utils/OrientationLiveData.kt | 95 +++++++ .../java/com/example/android/camera/utils/Yuv.kt | 191 ++++++++++++++ .../android/camera/utils/YuvToRgbConverter.kt | 99 ++++++++ utils/src/main/res/drawable/ic_shutter.xml | 21 ++ utils/src/main/res/drawable/ic_shutter_focused.xml | 28 ++ utils/src/main/res/drawable/ic_shutter_normal.xml | 28 ++ utils/src/main/res/drawable/ic_shutter_pressed.xml | 28 ++ 57 files changed, 2185 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 tflite/.gitignore create mode 100644 tflite/build.gradle create mode 100644 tflite/src/main/AndroidManifest.xml create mode 100644 tflite/src/main/ic_launcher-playstore.png create mode 100644 tflite/src/main/java/com/example/android/camerax/tflite/CameraActivity.kt create mode 100644 tflite/src/main/res/drawable/ic_launcher_background.xml create mode 100644 tflite/src/main/res/drawable/ic_shutter.xml create mode 100644 tflite/src/main/res/drawable/ic_shutter_focused.xml create mode 100644 tflite/src/main/res/drawable/ic_shutter_normal.xml create mode 100644 tflite/src/main/res/drawable/ic_shutter_pressed.xml create mode 100644 tflite/src/main/res/drawable/shape_rectangle.xml create mode 100644 tflite/src/main/res/layout-land/activity_camera.xml create mode 100644 tflite/src/main/res/layout/activity_camera.xml create mode 100644 tflite/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 tflite/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 tflite/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 tflite/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 tflite/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 tflite/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 tflite/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 tflite/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 tflite/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 tflite/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 tflite/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 tflite/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 tflite/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 tflite/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 tflite/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 tflite/src/main/res/values/dimens.xml create mode 100644 tflite/src/main/res/values/strings.xml create mode 100644 tflite/src/main/res/values/styles.xml create mode 100644 utils/.gitignore create mode 100644 utils/README.md create mode 100644 utils/build.gradle create mode 100644 utils/src/main/AndroidManifest.xml create mode 100644 utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/Yuv.kt create mode 100644 utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt create mode 100644 utils/src/main/res/drawable/ic_shutter.xml create mode 100644 utils/src/main/res/drawable/ic_shutter_focused.xml create mode 100644 utils/src/main/res/drawable/ic_shutter_normal.xml create mode 100644 utils/src/main/res/drawable/ic_shutter_pressed.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..24bddc8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Built application files +app/release +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/* +!.idea/runConfigurations + +# Keystore files +*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e90f33 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# CCCB Display Camera Control Tool + + +Main logic is in + +`CCCB_Camera_Andoroid/tflite/src/main/java/com/example/android/camerax/tflite` + +the rest is boilerplate from + +https://github.com/android/camera-samples diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..30ce363 --- /dev/null +++ b/build.gradle @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + * + * https://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. + */ + +buildscript { + + ext { + // Top-level variables used for versioning + ext.kotlin_version = '1.5.21' + ext.java_version = JavaVersion.VERSION_1_8 + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath 'com.diffplug.spotless:spotless-plugin-gradle:5.11.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { // repo for TFLite snapshot + name 'ossrh-snapshot' + url 'https://s01.oss.sonatype.org/content/repositories/snapshots' + } + } +} + +subprojects { + apply plugin: 'com.diffplug.spotless' + spotless { + java { + target "**/*.java" + trimTrailingWhitespace() + removeUnusedImports() + googleJavaFormat() + endWithNewline() + } + kotlin { + target "**/*.kt" + trimTrailingWhitespace() + endWithNewline() + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..23339e0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..38c6ed2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Dec 16 12:00:46 WET 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..844db50 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include 'tflite' diff --git a/tflite/.gitignore b/tflite/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/tflite/.gitignore @@ -0,0 +1 @@ +/build diff --git a/tflite/build.gradle b/tflite/build.gradle new file mode 100644 index 0000000..8e9fcf4 --- /dev/null +++ b/tflite/build.gradle @@ -0,0 +1,75 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + * + * https://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. + */ + +apply plugin: "com.android.application" +apply plugin: "kotlin-android" + +android { + compileSdkVersion 31 + ndkVersion "21.3.6528147" + + defaultConfig { + applicationId 'com.android.example.camerax.tflite' + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "0.0.1" + } + + compileOptions { + sourceCompatibility rootProject.ext.java_version + targetCompatibility rootProject.ext.java_version + } + + kotlinOptions { + jvmTarget = rootProject.ext.java_version + } + + + buildFeatures { + viewBinding true + } + androidResources { + noCompress 'lite' + } + namespace 'com.android.example.camerax.tflite' +} + +dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + + // App compat and UI things + implementation 'androidx.appcompat:appcompat:1.3.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + + // CameraX + def camerax_version = "1.1.0-beta01" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + implementation "androidx.camera:camera-video:${camerax_version}" + + + // Tensorflow lite dependencies + implementation 'org.tensorflow:tensorflow-lite:2.9.0' + implementation 'org.tensorflow:tensorflow-lite-gpu:2.9.0' + implementation 'org.tensorflow:tensorflow-lite-support:0.4.2' + + testImplementation 'org.robolectric:robolectric:4.4' + testImplementation 'junit:junit:4.13.2' +} diff --git a/tflite/src/main/AndroidManifest.xml b/tflite/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d38c505 --- /dev/null +++ b/tflite/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tflite/src/main/ic_launcher-playstore.png b/tflite/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..dd70d80 Binary files /dev/null and b/tflite/src/main/ic_launcher-playstore.png differ diff --git a/tflite/src/main/java/com/example/android/camerax/tflite/CameraActivity.kt b/tflite/src/main/java/com/example/android/camerax/tflite/CameraActivity.kt new file mode 100644 index 0000000..cde9778 --- /dev/null +++ b/tflite/src/main/java/com/example/android/camerax/tflite/CameraActivity.kt @@ -0,0 +1,281 @@ +/* + * Copyright 2020 Google LLC + * + * 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camerax.tflite + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.RectF +import android.os.Bundle +import android.util.Log +import android.util.Size +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.camera.core.AspectRatio +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.android.example.camerax.tflite.databinding.ActivityCameraBinding +import java.net.DatagramPacket +import java.net.DatagramSocket +import java.net.InetSocketAddress +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import kotlin.random.Random + + +/** Activity that displays the camera and performs object detection on the incoming frames */ +class CameraActivity : AppCompatActivity() { + + private lateinit var activityCameraBinding: ActivityCameraBinding + + private lateinit var bitmapBuffer: Bitmap + + private val executor = Executors.newSingleThreadExecutor() + private val permissions = listOf(Manifest.permission.CAMERA) + private val permissionsRequestCode = Random.nextInt(0, 10000) + + private var lensFacing: Int = CameraSelector.LENS_FACING_BACK + private val isFrontFacing get() = lensFacing == CameraSelector.LENS_FACING_FRONT + + private var pauseAnalysis = false + private var imageRotationDegrees: Int = 0 + private var socket: DatagramSocket = DatagramSocket() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityCameraBinding = ActivityCameraBinding.inflate(layoutInflater) + setContentView(activityCameraBinding.root) + } + + override fun onDestroy() { + + // Terminate all outstanding analyzing jobs (if there is any). + executor.apply { + shutdown() + awaitTermination(1000, TimeUnit.MILLISECONDS) + } + super.onDestroy() + } + + /** Declare and bind preview and analysis use cases */ + @SuppressLint("UnsafeExperimentalUsageError") + private fun bindCameraUseCases() = activityCameraBinding.viewFinder.post { + + val cameraProviderFuture = ProcessCameraProvider.getInstance(this) + cameraProviderFuture.addListener ({ + + // Camera provider is now guaranteed to be available + val cameraProvider = cameraProviderFuture.get() + + // Set up the view finder use case to display camera preview + val preview = Preview.Builder() + .setTargetAspectRatio(AspectRatio.RATIO_4_3) + .setTargetRotation(activityCameraBinding.viewFinder.display.rotation) + .build() + + // Set up the image analysis use case which will process frames in real time + val imageAnalysis = ImageAnalysis.Builder() + .setTargetResolution(Size(448, 236)) + .setTargetRotation(activityCameraBinding.viewFinder.display.rotation) + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888) + .build() + + var frameCounter = 0 + var lastFpsTimestamp = System.currentTimeMillis() + + imageAnalysis.setAnalyzer(executor, ImageAnalysis.Analyzer { image -> + if (!::bitmapBuffer.isInitialized) { + // The image rotation and RGB image buffer are initialized only once + // the analyzer has started running + imageRotationDegrees = image.imageInfo.rotationDegrees + bitmapBuffer = Bitmap.createBitmap( + image.width, image.height, Bitmap.Config.ARGB_8888) + } + + // Early exit: image analysis is in paused state + if (pauseAnalysis) { + image.close() + return@Analyzer + } + + // Copy out RGB bits to our shared buffer + image.use { bitmapBuffer.copyPixelsFromBuffer(image.planes[0].buffer) } + +// val CAM_WIDTH = 858 +// val CAM_HEIGHT = 480 + val CAM_WIDTH = 480 + val CAM_HEIGHT = 360 + + val DISPLAY_WIDTH = 448 + val DISPLAY_HEIGHT = 160 + val DISPLAY_VHEIGHT = 236 + + val scaledBitmap = Bitmap.createScaledBitmap(bitmapBuffer, DISPLAY_WIDTH, DISPLAY_VHEIGHT, true) + + val scratch = IntArray((2 + DISPLAY_VHEIGHT) * DISPLAY_WIDTH) + val output = UByteArray(10 + DISPLAY_HEIGHT * DISPLAY_WIDTH / 8) + output[1] = 0x12u + output[4] = 0x23u + var offset = 10 +/* + for ( row in 0..DISPLAY_VHEIGHT - 1) { + val zeile = (row * bitmapBuffer.height ) / DISPLAY_VHEIGHT + for (column in 0..DISPLAY_WIDTH - 1) { + val spalte = (column * bitmapBuffer.width ) / DISPLAY_WIDTH + val pixel = bitmapBuffer.getPixel(spalte, zeile) + scratch[row * DISPLAY_WIDTH + column] = Color.red(pixel) * 19535 + Color.green(pixel) * 38470 + Color.blue(pixel) * 7448 + } + } +*/ + scaledBitmap.getPixels(scratch, 0, DISPLAY_WIDTH, 0, 0, DISPLAY_WIDTH, DISPLAY_VHEIGHT) + for (off in 0..DISPLAY_VHEIGHT * DISPLAY_WIDTH) + scratch[off] = Color.red(scratch[off]) * 19535 + Color.green(scratch[off]) * 38470 + Color.blue(scratch[off]) * 7448 + + var acc = 0 + var accv = 0 + + for ( row in 0.. DISPLAY_VHEIGHT - 1) + for ( column in 0..DISPLAY_WIDTH - 1) { + val pixel = scratch[row * DISPLAY_WIDTH + column] + val bwpixel = if (pixel < 0x810000) 0 else 0xFFFFFF + + if (row % 12 < 8) { + acc = (acc shl 1) + (bwpixel shr 23) + if (++accv == 8) { + output[offset++] = acc.toUByte() + acc = 0 + accv = 0 + } + } + + val err = (pixel - bwpixel) / 42 + fun AddSatShift(xoff : Int, yoff: Int, shift : Int) { + val pixelold = scratch[(row + yoff) * DISPLAY_WIDTH + column + xoff ] + var r = pixelold + (err shl (16 - shift)) + if ( r < 0 ) r = 0 + if ( r > 0xFFFFFF) r = 0xFFFFFF + scratch[(row + yoff) * DISPLAY_WIDTH + column + xoff ] = r + } + + AddSatShift(0, 1, 13) + AddSatShift(0, 2, 14) + if (column > 0) { + AddSatShift(-1, 1, 14) + AddSatShift(-1, 2, 15) + } + + if (column > 1) { + AddSatShift(-2, 1, 15) + AddSatShift(-2, 2, 16) + } + + if (column < DISPLAY_WIDTH - 1) { + AddSatShift( 1, 0, 13) + AddSatShift( 1, 1, 14) + AddSatShift( 1, 2, 15) + } + + if (column < DISPLAY_WIDTH - 2) { + AddSatShift( 2, 0, 14) + AddSatShift( 2, 1, 15) + AddSatShift( 2, 2, 16) + } + } + val address = InetSocketAddress("172.23.42.29", 2342 ) +// val address = InetSocketAddress("192.168.178.69", 2342 ) + try { + socket.send(DatagramPacket(output.toByteArray(), offset, address)) + } catch (e: Exception) { + // Ignore network exceptions + } + + // Compute the FPS of the entire pipeline + val frameCount = 10 + if (++frameCounter % frameCount == 0) { + frameCounter = 0 + val now = System.currentTimeMillis() + val delta = now - lastFpsTimestamp + val fps = 1000 * frameCount.toFloat() / delta +// Log.d(TAG, "FPS: ${"%.02f".format(fps)} " + bitmapBuffer.width + " x " + bitmapBuffer.height) + + activityCameraBinding.viewFinder.post { + activityCameraBinding.textPrediction.text = "FPS: ${"%.02f".format(fps)}" + } + + lastFpsTimestamp = now + } + }) + + // Create a new camera selector each time, enforcing lens facing + val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() + + // Apply declared configs to CameraX using the same lifecycle owner + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle( + this as LifecycleOwner, cameraSelector, preview, imageAnalysis) + + // Use the camera object to link our preview use case with the view + preview.setSurfaceProvider(activityCameraBinding.viewFinder.surfaceProvider) + + }, ContextCompat.getMainExecutor(this)) + } + + override fun onResume() { + super.onResume() + + // Request permissions each time the app resumes, since they can be revoked at any time + if (!hasPermissions(this)) { + ActivityCompat.requestPermissions( + this, permissions.toTypedArray(), permissionsRequestCode) + } else { + bindCameraUseCases() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == permissionsRequestCode && hasPermissions(this)) { + bindCameraUseCases() + } else { + finish() // If we don't have the required permissions, we can't run + } + } + + /** Convenience method used to check if all permissions required by this app are granted */ + private fun hasPermissions(context: Context) = permissions.all { + ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED + } + + companion object { + private val TAG = CameraActivity::class.java.simpleName + } +} diff --git a/tflite/src/main/res/drawable/ic_launcher_background.xml b/tflite/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..268681b --- /dev/null +++ b/tflite/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tflite/src/main/res/drawable/ic_shutter.xml b/tflite/src/main/res/drawable/ic_shutter.xml new file mode 100644 index 0000000..ab352ba --- /dev/null +++ b/tflite/src/main/res/drawable/ic_shutter.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/tflite/src/main/res/drawable/ic_shutter_focused.xml b/tflite/src/main/res/drawable/ic_shutter_focused.xml new file mode 100644 index 0000000..fa852ac --- /dev/null +++ b/tflite/src/main/res/drawable/ic_shutter_focused.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/tflite/src/main/res/drawable/ic_shutter_normal.xml b/tflite/src/main/res/drawable/ic_shutter_normal.xml new file mode 100644 index 0000000..25a10e1 --- /dev/null +++ b/tflite/src/main/res/drawable/ic_shutter_normal.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/tflite/src/main/res/drawable/ic_shutter_pressed.xml b/tflite/src/main/res/drawable/ic_shutter_pressed.xml new file mode 100644 index 0000000..fa852ac --- /dev/null +++ b/tflite/src/main/res/drawable/ic_shutter_pressed.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/tflite/src/main/res/drawable/shape_rectangle.xml b/tflite/src/main/res/drawable/shape_rectangle.xml new file mode 100644 index 0000000..5365e4c --- /dev/null +++ b/tflite/src/main/res/drawable/shape_rectangle.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/tflite/src/main/res/layout-land/activity_camera.xml b/tflite/src/main/res/layout-land/activity_camera.xml new file mode 100644 index 0000000..1cc66f1 --- /dev/null +++ b/tflite/src/main/res/layout-land/activity_camera.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tflite/src/main/res/layout/activity_camera.xml b/tflite/src/main/res/layout/activity_camera.xml new file mode 100644 index 0000000..c094f94 --- /dev/null +++ b/tflite/src/main/res/layout/activity_camera.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tflite/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/tflite/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..fe90d56 --- /dev/null +++ b/tflite/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/tflite/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/tflite/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..fe90d56 --- /dev/null +++ b/tflite/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/tflite/src/main/res/mipmap-hdpi/ic_launcher.png b/tflite/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..edd7b36 Binary files /dev/null and b/tflite/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/tflite/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/tflite/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a050bf2 Binary files /dev/null and b/tflite/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/tflite/src/main/res/mipmap-hdpi/ic_launcher_round.png b/tflite/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..ec13a43 Binary files /dev/null and b/tflite/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/tflite/src/main/res/mipmap-mdpi/ic_launcher.png b/tflite/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..6fd4f91 Binary files /dev/null and b/tflite/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/tflite/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/tflite/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df81982 Binary files /dev/null and b/tflite/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/tflite/src/main/res/mipmap-mdpi/ic_launcher_round.png b/tflite/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..2616048 Binary files /dev/null and b/tflite/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/tflite/src/main/res/mipmap-xhdpi/ic_launcher.png b/tflite/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..fdae11d Binary files /dev/null and b/tflite/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/tflite/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/tflite/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..667d566 Binary files /dev/null and b/tflite/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/tflite/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/tflite/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..cb11f72 Binary files /dev/null and b/tflite/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/tflite/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tflite/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..5d4fa36 Binary files /dev/null and b/tflite/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/tflite/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/tflite/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..33dc1c6 Binary files /dev/null and b/tflite/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/tflite/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/tflite/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..07d1393 Binary files /dev/null and b/tflite/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..675e313 Binary files /dev/null and b/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9f9808a Binary files /dev/null and b/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..54e07e0 Binary files /dev/null and b/tflite/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/tflite/src/main/res/values/dimens.xml b/tflite/src/main/res/values/dimens.xml new file mode 100644 index 0000000..53948b6 --- /dev/null +++ b/tflite/src/main/res/values/dimens.xml @@ -0,0 +1,33 @@ + + + + 16dp + 32dp + 48dp + 64dp + 92dp + + 4dp + 8dp + 16dp + + 32dp + 64dp + 92dp + + 80dp + \ No newline at end of file diff --git a/tflite/src/main/res/values/strings.xml b/tflite/src/main/res/values/strings.xml new file mode 100644 index 0000000..dbfb4c0 --- /dev/null +++ b/tflite/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + + + Camera Object Detector + Capture + UNKNOWN + diff --git a/tflite/src/main/res/values/styles.xml b/tflite/src/main/res/values/styles.xml new file mode 100644 index 0000000..8bfac62 --- /dev/null +++ b/tflite/src/main/res/values/styles.xml @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/utils/.gitignore b/utils/.gitignore new file mode 100644 index 0000000..38c5875 --- /dev/null +++ b/utils/.gitignore @@ -0,0 +1,2 @@ +/build +.idea \ No newline at end of file diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000..7a10722 --- /dev/null +++ b/utils/README.md @@ -0,0 +1,2 @@ +Do not modify code under this folder outside of `CameraUtils`, it is copied +automatically by `.github/scripts/copy_utils.sh`. \ No newline at end of file diff --git a/utils/build.gradle b/utils/build.gradle new file mode 100644 index 0000000..1ef65fa --- /dev/null +++ b/utils/build.gradle @@ -0,0 +1,75 @@ +/* + * Copyright 2020 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 + * + * https://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. + */ + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + compileOptions { + sourceCompatibility rootProject.ext.java_version + targetCompatibility rootProject.ext.java_version + } + + kotlinOptions { + jvmTarget = "$rootProject.ext.java_version" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + + // Kotlin lang + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.4' + + // App compat and UI things + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + + // EXIF Interface + implementation 'androidx.exifinterface:exifinterface:1.2.0' + + // Unit testing + testImplementation 'androidx.test.ext:junit:1.1.1' + testImplementation 'androidx.test:rules:1.2.0' + testImplementation 'androidx.test:runner:1.2.0' + testImplementation 'androidx.test.espresso:espresso-core:3.2.0' + testImplementation 'org.robolectric:robolectric:4.3.1' + + // Instrumented testing + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test:rules:1.2.0' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/utils/src/main/AndroidManifest.xml b/utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2e13c37 --- /dev/null +++ b/utils/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt b/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt new file mode 100644 index 0000000..3d900d1 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/AutoFitSurfaceView.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.content.Context +import android.util.AttributeSet +import android.util.Log +import android.view.SurfaceView +import kotlin.math.roundToInt + +/** + * A [SurfaceView] that can be adjusted to a specified aspect ratio and + * performs center-crop transformation of input frames. + */ +class AutoFitSurfaceView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : SurfaceView(context, attrs, defStyle) { + + private var aspectRatio = 0f + + /** + * Sets the aspect ratio for this view. The size of the view will be + * measured based on the ratio calculated from the parameters. + * + * @param width Camera resolution horizontal size + * @param height Camera resolution vertical size + */ + fun setAspectRatio(width: Int, height: Int) { + require(width > 0 && height > 0) { "Size cannot be negative" } + aspectRatio = width.toFloat() / height.toFloat() + holder.setFixedSize(width, height) + requestLayout() + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = MeasureSpec.getSize(widthMeasureSpec) + val height = MeasureSpec.getSize(heightMeasureSpec) + if (aspectRatio == 0f) { + setMeasuredDimension(width, height) + } else { + + // Performs center-crop transformation of the camera frames + val newWidth: Int + val newHeight: Int + val actualRatio = if (width > height) aspectRatio else 1f / aspectRatio + if (width < height * actualRatio) { + newHeight = height + newWidth = (height * actualRatio).roundToInt() + } else { + newWidth = width + newHeight = (width / actualRatio).roundToInt() + } + + Log.d(TAG, "Measured dimensions set: $newWidth x $newHeight") + setMeasuredDimension(newWidth, newHeight) + } + } + + companion object { + private val TAG = AutoFitSurfaceView::class.java.simpleName + } +} diff --git a/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt b/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt new file mode 100644 index 0000000..6db01d3 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/CameraSizes.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.graphics.Point +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.params.StreamConfigurationMap +import android.util.Size +import android.view.Display +import kotlin.math.max +import kotlin.math.min + +/** Helper class used to pre-compute shortest and longest sides of a [Size] */ +class SmartSize(width: Int, height: Int) { + var size = Size(width, height) + var long = max(size.width, size.height) + var short = min(size.width, size.height) + override fun toString() = "SmartSize(${long}x${short})" +} + +/** Standard High Definition size for pictures and video */ +val SIZE_1080P: SmartSize = SmartSize(1920, 1080) + +/** Returns a [SmartSize] object for the given [Display] */ +fun getDisplaySmartSize(display: Display): SmartSize { + val outPoint = Point() + display.getRealSize(outPoint) + return SmartSize(outPoint.x, outPoint.y) +} + +/** + * Returns the largest available PREVIEW size. For more information, see: + * https://d.android.com/reference/android/hardware/camera2/CameraDevice and + * https://developer.android.com/reference/android/hardware/camera2/params/StreamConfigurationMap + */ +fun getPreviewOutputSize( + display: Display, + characteristics: CameraCharacteristics, + targetClass: Class, + format: Int? = null +): Size { + + // Find which is smaller: screen or 1080p + val screenSize = getDisplaySmartSize(display) + val hdScreen = screenSize.long >= SIZE_1080P.long || screenSize.short >= SIZE_1080P.short + val maxSize = if (hdScreen) SIZE_1080P else screenSize + + // If image format is provided, use it to determine supported sizes; else use target class + val config = characteristics.get( + CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!! + if (format == null) + assert(StreamConfigurationMap.isOutputSupportedFor(targetClass)) + else + assert(config.isOutputSupportedFor(format)) + val allSizes = if (format == null) + config.getOutputSizes(targetClass) else config.getOutputSizes(format) + + // Get available sizes and sort them by area from largest to smallest + val validSizes = allSizes + .sortedWith(compareBy { it.height * it.width }) + .map { SmartSize(it.width, it.height) }.reversed() + + // Then, get the largest output size that is smaller or equal than our max size + return validSizes.first { it.long <= maxSize.long && it.short <= maxSize.short }.size +} \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt b/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt new file mode 100644 index 0000000..561c14b --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/ExifUtils.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.util.Log +import androidx.exifinterface.media.ExifInterface + +private const val TAG: String = "ExifUtils" + +/** Transforms rotation and mirroring information into one of the [ExifInterface] constants */ +fun computeExifOrientation(rotationDegrees: Int, mirrored: Boolean) = when { + rotationDegrees == 0 && !mirrored -> ExifInterface.ORIENTATION_NORMAL + rotationDegrees == 0 && mirrored -> ExifInterface.ORIENTATION_FLIP_HORIZONTAL + rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180 + rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL + rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_TRANSVERSE + rotationDegrees == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90 + rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE + rotationDegrees == 270 && mirrored -> ExifInterface.ORIENTATION_ROTATE_270 + rotationDegrees == 270 && !mirrored -> ExifInterface.ORIENTATION_TRANSVERSE + else -> ExifInterface.ORIENTATION_UNDEFINED +} + +/** + * Helper function used to convert an EXIF orientation enum into a transformation matrix + * that can be applied to a bitmap. + * + * @return matrix - Transformation required to properly display [Bitmap] + */ +fun decodeExifOrientation(exifOrientation: Int): Matrix { + val matrix = Matrix() + + // Apply transformation corresponding to declared EXIF orientation + when (exifOrientation) { + ExifInterface.ORIENTATION_NORMAL -> Unit + ExifInterface.ORIENTATION_UNDEFINED -> Unit + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90F) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180F) + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270F) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1F, 1F) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1F, -1F) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.postScale(-1F, 1F) + matrix.postRotate(270F) + } + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.postScale(-1F, 1F) + matrix.postRotate(90F) + } + + // Error out if the EXIF orientation is invalid + else -> Log.e(TAG, "Invalid orientation: $exifOrientation") + } + + // Return the resulting matrix + return matrix +} diff --git a/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt b/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt new file mode 100644 index 0000000..a55af27 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/GenericListAdapter.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView + +/** Type helper used for the callback triggered once our view has been bound */ +typealias BindCallback = (view: View, data: T, position: Int) -> Unit + +/** List adapter for generic types, intended used for small-medium lists of data */ +class GenericListAdapter( + private val dataset: List, + private val itemLayoutId: Int? = null, + private val itemViewFactory: (() -> View)? = null, + private val onBind: BindCallback +) : RecyclerView.Adapter() { + + class GenericListViewHolder(val view: View) : RecyclerView.ViewHolder(view) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = GenericListViewHolder(when { + itemViewFactory != null -> itemViewFactory.invoke() + itemLayoutId != null -> { + LayoutInflater.from(parent.context) + .inflate(itemLayoutId, parent, false) + } + else -> { + throw IllegalStateException( + "Either the layout ID or the view factory need to be non-null") + } + }) + + override fun onBindViewHolder(holder: GenericListViewHolder, position: Int) { + if (position < 0 || position > dataset.size) return + onBind(holder.view, dataset[position], position) + } + + override fun getItemCount() = dataset.size +} \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt b/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt new file mode 100644 index 0000000..f9d9a47 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/OrientationLiveData.kt @@ -0,0 +1,95 @@ +/* + * Copyright 2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.content.Context +import android.hardware.camera2.CameraCharacteristics +import android.view.OrientationEventListener +import android.view.Surface +import androidx.lifecycle.LiveData + + +/** + * Calculates closest 90-degree orientation to compensate for the device + * rotation relative to sensor orientation, i.e., allows user to see camera + * frames with the expected orientation. + */ +class OrientationLiveData( + context: Context, + characteristics: CameraCharacteristics +): LiveData() { + + private val listener = object : OrientationEventListener(context.applicationContext) { + override fun onOrientationChanged(orientation: Int) { + val rotation = when { + orientation <= 45 -> Surface.ROTATION_0 + orientation <= 135 -> Surface.ROTATION_90 + orientation <= 225 -> Surface.ROTATION_180 + orientation <= 315 -> Surface.ROTATION_270 + else -> Surface.ROTATION_0 + } + val relative = computeRelativeRotation(characteristics, rotation) + if (relative != value) postValue(relative) + } + } + + override fun onActive() { + super.onActive() + listener.enable() + } + + override fun onInactive() { + super.onInactive() + listener.disable() + } + + companion object { + + /** + * Computes rotation required to transform from the camera sensor orientation to the + * device's current orientation in degrees. + * + * @param characteristics the [CameraCharacteristics] to query for the sensor orientation. + * @param surfaceRotation the current device orientation as a Surface constant + * @return the relative rotation from the camera sensor to the current device orientation. + */ + @JvmStatic + private fun computeRelativeRotation( + characteristics: CameraCharacteristics, + surfaceRotation: Int + ): Int { + val sensorOrientationDegrees = + characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!! + + val deviceOrientationDegrees = when (surfaceRotation) { + Surface.ROTATION_0 -> 0 + Surface.ROTATION_90 -> 90 + Surface.ROTATION_180 -> 180 + Surface.ROTATION_270 -> 270 + else -> 0 + } + + // Reverse device orientation for front-facing cameras + val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) == + CameraCharacteristics.LENS_FACING_FRONT) 1 else -1 + + // Calculate desired JPEG orientation relative to camera orientation to make + // the image upright relative to the device orientation + return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360 + } + } +} diff --git a/utils/src/main/java/com/example/android/camera/utils/Yuv.kt b/utils/src/main/java/com/example/android/camera/utils/Yuv.kt new file mode 100644 index 0000000..c476ad0 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/Yuv.kt @@ -0,0 +1,191 @@ +package com.example.android.camera.utils + +import android.graphics.ImageFormat +import android.media.Image +import androidx.annotation.IntDef +import java.nio.ByteBuffer + +/* +This file is converted from part of https://github.com/gordinmitya/yuv2buf. +Follow the link to find demo app, performance benchmarks and unit tests. + +Intro to YUV image formats: +YUV_420_888 - is a generic format that can be represented as I420, YV12, NV21, and NV12. +420 means that for each 4 luminosity pixels we have 2 chroma pixels: U and V. + +* I420 format represents an image as Y plane followed by U then followed by V plane + without chroma channels interleaving. + For example: + Y Y Y Y + Y Y Y Y + U U V V + +* NV21 format represents an image as Y plane followed by V and U interleaved. First V then U. + For example: + Y Y Y Y + Y Y Y Y + V U V U + +* YV12 and NV12 are the same as previous formats but with swapped order of V and U. (U then V) + +Visualization of these 4 formats: +https://user-images.githubusercontent.com/9286092/89119601-4f6f8100-d4b8-11ea-9a51-2765f7e513c2.jpg + +It's guaranteed that image.getPlanes() always returns planes in order Y U V for YUV_420_888. +https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + +Because I420 and NV21 are more widely supported (RenderScript, OpenCV, MNN) +the conversion is done into these formats. + +More about each format: https://www.fourcc.org/yuv.php +*/ + +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) +@IntDef(ImageFormat.NV21, ImageFormat.YUV_420_888) +annotation class YuvType + +class YuvByteBuffer(image: Image, dstBuffer: ByteBuffer? = null) { + @YuvType + val type: Int + val buffer: ByteBuffer + + init { + val wrappedImage = ImageWrapper(image) + + type = if (wrappedImage.u.pixelStride == 1) { + ImageFormat.YUV_420_888 + } else { + ImageFormat.NV21 + } + val size = image.width * image.height * 3 / 2 + buffer = if ( + dstBuffer == null || dstBuffer.capacity() < size || + dstBuffer.isReadOnly || !dstBuffer.isDirect + ) { + ByteBuffer.allocateDirect(size) } + else { + dstBuffer + } + buffer.rewind() + + removePadding(wrappedImage) + } + + // Input buffers are always direct as described in + // https://developer.android.com/reference/android/media/Image.Plane#getBuffer() + private fun removePadding(image: ImageWrapper) { + val sizeLuma = image.y.width * image.y.height + val sizeChroma = image.u.width * image.u.height + if (image.y.rowStride > image.y.width) { + removePaddingCompact(image.y, buffer, 0) + } else { + buffer.position(0) + buffer.put(image.y.buffer) + } + if (type == ImageFormat.YUV_420_888) { + if (image.u.rowStride > image.u.width) { + removePaddingCompact(image.u, buffer, sizeLuma) + removePaddingCompact(image.v, buffer, sizeLuma + sizeChroma) + } else { + buffer.position(sizeLuma) + buffer.put(image.u.buffer) + buffer.position(sizeLuma + sizeChroma) + buffer.put(image.v.buffer) + } + } else { + if (image.u.rowStride > image.u.width * 2) { + removePaddingNotCompact(image, buffer, sizeLuma) + } else { + buffer.position(sizeLuma) + var uv = image.v.buffer + val properUVSize = image.v.height * image.v.rowStride - 1 + if (uv.capacity() > properUVSize) { + uv = clipBuffer(image.v.buffer, 0, properUVSize) + } + buffer.put(uv) + val lastOne = image.u.buffer[image.u.buffer.capacity() - 1] + buffer.put(buffer.capacity() - 1, lastOne) + } + } + buffer.rewind() + } + + private fun removePaddingCompact( + plane: PlaneWrapper, + dst: ByteBuffer, + offset: Int + ) { + require(plane.pixelStride == 1) { + "use removePaddingCompact with pixelStride == 1" + } + + val src = plane.buffer + val rowStride = plane.rowStride + var row: ByteBuffer + dst.position(offset) + for (i in 0 until plane.height) { + row = clipBuffer(src, i * rowStride, plane.width) + dst.put(row) + } + } + + private fun removePaddingNotCompact( + image: ImageWrapper, + dst: ByteBuffer, + offset: Int + ) { + require(image.u.pixelStride == 2) { + "use removePaddingNotCompact pixelStride == 2" + } + val width = image.u.width + val height = image.u.height + val rowStride = image.u.rowStride + var row: ByteBuffer + dst.position(offset) + for (i in 0 until height - 1) { + row = clipBuffer(image.v.buffer, i * rowStride, width * 2) + dst.put(row) + } + row = clipBuffer(image.u.buffer, (height - 1) * rowStride - 1, width * 2) + dst.put(row) + } + + private fun clipBuffer(buffer: ByteBuffer, start: Int, size: Int): ByteBuffer { + val duplicate = buffer.duplicate() + duplicate.position(start) + duplicate.limit(start + size) + return duplicate.slice() + } + + private class ImageWrapper(image:Image) { + val width= image.width + val height = image.height + val y = PlaneWrapper(width, height, image.planes[0]) + val u = PlaneWrapper(width / 2, height / 2, image.planes[1]) + val v = PlaneWrapper(width / 2, height / 2, image.planes[2]) + + // Check this is a supported image format + // https://developer.android.com/reference/android/graphics/ImageFormat#YUV_420_888 + init { + require(y.pixelStride == 1) { + "Pixel stride for Y plane must be 1 but got ${y.pixelStride} instead." + } + require(u.pixelStride == v.pixelStride && u.rowStride == v.rowStride) { + "U and V planes must have the same pixel and row strides " + + "but got pixel=${u.pixelStride} row=${u.rowStride} for U " + + "and pixel=${v.pixelStride} and row=${v.rowStride} for V" + } + require(u.pixelStride == 1 || u.pixelStride == 2) { + "Supported" + " pixel strides for U and V planes are 1 and 2" + } + } + } + + private class PlaneWrapper(width: Int, height: Int, plane: Image.Plane) { + val width = width + val height = height + val buffer: ByteBuffer = plane.buffer + val rowStride = plane.rowStride + val pixelStride = plane.pixelStride + } +} \ No newline at end of file diff --git a/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt b/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt new file mode 100644 index 0000000..8dcd559 --- /dev/null +++ b/utils/src/main/java/com/example/android/camera/utils/YuvToRgbConverter.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2020 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.android.camera.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.media.Image +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicYuvToRGB +import android.renderscript.Type +import java.nio.ByteBuffer + +/** + * Helper class used to convert a [Image] object from + * [ImageFormat.YUV_420_888] format to an RGB [Bitmap] object, it has equivalent + * functionality to https://github + * .com/androidx/androidx/blob/androidx-main/camera/camera-core/src/main/java/androidx/camera/core/ImageYuvToRgbConverter.java + * + * NOTE: This has been tested in a limited number of devices and is not + * considered production-ready code. It was created for illustration purposes, + * since this is not an efficient camera pipeline due to the multiple copies + * required to convert each frame. For example, this + * implementation + * (https://stackoverflow.com/questions/52726002/camera2-captured-picture-conversion-from-yuv-420-888-to-nv21/52740776#52740776) + * might have better performance. + */ +class YuvToRgbConverter(context: Context) { + private val rs = RenderScript.create(context) + private val scriptYuvToRgb = + ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) + + // Do not add getters/setters functions to these private variables + // because yuvToRgb() assume they won't be modified elsewhere + private var yuvBits: ByteBuffer? = null + private var bytes: ByteArray = ByteArray(0) + private var inputAllocation: Allocation? = null + private var outputAllocation: Allocation? = null + + @Synchronized + fun yuvToRgb(image: Image, output: Bitmap) { + val yuvBuffer = YuvByteBuffer(image, yuvBits) + yuvBits = yuvBuffer.buffer + + if (needCreateAllocations(image, yuvBuffer)) { + val yuvType = Type.Builder(rs, Element.U8(rs)) + .setX(image.width) + .setY(image.height) + .setYuvFormat(yuvBuffer.type) + inputAllocation = Allocation.createTyped( + rs, + yuvType.create(), + Allocation.USAGE_SCRIPT + ) + bytes = ByteArray(yuvBuffer.buffer.capacity()) + val rgbaType = Type.Builder(rs, Element.RGBA_8888(rs)) + .setX(image.width) + .setY(image.height) + outputAllocation = Allocation.createTyped( + rs, + rgbaType.create(), + Allocation.USAGE_SCRIPT + ) + } + + yuvBuffer.buffer.get(bytes) + inputAllocation!!.copyFrom(bytes) + + // Convert NV21 or YUV_420_888 format to RGB + inputAllocation!!.copyFrom(bytes) + scriptYuvToRgb.setInput(inputAllocation) + scriptYuvToRgb.forEach(outputAllocation) + outputAllocation!!.copyTo(output) + } + + private fun needCreateAllocations(image: Image, yuvBuffer: YuvByteBuffer): Boolean { + return (inputAllocation == null || // the very 1st call + inputAllocation!!.type.x != image.width || // image size changed + inputAllocation!!.type.y != image.height || + inputAllocation!!.type.yuv != yuvBuffer.type || // image format changed + bytes.size == yuvBuffer.buffer.capacity()) + } +} diff --git a/utils/src/main/res/drawable/ic_shutter.xml b/utils/src/main/res/drawable/ic_shutter.xml new file mode 100644 index 0000000..9bb91ab --- /dev/null +++ b/utils/src/main/res/drawable/ic_shutter.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/utils/src/main/res/drawable/ic_shutter_focused.xml b/utils/src/main/res/drawable/ic_shutter_focused.xml new file mode 100644 index 0000000..9bf521d --- /dev/null +++ b/utils/src/main/res/drawable/ic_shutter_focused.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/utils/src/main/res/drawable/ic_shutter_normal.xml b/utils/src/main/res/drawable/ic_shutter_normal.xml new file mode 100644 index 0000000..cb50026 --- /dev/null +++ b/utils/src/main/res/drawable/ic_shutter_normal.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/utils/src/main/res/drawable/ic_shutter_pressed.xml b/utils/src/main/res/drawable/ic_shutter_pressed.xml new file mode 100644 index 0000000..9bf521d --- /dev/null +++ b/utils/src/main/res/drawable/ic_shutter_pressed.xml @@ -0,0 +1,28 @@ + + + + + + -- cgit v1.2.3