将 ImageProxy 转换为位图
Converting ImageProxy to Bitmap
所以,我想探索新 Google 的相机 API - CameraX
。
我想做的是每秒从相机中获取一张图像,然后将其传递给一个接受位图以用于机器学习目的的函数。
我阅读了有关 Camera X
Image Analyzer 的文档:
The image analysis use case provides your app with a CPU-accessible
image to perform image processing, computer vision, or machine
learning inference on. The application implements an Analyzer method
that is run on each frame.
..这基本上就是我所需要的。所以,我像这样实现了这个图像分析器:
imageAnalysis.setAnalyzer { image: ImageProxy, _: Int ->
viewModel.onAnalyzeImage(image)
}
我得到的是image: ImageProxy
。我怎样才能将这个 ImageProxy
转移到 Bitmap
?
我试过这样解决:
fun decodeBitmap(image: ImageProxy): Bitmap? {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.capacity()).also { buffer.get(it) }
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
但它 returns null
- 因为 decodeByteArray
没有收到有效的 (?) 位图字节。有什么想法吗?
您需要检查 image.format
是否为 ImageFormat.YUV_420_888
。如果是这样,那么您可以使用此扩展程序将图像转换为位图:
fun Image.toBitmap(): Bitmap {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
这适用于多种相机配置。但是,您可能需要使用考虑像素步幅的更高级方法。
我需要 Java 中的 of Mike A,所以我转换了它。
您可以先使用
在 Java 中将 ImageProxy 转换为图像
Image image = imageProxy.getImage();
然后你可以使用上层函数将Image转换成Bitmap Java
private Bitmap toBitmap(Image image) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer yBuffer = planes[0].getBuffer();
ByteBuffer uBuffer = planes[1].getBuffer();
ByteBuffer vBuffer = planes[2].getBuffer();
int ySize = yBuffer.remaining();
int uSize = uBuffer.remaining();
int vSize = vBuffer.remaining();
byte[] nv21 = new byte[ySize + uSize + vSize];
//U and V are swapped
yBuffer.get(nv21, 0, ySize);
vBuffer.get(nv21, ySize, vSize);
uBuffer.get(nv21, ySize + vSize, uSize);
YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 75, out);
byte[] imageBytes = out.toByteArray();
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
}
此答案的权利保留给 Mike A
有一个更简单的解决方案。您无需任何转换即可从 TextureView
中获取 Bitmap
。 documentation.
中的更多信息
imageAnalysis.setAnalyzer { image: ImageProxy, _: Int ->
val bitmap = textureView.bitmap
}
灵感来自@mike-a
的回答
private fun ImageProxy.toMat(): Mat {
val graySourceMatrix = Mat(height, width, CvType.CV_8UC1)
val yBuffer = planes[0].buffer
val ySize = yBuffer.remaining()
val yPlane = ByteArray(ySize)
yBuffer[yPlane, 0, ySize]
graySourceMatrix.put(0, 0, yPlane)
return graySourceMatrix
}
如果您打算使用 OpenCV,这将直接带您进入灰色矩阵领域,颜色对您来说不再重要。
为了提高性能,如果您在每一帧都这样做,您可以将 Mat
的初始化移到外面。
还有一个implementation of this conversion. At first YUV_420_888
is converted to NV21
and then RenderScript
是用来转换位图的(所以估计效率更高)。此外,它考虑了更正确的像素步幅。它也来自官方 android 相机样本回购。
如果谁不想处理RenderScript
和同步这里是修改后的代码:
fun ImageProxy.toBitmap(): Bitmap? {
val nv21 = yuv420888ToNv21(this)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
return yuvImage.toBitmap()
}
private fun YuvImage.toBitmap(): Bitmap? {
val out = ByteArrayOutputStream()
if (!compressToJpeg(Rect(0, 0, width, height), 100, out))
return null
val imageBytes: ByteArray = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
private fun yuv420888ToNv21(image: ImageProxy): ByteArray {
val pixelCount = image.cropRect.width() * image.cropRect.height()
val pixelSizeBits = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888)
val outputBuffer = ByteArray(pixelCount * pixelSizeBits / 8)
imageToByteBuffer(image, outputBuffer, pixelCount)
return outputBuffer
}
private fun imageToByteBuffer(image: ImageProxy, outputBuffer: ByteArray, pixelCount: Int) {
assert(image.format == ImageFormat.YUV_420_888)
val imageCrop = image.cropRect
val imagePlanes = image.planes
imagePlanes.forEachIndexed { planeIndex, plane ->
// How many values are read in input for each output value written
// Only the Y plane has a value for every pixel, U and V have half the resolution i.e.
//
// Y Plane U Plane V Plane
// =============== ======= =======
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
val outputStride: Int
// The index in the output buffer the next value will be written at
// For Y it's zero, for U and V we start at the end of Y and interleave them i.e.
//
// First chunk Second chunk
// =============== ===============
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
var outputOffset: Int
when (planeIndex) {
0 -> {
outputStride = 1
outputOffset = 0
}
1 -> {
outputStride = 2
// For NV21 format, U is in odd-numbered indices
outputOffset = pixelCount + 1
}
2 -> {
outputStride = 2
// For NV21 format, V is in even-numbered indices
outputOffset = pixelCount
}
else -> {
// Image contains more than 3 planes, something strange is going on
return@forEachIndexed
}
}
val planeBuffer = plane.buffer
val rowStride = plane.rowStride
val pixelStride = plane.pixelStride
// We have to divide the width and height by two if it's not the Y plane
val planeCrop = if (planeIndex == 0) {
imageCrop
} else {
Rect(
imageCrop.left / 2,
imageCrop.top / 2,
imageCrop.right / 2,
imageCrop.bottom / 2
)
}
val planeWidth = planeCrop.width()
val planeHeight = planeCrop.height()
// Intermediate buffer used to store the bytes of each row
val rowBuffer = ByteArray(plane.rowStride)
// Size of each row in bytes
val rowLength = if (pixelStride == 1 && outputStride == 1) {
planeWidth
} else {
// Take into account that the stride may include data from pixels other than this
// particular plane and row, and that could be between pixels and not after every
// pixel:
//
// |---- Pixel stride ----| Row ends here --> |
// | Pixel 1 | Other Data | Pixel 2 | Other Data | ... | Pixel N |
//
// We need to get (N-1) * (pixel stride bytes) per row + 1 byte for the last pixel
(planeWidth - 1) * pixelStride + 1
}
for (row in 0 until planeHeight) {
// Move buffer position to the beginning of this row
planeBuffer.position(
(row + planeCrop.top) * rowStride + planeCrop.left * pixelStride)
if (pixelStride == 1 && outputStride == 1) {
// When there is a single stride value for pixel and output, we can just copy
// the entire row in a single step
planeBuffer.get(outputBuffer, outputOffset, rowLength)
outputOffset += rowLength
} else {
// When either pixel or output have a stride > 1 we must copy pixel by pixel
planeBuffer.get(rowBuffer, 0, rowLength)
for (col in 0 until planeWidth) {
outputBuffer[outputOffset] = rowBuffer[col * pixelStride]
outputOffset += outputStride
}
}
}
}
}
注意。 There is OpenCV android SDK 中的类似转换。
嗯,给textureview设置预览,就可以了
位图位图=textureView.getBitmap();
我在从 image.getPlanes() 访问缓冲区时遇到 ArrayIndexOutOfBoundsException。下面的函数可以无一例外地将ImageProxy转为Bitmap
Java
private Bitmap convertImageProxyToBitmap(ImageProxy image) {
ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
byteBuffer.rewind();
byte[] bytes = new byte[byteBuffer.capacity()];
byteBuffer.get(bytes);
byte[] clonedBytes = bytes.clone();
return BitmapFactory.decodeByteArray(clonedBytes, 0, clonedBytes.length);
}
Kotlin 扩展函数
fun ImageProxy.convertImageProxyToBitmap(): Bitmap {
val buffer = planes[0].buffer
buffer.rewind()
val bytes = ByteArray(buffer.capacity())
buffer.get(bytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
请看这个answer。将它应用于您的问题所需要做的就是从您的 ImageProxy
中获取图像
Image img = imaget.getImage();
在使用 尝试转换高分辨率(1080p 及更高)图像时出现绿色乱码/位图故障的解决方案 ,尤其是在小米设备上.故障示例:
通过 Google 从 MLKit 示例 中试用此转换器:https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java
要使其正常工作,您还需要添加以下内容:https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/FrameMetadata.java
然后 BitmapUtils.getBitmap(imageProxy)
.
使用 3200x2400 图像在 Poco X3 NFC 上测试。
所以,我想探索新 Google 的相机 API - CameraX
。
我想做的是每秒从相机中获取一张图像,然后将其传递给一个接受位图以用于机器学习目的的函数。
我阅读了有关 Camera X
Image Analyzer 的文档:
The image analysis use case provides your app with a CPU-accessible image to perform image processing, computer vision, or machine learning inference on. The application implements an Analyzer method that is run on each frame.
..这基本上就是我所需要的。所以,我像这样实现了这个图像分析器:
imageAnalysis.setAnalyzer { image: ImageProxy, _: Int ->
viewModel.onAnalyzeImage(image)
}
我得到的是image: ImageProxy
。我怎样才能将这个 ImageProxy
转移到 Bitmap
?
我试过这样解决:
fun decodeBitmap(image: ImageProxy): Bitmap? {
val buffer = image.planes[0].buffer
val bytes = ByteArray(buffer.capacity()).also { buffer.get(it) }
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
但它 returns null
- 因为 decodeByteArray
没有收到有效的 (?) 位图字节。有什么想法吗?
您需要检查 image.format
是否为 ImageFormat.YUV_420_888
。如果是这样,那么您可以使用此扩展程序将图像转换为位图:
fun Image.toBitmap(): Bitmap {
val yBuffer = planes[0].buffer // Y
val vuBuffer = planes[2].buffer // VU
val ySize = yBuffer.remaining()
val vuSize = vuBuffer.remaining()
val nv21 = ByteArray(ySize + vuSize)
yBuffer.get(nv21, 0, ySize)
vuBuffer.get(nv21, ySize, vuSize)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
val out = ByteArrayOutputStream()
yuvImage.compressToJpeg(Rect(0, 0, yuvImage.width, yuvImage.height), 50, out)
val imageBytes = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
这适用于多种相机配置。但是,您可能需要使用考虑像素步幅的更高级方法。
我需要 Java 中的
您可以先使用
在 Java 中将 ImageProxy 转换为图像Image image = imageProxy.getImage();
然后你可以使用上层函数将Image转换成Bitmap Java
private Bitmap toBitmap(Image image) {
Image.Plane[] planes = image.getPlanes();
ByteBuffer yBuffer = planes[0].getBuffer();
ByteBuffer uBuffer = planes[1].getBuffer();
ByteBuffer vBuffer = planes[2].getBuffer();
int ySize = yBuffer.remaining();
int uSize = uBuffer.remaining();
int vSize = vBuffer.remaining();
byte[] nv21 = new byte[ySize + uSize + vSize];
//U and V are swapped
yBuffer.get(nv21, 0, ySize);
vBuffer.get(nv21, ySize, vSize);
uBuffer.get(nv21, ySize + vSize, uSize);
YuvImage yuvImage = new YuvImage(nv21, ImageFormat.NV21, image.getWidth(), image.getHeight(), null);
ByteArrayOutputStream out = new ByteArrayOutputStream();
yuvImage.compressToJpeg(new Rect(0, 0, yuvImage.getWidth(), yuvImage.getHeight()), 75, out);
byte[] imageBytes = out.toByteArray();
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.length);
}
此答案的权利保留给 Mike A
有一个更简单的解决方案。您无需任何转换即可从 TextureView
中获取 Bitmap
。 documentation.
imageAnalysis.setAnalyzer { image: ImageProxy, _: Int ->
val bitmap = textureView.bitmap
}
灵感来自@mike-a
的回答private fun ImageProxy.toMat(): Mat {
val graySourceMatrix = Mat(height, width, CvType.CV_8UC1)
val yBuffer = planes[0].buffer
val ySize = yBuffer.remaining()
val yPlane = ByteArray(ySize)
yBuffer[yPlane, 0, ySize]
graySourceMatrix.put(0, 0, yPlane)
return graySourceMatrix
}
如果您打算使用 OpenCV,这将直接带您进入灰色矩阵领域,颜色对您来说不再重要。
为了提高性能,如果您在每一帧都这样做,您可以将 Mat
的初始化移到外面。
还有一个implementation of this conversion. At first YUV_420_888
is converted to NV21
and then RenderScript
是用来转换位图的(所以估计效率更高)。此外,它考虑了更正确的像素步幅。它也来自官方 android 相机样本回购。
如果谁不想处理RenderScript
和同步这里是修改后的代码:
fun ImageProxy.toBitmap(): Bitmap? {
val nv21 = yuv420888ToNv21(this)
val yuvImage = YuvImage(nv21, ImageFormat.NV21, width, height, null)
return yuvImage.toBitmap()
}
private fun YuvImage.toBitmap(): Bitmap? {
val out = ByteArrayOutputStream()
if (!compressToJpeg(Rect(0, 0, width, height), 100, out))
return null
val imageBytes: ByteArray = out.toByteArray()
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
private fun yuv420888ToNv21(image: ImageProxy): ByteArray {
val pixelCount = image.cropRect.width() * image.cropRect.height()
val pixelSizeBits = ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888)
val outputBuffer = ByteArray(pixelCount * pixelSizeBits / 8)
imageToByteBuffer(image, outputBuffer, pixelCount)
return outputBuffer
}
private fun imageToByteBuffer(image: ImageProxy, outputBuffer: ByteArray, pixelCount: Int) {
assert(image.format == ImageFormat.YUV_420_888)
val imageCrop = image.cropRect
val imagePlanes = image.planes
imagePlanes.forEachIndexed { planeIndex, plane ->
// How many values are read in input for each output value written
// Only the Y plane has a value for every pixel, U and V have half the resolution i.e.
//
// Y Plane U Plane V Plane
// =============== ======= =======
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y U U U U V V V V
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
val outputStride: Int
// The index in the output buffer the next value will be written at
// For Y it's zero, for U and V we start at the end of Y and interleave them i.e.
//
// First chunk Second chunk
// =============== ===============
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y V U V U V U V U
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
// Y Y Y Y Y Y Y Y
var outputOffset: Int
when (planeIndex) {
0 -> {
outputStride = 1
outputOffset = 0
}
1 -> {
outputStride = 2
// For NV21 format, U is in odd-numbered indices
outputOffset = pixelCount + 1
}
2 -> {
outputStride = 2
// For NV21 format, V is in even-numbered indices
outputOffset = pixelCount
}
else -> {
// Image contains more than 3 planes, something strange is going on
return@forEachIndexed
}
}
val planeBuffer = plane.buffer
val rowStride = plane.rowStride
val pixelStride = plane.pixelStride
// We have to divide the width and height by two if it's not the Y plane
val planeCrop = if (planeIndex == 0) {
imageCrop
} else {
Rect(
imageCrop.left / 2,
imageCrop.top / 2,
imageCrop.right / 2,
imageCrop.bottom / 2
)
}
val planeWidth = planeCrop.width()
val planeHeight = planeCrop.height()
// Intermediate buffer used to store the bytes of each row
val rowBuffer = ByteArray(plane.rowStride)
// Size of each row in bytes
val rowLength = if (pixelStride == 1 && outputStride == 1) {
planeWidth
} else {
// Take into account that the stride may include data from pixels other than this
// particular plane and row, and that could be between pixels and not after every
// pixel:
//
// |---- Pixel stride ----| Row ends here --> |
// | Pixel 1 | Other Data | Pixel 2 | Other Data | ... | Pixel N |
//
// We need to get (N-1) * (pixel stride bytes) per row + 1 byte for the last pixel
(planeWidth - 1) * pixelStride + 1
}
for (row in 0 until planeHeight) {
// Move buffer position to the beginning of this row
planeBuffer.position(
(row + planeCrop.top) * rowStride + planeCrop.left * pixelStride)
if (pixelStride == 1 && outputStride == 1) {
// When there is a single stride value for pixel and output, we can just copy
// the entire row in a single step
planeBuffer.get(outputBuffer, outputOffset, rowLength)
outputOffset += rowLength
} else {
// When either pixel or output have a stride > 1 we must copy pixel by pixel
planeBuffer.get(rowBuffer, 0, rowLength)
for (col in 0 until planeWidth) {
outputBuffer[outputOffset] = rowBuffer[col * pixelStride]
outputOffset += outputStride
}
}
}
}
}
注意。 There is OpenCV android SDK 中的类似转换。
嗯,给textureview设置预览,就可以了
位图位图=textureView.getBitmap();
我在从 image.getPlanes() 访问缓冲区时遇到 ArrayIndexOutOfBoundsException。下面的函数可以无一例外地将ImageProxy转为Bitmap
Java
private Bitmap convertImageProxyToBitmap(ImageProxy image) {
ByteBuffer byteBuffer = image.getPlanes()[0].getBuffer();
byteBuffer.rewind();
byte[] bytes = new byte[byteBuffer.capacity()];
byteBuffer.get(bytes);
byte[] clonedBytes = bytes.clone();
return BitmapFactory.decodeByteArray(clonedBytes, 0, clonedBytes.length);
}
Kotlin 扩展函数
fun ImageProxy.convertImageProxyToBitmap(): Bitmap {
val buffer = planes[0].buffer
buffer.rewind()
val bytes = ByteArray(buffer.capacity())
buffer.get(bytes)
return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
请看这个answer。将它应用于您的问题所需要做的就是从您的 ImageProxy
中获取图像Image img = imaget.getImage();
在使用
通过 Google 从 MLKit 示例 中试用此转换器:https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/BitmapUtils.java
要使其正常工作,您还需要添加以下内容:https://github.com/googlesamples/mlkit/blob/master/android/vision-quickstart/app/src/main/java/com/google/mlkit/vision/demo/FrameMetadata.java
然后 BitmapUtils.getBitmap(imageProxy)
.
使用 3200x2400 图像在 Poco X3 NFC 上测试。