From 10db8b5bac6285f611b861a11371c00870fae55b Mon Sep 17 00:00:00 2001 From: Brian Nenninger Date: Mon, 9 Oct 2017 22:58:21 -0400 Subject: [PATCH 01/93] Initial commit --- app/.gitignore | 1 + app/CMakeLists.txt | 44 ++++ app/build.gradle | 45 ++++ app/proguard-rules.pro | 21 ++ .../boojiecam/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 30 +++ app/src/main/cpp/native-lib.cpp | 58 +++++ .../boojiecam/CameraImage.kt | 22 ++ .../boojiecam/CameraImageGenerator.kt | 243 ++++++++++++++++++ .../boojiecam/CameraImageProcessor.kt | 147 +++++++++++ .../boojiecam/CameraSelector.kt | 30 +++ .../boojiecam/CameraStatus.kt | 20 ++ .../boojiecam/EdgeColorImageProcessor.kt | 96 +++++++ .../boojiecam/EdgeImageProcessor.kt | 143 +++++++++++ .../boojiecam/GrayscaleImageGenerator.kt | 26 ++ .../boojiecam/ImageOrientation.kt | 6 + .../boojiecam/LifeBitmapGenerator.kt | 111 ++++++++ .../boojiecam/MainActivity.kt | 219 ++++++++++++++++ .../boojiecam/OverlayView.kt | 60 +++++ .../boojiecam/PermissionsChecker.kt | 56 ++++ .../boojiecam/PhotoLibrary.kt | 65 +++++ .../boojiecam/PlanarImage.kt | 55 ++++ .../boojiecam/ProcessedBitmap.kt | 10 + .../boojiecam/WireframeColorScheme.kt | 15 ++ .../com/dozingcatsoftware/boojiecam/utils.kt | 56 ++++ .../res/drawable/ic_launcher_background.xml | 113 ++++++++ app/src/main/res/layout/activity_main.xml | 39 +++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3358 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 5117 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5084 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2386 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2652 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3179 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4648 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 7011 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7381 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7008 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 14578 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11545 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9442 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 21908 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16109 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 11 + .../boojiecam/ExampleUnitTest.kt | 17 ++ build.gradle | 27 ++ gradle.properties | 17 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 ++++++++++++ gradlew.bat | 90 +++++++ settings.gradle | 1 + 55 files changed, 2103 insertions(+) create mode 100644 app/.gitignore create mode 100644 app/CMakeLists.txt create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/dozingcatsoftware/boojiecam/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/cpp/native-lib.cpp create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImage.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageGenerator.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageProcessor.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/CameraSelector.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/CameraStatus.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeColorImageProcessor.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeImageProcessor.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/GrayscaleImageGenerator.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/ImageOrientation.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/LifeBitmapGenerator.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/MainActivity.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/OverlayView.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/PermissionsChecker.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/PhotoLibrary.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/PlanarImage.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/ProcessedBitmap.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/WireframeColorScheme.kt create mode 100644 app/src/main/java/com/dozingcatsoftware/boojiecam/utils.kt create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/com/dozingcatsoftware/boojiecam/ExampleUnitTest.kt 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 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt new file mode 100644 index 0000000..f8e6e8b --- /dev/null +++ b/app/CMakeLists.txt @@ -0,0 +1,44 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. + +cmake_minimum_required(VERSION 3.4.1) + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +add_library( # Sets the name of the library. + native-lib + + # Sets the library as a shared library. + SHARED + + # Provides a relative path to your source file(s). + src/main/cpp/native-lib.cpp ) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log ) + +# Specifies libraries CMake should link to your target library. You +# can link multiple libraries, such as libraries you define in this +# build script, prebuilt third-party libraries, or system libraries. + +target_link_libraries( # Specifies the target library. + native-lib + + # Links the target library to the log library + # included in the NDK. + ${log-lib} ) \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..084e92b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,45 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 26 + buildToolsVersion '26.0.2' + defaultConfig { + applicationId "com.dozingcatsoftware.boojiecam" + minSdkVersion 21 + targetSdkVersion 26 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + cppFlags "-O3" + } + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + path 'CMakeLists.txt' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.android.support:appcompat-v7:26.1.0' + implementation 'com.android.support.constraint:constraint-layout:1.0.2' + testImplementation 'junit:junit:4.12' + androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/com/dozingcatsoftware/boojiecam/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/dozingcatsoftware/boojiecam/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..02f2e41 --- /dev/null +++ b/app/src/androidTest/java/com/dozingcatsoftware/boojiecam/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.dozingcatsoftware.boojiecam + +import android.support.test.InstrumentationRegistry +import android.support.test.runner.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getTargetContext() + assertEquals("com.dozingcatsoftware.boojiecam", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3bc57ba --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp new file mode 100644 index 0000000..d265db0 --- /dev/null +++ b/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,58 @@ +#include +#include + +extern "C" +JNIEXPORT jstring +JNICALL +Java_com_dozingcatsoftware_boojiecam_MainActivity_stringFromJNI( + JNIEnv *env, + jobject /* this */) { + std::string hello = "Hello from C++"; + return env->NewStringUTF(hello.c_str()); +} + +static inline int32_t toUInt(jbyte jb) { + return jb & 0xff; +} + +extern "C" +JNIEXPORT void +JNICALL +Java_com_dozingcatsoftware_boojiecam_WireframeImageGenerator_computeEdgesNative( + JNIEnv *env, jobject thiz, + jbyteArray jbright, jint width, jint height, jint minRow, jint maxRow, + jintArray jcolorTable, jintArray joutput) { + jbyte *bright = env->GetByteArrayElements(jbright, 0); + jint *colorTable = env->GetIntArrayElements(jcolorTable, 0); + jint *output = env->GetIntArrayElements(joutput, 0); + + for (int32_t y = minRow; y < maxRow; y++) { + if (y == 0 || y == height - 1) { + int32_t minOffset = y * width; + int32_t maxOffset = minOffset + width; + for (int32_t i = minOffset; i < maxOffset; i++) { + output[i] = colorTable[0]; + } + } + else { + int32_t minIndex = y * width + 1; + int32_t maxIndex = minIndex + width - 2; + output[minIndex - 1] = colorTable[0]; + for (int32_t index = minIndex; index < maxIndex; index++) { + int32_t up = index - width; + int32_t down = index + width; + int32_t edgeStrength = 8 * toUInt(bright[index]) - ( + toUInt(bright[up-1]) + toUInt(bright[up]) + toUInt(bright[up+1]) + + toUInt(bright[index-1]) + toUInt(bright[index+1]) + + toUInt(bright[down-1]) + toUInt(bright[down]) + toUInt(bright[down+1])); + int32_t b = std::min(255, std::max(0, 2 * (edgeStrength >= 0 ? edgeStrength : -edgeStrength))); + output[index] = colorTable[b]; + } + output[maxIndex] = colorTable[0]; + } + } + + env->ReleaseByteArrayElements(jbright, bright, 0); + env->ReleaseIntArrayElements(jcolorTable, colorTable, 0); + env->ReleaseIntArrayElements(joutput, output, 0); +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImage.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImage.kt new file mode 100644 index 0000000..4cc34d7 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImage.kt @@ -0,0 +1,22 @@ +package com.dozingcatsoftware.boojiecam + +import android.media.Image +import android.util.Log + +/** + * Created by brian on 9/30/17. + */ +data class CameraImage(val image: PlanarImage, val orientation: ImageOrientation, + val status: CameraStatus, val timestamp: Long) { + var isClosed = false + + fun close() { + if (isClosed) { + Log.w("CameraImage", "Image is already closed!") + } + else { + image.close() + isClosed = true + } + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageGenerator.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageGenerator.kt new file mode 100644 index 0000000..012082f --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageGenerator.kt @@ -0,0 +1,243 @@ +package com.dozingcatsoftware.boojiecam + +import android.content.Context +import android.graphics.ImageFormat +import android.hardware.camera2.* +import android.media.ImageReader +import android.os.Handler +import android.util.Log +import android.util.Size + +/** + * Created by brian on 9/20/17. + */ +class CameraImageGenerator(val context: Context, + val cameraManager: CameraManager, val cameraId: String, + val timestampFn: () -> Long = System::currentTimeMillis) { + + + private var camera: CameraDevice? = null + private var imageReader: ImageReader? = null + private var captureSession: CameraCaptureSession? = null + private var captureSize: Size? = null + var status = CameraStatus.CLOSED + private var targetStatus = CameraStatus.CLOSED + private var imageCallback: ((CameraImage) -> Unit)? = null + private var handler = Handler() + + private val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) + + // https://stackoverflow.com/questions/33902832/upside-down-camera-preview-byte-array + // https://www.reddit.com/r/Android/comments/3rjbo8/nexus5x_marshmallow_camera_problem/cwqzqgh + private val imageOrientation = { + val facing = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + val isFrontFacing = (facing == CameraMetadata.LENS_FACING_FRONT) + val orientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) + if (isFrontFacing && orientation == 90 || !isFrontFacing && orientation == 270) { + ImageOrientation.ROTATED_180 + } + else { + ImageOrientation.NORMAL + } + }() + + fun isCapturing(): Boolean { + return this.status.isCapturing() + } + + /* + private val cameraSize = pickBestSize( + cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + .getOutputSizes(ImageFormat.YUV_420_888), + imageWidth, imageHeight) + */ + + fun start(targetStatus: CameraStatus, targetSize: Size, callback: (CameraImage) -> Unit) { + this.captureSize = pickBestSize( + cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) + .getOutputSizes(ImageFormat.YUV_420_888), + targetSize) + this.targetStatus = targetStatus + this.imageCallback = callback + + if (this.status.isCapturing()) { + Log.i(TAG, "Restarting capture") + captureSession!!.close() + } + else { + updateStatus(this.status) + } + } + + fun stop() { + this.targetStatus = CameraStatus.CLOSED + updateStatus(this.status) + } + + private fun updateStatus(status: CameraStatus) { + Log.i(TAG, "CameraStatus change: " + status) + this.status = status + + if (this.targetStatus == CameraStatus.CLOSED) { + if (this.status.isCapturing()) { + this.status = CameraStatus.CLOSING + stopCaptureSession() + } + } + else if (this.targetStatus.isCapturing()) { + when (this.status) { + // The status never actually gets set to CLOSED? + CameraStatus.CLOSED, + CameraStatus.CLOSING -> { + openCamera() + } + CameraStatus.OPENED -> { + setupCameraPreview() + } + CameraStatus.CAPTURE_READY -> { + startCapture() + } + CameraStatus.ERROR -> { + Log.i(TAG, "Trying to recover from error") + handler.postDelayed(this::openCamera, 1000) + } + else -> { + + } + } + } + } + + fun openCamera() { + val manager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + stopCamera() + try { + updateStatus(CameraStatus.OPENING) + manager.openCamera(cameraId, object : CameraDevice.StateCallback() { + override fun onOpened(cam: CameraDevice) { + camera = cam + updateStatus(CameraStatus.OPENED) + } + + override fun onDisconnected(cam: CameraDevice) { + updateStatus(CameraStatus.CLOSED) + } + + override fun onError(camera: CameraDevice, error: Int) { + updateStatus(CameraStatus.ERROR) + } + }, null) + } + catch (ex: SecurityException) { + throw ex + } + } + + private fun setupCameraPreview() { + try { + val size = this.captureSize!! + Log.i(TAG, "Using camera size: " + size) + imageReader = ImageReader.newInstance(size.width, size.height, + ImageFormat.YUV_420_888, 4) + imageReader!!.setOnImageAvailableListener({ reader -> + val image = try { + reader.acquireLatestImage() + } catch (ex: IllegalStateException) { + Log.w(TAG, "max images already acquired") + null + } + if (image != null) { + if (captureSession != null) { + this.imageCallback!!(CameraImage( + PlanarImage.fromMediaImage(image), + imageOrientation, + this.status, + this.timestampFn())) + } + else { + Log.i(TAG, "captureSession is null, closing image") + image.close() + } + } + }, null) + + camera!!.createCaptureSession( + listOf(imageReader!!.surface), + object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + captureSession = session + updateStatus(CameraStatus.CAPTURE_READY) + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + updateStatus(CameraStatus.ERROR) + } + + override fun onClosed(session: CameraCaptureSession?) { + super.onClosed(session) + updateStatus(CameraStatus.OPENED) + } + }, + null) + } + catch (ex: CameraAccessException) { + Log.e(TAG, "setupCameraPreview error", ex) + } + } + + private fun startCapture() { + val request = when(this.targetStatus) { + CameraStatus.CAPTURING_PREVIEW -> + camera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + CameraStatus.CAPTURING_PHOTO -> + camera!!.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) + CameraStatus.CAPTURING_VIDEO -> + camera!!.createCaptureRequest(CameraDevice.TEMPLATE_RECORD) + else -> { + throw IllegalStateException("Invalid status: " + this.status) + } + } + request.addTarget(imageReader!!.surface) + if (this.targetStatus == CameraStatus.CAPTURING_PHOTO) { + captureSession!!.capture(request.build(), null, null) + } + else { + captureSession!!.setRepeatingRequest(request.build(), null, null) + } + this.status = this.targetStatus + } + + private fun stopCaptureSession() { + Log.i(TAG, "stopCameraSession") + captureSession?.close() + captureSession = null + imageReader?.close() + imageReader = null + } + + private fun stopCamera() { + Log.i(TAG, "stopCamera") + stopCaptureSession() + camera?.close() + camera = null + } + + companion object { + val TAG = "CameraImageGenerator" + + fun pickBestSize(sizes: Array, target: Size): Size { + fun differenceFromRequested(s: Size) = + Math.abs(s.width - target.width) + Math.abs(s.height - target.height) + var bestSize = sizes[0] + var bestDiff = differenceFromRequested(sizes[0]) + for (i in 1 until sizes.size) { + val diff = differenceFromRequested(sizes[i]) + if (diff < bestDiff) { + bestSize = sizes[i] + bestDiff = diff + } + } + return bestSize + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageProcessor.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageProcessor.kt new file mode 100644 index 0000000..55ceca9 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraImageProcessor.kt @@ -0,0 +1,147 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.* +import android.media.Image +import android.util.Log +import java.io.ByteArrayOutputStream +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +abstract class CameraImageProcessor { + private var consumerThread: Thread? = null + private var nextImage: CameraImage? = null + private val threadLock = ReentrantLock() + private val imageLock = ReentrantLock() + private val imageAvailable = imageLock.newCondition() + protected val maxThreads = Math.min(Runtime.getRuntime().availableProcessors(), 4) + + fun start(callback: (ProcessedBitmap) -> Unit) { + threadLock.withLock({ + if (consumerThread == null) { + consumerThread = Thread({this.threadEntry(callback)}) + consumerThread!!.start() + } + }) + + } + + fun pause() { + debugLog("CameraImageProcessor.pause") + threadLock.withLock({ + consumerThread = null + }) + imageLock.withLock({ + nextImage?.close() + nextImage = null + }) + } + + fun queueImage(image: CameraImage) { + threadLock.withLock({ + if (consumerThread == null) { + image.close() + return + } + }) + imageLock.withLock({ + debugLog("Setting image: " + image.hashCode()) + if (nextImage != null) { + debugLog("Closing previous: " + nextImage!!.hashCode()) + nextImage!!.close() + nextImage = image + } + else { + nextImage = image + imageAvailable.signal() + } + }) + } + + private fun shouldCurrentThreadContinue(): Boolean { + return threadLock.withLock({ + Thread.currentThread() == consumerThread + }) + } + + private fun threadEntry(callback: (ProcessedBitmap) -> Unit) { + debugLog("Thread started") + var image: CameraImage? = null + while (true) { + while (image == null) { + if (!shouldCurrentThreadContinue()) { + return + } + imageLock.withLock({ + image = nextImage + if (image == null) { + debugLog("Waiting for image") + imageAvailable.awaitNanos(250000000) + } + else { + debugLog("Got image: " + image!!.hashCode()) + nextImage = null + } + }) + } + if (!shouldCurrentThreadContinue()) { + debugLog("Closing image before thread exits: " + image!!.hashCode()) + image!!.close() + image = null + return + } + try { + val bitmap = createBitmapFromImage(image!!.image) + val backgroundPaintFn = createPaintFn(image!!.image) + image!!.close() + callback(ProcessedBitmap(image!!, bitmap, backgroundPaintFn)) + image = null + } + catch (ex: Exception) { + Log.e(TAG, "Error with image: " + image?.hashCode(), ex) + if (ex is IllegalStateException) { + // Terrible, but images keep getting closed out from under us. + } + else { + throw ex + } + } + finally { + if (image != null) { + debugLog("Closing image after processing: " + (image?.hashCode())) + image?.close() + image = null + } + } + } + } + + abstract fun createBitmapFromImage(image: PlanarImage): Bitmap + + open fun createPaintFn(image: PlanarImage): (RectF) -> Paint? { + return {null} + } + + companion object { + val TAG = "CameraImageProcessor" + var DEBUG = false + var TIMING = false + + inline fun toUInt(b: Byte): Int { + return b.toInt() and 0xff + } + + fun debugLog(msg: String) { + if (DEBUG) { + Log.i(TAG, msg) + } + } + + fun timingLog(msg: String) { + if (TIMING) { + Log.i(TAG, msg) + } + } + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraSelector.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraSelector.kt new file mode 100644 index 0000000..1f56f2d --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraSelector.kt @@ -0,0 +1,30 @@ +package com.dozingcatsoftware.boojiecam + +import android.content.Context +import android.hardware.camera2.CameraManager + + +enum class ImageSize { + FULL_SCREEN, + HALF_SCREEN, + VIDEO_RECORDING, +} + +/** + * Created by brian on 9/30/17. + */ +class CameraSelector(val context: Context) { + + private val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + private val cameraIds = cameraManager.cameraIdList + val cameraCount = cameraIds.size + var selectedCameraIndex = 0 + + fun selectNextCamera() { + selectedCameraIndex = (1 + selectedCameraIndex) % cameraCount + } + + fun createImageGenerator(): CameraImageGenerator { + return CameraImageGenerator(context, cameraManager, cameraIds[selectedCameraIndex]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraStatus.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraStatus.kt new file mode 100644 index 0000000..c824f88 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/CameraStatus.kt @@ -0,0 +1,20 @@ +package com.dozingcatsoftware.boojiecam + +enum class CameraStatus { + CLOSED, + OPENING, + OPENED, + CAPTURE_READY, + CAPTURING_PREVIEW, + CAPTURING_PHOTO, + CAPTURING_VIDEO, + STOPPING_CAPTURE, + CLOSING, + ERROR, + ; + + fun isCapturing(): Boolean { + return this == CAPTURING_PREVIEW || this == CAPTURING_PHOTO || + this == CAPTURING_VIDEO + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeColorImageProcessor.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeColorImageProcessor.kt new file mode 100644 index 0000000..da94ea8 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeColorImageProcessor.kt @@ -0,0 +1,96 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.* +import android.media.Image +import android.util.Log +import java.io.ByteArrayOutputStream +import java.util.concurrent.Callable +import java.util.concurrent.Executors + +class EdgeColorImageProcessor: CameraImageProcessor() { + private val threadPool = Executors.newFixedThreadPool(maxThreads) + + override fun createBitmapFromImage(image: PlanarImage): Bitmap { + val t1 = System.currentTimeMillis() + val width = image.width + val height = image.height + + val bright = ByteArray(image.planes[0].buffer.capacity()) + val uBytes = ByteArray(image.planes[1].buffer.capacity()) + val vBytes = ByteArray(image.planes[2].buffer.capacity()) + image.planes[0].buffer.get(bright) + image.planes[1].buffer.get(uBytes) + image.planes[2].buffer.get(vBytes) + + val yRowStride = image.planes[0].rowStride + val uRowStride = image.planes[1].rowStride + val uPixelStride = image.planes[1].pixelStride + val edgeYuv = ByteArray(bright.size * 3 / 2) + + val tasks = mutableListOf>() + for (i in 0 until maxThreads) { + val minRow = height * i / maxThreads + val maxRow = height * (i + 1) / maxThreads + tasks.add(Callable {computeEdges(bright, uBytes, vBytes, width, height, minRow, maxRow, + yRowStride, uRowStride, uPixelStride, edgeYuv)}) + } + val t2 = System.currentTimeMillis() + threadPool.invokeAll(tasks) + val t3 = System.currentTimeMillis() + + val yuvImage = YuvImage(edgeYuv, ImageFormat.NV21, width, height, null) + val outStream = ByteArrayOutputStream() + yuvImage.compressToJpeg(Rect(0, 0, width, height), 90, outStream) + val edgeBitmap = BitmapFactory.decodeByteArray(outStream.toByteArray(), 0, outStream.size()) + val t4 = System.currentTimeMillis() + timingLog("Created edge bitmap: " + (t2-t1) + " " + (t3-t2) + " " + (t4-t3)) + return edgeBitmap + } + + private fun computeEdges(bright: ByteArray, uBytes: ByteArray, vBytes: ByteArray, + width: Int, height: Int, minRow: Int, maxRow: Int, + yRowStride: Int, uRowStride: Int, uPixelStride: Int, + edgeYuv: ByteArray) { + val multiplier = minOf(4, maxOf(2, Math.round(width / 480f))) + // Convert Y channel (luminance) to edge strength, leave U and V channels unmodified. + // This causes solid areas to keep their color but be shifted darker. + // http://softpixel.com/~cwright/programming/colorspace/yuv/ + var yuvIndex = minRow * width + for (y in minRow until maxRow) { + if (y == 0 || y == height - 1) { + for (i in 0 until width) { + edgeYuv[yuvIndex++] = 0 + } + } + else { + val minIndex = y * yRowStride + 1 + val maxIndex = minIndex + width - 2 + edgeYuv[yuvIndex++] = 0 + for (yIndex in minIndex until maxIndex) { + var up = yIndex - yRowStride + var down = yIndex + yRowStride + var edgeStrength = 8 * toUInt(bright[yIndex]) - ( + toUInt(bright[up-1]) + toUInt(bright[up]) + toUInt(bright[up+1]) + + toUInt(bright[yIndex-1]) + toUInt(bright[yIndex+1]) + + toUInt(bright[down-1]) + toUInt(bright[down]) + toUInt(bright[down+1])) + edgeYuv[yuvIndex++] = minOf(255, maxOf(0, multiplier * edgeStrength)).toByte() + } + edgeYuv[yuvIndex++] = 0 + } + } + + var minUVRow = minRow / 2 + (minRow % 2) + var maxUVRow = maxRow / 2 + // Interleaved? + val uvWidth = width / 2 + yuvIndex = width * height + 2 * minUVRow * uvWidth + for (y in minUVRow until maxUVRow) { + var uvIndex = y * uRowStride + for (x in 0 until uvWidth) { + edgeYuv[yuvIndex++] = vBytes[uvIndex] + edgeYuv[yuvIndex++] = uBytes[uvIndex] + uvIndex += uPixelStride + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeImageProcessor.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeImageProcessor.kt new file mode 100644 index 0000000..cdd8db5 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/EdgeImageProcessor.kt @@ -0,0 +1,143 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.* +import android.media.Image +import java.util.concurrent.Callable +import java.util.concurrent.Executors + +/** + * Created by brian on 10/1/17. + */ +class EdgeImageProcessor(private val colorTable: IntArray, + private val paintFn: (PlanarImage, RectF) -> Paint?) : + CameraImageProcessor() { + private val threadPool = Executors.newFixedThreadPool(maxThreads) + private var resultBitmap: Bitmap? = null + + override fun createBitmapFromImage(image: PlanarImage): Bitmap { + val t1 = System.currentTimeMillis() + if (resultBitmap == null || + resultBitmap!!.width != image.width || resultBitmap!!.height != image.height) { + resultBitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888) + } + val width = image.width + val height = image.height + val bright = ByteArray(image.planes[0].buffer.capacity()) + image.planes[0].buffer.get(bright) + val rowStride = image.planes[0].rowStride + val pixels = IntArray(width * height) + + val tasks = mutableListOf>() + for (i in 0 until maxThreads) { + val minRow = height * i / maxThreads + val maxRow = height * (i + 1) / maxThreads + tasks.add(Callable {computeEdges(bright, width, height, minRow, maxRow, rowStride, colorTable, pixels)}) + } + threadPool.invokeAll(tasks) + + val t2 = System.currentTimeMillis() + resultBitmap!!.setPixels(pixels, 0, width, 0, 0, width, height) + val t3 = System.currentTimeMillis() + timingLog("Created edge bitmap: " + (t2-t1) + " " + (t3-t2)) + return resultBitmap!! + } + + override fun createPaintFn(image: PlanarImage): (RectF) -> Paint? { + return {rect -> paintFn(image, rect)} + } + + private fun computeEdges(bright: ByteArray, width: Int, height: Int, minRow: Int, maxRow: Int, + rowStride: Int, colorTable: IntArray, pixels: IntArray) { + val multiplier = minOf(4, maxOf(2, Math.round(width / 480f))) + for (y in minRow until maxRow) { + if (y == 0 || y == height - 1) { + val pixOffset = y * width + for (i in pixOffset until pixOffset + width) { + pixels[i] = colorTable[0] + } + } + else { + // Changing the center multiplier from 8 to 9 gives an edge sharpening effect. + // (If the center and its neighbors are equal, it will be unchanged rather than 0). + // Taking the absolute value of edge strength makes the edge lines thicker (because + // both dark->light and light->dark will have high values). + val minBrightIndex = y * rowStride + 1 + val maxBrightIndex = minBrightIndex + width - 2 + var pixOffset = y * width + pixels[pixOffset++] = colorTable[0] + for (index in minBrightIndex until maxBrightIndex) { + var up = index - rowStride + var down = index + rowStride + var edgeStrength = 8 * toUInt(bright[index]) - ( + toUInt(bright[up-1]) + toUInt(bright[up]) + toUInt(bright[up+1]) + + toUInt(bright[index-1]) + toUInt(bright[index+1]) + + toUInt(bright[down-1]) + toUInt(bright[down]) + toUInt(bright[down+1])) + val b = Math.min(255, Math.max(0, multiplier * edgeStrength)) + pixels[pixOffset++] = colorTable[b] + } + pixels[pixOffset++] = colorTable[0] + } + } + } + + external private fun computeEdgesNative(bright: ByteArray, + width: Int, height: Int, minRow: Int, maxRow: Int, + colorTable: IntArray, pixels: IntArray) + + companion object { + fun withFixedColors(minEdgeColor: Int, maxEdgeColor: Int): EdgeImageProcessor { + return EdgeImageProcessor(makeRangeColorMap(minEdgeColor, maxEdgeColor), {_, _ -> null}) + } + + fun withLinearGradient(minEdgeColor: Int, gradientStartColor: Int, gradientEndColor: Int) + : EdgeImageProcessor { + val paintFn = fun(image: PlanarImage, rect: RectF): Paint { + val p = Paint() + p.shader = LinearGradient( + rect.left, rect.top, rect.right, rect.bottom, + addAlpha(gradientStartColor), addAlpha(gradientEndColor), + Shader.TileMode.MIRROR) + return p + } + return EdgeImageProcessor(makeAlphaColorMap(minEdgeColor), paintFn) + } + + fun withRadialGradient(minEdgeColor: Int, centerColor: Int, outerColor: Int) + : EdgeImageProcessor { + val paintFn = fun(image: PlanarImage, rect: RectF): Paint { + val p = Paint() + p.shader = RadialGradient( + rect.width() / 2, rect.height() / 2, + maxOf(rect.width(), rect.height()) / 2f, + addAlpha(centerColor), addAlpha(outerColor), Shader.TileMode.MIRROR) + return p + } + return EdgeImageProcessor(makeAlphaColorMap(minEdgeColor), paintFn) + } + + private fun makeRangeColorMap(minEdgeColor: Int, maxEdgeColor: Int): IntArray { + val r0 = (minEdgeColor shr 16) and 0xff + val g0 = (minEdgeColor shr 8) and 0xff + val b0 = (minEdgeColor) and 0xff + val r1 = (maxEdgeColor shr 16) and 0xff + val g1 = (maxEdgeColor shr 8) and 0xff + val b1 = (maxEdgeColor) and 0xff + return IntArray(256, fun(index): Int { + val fraction = index / 255f + val r = Math.round(r0 + (r1 - r0) * fraction) + val g = Math.round(g0 + (g1 - g0) * fraction) + val b = Math.round(b0 + (b1 - b0) * fraction) + return (0xff shl 24) or (r shl 16) or (g shl 8) or b + }) + } + + private fun makeAlphaColorMap(color: Int): IntArray { + val colorWithoutAlpha = 0xffffff and color + return IntArray(256, {i -> ((255 - i) shl 24) or colorWithoutAlpha}) + } + + private fun addAlpha(color: Int): Int { + return 0xff000000.toInt() or color + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/GrayscaleImageGenerator.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/GrayscaleImageGenerator.kt new file mode 100644 index 0000000..c303090 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/GrayscaleImageGenerator.kt @@ -0,0 +1,26 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.Bitmap + +class GrayscaleImageGenerator: CameraImageProcessor() { + + override fun createBitmapFromImage(image: PlanarImage): Bitmap { + val bitmap = Bitmap.createBitmap(image.width, image.height, Bitmap.Config.ARGB_8888) + val brightness = getBufferBytes(image.planes[0].buffer) + val rowStride = image.planes[0].rowStride + val pixelStride = image.planes[0].pixelStride + var pixelIndex = 0 + val pixels = IntArray(brightness.size) + for (y in 0 until image.height) { + var offset = y * rowStride + for (x in 0 until image.width) { + val b = brightness[offset].toInt() and 0xff + val color = (0xFF shl 24) or (b shl 16) or (b shl 8) or b + pixels[pixelIndex++] = color + offset += pixelStride + } + } + bitmap.setPixels(pixels, 0, image.width, 0, 0, image.width, image.height) + return bitmap + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/ImageOrientation.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/ImageOrientation.kt new file mode 100644 index 0000000..d42e99b --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/ImageOrientation.kt @@ -0,0 +1,6 @@ +package com.dozingcatsoftware.boojiecam + +enum class ImageOrientation { + NORMAL, + ROTATED_180, +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/LifeBitmapGenerator.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/LifeBitmapGenerator.kt new file mode 100644 index 0000000..e5d79ab --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/LifeBitmapGenerator.kt @@ -0,0 +1,111 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import java.util.* + +class LifeBitmapGenerator() { + + internal var thread: Thread? = null + + fun start(callback: (Bitmap) -> Unit) { + if (thread == null) { + thread = Thread({runInThread(callback)}) + thread!!.start() + } + } + + fun pause() { + thread = null + } + + private fun runInThread(callback: (Bitmap) -> Unit) { + val bitmap1 = Bitmap.createBitmap(800, 800, Bitmap.Config.ARGB_8888) + val canvas1 = Canvas(bitmap1) + val rand = Random() + val paint = Paint() + + val boardHeight = 80 + val boardWidth = 80 + val cellHeight = 10f + val cellWidth = 10f + var board = BooleanArray(boardHeight * boardWidth, {rand.nextBoolean()}) + var nextBoard = BooleanArray(boardHeight * boardWidth) + + + while (true) { + if (Thread.currentThread() != thread) break + /* + val x = Math.abs(rand.nextInt() % 700).toFloat() + val y = Math.abs(rand.nextInt() % 700).toFloat() + val isRed = rand.nextBoolean() + paint.color = (if (isRed) 0xffff0000 else 0xff0000ff).toInt() + canvas1.drawRect(x, y, x + 100, y + 100, paint) + */ + computeNextState(board, nextBoard, boardWidth, boardHeight, rand) + var tmp = board + board = nextBoard + nextBoard = tmp + + var index = 0 + var h = 0 + while (h < boardHeight) { + var w = 0 + val y = h * cellHeight + while (w < boardWidth) { + paint.color = (if (board[index]) 0xffff0000 else 0xff0000ff).toInt() + val x = w * cellWidth + canvas1.drawRect(x, y, x + cellWidth, y + cellHeight, paint) + w++ + index++ + } + h++ + } + + + callback(bitmap1) + Thread.sleep(15) + } + } + + companion object { + fun computeNextState(board: BooleanArray, next: BooleanArray, width: Int, height: Int, rand: Random) { + var index = 0 + var h = 0 + while (h < height) { + val topEdge = (h == 0) + val bottomEdge = (h == height - 1) + var w = 0 + while (w < width) { + val leftEdge = (w == 0) + val rightEdge = (w == width - 1) + + var count = 0 + if (!topEdge) { + val upIndex = index - width + if (!leftEdge && board[upIndex - 1]) count++ + if (board[upIndex]) count++ + if (!rightEdge && board[upIndex + 1]) count++ + } + if (!leftEdge && board[index - 1]) count++ + if (!rightEdge && board[index + 1]) count++ + if (!bottomEdge) { + val downIndex = index + width + if (!leftEdge && board[downIndex - 1]) count++ + if (board[downIndex]) count++ + if (!rightEdge && board[downIndex + 1]) count++ + } + next[index] = board[index] + if (rand.nextDouble() < 0.1) { + next[index] = (count == 3 || (count == 2 && board[index])) + } + + w++ + index++ + } + h++ + } + } + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/MainActivity.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/MainActivity.kt new file mode 100644 index 0000000..f03ae6d --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/MainActivity.kt @@ -0,0 +1,219 @@ +package com.dozingcatsoftware.boojiecam + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.os.Environment +import android.os.Handler +import android.util.DisplayMetrics +import android.util.Log +import android.util.Size +import android.view.View +import android.view.Window +import android.view.WindowManager +import kotlinx.android.synthetic.main.activity_main.* +import java.io.File + +class MainActivity : Activity() { + + private val handler = Handler() + private val lifeGenerator = LifeBitmapGenerator() + private lateinit var cameraSelector: CameraSelector + private lateinit var cameraImageGenerator: CameraImageGenerator + + private lateinit var imageProcessor: CameraImageProcessor + private var preferredImageSize = ImageSize.HALF_SCREEN + + private val photoLibrary = PhotoLibrary( + File(Environment.getExternalStorageDirectory(), "BoojieCam")) + + private val allImageProcessors = arrayOf( + EdgeColorImageProcessor(), + EdgeImageProcessor.withFixedColors(0x000000, 0x00ff00), + EdgeImageProcessor.withFixedColors(0x00ffff, 0xff0000), + EdgeImageProcessor.withFixedColors(0xffffff, 0x000000), + EdgeImageProcessor.withLinearGradient(0x000000, 0xff0000, 0x0000ff), + EdgeImageProcessor.withRadialGradient(0x191970, 0xffff00, 0xff4500), + GrayscaleImageGenerator() + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + requestWindowFeature(Window.FEATURE_NO_TITLE) + setContentView(R.layout.activity_main) + + cameraSelector = CameraSelector(this) + cameraImageGenerator = cameraSelector.createImageGenerator() + imageProcessor = allImageProcessors[0] + + switchCameraButton.setOnClickListener(this::switchToNextCamera) + switchResolutionButton.setOnClickListener(this::switchResolution) + switchEffectButton.setOnClickListener(this::switchEffect) + takePictureButton.setOnClickListener(this::takePicture) + } + + override fun onResume() { + super.onResume() + Log.i(TAG, "onResume") + // lifeGenerator.start(this::handleGeneratedBitmap) + checkPermissionAndStartCamera() + } + + override fun onPause() { + lifeGenerator.pause() + imageProcessor.pause() + cameraImageGenerator.stop() + super.onPause() + } + + private fun targetCameraImageSize(): Size { + val metrics = DisplayMetrics() + val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager + windowManager.defaultDisplay.getMetrics(metrics) + val displayWidth = metrics.widthPixels + val displayHeight = metrics.heightPixels + return when (preferredImageSize) { + ImageSize.FULL_SCREEN -> Size(displayWidth, displayHeight) + ImageSize.HALF_SCREEN -> Size(displayWidth / 2, displayHeight / 2) + ImageSize.VIDEO_RECORDING -> Size(640, 360) + } + } + + private fun cameraImageSizeForSavedPicture() = Size(1920, 1080) + + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, grantResults: IntArray) { + when (requestCode) { + PermissionsChecker.CAMERA_AND_STORAGE_REQUEST_CODE -> { + if (PermissionsChecker.hasCameraPermission(this)) { + cameraImageGenerator.start( + CameraStatus.CAPTURING_PREVIEW, + this.targetCameraImageSize(), + this::handleImageFromCamera) + } + } + } + } + + private fun checkPermissionAndStartCamera() { + if (PermissionsChecker.hasCameraPermission(this)) { + cameraImageGenerator.start( + CameraStatus.CAPTURING_PREVIEW, + this.targetCameraImageSize(), + this::handleImageFromCamera) + } + else { + PermissionsChecker.requestCameraAndStoragePermissions(this) + } + } + + private fun handleGeneratedBitmap(processedBitmap: ProcessedBitmap) { + handler.post({ + overlayView.processedBitmap = processedBitmap + overlayView.invalidate() + if (processedBitmap.sourceImage.status == CameraStatus.CAPTURING_PHOTO) { + Log.i(TAG, "Saving picture") + Thread({ + photoLibrary.savePhoto(processedBitmap, + fun(photoId: String) { + Log.i(TAG, "Saved $photoId") + }, + fun(ex: Exception) { + Log.w(TAG, "Error saving photo: " + ex) + }) + }).start() + } + }) + } + + private fun handleImageFromCamera(image: CameraImage) { + handler.post({ + imageProcessor.start(this::handleGeneratedBitmap) + // Log.i(TAG, "Received image from camera") + if (image.status == CameraStatus.CAPTURING_PHOTO) { + Log.i(TAG, "Restarting preview capture") + cameraImageGenerator.start( + CameraStatus.CAPTURING_PREVIEW, + this.targetCameraImageSize(), + this::handleImageFromCamera) + // We're going to save the raw data from the camera image, so extract it now. + // (If we wait until handleGeneratedBitmap is called, the underlying + // android.media.Image will have been closed). + val yuvBytes = flattenedYuvImageBytes(image.image) + val inMemoryImage = CameraImage( + PlanarImage.fromFlattenedYuvBytes(yuvBytes, + image.image.width, image.image.height), + image.orientation, image.status, image.timestamp) + image.close() + imageProcessor.queueImage(inMemoryImage) + } + else { + imageProcessor.queueImage(image) + } + }) + } + + private fun restartCameraImageGenerator() { + // cameraImageGenerator?.pause() + Log.i(TAG, "recreateCameraImageGenerator: " + this.targetCameraImageSize()) + cameraImageGenerator.start( + CameraStatus.CAPTURING_PREVIEW, + this.targetCameraImageSize(), + this::handleImageFromCamera) + } + + private fun switchToNextCamera(view: View) { + if (cameraImageGenerator.status != CameraStatus.CAPTURING_PREVIEW) { + return + } + imageProcessor.pause() + cameraSelector.selectNextCamera() + cameraImageGenerator.stop() + cameraImageGenerator = cameraSelector.createImageGenerator() + restartCameraImageGenerator() + } + + private fun switchResolution(view: View) { + if (cameraImageGenerator.status != CameraStatus.CAPTURING_PREVIEW) { + return + } + preferredImageSize = + if (preferredImageSize == ImageSize.FULL_SCREEN) + ImageSize.HALF_SCREEN + else + ImageSize.FULL_SCREEN + restartCameraImageGenerator() + } + + private fun switchEffect(view: View) { + imageProcessor.pause() + val index = allImageProcessors.indexOf(imageProcessor) + imageProcessor = allImageProcessors[(index + 1) % allImageProcessors.size] + } + + private fun takePicture(view: View) { + if (cameraImageGenerator.status != CameraStatus.CAPTURING_PREVIEW) { + return + } + imageProcessor.pause() + cameraImageGenerator.start( + CameraStatus.CAPTURING_PHOTO, + this.cameraImageSizeForSavedPicture(), + this::handleImageFromCamera) + } + + /** + * A native method that is implemented by the 'native-lib' native library, + * which is packaged with this application. + */ + external fun stringFromJNI(): String + + companion object { + val TAG = "MainActivity" + + // Used to load the 'native-lib' library on application startup. + init { + System.loadLibrary("native-lib") + } + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/OverlayView.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/OverlayView.kt new file mode 100644 index 0000000..7c2d837 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/OverlayView.kt @@ -0,0 +1,60 @@ +package com.dozingcatsoftware.boojiecam + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View + +class OverlayView(context: Context, attrs: AttributeSet) : View(context, attrs) { + + var processedBitmap: ProcessedBitmap? = null + + private val flipMatrix = Matrix() + private val blackPaint = Paint() + private val imageRect = RectF() + + init { + blackPaint.setARGB(255, 0, 0, 0) + } + + override fun onDraw(canvas: Canvas) { + val pb = this.processedBitmap ?: return + val bitmap = pb.bitmap + val scaleFactor = Math.min(this.width.toFloat() / bitmap.width, + this.height.toFloat() / bitmap.height) + val scaledWidth = bitmap.width * scaleFactor + val scaledHeight = bitmap.height * scaleFactor + + val flipHorizontal = (pb.sourceImage.orientation == ImageOrientation.ROTATED_180) + val flipVertical = (pb.sourceImage.orientation == ImageOrientation.ROTATED_180) + var xOffset = (width - scaledWidth) / 2 + var yOffset = (height - scaledHeight) / 2 + + flipMatrix.setScale(if (flipHorizontal) -scaleFactor else scaleFactor, + if (flipVertical) -scaleFactor else scaleFactor) + flipMatrix.postTranslate(if (flipHorizontal) xOffset + scaledWidth else xOffset, + if (flipVertical) yOffset + scaledHeight else yOffset) + + if (xOffset > 0) { + canvas.drawRect(0f, 0f, xOffset, height.toFloat(), blackPaint) + canvas.drawRect(width - xOffset, 0f, width.toFloat(), height.toFloat(), blackPaint) + } + if (yOffset > 0) { + canvas.drawRect(0f, 0f, width.toFloat(), yOffset, blackPaint) + canvas.drawRect(0f, height - yOffset, width.toFloat(), height.toFloat(), blackPaint) + } + imageRect.set(xOffset, yOffset, xOffset + scaledWidth, yOffset + scaledHeight) + val paint = pb.backgroundPaintFn(imageRect) + if (paint != null) { + canvas.drawRect(imageRect, paint) + } + canvas.drawBitmap(bitmap, flipMatrix, null) + } + + companion object { + val TAG = "OverlayView" + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/PermissionsChecker.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/PermissionsChecker.kt new file mode 100644 index 0000000..0492e74 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/PermissionsChecker.kt @@ -0,0 +1,56 @@ +package com.dozingcatsoftware.boojiecam + +import android.Manifest +import android.annotation.TargetApi +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Build + +@TargetApi(23) +object PermissionsChecker { + + val CAMERA_AND_STORAGE_REQUEST_CODE = 1001 + val STORAGE_FOR_PHOTO_REQUEST_CODE = 1002 + val STORAGE_FOR_LIBRARY_REQUEST_CODE = 1003 + + fun hasPermission(activity: Activity, perm: String): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + return activity.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED + } + + fun hasCameraPermission(activity: Activity): Boolean { + return hasPermission(activity, Manifest.permission.CAMERA) + } + + fun hasStoragePermission(activity: Activity): Boolean { + return hasPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE) && + hasPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + fun requestCameraAndStoragePermissions(activity: Activity) { + activity.requestPermissions( + arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE), + CAMERA_AND_STORAGE_REQUEST_CODE) + } + + fun requestStoragePermissionsToTakePhoto(activity: Activity) { + activity.requestPermissions( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE), + STORAGE_FOR_PHOTO_REQUEST_CODE); + } + + fun requestStoragePermissionsToGoToLibrary(activity: Activity) { + activity.requestPermissions( + arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE), + STORAGE_FOR_LIBRARY_REQUEST_CODE); + } +} diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/PhotoLibrary.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/PhotoLibrary.kt new file mode 100644 index 0000000..15fc3ef --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/PhotoLibrary.kt @@ -0,0 +1,65 @@ +package com.dozingcatsoftware.boojiecam + +import android.util.Log +import org.json.JSONObject +import org.json.JSONStringer +import java.io.File +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.* +import java.util.zip.GZIPOutputStream + +/** + * Created by brian on 10/9/17. + */ +class PhotoLibrary(val rootDirectory: File) { + val PHOTO_ID_FORMAT = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS") + + init { + PHOTO_ID_FORMAT.timeZone = TimeZone.getTimeZone("UTC") + } + + fun savePhoto(processedBitmap: ProcessedBitmap, + successFn: (String) -> Unit, + errorFn: (Exception) -> Unit) { + try { + Log.i(TAG, "savePhoto start") + val sourceImage = processedBitmap.sourceImage + val photoId = PHOTO_ID_FORMAT.format(Date(sourceImage.timestamp)) + val photoDir = File(rootDirectory, photoId) + photoDir.mkdirs() + + val rawImageFile = File(photoDir, "image.gz") + GZIPOutputStream(FileOutputStream(rawImageFile)).use({ + for (plane in sourceImage.image.planes) { + writeBufferToOuptutStream(plane.buffer, it) + } + }) + val uncompressedSize = sourceImage.image.width * sourceImage.image.height * 3 / 2 + val compressedSize = rawImageFile.length() + val compressedPercent = Math.round(100.0 * compressedSize / uncompressedSize) + Log.i(TAG, "Wrote $compressedSize bytes, compressed to $compressedPercent") + + val metadata = mapOf( + "width" to sourceImage.image.width, + "height" to sourceImage.image.height, + "timestamp" to sourceImage.timestamp + ) + val json = JSONObject(metadata).toString(2) + FileOutputStream(File(photoDir, "metadata.json")).use({ + it.write(json.toByteArray(Charsets.UTF_8)) + }) + + // TODO: Write full size image and thumbnail. + + successFn(photoId) + } + catch (ex: Exception) { + errorFn(ex) + } + } + + companion object { + val TAG = "PhotoLibrary" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/PlanarImage.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/PlanarImage.kt new file mode 100644 index 0000000..8275c75 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/PlanarImage.kt @@ -0,0 +1,55 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.ImageFormat +import android.media.Image +import java.nio.ByteBuffer + +/** + * Wrapper for android.media.image so we can create other implementations. + */ +interface PlanarImage { + data class Plane(val buffer: ByteBuffer, val rowStride: Int, val pixelStride: Int) {} + + val width: Int + val height: Int + val format: Int + val planes: Array + + fun close() + + companion object { + fun fromMediaImage(image: Image): PlanarImage { + return object: PlanarImage { + override val width = image.width + override val height = image.height + override val format = image.format + override val planes = image.planes.map( + {p -> Plane(p.buffer, p.rowStride, p.pixelStride)}).toTypedArray() + + override fun close() { + image.close() + } + } + } + + fun fromFlattenedYuvBytes(bytes: ByteArray, width: Int, height: Int): PlanarImage { + val numYPixels = width * height + val uvWidth = width / 2 + val numUVPixels = uvWidth * (height / 2) + return object: PlanarImage { + override val width = width + override val height = height + override val format = ImageFormat.YUV_420_888 + override val planes = arrayOf( + Plane(ByteBuffer.wrap(bytes.copyOfRange(0, numYPixels)), + width, 1), + Plane(ByteBuffer.wrap(bytes.copyOfRange(numYPixels, numYPixels + numUVPixels)), + uvWidth, 1), + Plane(ByteBuffer.wrap(bytes.copyOfRange(numYPixels + numUVPixels, numYPixels + 2 * numUVPixels)), + uvWidth, 1)) + + override fun close() {} + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/ProcessedBitmap.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/ProcessedBitmap.kt new file mode 100644 index 0000000..b29f70a --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/ProcessedBitmap.kt @@ -0,0 +1,10 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.Bitmap +import android.graphics.Paint +import android.graphics.RectF + +data class ProcessedBitmap( + val sourceImage: CameraImage, + val bitmap: Bitmap, + val backgroundPaintFn: (RectF) -> Paint?) diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/WireframeColorScheme.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/WireframeColorScheme.kt new file mode 100644 index 0000000..d575e42 --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/WireframeColorScheme.kt @@ -0,0 +1,15 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.Bitmap + +/** + * Created by brian on 10/3/17. + */ +class WireframeColorScheme() { + lateinit var colorMap: IntArray + var backgroundBitmap: Bitmap? = null + + companion object { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dozingcatsoftware/boojiecam/utils.kt b/app/src/main/java/com/dozingcatsoftware/boojiecam/utils.kt new file mode 100644 index 0000000..7eca1af --- /dev/null +++ b/app/src/main/java/com/dozingcatsoftware/boojiecam/utils.kt @@ -0,0 +1,56 @@ +package com.dozingcatsoftware.boojiecam + +import android.graphics.ImageFormat +import java.io.OutputStream +import java.nio.ByteBuffer + +fun getBufferBytes(buffer: ByteBuffer): ByteArray { + if (buffer.hasArray() && buffer.arrayOffset() == 0) { + val arr = buffer.array() + if (arr.size == buffer.limit()) { + return arr + } + } + val arr = ByteArray(buffer.limit()) + buffer.get(arr) + return arr +} + +fun flattenedYuvImageBytes(image: PlanarImage): ByteArray { + if (image.format != ImageFormat.YUV_420_888) { + throw IllegalArgumentException("Unexpected image format: " + image.format) + } + val uvWidth = image.width / 2 + val uvHeight = image.height / 2 + val outputBytes = ByteArray(image.width * image.height + 2 * uvWidth * uvHeight) + var outputIndex = 0 + + fun appendBytes(plane: PlanarImage.Plane, width: Int, height: Int) { + val planeBytes = getBufferBytes(plane.buffer) + val pixelStride = plane.pixelStride + for (y in 0 until height) { + var offset = y * plane.rowStride + for (x in 0 until width) { + outputBytes[outputIndex++] = planeBytes[offset] + offset += pixelStride + } + } + } + + appendBytes(image.planes[0], image.width, image.height) + appendBytes(image.planes[1], uvWidth, uvHeight) + appendBytes(image.planes[2], uvWidth, uvHeight) + return outputBytes +} + +fun writeBufferToOuptutStream(buffer: ByteBuffer, output: OutputStream) { + if (buffer.hasArray()) { + val arr = buffer.array() + output.write(arr, buffer.arrayOffset(), buffer.limit()) + } + else { + val arr = ByteArray(buffer.limit()) + buffer.get(arr) + output.write(arr) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..1cd2a36 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..382868d --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,39 @@ + + + + + + +