Skip to content

Implement Camera2 - Image Capture Sample #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref
androidx-activity = "androidx.activity:activity:1.7.2"
androidx-core = "androidx.core:core-ktx:1.10.1"
androidx-appcompat = "androidx.appcompat:appcompat:1.6.1"
androidx-exifinterface = "androidx.exifinterface:exifinterface:1.3.6"
androidx-fragment = "androidx.fragment:fragment-ktx:1.6.0"
androidx-activity-compose = "androidx.activity:activity-compose:1.7.2"
androidx-navigation-fragment = { module = "androidx.navigation:navigation-fragment", version.ref = "androidx-navigation" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
androidx-navigation-testing = { module = "androidx.navigation:navigation-testing", version.ref = "androidx-navigation" }
androidx-lifecycle-viewmodel-compose = "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
androidx-viewpager2 = "androidx.viewpager2:viewpager2:1.0.0"

accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }

Expand Down
2 changes: 2 additions & 0 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Showcases how to pin widget within the app. Check the launcher widget menu for a
This sample will show you how get all audio sources and set an audio device. Covers Bluetooth, LEA, Wired and internal speakers
- [Call Notification Sample](connectivity/callnotification/src/main/java/com/example/platform/connectivity/callnotification/CallNotificationSample.kt):
Sample demonstrating how to make incoming call notifications and in call notifications
- [Camera2 - Image Capture](camera/camera2/src/main/java/com/example/platform/camera/imagecapture/Camera2ImageCapture.kt):
This sample demonstrates how to capture and image and encode it into a JPEG
- [Camera2 - Preview](camera/camera2/src/main/java/com/example/platform/camera/preview/Camera2Preview.kt):
Demonstrates displaying processed pixel data directly from the camera sensor
- [Color Contrast](accessibility/src/main/java/com/example/platform/accessibility/ColorContrast.kt):
Expand Down
6 changes: 5 additions & 1 deletion samples/camera/camera2/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ android {
}

dependencies {
// Add samples specific dependencies
// EXIF Interface
implementation(libs.androidx.exifinterface)

// ViewPager2
implementation(libs.androidx.viewpager2)
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
* Copyright 2023 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.platform.camera.common

import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.ImageView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.widget.ViewPager2
import coil.load
import com.example.platform.camera.databinding.FragmentImageViewerBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.BufferedInputStream
import java.io.File
import kotlin.math.max

class AdvancedImageViewer() : Fragment() {
/**
* Android ViewBinding.
*/
private var _binding: FragmentImageViewerBinding? = null
private val binding get() = _binding!!

/**
* Default Bitmap decoding options
*/
private val bitmapOptions = BitmapFactory.Options().apply {
inJustDecodeBounds = false
// Keep Bitmaps at less than 1 MP
if (max(outHeight, outWidth) > DOWN_SAMPLE_SIZE) {
val scaleFactorX = outWidth / DOWN_SAMPLE_SIZE + 1
val scaleFactorY = outHeight / DOWN_SAMPLE_SIZE + 1
inSampleSize = max(scaleFactorX, scaleFactorY)
}
}

/** Bitmap transformation derived from passed arguments */
private val bitmapTransformation: Matrix by lazy {
decodeExifOrientation(requireArguments().getInt(ARG_KEY_ORIENTATION))
}

/**
* Whether or not the resulting image has depth data or not.
*/
private val isDepth: Boolean by lazy {
requireArguments().getBoolean(ARG_KEY_IS_DEPTH)
}

/**
* Location of the image file to load.
*/
private val location: String by lazy {
requireArguments().getString(ARG_KEY_LOCATION, "")
}

/** Data backing our Bitmap viewpager */
private val bitmapList: MutableList<Bitmap> = mutableListOf()

private fun imageViewFactory() = ImageView(requireContext()).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentImageViewerBinding.inflate(inflater, container, false)
binding.fragmentImageViewerViewpager2.apply {
// Populate the ViewPager and implement a cache of two media items
offscreenPageLimit = 2
adapter = GenericListAdapter(
bitmapList,
itemViewFactory = { imageViewFactory() },
) { view, item, _ ->
(view as ImageView).load(item) {
crossfade(true)
}
}
}

binding.fragmentImageViewerBack.setOnClickListener {
parentFragmentManager
.beginTransaction()
.remove(this)
.commit()
}

return binding.root
}


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch(Dispatchers.IO) {

val pager = binding.fragmentImageViewerViewpager2
// Load input image file
val inputBuffer = loadInputBuffer()

// Load the main JPEG image
addItemToViewPager(pager, decodeBitmap(inputBuffer, 0, inputBuffer.size))

// If we have depth data attached, attempt to load it
if (isDepth) {
try {
val depthStart = findNextJpegEndMarker(inputBuffer, 2)
addItemToViewPager(
pager,
decodeBitmap(
inputBuffer, depthStart, inputBuffer.size - depthStart,
),
)

val confidenceStart = findNextJpegEndMarker(inputBuffer, depthStart)
addItemToViewPager(
pager,
decodeBitmap(
inputBuffer, confidenceStart, inputBuffer.size - confidenceStart,
),
)

} catch (exc: RuntimeException) {
Log.e(TAG, "Invalid start marker for depth or confidence data")
}
}
}
}

/**
* Utility function used to read input file into a byte array.
*/
private fun loadInputBuffer(): ByteArray {
val inputFile = File(location)
return BufferedInputStream(inputFile.inputStream()).let { stream ->
ByteArray(stream.available()).also {
stream.read(it)
stream.close()
}
}
}

/**
* Utility function used to add an item to the viewpager and notify it, in the main thread.
*/
private fun addItemToViewPager(view: ViewPager2, item: Bitmap) = view.post {
bitmapList.add(item)
view.adapter?.notifyItemChanged(bitmapList.size - 1)
}

/**
* Utility function used to decode a [Bitmap] from a byte array
*/
private fun decodeBitmap(buffer: ByteArray, start: Int, length: Int): Bitmap {

// Load bitmap from given buffer
val bitmap = BitmapFactory.decodeByteArray(buffer, start, length, bitmapOptions)

// Transform bitmap orientation using provided metadata
return Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, bitmapTransformation, true,
)
}

companion object {
private val TAG = AdvancedImageViewer::class.java.simpleName

/**
* Argument keys
*/
const val ARG_KEY_IS_DEPTH = "depth"
const val ARG_KEY_ORIENTATION = "orientation"
const val ARG_KEY_LOCATION = "location"

/** Maximum size of [Bitmap] decoded */
private const val DOWN_SAMPLE_SIZE: Int = 1024 // 1MP

/** These are the magic numbers used to separate the different JPG data chunks */
private val JPEG_DELIMITER_BYTES = arrayOf(-1, -39)

/**
* Utility function used to find the markers indicating separation between JPEG data chunks
*/
private fun findNextJpegEndMarker(jpegBuffer: ByteArray, start: Int): Int {

// Sanitize input arguments
assert(start >= 0) { "Invalid start marker: $start" }
assert(jpegBuffer.size > start) {
"Buffer size (${jpegBuffer.size}) smaller than start marker ($start)"
}

// Perform a linear search until the delimiter is found
for (i in start until jpegBuffer.size - 1) {
if (jpegBuffer[i].toInt() == JPEG_DELIMITER_BYTES[0] &&
jpegBuffer[i + 1].toInt() == JPEG_DELIMITER_BYTES[1]
) {
return i + 2
}
}

// If we reach this, it means that no marker was found
throw RuntimeException("Separator marker not found in buffer (${jpegBuffer.size})")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2023 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.platform.camera.common

import android.hardware.camera2.CaptureResult
import android.media.Image
import java.io.Closeable

/**
* Helper data class used to hold capture metadata with their associated image
*/
data class CombinedCaptureResult(
val image: Image,
val metadata: CaptureResult,
val orientation: Int,
val format: Int,
) : Closeable {
override fun close() = image.close()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2023 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.platform.camera.common

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 == 90 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_90
rotationDegrees == 90 && mirrored -> ExifInterface.ORIENTATION_TRANSPOSE
rotationDegrees == 180 && !mirrored -> ExifInterface.ORIENTATION_ROTATE_180
rotationDegrees == 180 && mirrored -> ExifInterface.ORIENTATION_FLIP_VERTICAL
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
}
Loading