Я создаю приложение для Android, использующее Camera2 API с Jetpack Compose для захвата изображений при нажатии кнопки. Моя цель — отобразить предварительный просмотр камеры с помощью TextView, захватывать изображения в формате JPEG с помощью ImageReader и, если это поддерживается, также захватывать данные о глубине в формате DEPTH16. Однако я столкнулся с проблемой, когда блок кода в ImageReader.setOnImageAvailableListener() вообще не выполняется, что не позволяет мне получить захваченное изображение.
Мои коды следующие: :
package com.example.tutorial.camera
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.ImageFormat
import android.graphics.SurfaceTexture
import android.hardware.camera2.CameraCaptureSession
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CameraManager
import android.hardware.camera2.CaptureRequest
import android.media.ImageReader
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.MediaStore
import android.util.Log
import android.util.Size
import android.view.SurfaceHolder
import android.view.TextureView
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.*
import androidx.compose.ui.viewinterop.AndroidView
import java.io.File
import java.io.FileOutputStream
import java.lang.Exception
@Composable
fun CamPreview(
onImageCaptured: (Bitmap) -> Unit,
onImageDCaptured: (ByteArray) -> Unit,
onDismiss: () -> Unit
) {
var cameraHandler: CameraHandler? = remember { null }
var errorMessage by remember { mutableStateOf(null) }
// Display camera preview using TextureView
AndroidView(factory = { context ->
TextureView(context).apply {
// Use SurfaceTextureListener to ensure Surface is created
surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
cameraHandler =
CameraHandler(
context = context,
view = this@apply,
onImageCaptured = { bitmap ->
Log.d("CameraPreview", "Image captured successfully: ${bitmap.width}x${bitmap.height}")
onImageCaptured(bitmap)
},
onImageDCaptured = onImageDCaptured,
onDismiss = onDismiss,
onError = { message -> errorMessage = message })
cameraHandler?.startCamera()
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
cameraHandler?.stopCam()
onDismiss()
return true
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}
}
})
// Display error alert box
errorMessage?.let { message ->
AlertDialog(
onDismissRequest = { errorMessage = null; },
title = { Text("Notice") },
text = { Text(message) },
confirmButton = {
Button(onClick = { errorMessage = null; }) { Text("Confirm") }
}
)
}
// Capture button at the bottom of the screen
Box(modifier = Modifier.fillMaxSize()) {
Button(
onClick = {
cameraHandler?.takePic()
onDismiss() // Close camera preview after capturing
},
modifier = Modifier.align(Alignment.BottomCenter)
) {
Text("Capture")
}
}
}
class CameraHandler(
private val context: Context,
private val view: TextureView,
private val onImageCaptured: (Bitmap) -> Unit,
private val onImageDCaptured: (ByteArray) -> Unit,
private val onDismiss: () -> Unit, // Use onDismiss to close camera preview
private val onError: (String) -> Unit
) {
val camManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
lateinit var camDevice: CameraDevice
lateinit var captSession: CameraCaptureSession
lateinit var imgReader: ImageReader
lateinit var depImgReader: ImageReader
lateinit var handler: Handler
lateinit var captReq: CaptureRequest.Builder
val camId by mutableStateOf(getDepSupportedCamId())
var supportsDep: Boolean = false
private fun getDepSupportedCamId(): String? {
for (cameraId in camManager.cameraIdList) {
val characteristics = camManager.getCameraCharacteristics(cameraId)
// Check the camera's facing direction
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
// Check if the device supports DEPTH16 format
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
if (capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) == true) {
// Prefer rear-facing camera
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
return cameraId
}
}
}
// If no rear-facing camera found, return another camera ID that supports DEPTH16
for (cameraId in camManager.cameraIdList) {
val characteristics = camManager.getCameraCharacteristics(cameraId)
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
if (capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) == true) {
return cameraId
}
}
// If no camera supports DEPTH16, return rear-facing camera ID
for (cameraId in camManager.cameraIdList) {
val characteristics = camManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_BACK) {
return cameraId
}
}
return null
}
@SuppressLint("MissingPermission")
fun startCamera() {
if (camId == null) {
showError("No usable camera")
return
}
val characteristics = camManager.getCameraCharacteristics(camId!!)
val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
supportsDep = capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) == true
//if(!supportsDepth)showError("This device doesn't support DEPTH16")
try {
handler = Handler(Looper.getMainLooper())
// Open camera
camManager.openCamera(camId!!, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) {
camDevice = device
createCameraPreviewSession()
}
override fun onDisconnected(device: CameraDevice) {
device.close()
onDismiss() // Close camera preview on disconnection
}
override fun onError(device: CameraDevice, error: Int) {
device.close()
showError("Camera Error: $error")
}
}, handler)
} catch (e: Exception) {
showError("Failed to start camera: ${e.message}")
}
}
private fun createCameraPreviewSession() {
try {
// Configure preview with camera's best size
val characteristics = camId?.let { camManager.getCameraCharacteristics(it) }
val map = characteristics?.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val previewSize =
map?.getOutputSizes(SurfaceHolder::class.java)?.maxByOrNull { it.width * it.height } ?: Size(640, 480)
val width = previewSize.width
val height = previewSize.height
val texture = view.surfaceTexture ?: throw IllegalStateException("SurfaceTexture is null")
texture.setDefaultBufferSize(width, height)
val previewSurface = android.view.Surface(texture)
//Log.d("CameraHandler", "")
Log.d("CameraHandler", "Initializing ImageReader with width=$width, height=$height")
imgReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 2)
if (supportsDep) {
Log.d("CameraHandler", "Initializing DepthImageReader with width=$width, height=$height")
depImgReader =ImageReader.newInstance(width, height, ImageFormat.DEPTH16, 2)
}
// Create CaptureSession
camDevice.createCaptureSession(
listOf(previewSurface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
captSession = session
captReq = camDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captReq.addTarget(previewSurface)
captSession.setRepeatingRequest(captReq.build(), null, null)
}
override fun onConfigureFailed(session: CameraCaptureSession) {
showError("Failed to configure camera session")
}
},
null
)
} catch (e: Exception) {
showError("Failed to create preview session: ${e.message}")
}
}
fun takePic() {
try {
captReq = camDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captReq.addTarget(imgReader.surface)
Log.d("CameraHandler", "At least tried successfully")
imgReader.setOnImageAvailableListener({ reader ->
val image = reader.acquireLatestImage()
if (image != null) { // Check if image was successfully captured
Log.d("CameraHandler", "Image captured")
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.remaining())
buffer.get(bytes)
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "img.jpeg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
uri?.let {
context.contentResolver.openOutputStream(it)?.use { outputStream ->
outputStream.write(bytes)
}
}
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
Log.d("CameraPreview", "Image captured successfully: ${bitmap.width}x${bitmap.height}")
onImageCaptured(bitmap)
image.close()
} else {
Log.d("CameraHandler", "Failed to capture Image")
showError("Failed to acquire image from ImageReader")
}
}, handler)
// Check if depth image needs to be captured
if (supportsDep) {
captReq.addTarget(depImgReader.surface)
depImgReader.setOnImageAvailableListener({ depthReader ->
val depthImage = depthReader.acquireLatestImage()
val depthBuffer = depthImage.planes[0].buffer
val depthBytes = ByteArray(depthBuffer.capacity())
depthBuffer.get(depthBytes)
// Invoke callback for depth image
onImageDCaptured(depthBytes)
depthImage.close()
}, handler)
}
captSession.capture(captReq.build(), null, null)
Log.d("CameraHandler", "Capture request for image sent")
} catch (e: Exception) {
showError("Failed to take picture: ${e.message}")
}
}
// Display error popup
fun showError(message: String) {
onError(message)
}
fun stopCam() {
try {
// Stop capture session
captSession.close()
// Close camera device
camDevice.close()
// Release ImageReader
imgReader.close()
// If there is depthImageReader, release it
if (supportsDep) depImgReader.close()
} catch (e: Exception) {
onError("Error closing camera: ${e.message}")
}
}
}
Я ожидал, что прослушиватель в ImageReader.setOnImageAvailableListener() будет выполнять свой блок кода после каждого запроса на захват, что позволит мне получить изображение и обработать его. Для устранения неполадок я убедился, что TextView, imgReader и captSession правильно инициализированы, а также удостоверился, что разрешения камеры предоставлены перед ее запуском. Я также поэкспериментировал с разными размерами буфера для ImageReader и подтвердил, что CaptReq включает imgReader.surface. Ведение журнала показывает, что takePicture() правильно инициирует запрос захвата и нацелен на ImageReader.surface, однако методacquireLatestImage() никогда не может получить изображение, поскольку прослушиватель не запускается. Никаких исключений или ошибок не возникает, и я остаюсь без каких-либо данных изображения.
Что может быть причиной того, что ImageReader.setOnImageAvailableListener() пропускает выполнение своего блока кода, и какие шаги я могу предпринять для дальнейшего устранения этой проблемы? Есть ли что-то особенное в настройке CaptureRequest или ImageReader камеры 2, которое я, возможно, упустил из виду?
Кстати, я пробовал запускать это приложение на разных типах телефонов, и я уверен, что они должны поддерживают вывод данных о глубине, но ни один из них не ответил с поддержкой изображений DEPTH16. Я не знаю, действительно ли эти телефоны не поддерживают изображения такого типа, или что-то не так с тем, как я их запрашиваю.
Спасибо за помощь!< /p> ответ на комментарий dev.bmax: Я попробовал изменить свой код на более новую версию, но он по-прежнему не работает. Не могли бы вы дать еще несколько более четких предложений? спасибо
Я создаю приложение для Android, использующее Camera2 API с Jetpack Compose для захвата изображений при нажатии кнопки. Моя цель — отобразить предварительный просмотр камеры с помощью TextView, захватывать изображения в формате JPEG с помощью ImageReader и, если это поддерживается, также захватывать данные о глубине в формате DEPTH16. Однако я столкнулся с проблемой, когда блок кода в ImageReader.setOnImageAvailableListener() вообще не выполняется, что не позволяет мне получить захваченное изображение. Мои коды следующие: : [code]package com.example.tutorial.camera
@Composable fun CamPreview( onImageCaptured: (Bitmap) -> Unit, onImageDCaptured: (ByteArray) -> Unit, onDismiss: () -> Unit ) { var cameraHandler: CameraHandler? = remember { null } var errorMessage by remember { mutableStateOf(null) }
// Capture button at the bottom of the screen Box(modifier = Modifier.fillMaxSize()) { Button( onClick = { cameraHandler?.takePic() onDismiss() // Close camera preview after capturing }, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("Capture") } } }
class CameraHandler( private val context: Context, private val view: TextureView, private val onImageCaptured: (Bitmap) -> Unit, private val onImageDCaptured: (ByteArray) -> Unit, private val onDismiss: () -> Unit, // Use onDismiss to close camera preview private val onError: (String) -> Unit ) { val camManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager lateinit var camDevice: CameraDevice lateinit var captSession: CameraCaptureSession lateinit var imgReader: ImageReader lateinit var depImgReader: ImageReader lateinit var handler: Handler lateinit var captReq: CaptureRequest.Builder
val camId by mutableStateOf(getDepSupportedCamId()) var supportsDep: Boolean = false
private fun getDepSupportedCamId(): String? { for (cameraId in camManager.cameraIdList) { val characteristics = camManager.getCameraCharacteristics(cameraId) // Check the camera's facing direction val facing = characteristics.get(CameraCharacteristics.LENS_FACING) // Check if the device supports DEPTH16 format val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) if (capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) == true) { // Prefer rear-facing camera if (facing == CameraCharacteristics.LENS_FACING_BACK) { return cameraId } } } // If no rear-facing camera found, return another camera ID that supports DEPTH16 for (cameraId in camManager.cameraIdList) { val characteristics = camManager.getCameraCharacteristics(cameraId) val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) if (capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) == true) { return cameraId } } // If no camera supports DEPTH16, return rear-facing camera ID for (cameraId in camManager.cameraIdList) { val characteristics = camManager.getCameraCharacteristics(cameraId) val facing = characteristics.get(CameraCharacteristics.LENS_FACING) if (facing == CameraCharacteristics.LENS_FACING_BACK) { return cameraId } } return null }
@SuppressLint("MissingPermission") fun startCamera() {
if (camId == null) { showError("No usable camera") return }
val characteristics = camManager.getCameraCharacteristics(camId!!) val capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES) supportsDep = capabilities?.contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) == true //if(!supportsDepth)showError("This device doesn't support DEPTH16")
try { handler = Handler(Looper.getMainLooper())
// Open camera camManager.openCamera(camId!!, object : CameraDevice.StateCallback() { override fun onOpened(device: CameraDevice) { camDevice = device createCameraPreviewSession() }
override fun onDisconnected(device: CameraDevice) { device.close() onDismiss() // Close camera preview on disconnection }
private fun createCameraPreviewSession() { try { // Configure preview with camera's best size val characteristics = camId?.let { camManager.getCameraCharacteristics(it) } val map = characteristics?.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) val previewSize = map?.getOutputSizes(SurfaceHolder::class.java)?.maxByOrNull { it.width * it.height } ?: Size(640, 480) val width = previewSize.width val height = previewSize.height
val texture = view.surfaceTexture ?: throw IllegalStateException("SurfaceTexture is null") texture.setDefaultBufferSize(width, height) val previewSurface = android.view.Surface(texture)
//Log.d("CameraHandler", "") Log.d("CameraHandler", "Initializing ImageReader with width=$width, height=$height") imgReader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 2) if (supportsDep) { Log.d("CameraHandler", "Initializing DepthImageReader with width=$width, height=$height") depImgReader =ImageReader.newInstance(width, height, ImageFormat.DEPTH16, 2) }
override fun onConfigureFailed(session: CameraCaptureSession) { showError("Failed to configure camera session") } }, null ) } catch (e: Exception) { showError("Failed to create preview session: ${e.message}") } }
fun takePic() { try { captReq = camDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) captReq.addTarget(imgReader.surface) Log.d("CameraHandler", "At least tried successfully") imgReader.setOnImageAvailableListener({ reader -> val image = reader.acquireLatestImage() if (image != null) { // Check if image was successfully captured Log.d("CameraHandler", "Image captured") val buffer = image.planes[0].buffer val bytes = ByteArray(buffer.remaining()) buffer.get(bytes)
val contentValues = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, "img.jpeg") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES) } val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) uri?.let { context.contentResolver.openOutputStream(it)?.use { outputStream -> outputStream.write(bytes) } }
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) Log.d("CameraPreview", "Image captured successfully: ${bitmap.width}x${bitmap.height}") onImageCaptured(bitmap) image.close() } else { Log.d("CameraHandler", "Failed to capture Image") showError("Failed to acquire image from ImageReader") } }, handler) // Check if depth image needs to be captured if (supportsDep) { captReq.addTarget(depImgReader.surface) depImgReader.setOnImageAvailableListener({ depthReader -> val depthImage = depthReader.acquireLatestImage() val depthBuffer = depthImage.planes[0].buffer val depthBytes = ByteArray(depthBuffer.capacity()) depthBuffer.get(depthBytes)
// Invoke callback for depth image onImageDCaptured(depthBytes) depthImage.close() }, handler) } captSession.capture(captReq.build(), null, null) Log.d("CameraHandler", "Capture request for image sent") } catch (e: Exception) { showError("Failed to take picture: ${e.message}") } }
// Display error popup fun showError(message: String) { onError(message) }
fun stopCam() { try { // Stop capture session captSession.close() // Close camera device camDevice.close() // Release ImageReader imgReader.close() // If there is depthImageReader, release it if (supportsDep) depImgReader.close() } catch (e: Exception) { onError("Error closing camera: ${e.message}") } } } [/code] Я ожидал, что прослушиватель в ImageReader.setOnImageAvailableListener() будет выполнять свой блок кода после каждого запроса на захват, что позволит мне получить изображение и обработать его. Для устранения неполадок я убедился, что TextView, imgReader и captSession правильно инициализированы, а также удостоверился, что разрешения камеры предоставлены перед ее запуском. Я также поэкспериментировал с разными размерами буфера для ImageReader и подтвердил, что CaptReq включает imgReader.surface. Ведение журнала показывает, что takePicture() правильно инициирует запрос захвата и нацелен на ImageReader.surface, однако методacquireLatestImage() никогда не может получить изображение, поскольку прослушиватель не запускается. Никаких исключений или ошибок не возникает, и я остаюсь без каких-либо данных изображения. Что может быть причиной того, что ImageReader.setOnImageAvailableListener() пропускает выполнение своего блока кода, и какие шаги я могу предпринять для дальнейшего устранения этой проблемы? Есть ли что-то особенное в настройке CaptureRequest или ImageReader камеры 2, которое я, возможно, упустил из виду? Кстати, я пробовал запускать это приложение на разных типах телефонов, и я уверен, что они должны поддерживают вывод данных о глубине, но ни один из них не ответил с поддержкой изображений DEPTH16. Я не знаю, действительно ли эти телефоны не поддерживают изображения такого типа, или что-то не так с тем, как я их запрашиваю. Спасибо за помощь!< /p> [b]ответ на комментарий[/b] dev.bmax: Я попробовал изменить свой код на более новую версию, но он по-прежнему не работает. Не могли бы вы дать еще несколько более четких предложений? спасибо