diff --git a/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt new file mode 100644 index 0000000..06fd16d --- /dev/null +++ b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt @@ -0,0 +1,25 @@ +package com.casic.br.app.extensions + +import android.graphics.Bitmap +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageProxy +import com.casic.br.app.utils.FrameMetadata +import com.casic.br.app.utils.ImageProxyManager + +/** + * Converts a YUV_420_888 image from CameraX API to a bitmap. + */ +@ExperimentalGetImage +fun ImageProxy.toBitmap(): Bitmap? { + val frameMetadata = FrameMetadata.Builder() + .setWidth(width) + .setHeight(height) + .setRotation(imageInfo.rotationDegrees) + .build() + image?.apply { + val nv21Buffer = ImageProxyManager + .yuv420ThreePlanesToNV21(this.planes, width, height) ?: return@apply + return ImageProxyManager.getBitmap(nv21Buffer, frameMetadata); + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt new file mode 100644 index 0000000..06fd16d --- /dev/null +++ b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt @@ -0,0 +1,25 @@ +package com.casic.br.app.extensions + +import android.graphics.Bitmap +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageProxy +import com.casic.br.app.utils.FrameMetadata +import com.casic.br.app.utils.ImageProxyManager + +/** + * Converts a YUV_420_888 image from CameraX API to a bitmap. + */ +@ExperimentalGetImage +fun ImageProxy.toBitmap(): Bitmap? { + val frameMetadata = FrameMetadata.Builder() + .setWidth(width) + .setHeight(height) + .setRotation(imageInfo.rotationDegrees) + .build() + image?.apply { + val nv21Buffer = ImageProxyManager + .yuv420ThreePlanesToNV21(this.planes, width, height) ?: return@apply + return ImageProxyManager.getBitmap(nv21Buffer, frameMetadata); + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java b/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java deleted file mode 100644 index 4ddec0f..0000000 --- a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2020 Google LLC. All rights reserved. - * - * 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 - * - * http://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.casic.br.app.utils; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.ImageFormat; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.YuvImage; -import android.media.Image; -import android.media.Image.Plane; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.camera.core.ExperimentalGetImage; -import androidx.camera.core.ImageProxy; -import androidx.exifinterface.media.ExifInterface; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Utils functions for bitmap conversions. - */ -public class BitmapUtils { - private static final String TAG = "BitmapUtils"; - - /** - * Converts NV21 format byte buffer to bitmap. - */ - @Nullable - public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) { - data.rewind(); - byte[] imageInBuffer = new byte[data.limit()]; - data.get(imageInBuffer, 0, imageInBuffer.length); - try { - YuvImage image = - new YuvImage( - imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream); - - Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); - - stream.close(); - return rotateBitmap(bmp, metadata.getRotation(), false, false); - } catch (Exception e) { - Log.e(TAG, "Error: " + e.getMessage()); - } - return null; - } - - /** - * Converts a YUV_420_888 image from CameraX API to a bitmap. - */ - @Nullable - @ExperimentalGetImage - public static Bitmap getBitmap(ImageProxy image) { - FrameMetadata frameMetadata = - new FrameMetadata.Builder() - .setWidth(image.getWidth()) - .setHeight(image.getHeight()) - .setRotation(image.getImageInfo().getRotationDegrees()) - .build(); - - ByteBuffer nv21Buffer = - yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight()); - return getBitmap(nv21Buffer, frameMetadata); - } - - /** - * Rotates a bitmap if it is converted from a bytebuffer. - */ - private static Bitmap rotateBitmap( - Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) { - Matrix matrix = new Matrix(); - - // Rotate the image back to straight. - matrix.postRotate(rotationDegrees); - - // Mirror the image along the X or Y axis. - matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f); - Bitmap rotatedBitmap = - Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - - // Recycle the old bitmap if it has changed. - if (rotatedBitmap != bitmap) { - bitmap.recycle(); - } - return rotatedBitmap; - } - - @Nullable - public static Bitmap getBitmapFromContentUri(ContentResolver contentResolver, Uri imageUri) - throws IOException { - Bitmap decodedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); - if (decodedBitmap == null) { - return null; - } - int orientation = getExifOrientationTag(contentResolver, imageUri); - - int rotationDegrees = 0; - boolean flipX = false; - boolean flipY = false; - // See e.g. https://magnushoff.com/articles/jpeg-orientation/ for a detailed explanation on each - // orientation. - switch (orientation) { - case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_90: - rotationDegrees = 90; - break; - case ExifInterface.ORIENTATION_TRANSPOSE: - rotationDegrees = 90; - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - rotationDegrees = 180; - break; - case ExifInterface.ORIENTATION_FLIP_VERTICAL: - flipY = true; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - rotationDegrees = -90; - break; - case ExifInterface.ORIENTATION_TRANSVERSE: - rotationDegrees = -90; - flipX = true; - break; - case ExifInterface.ORIENTATION_UNDEFINED: - case ExifInterface.ORIENTATION_NORMAL: - default: - // No transformations necessary in this case. - } - - return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY); - } - - private static int getExifOrientationTag(ContentResolver resolver, Uri imageUri) { - // We only support parsing EXIF orientation tag from local file on the device. - // See also: - // https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html - if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme()) - && !ContentResolver.SCHEME_FILE.equals(imageUri.getScheme())) { - return 0; - } - - ExifInterface exif; - try (InputStream inputStream = resolver.openInputStream(imageUri)) { - if (inputStream == null) { - return 0; - } - - exif = new ExifInterface(inputStream); - } catch (IOException e) { - Log.e(TAG, "failed to open file to read rotation meta data: " + imageUri, e); - return 0; - } - - return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - } - - /** - * Converts YUV_420_888 to NV21 bytebuffer. - * - *

The NV21 format consists of a single byte array containing the Y, U and V values. For an - * image of size S, the first S positions of the array contain all the Y values. The remaining - * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both - * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain - * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU - * - *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled - * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and - * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into - * the first part of the NV21 array. The U and V planes may already have the representation in the - * NV21 format. This happens if the planes share the same buffer, the V buffer is one position - * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy - * them to the NV21 array. - */ - private static ByteBuffer yuv420ThreePlanesToNV21( - Plane[] yuv420888planes, int width, int height) { - int imageSize = width * height; - byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; - - if (areUVPlanesNV21(yuv420888planes, width, height)) { - // Copy the Y values. - yuv420888planes[0].getBuffer().get(out, 0, imageSize); - - ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); - ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); - // Get the first V value from the V buffer, since the U buffer does not contain it. - vBuffer.get(out, imageSize, 1); - // Copy the first U value and the remaining VU values from the U buffer. - uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); - } else { - // Fallback to copying the UV values one by one, which is slower but also works. - // Unpack Y. - unpackPlane(yuv420888planes[0], width, height, out, 0, 1); - // Unpack U. - unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); - // Unpack V. - unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); - } - - return ByteBuffer.wrap(out); - } - - /** - * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. - */ - private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { - int imageSize = width * height; - - ByteBuffer uBuffer = planes[1].getBuffer(); - ByteBuffer vBuffer = planes[2].getBuffer(); - - // Backup buffer properties. - int vBufferPosition = vBuffer.position(); - int uBufferLimit = uBuffer.limit(); - - // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. - vBuffer.position(vBufferPosition + 1); - // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. - uBuffer.limit(uBufferLimit - 1); - - // Check that the buffers are equal and have the expected number of elements. - boolean areNV21 = - (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); - - // Restore buffers to their initial state. - vBuffer.position(vBufferPosition); - uBuffer.limit(uBufferLimit); - - return areNV21; - } - - /** - * Unpack an image plane into a byte array. - * - *

The input plane data will be copied in 'out', starting at 'offset' and every pixel will be - * spaced by 'pixelStride'. Note that there is no row padding on the output. - */ - private static void unpackPlane( - Plane plane, int width, int height, byte[] out, int offset, int pixelStride) { - ByteBuffer buffer = plane.getBuffer(); - buffer.rewind(); - - // Compute the size of the current plane. - // We assume that it has the aspect ratio as the original image. - int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); - if (numRow == 0) { - return; - } - int scaleFactor = height / numRow; - int numCol = width / scaleFactor; - - // Extract the data in the output buffer. - int outputPos = offset; - int rowStart = 0; - for (int row = 0; row < numRow; row++) { - int inputPos = rowStart; - for (int col = 0; col < numCol; col++) { - out[outputPos] = buffer.get(inputPos); - outputPos += pixelStride; - inputPos += plane.getPixelStride(); - } - rowStart += plane.getRowStride(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt new file mode 100644 index 0000000..06fd16d --- /dev/null +++ b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt @@ -0,0 +1,25 @@ +package com.casic.br.app.extensions + +import android.graphics.Bitmap +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageProxy +import com.casic.br.app.utils.FrameMetadata +import com.casic.br.app.utils.ImageProxyManager + +/** + * Converts a YUV_420_888 image from CameraX API to a bitmap. + */ +@ExperimentalGetImage +fun ImageProxy.toBitmap(): Bitmap? { + val frameMetadata = FrameMetadata.Builder() + .setWidth(width) + .setHeight(height) + .setRotation(imageInfo.rotationDegrees) + .build() + image?.apply { + val nv21Buffer = ImageProxyManager + .yuv420ThreePlanesToNV21(this.planes, width, height) ?: return@apply + return ImageProxyManager.getBitmap(nv21Buffer, frameMetadata); + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java b/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java deleted file mode 100644 index 4ddec0f..0000000 --- a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2020 Google LLC. All rights reserved. - * - * 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 - * - * http://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.casic.br.app.utils; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.ImageFormat; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.YuvImage; -import android.media.Image; -import android.media.Image.Plane; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.camera.core.ExperimentalGetImage; -import androidx.camera.core.ImageProxy; -import androidx.exifinterface.media.ExifInterface; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Utils functions for bitmap conversions. - */ -public class BitmapUtils { - private static final String TAG = "BitmapUtils"; - - /** - * Converts NV21 format byte buffer to bitmap. - */ - @Nullable - public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) { - data.rewind(); - byte[] imageInBuffer = new byte[data.limit()]; - data.get(imageInBuffer, 0, imageInBuffer.length); - try { - YuvImage image = - new YuvImage( - imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream); - - Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); - - stream.close(); - return rotateBitmap(bmp, metadata.getRotation(), false, false); - } catch (Exception e) { - Log.e(TAG, "Error: " + e.getMessage()); - } - return null; - } - - /** - * Converts a YUV_420_888 image from CameraX API to a bitmap. - */ - @Nullable - @ExperimentalGetImage - public static Bitmap getBitmap(ImageProxy image) { - FrameMetadata frameMetadata = - new FrameMetadata.Builder() - .setWidth(image.getWidth()) - .setHeight(image.getHeight()) - .setRotation(image.getImageInfo().getRotationDegrees()) - .build(); - - ByteBuffer nv21Buffer = - yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight()); - return getBitmap(nv21Buffer, frameMetadata); - } - - /** - * Rotates a bitmap if it is converted from a bytebuffer. - */ - private static Bitmap rotateBitmap( - Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) { - Matrix matrix = new Matrix(); - - // Rotate the image back to straight. - matrix.postRotate(rotationDegrees); - - // Mirror the image along the X or Y axis. - matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f); - Bitmap rotatedBitmap = - Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - - // Recycle the old bitmap if it has changed. - if (rotatedBitmap != bitmap) { - bitmap.recycle(); - } - return rotatedBitmap; - } - - @Nullable - public static Bitmap getBitmapFromContentUri(ContentResolver contentResolver, Uri imageUri) - throws IOException { - Bitmap decodedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); - if (decodedBitmap == null) { - return null; - } - int orientation = getExifOrientationTag(contentResolver, imageUri); - - int rotationDegrees = 0; - boolean flipX = false; - boolean flipY = false; - // See e.g. https://magnushoff.com/articles/jpeg-orientation/ for a detailed explanation on each - // orientation. - switch (orientation) { - case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_90: - rotationDegrees = 90; - break; - case ExifInterface.ORIENTATION_TRANSPOSE: - rotationDegrees = 90; - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - rotationDegrees = 180; - break; - case ExifInterface.ORIENTATION_FLIP_VERTICAL: - flipY = true; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - rotationDegrees = -90; - break; - case ExifInterface.ORIENTATION_TRANSVERSE: - rotationDegrees = -90; - flipX = true; - break; - case ExifInterface.ORIENTATION_UNDEFINED: - case ExifInterface.ORIENTATION_NORMAL: - default: - // No transformations necessary in this case. - } - - return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY); - } - - private static int getExifOrientationTag(ContentResolver resolver, Uri imageUri) { - // We only support parsing EXIF orientation tag from local file on the device. - // See also: - // https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html - if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme()) - && !ContentResolver.SCHEME_FILE.equals(imageUri.getScheme())) { - return 0; - } - - ExifInterface exif; - try (InputStream inputStream = resolver.openInputStream(imageUri)) { - if (inputStream == null) { - return 0; - } - - exif = new ExifInterface(inputStream); - } catch (IOException e) { - Log.e(TAG, "failed to open file to read rotation meta data: " + imageUri, e); - return 0; - } - - return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - } - - /** - * Converts YUV_420_888 to NV21 bytebuffer. - * - *

The NV21 format consists of a single byte array containing the Y, U and V values. For an - * image of size S, the first S positions of the array contain all the Y values. The remaining - * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both - * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain - * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU - * - *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled - * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and - * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into - * the first part of the NV21 array. The U and V planes may already have the representation in the - * NV21 format. This happens if the planes share the same buffer, the V buffer is one position - * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy - * them to the NV21 array. - */ - private static ByteBuffer yuv420ThreePlanesToNV21( - Plane[] yuv420888planes, int width, int height) { - int imageSize = width * height; - byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; - - if (areUVPlanesNV21(yuv420888planes, width, height)) { - // Copy the Y values. - yuv420888planes[0].getBuffer().get(out, 0, imageSize); - - ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); - ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); - // Get the first V value from the V buffer, since the U buffer does not contain it. - vBuffer.get(out, imageSize, 1); - // Copy the first U value and the remaining VU values from the U buffer. - uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); - } else { - // Fallback to copying the UV values one by one, which is slower but also works. - // Unpack Y. - unpackPlane(yuv420888planes[0], width, height, out, 0, 1); - // Unpack U. - unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); - // Unpack V. - unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); - } - - return ByteBuffer.wrap(out); - } - - /** - * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. - */ - private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { - int imageSize = width * height; - - ByteBuffer uBuffer = planes[1].getBuffer(); - ByteBuffer vBuffer = planes[2].getBuffer(); - - // Backup buffer properties. - int vBufferPosition = vBuffer.position(); - int uBufferLimit = uBuffer.limit(); - - // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. - vBuffer.position(vBufferPosition + 1); - // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. - uBuffer.limit(uBufferLimit - 1); - - // Check that the buffers are equal and have the expected number of elements. - boolean areNV21 = - (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); - - // Restore buffers to their initial state. - vBuffer.position(vBufferPosition); - uBuffer.limit(uBufferLimit); - - return areNV21; - } - - /** - * Unpack an image plane into a byte array. - * - *

The input plane data will be copied in 'out', starting at 'offset' and every pixel will be - * spaced by 'pixelStride'. Note that there is no row padding on the output. - */ - private static void unpackPlane( - Plane plane, int width, int height, byte[] out, int offset, int pixelStride) { - ByteBuffer buffer = plane.getBuffer(); - buffer.rewind(); - - // Compute the size of the current plane. - // We assume that it has the aspect ratio as the original image. - int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); - if (numRow == 0) { - return; - } - int scaleFactor = height / numRow; - int numCol = width / scaleFactor; - - // Extract the data in the output buffer. - int outputPos = offset; - int rowStart = 0; - for (int row = 0; row < numRow; row++) { - int inputPos = rowStart; - for (int col = 0; col < numCol; col++) { - out[outputPos] = buffer.get(inputPos); - outputPos += pixelStride; - inputPos += plane.getPixelStride(); - } - rowStart += plane.getRowStride(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt b/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt index a9ab2d0..bfe33cb 100644 --- a/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt +++ b/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt @@ -13,58 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.casic.br.app.utils -package com.casic.br.app.utils; +/** Describing a frame info. */ +class FrameMetadata private constructor(builder: Builder) { -/** Describing a frame info. */ -public class FrameMetadata { + val width = builder.width + val height = builder.height + val rotation = builder.rotation - private final int width; - private final int height; - private final int rotation; + /** Builder of [FrameMetadata]. */ + class Builder { + var width = 0 + var height = 0 + var rotation = 0 - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getRotation() { - return rotation; - } - - private FrameMetadata(int width, int height, int rotation) { - this.width = width; - this.height = height; - this.rotation = rotation; - } - - /** Builder of {@link FrameMetadata}. */ - public static class Builder { - - private int width; - private int height; - private int rotation; - - public Builder setWidth(int width) { - this.width = width; - return this; + fun setWidth(width: Int): Builder { + this.width = width + return this } - public Builder setHeight(int height) { - this.height = height; - return this; + fun setHeight(height: Int): Builder { + this.height = height + return this } - public Builder setRotation(int rotation) { - this.rotation = rotation; - return this; + fun setRotation(rotation: Int): Builder { + this.rotation = rotation + return this } - public FrameMetadata build() { - return new FrameMetadata(width, height, rotation); + fun build(): FrameMetadata { + return FrameMetadata(this) } } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt new file mode 100644 index 0000000..06fd16d --- /dev/null +++ b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt @@ -0,0 +1,25 @@ +package com.casic.br.app.extensions + +import android.graphics.Bitmap +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageProxy +import com.casic.br.app.utils.FrameMetadata +import com.casic.br.app.utils.ImageProxyManager + +/** + * Converts a YUV_420_888 image from CameraX API to a bitmap. + */ +@ExperimentalGetImage +fun ImageProxy.toBitmap(): Bitmap? { + val frameMetadata = FrameMetadata.Builder() + .setWidth(width) + .setHeight(height) + .setRotation(imageInfo.rotationDegrees) + .build() + image?.apply { + val nv21Buffer = ImageProxyManager + .yuv420ThreePlanesToNV21(this.planes, width, height) ?: return@apply + return ImageProxyManager.getBitmap(nv21Buffer, frameMetadata); + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java b/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java deleted file mode 100644 index 4ddec0f..0000000 --- a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2020 Google LLC. All rights reserved. - * - * 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 - * - * http://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.casic.br.app.utils; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.ImageFormat; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.YuvImage; -import android.media.Image; -import android.media.Image.Plane; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.camera.core.ExperimentalGetImage; -import androidx.camera.core.ImageProxy; -import androidx.exifinterface.media.ExifInterface; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Utils functions for bitmap conversions. - */ -public class BitmapUtils { - private static final String TAG = "BitmapUtils"; - - /** - * Converts NV21 format byte buffer to bitmap. - */ - @Nullable - public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) { - data.rewind(); - byte[] imageInBuffer = new byte[data.limit()]; - data.get(imageInBuffer, 0, imageInBuffer.length); - try { - YuvImage image = - new YuvImage( - imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream); - - Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); - - stream.close(); - return rotateBitmap(bmp, metadata.getRotation(), false, false); - } catch (Exception e) { - Log.e(TAG, "Error: " + e.getMessage()); - } - return null; - } - - /** - * Converts a YUV_420_888 image from CameraX API to a bitmap. - */ - @Nullable - @ExperimentalGetImage - public static Bitmap getBitmap(ImageProxy image) { - FrameMetadata frameMetadata = - new FrameMetadata.Builder() - .setWidth(image.getWidth()) - .setHeight(image.getHeight()) - .setRotation(image.getImageInfo().getRotationDegrees()) - .build(); - - ByteBuffer nv21Buffer = - yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight()); - return getBitmap(nv21Buffer, frameMetadata); - } - - /** - * Rotates a bitmap if it is converted from a bytebuffer. - */ - private static Bitmap rotateBitmap( - Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) { - Matrix matrix = new Matrix(); - - // Rotate the image back to straight. - matrix.postRotate(rotationDegrees); - - // Mirror the image along the X or Y axis. - matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f); - Bitmap rotatedBitmap = - Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - - // Recycle the old bitmap if it has changed. - if (rotatedBitmap != bitmap) { - bitmap.recycle(); - } - return rotatedBitmap; - } - - @Nullable - public static Bitmap getBitmapFromContentUri(ContentResolver contentResolver, Uri imageUri) - throws IOException { - Bitmap decodedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); - if (decodedBitmap == null) { - return null; - } - int orientation = getExifOrientationTag(contentResolver, imageUri); - - int rotationDegrees = 0; - boolean flipX = false; - boolean flipY = false; - // See e.g. https://magnushoff.com/articles/jpeg-orientation/ for a detailed explanation on each - // orientation. - switch (orientation) { - case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_90: - rotationDegrees = 90; - break; - case ExifInterface.ORIENTATION_TRANSPOSE: - rotationDegrees = 90; - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - rotationDegrees = 180; - break; - case ExifInterface.ORIENTATION_FLIP_VERTICAL: - flipY = true; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - rotationDegrees = -90; - break; - case ExifInterface.ORIENTATION_TRANSVERSE: - rotationDegrees = -90; - flipX = true; - break; - case ExifInterface.ORIENTATION_UNDEFINED: - case ExifInterface.ORIENTATION_NORMAL: - default: - // No transformations necessary in this case. - } - - return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY); - } - - private static int getExifOrientationTag(ContentResolver resolver, Uri imageUri) { - // We only support parsing EXIF orientation tag from local file on the device. - // See also: - // https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html - if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme()) - && !ContentResolver.SCHEME_FILE.equals(imageUri.getScheme())) { - return 0; - } - - ExifInterface exif; - try (InputStream inputStream = resolver.openInputStream(imageUri)) { - if (inputStream == null) { - return 0; - } - - exif = new ExifInterface(inputStream); - } catch (IOException e) { - Log.e(TAG, "failed to open file to read rotation meta data: " + imageUri, e); - return 0; - } - - return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - } - - /** - * Converts YUV_420_888 to NV21 bytebuffer. - * - *

The NV21 format consists of a single byte array containing the Y, U and V values. For an - * image of size S, the first S positions of the array contain all the Y values. The remaining - * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both - * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain - * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU - * - *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled - * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and - * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into - * the first part of the NV21 array. The U and V planes may already have the representation in the - * NV21 format. This happens if the planes share the same buffer, the V buffer is one position - * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy - * them to the NV21 array. - */ - private static ByteBuffer yuv420ThreePlanesToNV21( - Plane[] yuv420888planes, int width, int height) { - int imageSize = width * height; - byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; - - if (areUVPlanesNV21(yuv420888planes, width, height)) { - // Copy the Y values. - yuv420888planes[0].getBuffer().get(out, 0, imageSize); - - ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); - ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); - // Get the first V value from the V buffer, since the U buffer does not contain it. - vBuffer.get(out, imageSize, 1); - // Copy the first U value and the remaining VU values from the U buffer. - uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); - } else { - // Fallback to copying the UV values one by one, which is slower but also works. - // Unpack Y. - unpackPlane(yuv420888planes[0], width, height, out, 0, 1); - // Unpack U. - unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); - // Unpack V. - unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); - } - - return ByteBuffer.wrap(out); - } - - /** - * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. - */ - private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { - int imageSize = width * height; - - ByteBuffer uBuffer = planes[1].getBuffer(); - ByteBuffer vBuffer = planes[2].getBuffer(); - - // Backup buffer properties. - int vBufferPosition = vBuffer.position(); - int uBufferLimit = uBuffer.limit(); - - // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. - vBuffer.position(vBufferPosition + 1); - // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. - uBuffer.limit(uBufferLimit - 1); - - // Check that the buffers are equal and have the expected number of elements. - boolean areNV21 = - (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); - - // Restore buffers to their initial state. - vBuffer.position(vBufferPosition); - uBuffer.limit(uBufferLimit); - - return areNV21; - } - - /** - * Unpack an image plane into a byte array. - * - *

The input plane data will be copied in 'out', starting at 'offset' and every pixel will be - * spaced by 'pixelStride'. Note that there is no row padding on the output. - */ - private static void unpackPlane( - Plane plane, int width, int height, byte[] out, int offset, int pixelStride) { - ByteBuffer buffer = plane.getBuffer(); - buffer.rewind(); - - // Compute the size of the current plane. - // We assume that it has the aspect ratio as the original image. - int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); - if (numRow == 0) { - return; - } - int scaleFactor = height / numRow; - int numCol = width / scaleFactor; - - // Extract the data in the output buffer. - int outputPos = offset; - int rowStart = 0; - for (int row = 0; row < numRow; row++) { - int inputPos = rowStart; - for (int col = 0; col < numCol; col++) { - out[outputPos] = buffer.get(inputPos); - outputPos += pixelStride; - inputPos += plane.getPixelStride(); - } - rowStart += plane.getRowStride(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt b/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt index a9ab2d0..bfe33cb 100644 --- a/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt +++ b/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt @@ -13,58 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.casic.br.app.utils -package com.casic.br.app.utils; +/** Describing a frame info. */ +class FrameMetadata private constructor(builder: Builder) { -/** Describing a frame info. */ -public class FrameMetadata { + val width = builder.width + val height = builder.height + val rotation = builder.rotation - private final int width; - private final int height; - private final int rotation; + /** Builder of [FrameMetadata]. */ + class Builder { + var width = 0 + var height = 0 + var rotation = 0 - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getRotation() { - return rotation; - } - - private FrameMetadata(int width, int height, int rotation) { - this.width = width; - this.height = height; - this.rotation = rotation; - } - - /** Builder of {@link FrameMetadata}. */ - public static class Builder { - - private int width; - private int height; - private int rotation; - - public Builder setWidth(int width) { - this.width = width; - return this; + fun setWidth(width: Int): Builder { + this.width = width + return this } - public Builder setHeight(int height) { - this.height = height; - return this; + fun setHeight(height: Int): Builder { + this.height = height + return this } - public Builder setRotation(int rotation) { - this.rotation = rotation; - return this; + fun setRotation(rotation: Int): Builder { + this.rotation = rotation + return this } - public FrameMetadata build() { - return new FrameMetadata(width, height, rotation); + fun build(): FrameMetadata { + return FrameMetadata(this) } } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/ImageProxyManager.kt b/app/src/main/java/com/casic/br/app/utils/ImageProxyManager.kt new file mode 100644 index 0000000..59fac2b --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/ImageProxyManager.kt @@ -0,0 +1,168 @@ +package com.casic.br.app.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageFormat +import android.graphics.Matrix +import android.graphics.Rect +import android.graphics.YuvImage +import android.media.Image.Plane +import android.util.Log +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +object ImageProxyManager { + + private const val kTag = "ImageProxyManager" + + /** + * Converts YUV_420_888 to NV21 bytebuffer. + * + *

The NV21 format consists of a single byte array containing the Y, U and V values. For an + * image of size S, the first S positions of the array contain all the Y values. The remaining + * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both + * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain + * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU + * + *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled + * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and + * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into + * the first part of the NV21 array. The U and V planes may already have the representation in the + * NV21 format. This happens if the planes share the same buffer, the V buffer is one position + * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy + * them to the NV21 array. + */ + fun yuv420ThreePlanesToNV21( + yuv420888planes: Array, width: Int, height: Int + ): ByteBuffer? { + val imageSize = width * height + val out = ByteArray(imageSize + 2 * (imageSize / 4)) + if (areUVPlanesNV21(yuv420888planes, width, height)) { + // Copy the Y values. + yuv420888planes[0].buffer[out, 0, imageSize] + val uBuffer = yuv420888planes[1].buffer + val vBuffer = yuv420888planes[2].buffer + // Get the first V value from the V buffer, since the U buffer does not contain it. + vBuffer[out, imageSize, 1] + // Copy the first U value and the remaining VU values from the U buffer. + uBuffer[out, imageSize + 1, 2 * imageSize / 4 - 1] + } else { + // Fallback to copying the UV values one by one, which is slower but also works. + // Unpack Y. + unpackPlane(yuv420888planes[0], width, height, out, 0, 1) + // Unpack U. + unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2) + // Unpack V. + unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2) + } + return ByteBuffer.wrap(out) + } + + /** + * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. + */ + private fun areUVPlanesNV21(planes: Array, width: Int, height: Int): Boolean { + val imageSize = width * height + val uBuffer = planes[1].buffer + val vBuffer = planes[2].buffer + + // Backup buffer properties. + val vBufferPosition = vBuffer.position() + val uBufferLimit = uBuffer.limit() + + // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. + vBuffer.position(vBufferPosition + 1) + // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. + uBuffer.limit(uBufferLimit - 1) + + // Check that the buffers are equal and have the expected number of elements. + val areNV21 = vBuffer.remaining() == 2 * imageSize / 4 - 2 + && vBuffer.compareTo(uBuffer) == 0 + + // Restore buffers to their initial state. + vBuffer.position(vBufferPosition) + uBuffer.limit(uBufferLimit) + return areNV21 + } + + /** + * Unpack an image plane into a byte array. + * + * + * The input plane data will be copied in 'out', starting at 'offset' and every pixel will be + * spaced by 'pixelStride'. Note that there is no row padding on the output. + */ + private fun unpackPlane( + plane: Plane, width: Int, height: Int, out: ByteArray, offset: Int, pixelStride: Int + ) { + val buffer = plane.buffer + buffer.rewind() + + // Compute the size of the current plane. + // We assume that it has the aspect ratio as the original image. + val numRow = (buffer.limit() + plane.rowStride - 1) / plane.rowStride + if (numRow == 0) { + return + } + val scaleFactor = height / numRow + val numCol = width / scaleFactor + + // Extract the data in the output buffer. + var outputPos = offset + var rowStart = 0 + for (row in 0 until numRow) { + var inputPos = rowStart + for (col in 0 until numCol) { + out[outputPos] = buffer[inputPos] + outputPos += pixelStride + inputPos += plane.pixelStride + } + rowStart += plane.rowStride + } + } + + /** + * Converts NV21 format byte buffer to bitmap. + */ + fun getBitmap(data: ByteBuffer, metadata: FrameMetadata): Bitmap? { + data.rewind() + val imageInBuffer = ByteArray(data.limit()) + data[imageInBuffer, 0, imageInBuffer.size] + try { + val image = YuvImage( + imageInBuffer, ImageFormat.NV21, metadata.width, metadata.height, null + ) + val stream = ByteArrayOutputStream() + image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream) + val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()) + stream.close() + return rotateBitmap(bmp, metadata.rotation, false, false) + } catch (e: Exception) { + Log.e(kTag, "Error: " + e.message) + } + return null + } + + /** + * Rotates a bitmap if it is converted from a bytebuffer. + */ + private fun rotateBitmap( + bitmap: Bitmap, rotationDegrees: Int, flipX: Boolean, flipY: Boolean + ): Bitmap? { + val matrix = Matrix() + + // Rotate the image back to straight. + matrix.postRotate(rotationDegrees.toFloat()) + + // Mirror the image along the X or Y axis. + matrix.postScale(if (flipX) -1.0f else 1.0f, if (flipY) -1.0f else 1.0f) + val rotatedBitmap = + Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + + // Recycle the old bitmap if it has changed. + if (rotatedBitmap != bitmap) { + bitmap.recycle() + } + return rotatedBitmap + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt new file mode 100644 index 0000000..06fd16d --- /dev/null +++ b/app/src/main/java/com/casic/br/app/extensions/ImageProxy.kt @@ -0,0 +1,25 @@ +package com.casic.br.app.extensions + +import android.graphics.Bitmap +import androidx.camera.core.ExperimentalGetImage +import androidx.camera.core.ImageProxy +import com.casic.br.app.utils.FrameMetadata +import com.casic.br.app.utils.ImageProxyManager + +/** + * Converts a YUV_420_888 image from CameraX API to a bitmap. + */ +@ExperimentalGetImage +fun ImageProxy.toBitmap(): Bitmap? { + val frameMetadata = FrameMetadata.Builder() + .setWidth(width) + .setHeight(height) + .setRotation(imageInfo.rotationDegrees) + .build() + image?.apply { + val nv21Buffer = ImageProxyManager + .yuv420ThreePlanesToNV21(this.planes, width, height) ?: return@apply + return ImageProxyManager.getBitmap(nv21Buffer, frameMetadata); + } + return null +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java b/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java deleted file mode 100644 index 4ddec0f..0000000 --- a/app/src/main/java/com/casic/br/app/utils/BitmapUtils.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2020 Google LLC. All rights reserved. - * - * 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 - * - * http://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.casic.br.app.utils; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.ImageFormat; -import android.graphics.Matrix; -import android.graphics.Rect; -import android.graphics.YuvImage; -import android.media.Image; -import android.media.Image.Plane; -import android.net.Uri; -import android.provider.MediaStore; -import android.util.Log; - -import androidx.annotation.Nullable; -import androidx.camera.core.ExperimentalGetImage; -import androidx.camera.core.ImageProxy; -import androidx.exifinterface.media.ExifInterface; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Utils functions for bitmap conversions. - */ -public class BitmapUtils { - private static final String TAG = "BitmapUtils"; - - /** - * Converts NV21 format byte buffer to bitmap. - */ - @Nullable - public static Bitmap getBitmap(ByteBuffer data, FrameMetadata metadata) { - data.rewind(); - byte[] imageInBuffer = new byte[data.limit()]; - data.get(imageInBuffer, 0, imageInBuffer.length); - try { - YuvImage image = - new YuvImage( - imageInBuffer, ImageFormat.NV21, metadata.getWidth(), metadata.getHeight(), null); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - image.compressToJpeg(new Rect(0, 0, metadata.getWidth(), metadata.getHeight()), 80, stream); - - Bitmap bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()); - - stream.close(); - return rotateBitmap(bmp, metadata.getRotation(), false, false); - } catch (Exception e) { - Log.e(TAG, "Error: " + e.getMessage()); - } - return null; - } - - /** - * Converts a YUV_420_888 image from CameraX API to a bitmap. - */ - @Nullable - @ExperimentalGetImage - public static Bitmap getBitmap(ImageProxy image) { - FrameMetadata frameMetadata = - new FrameMetadata.Builder() - .setWidth(image.getWidth()) - .setHeight(image.getHeight()) - .setRotation(image.getImageInfo().getRotationDegrees()) - .build(); - - ByteBuffer nv21Buffer = - yuv420ThreePlanesToNV21(image.getImage().getPlanes(), image.getWidth(), image.getHeight()); - return getBitmap(nv21Buffer, frameMetadata); - } - - /** - * Rotates a bitmap if it is converted from a bytebuffer. - */ - private static Bitmap rotateBitmap( - Bitmap bitmap, int rotationDegrees, boolean flipX, boolean flipY) { - Matrix matrix = new Matrix(); - - // Rotate the image back to straight. - matrix.postRotate(rotationDegrees); - - // Mirror the image along the X or Y axis. - matrix.postScale(flipX ? -1.0f : 1.0f, flipY ? -1.0f : 1.0f); - Bitmap rotatedBitmap = - Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); - - // Recycle the old bitmap if it has changed. - if (rotatedBitmap != bitmap) { - bitmap.recycle(); - } - return rotatedBitmap; - } - - @Nullable - public static Bitmap getBitmapFromContentUri(ContentResolver contentResolver, Uri imageUri) - throws IOException { - Bitmap decodedBitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); - if (decodedBitmap == null) { - return null; - } - int orientation = getExifOrientationTag(contentResolver, imageUri); - - int rotationDegrees = 0; - boolean flipX = false; - boolean flipY = false; - // See e.g. https://magnushoff.com/articles/jpeg-orientation/ for a detailed explanation on each - // orientation. - switch (orientation) { - case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_90: - rotationDegrees = 90; - break; - case ExifInterface.ORIENTATION_TRANSPOSE: - rotationDegrees = 90; - flipX = true; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - rotationDegrees = 180; - break; - case ExifInterface.ORIENTATION_FLIP_VERTICAL: - flipY = true; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - rotationDegrees = -90; - break; - case ExifInterface.ORIENTATION_TRANSVERSE: - rotationDegrees = -90; - flipX = true; - break; - case ExifInterface.ORIENTATION_UNDEFINED: - case ExifInterface.ORIENTATION_NORMAL: - default: - // No transformations necessary in this case. - } - - return rotateBitmap(decodedBitmap, rotationDegrees, flipX, flipY); - } - - private static int getExifOrientationTag(ContentResolver resolver, Uri imageUri) { - // We only support parsing EXIF orientation tag from local file on the device. - // See also: - // https://android-developers.googleblog.com/2016/12/introducing-the-exifinterface-support-library.html - if (!ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme()) - && !ContentResolver.SCHEME_FILE.equals(imageUri.getScheme())) { - return 0; - } - - ExifInterface exif; - try (InputStream inputStream = resolver.openInputStream(imageUri)) { - if (inputStream == null) { - return 0; - } - - exif = new ExifInterface(inputStream); - } catch (IOException e) { - Log.e(TAG, "failed to open file to read rotation meta data: " + imageUri, e); - return 0; - } - - return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); - } - - /** - * Converts YUV_420_888 to NV21 bytebuffer. - * - *

The NV21 format consists of a single byte array containing the Y, U and V values. For an - * image of size S, the first S positions of the array contain all the Y values. The remaining - * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both - * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain - * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU - * - *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled - * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and - * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into - * the first part of the NV21 array. The U and V planes may already have the representation in the - * NV21 format. This happens if the planes share the same buffer, the V buffer is one position - * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy - * them to the NV21 array. - */ - private static ByteBuffer yuv420ThreePlanesToNV21( - Plane[] yuv420888planes, int width, int height) { - int imageSize = width * height; - byte[] out = new byte[imageSize + 2 * (imageSize / 4)]; - - if (areUVPlanesNV21(yuv420888planes, width, height)) { - // Copy the Y values. - yuv420888planes[0].getBuffer().get(out, 0, imageSize); - - ByteBuffer uBuffer = yuv420888planes[1].getBuffer(); - ByteBuffer vBuffer = yuv420888planes[2].getBuffer(); - // Get the first V value from the V buffer, since the U buffer does not contain it. - vBuffer.get(out, imageSize, 1); - // Copy the first U value and the remaining VU values from the U buffer. - uBuffer.get(out, imageSize + 1, 2 * imageSize / 4 - 1); - } else { - // Fallback to copying the UV values one by one, which is slower but also works. - // Unpack Y. - unpackPlane(yuv420888planes[0], width, height, out, 0, 1); - // Unpack U. - unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2); - // Unpack V. - unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2); - } - - return ByteBuffer.wrap(out); - } - - /** - * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. - */ - private static boolean areUVPlanesNV21(Plane[] planes, int width, int height) { - int imageSize = width * height; - - ByteBuffer uBuffer = planes[1].getBuffer(); - ByteBuffer vBuffer = planes[2].getBuffer(); - - // Backup buffer properties. - int vBufferPosition = vBuffer.position(); - int uBufferLimit = uBuffer.limit(); - - // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. - vBuffer.position(vBufferPosition + 1); - // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. - uBuffer.limit(uBufferLimit - 1); - - // Check that the buffers are equal and have the expected number of elements. - boolean areNV21 = - (vBuffer.remaining() == (2 * imageSize / 4 - 2)) && (vBuffer.compareTo(uBuffer) == 0); - - // Restore buffers to their initial state. - vBuffer.position(vBufferPosition); - uBuffer.limit(uBufferLimit); - - return areNV21; - } - - /** - * Unpack an image plane into a byte array. - * - *

The input plane data will be copied in 'out', starting at 'offset' and every pixel will be - * spaced by 'pixelStride'. Note that there is no row padding on the output. - */ - private static void unpackPlane( - Plane plane, int width, int height, byte[] out, int offset, int pixelStride) { - ByteBuffer buffer = plane.getBuffer(); - buffer.rewind(); - - // Compute the size of the current plane. - // We assume that it has the aspect ratio as the original image. - int numRow = (buffer.limit() + plane.getRowStride() - 1) / plane.getRowStride(); - if (numRow == 0) { - return; - } - int scaleFactor = height / numRow; - int numCol = width / scaleFactor; - - // Extract the data in the output buffer. - int outputPos = offset; - int rowStart = 0; - for (int row = 0; row < numRow; row++) { - int inputPos = rowStart; - for (int col = 0; col < numCol; col++) { - out[outputPos] = buffer.get(inputPos); - outputPos += pixelStride; - inputPos += plane.getPixelStride(); - } - rowStart += plane.getRowStride(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt b/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt index a9ab2d0..bfe33cb 100644 --- a/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt +++ b/app/src/main/java/com/casic/br/app/utils/FrameMetadata.kt @@ -13,58 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package com.casic.br.app.utils -package com.casic.br.app.utils; +/** Describing a frame info. */ +class FrameMetadata private constructor(builder: Builder) { -/** Describing a frame info. */ -public class FrameMetadata { + val width = builder.width + val height = builder.height + val rotation = builder.rotation - private final int width; - private final int height; - private final int rotation; + /** Builder of [FrameMetadata]. */ + class Builder { + var width = 0 + var height = 0 + var rotation = 0 - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public int getRotation() { - return rotation; - } - - private FrameMetadata(int width, int height, int rotation) { - this.width = width; - this.height = height; - this.rotation = rotation; - } - - /** Builder of {@link FrameMetadata}. */ - public static class Builder { - - private int width; - private int height; - private int rotation; - - public Builder setWidth(int width) { - this.width = width; - return this; + fun setWidth(width: Int): Builder { + this.width = width + return this } - public Builder setHeight(int height) { - this.height = height; - return this; + fun setHeight(height: Int): Builder { + this.height = height + return this } - public Builder setRotation(int rotation) { - this.rotation = rotation; - return this; + fun setRotation(rotation: Int): Builder { + this.rotation = rotation + return this } - public FrameMetadata build() { - return new FrameMetadata(width, height, rotation); + fun build(): FrameMetadata { + return FrameMetadata(this) } } } \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/utils/ImageProxyManager.kt b/app/src/main/java/com/casic/br/app/utils/ImageProxyManager.kt new file mode 100644 index 0000000..59fac2b --- /dev/null +++ b/app/src/main/java/com/casic/br/app/utils/ImageProxyManager.kt @@ -0,0 +1,168 @@ +package com.casic.br.app.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.ImageFormat +import android.graphics.Matrix +import android.graphics.Rect +import android.graphics.YuvImage +import android.media.Image.Plane +import android.util.Log +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +object ImageProxyManager { + + private const val kTag = "ImageProxyManager" + + /** + * Converts YUV_420_888 to NV21 bytebuffer. + * + *

The NV21 format consists of a single byte array containing the Y, U and V values. For an + * image of size S, the first S positions of the array contain all the Y values. The remaining + * positions contain interleaved V and U values. U and V are subsampled by a factor of 2 in both + * dimensions, so there are S/4 U values and S/4 V values. In summary, the NV21 array will contain + * S Y values followed by S/4 VU values: YYYYYYYYYYYYYY(...)YVUVUVUVU(...)VU + * + *

YUV_420_888 is a generic format that can describe any YUV image where U and V are subsampled + * by a factor of 2 in both dimensions. {@link Image#getPlanes} returns an array with the Y, U and + * V planes. The Y plane is guaranteed not to be interleaved, so we can just copy its values into + * the first part of the NV21 array. The U and V planes may already have the representation in the + * NV21 format. This happens if the planes share the same buffer, the V buffer is one position + * before the U buffer and the planes have a pixelStride of 2. If this is case, we can just copy + * them to the NV21 array. + */ + fun yuv420ThreePlanesToNV21( + yuv420888planes: Array, width: Int, height: Int + ): ByteBuffer? { + val imageSize = width * height + val out = ByteArray(imageSize + 2 * (imageSize / 4)) + if (areUVPlanesNV21(yuv420888planes, width, height)) { + // Copy the Y values. + yuv420888planes[0].buffer[out, 0, imageSize] + val uBuffer = yuv420888planes[1].buffer + val vBuffer = yuv420888planes[2].buffer + // Get the first V value from the V buffer, since the U buffer does not contain it. + vBuffer[out, imageSize, 1] + // Copy the first U value and the remaining VU values from the U buffer. + uBuffer[out, imageSize + 1, 2 * imageSize / 4 - 1] + } else { + // Fallback to copying the UV values one by one, which is slower but also works. + // Unpack Y. + unpackPlane(yuv420888planes[0], width, height, out, 0, 1) + // Unpack U. + unpackPlane(yuv420888planes[1], width, height, out, imageSize + 1, 2) + // Unpack V. + unpackPlane(yuv420888planes[2], width, height, out, imageSize, 2) + } + return ByteBuffer.wrap(out) + } + + /** + * Checks if the UV plane buffers of a YUV_420_888 image are in the NV21 format. + */ + private fun areUVPlanesNV21(planes: Array, width: Int, height: Int): Boolean { + val imageSize = width * height + val uBuffer = planes[1].buffer + val vBuffer = planes[2].buffer + + // Backup buffer properties. + val vBufferPosition = vBuffer.position() + val uBufferLimit = uBuffer.limit() + + // Advance the V buffer by 1 byte, since the U buffer will not contain the first V value. + vBuffer.position(vBufferPosition + 1) + // Chop off the last byte of the U buffer, since the V buffer will not contain the last U value. + uBuffer.limit(uBufferLimit - 1) + + // Check that the buffers are equal and have the expected number of elements. + val areNV21 = vBuffer.remaining() == 2 * imageSize / 4 - 2 + && vBuffer.compareTo(uBuffer) == 0 + + // Restore buffers to their initial state. + vBuffer.position(vBufferPosition) + uBuffer.limit(uBufferLimit) + return areNV21 + } + + /** + * Unpack an image plane into a byte array. + * + * + * The input plane data will be copied in 'out', starting at 'offset' and every pixel will be + * spaced by 'pixelStride'. Note that there is no row padding on the output. + */ + private fun unpackPlane( + plane: Plane, width: Int, height: Int, out: ByteArray, offset: Int, pixelStride: Int + ) { + val buffer = plane.buffer + buffer.rewind() + + // Compute the size of the current plane. + // We assume that it has the aspect ratio as the original image. + val numRow = (buffer.limit() + plane.rowStride - 1) / plane.rowStride + if (numRow == 0) { + return + } + val scaleFactor = height / numRow + val numCol = width / scaleFactor + + // Extract the data in the output buffer. + var outputPos = offset + var rowStart = 0 + for (row in 0 until numRow) { + var inputPos = rowStart + for (col in 0 until numCol) { + out[outputPos] = buffer[inputPos] + outputPos += pixelStride + inputPos += plane.pixelStride + } + rowStart += plane.rowStride + } + } + + /** + * Converts NV21 format byte buffer to bitmap. + */ + fun getBitmap(data: ByteBuffer, metadata: FrameMetadata): Bitmap? { + data.rewind() + val imageInBuffer = ByteArray(data.limit()) + data[imageInBuffer, 0, imageInBuffer.size] + try { + val image = YuvImage( + imageInBuffer, ImageFormat.NV21, metadata.width, metadata.height, null + ) + val stream = ByteArrayOutputStream() + image.compressToJpeg(Rect(0, 0, metadata.width, metadata.height), 80, stream) + val bmp = BitmapFactory.decodeByteArray(stream.toByteArray(), 0, stream.size()) + stream.close() + return rotateBitmap(bmp, metadata.rotation, false, false) + } catch (e: Exception) { + Log.e(kTag, "Error: " + e.message) + } + return null + } + + /** + * Rotates a bitmap if it is converted from a bytebuffer. + */ + private fun rotateBitmap( + bitmap: Bitmap, rotationDegrees: Int, flipX: Boolean, flipY: Boolean + ): Bitmap? { + val matrix = Matrix() + + // Rotate the image back to straight. + matrix.postRotate(rotationDegrees.toFloat()) + + // Mirror the image along the X or Y axis. + matrix.postScale(if (flipX) -1.0f else 1.0f, if (flipY) -1.0f else 1.0f) + val rotatedBitmap = + Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + + // Recycle the old bitmap if it has changed. + if (rotatedBitmap != bitmap) { + bitmap.recycle() + } + return rotatedBitmap + } +} \ No newline at end of file diff --git a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt index f52e4d1..940b35c 100644 --- a/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt +++ b/app/src/main/java/com/casic/br/app/view/StartCheckActivity.kt @@ -3,10 +3,7 @@ import android.app.Activity import android.content.Intent import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.graphics.ImageFormat -import android.graphics.Rect -import android.graphics.YuvImage import android.os.Build import android.os.Bundle import android.util.Base64 @@ -29,8 +26,8 @@ import com.casic.br.app.R import com.casic.br.app.databinding.ActivityStartCheckBinding import com.casic.br.app.extensions.initImmersionBar +import com.casic.br.app.extensions.toBitmap import com.casic.br.app.model.DictionaryModel -import com.casic.br.app.utils.BitmapUtils import com.casic.br.app.utils.LocaleConstant import com.casic.br.app.utils.LocationManager import com.casic.br.app.vm.ConfigViewModel @@ -40,7 +37,6 @@ import com.pengxh.kt.lite.base.KotlinBaseActivity import com.pengxh.kt.lite.extensions.convertColor import com.pengxh.kt.lite.extensions.navigatePageTo -import com.pengxh.kt.lite.extensions.rotateImage import com.pengxh.kt.lite.extensions.show import com.pengxh.kt.lite.extensions.timestampToDate import com.pengxh.kt.lite.extensions.timestampToTime @@ -249,54 +245,27 @@ private val imageAnalyzer = ImageAnalysis.Analyzer { imageProxy -> if (imageProxy.format == ImageFormat.YUV_420_888) { executor.execute { - val bitmap = if (Build.BRAND == "Redmi" || Build.MANUFACTURER == "Xiaomi") { - BitmapUtils.getBitmap(imageProxy) ?: return@execute - } else { - val image = imageProxy.image ?: return@execute + /** + * CameraX 原始预览Image数据(imageProxy.format == ImageFormat.YUV_420_888)转Bitmap + * */ + val bitmap = imageProxy.toBitmap() + if (bitmap != null) { + val base64 = bitmap.encodeToBase64() - val yBuffer = image.planes[0].buffer - val uBuffer = image.planes[1].buffer - val vBuffer = image.planes[2].buffer + //先识别场景 + if (!isDetectingScene) { + isDetectingScene = true + imageFileViewModel.getScene(context, base64) + } - val ySize = yBuffer.remaining() - val uSize = uBuffer.remaining() - val vSize = vBuffer.remaining() - - val nv21 = ByteArray(ySize + uSize + vSize) - yBuffer.get(nv21, 0, ySize) - vBuffer.get(nv21, ySize, vSize) - uBuffer.get(nv21, ySize + vSize, uSize) - - val yuvImage = YuvImage( - nv21, ImageFormat.NV21, image.width, image.height, null - ) - val out = ByteArrayOutputStream() - yuvImage.compressToJpeg( - Rect(0, 0, yuvImage.width, yuvImage.height), 100, out - ) - val imageBytes = out.toByteArray() - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).rotateImage(90) + //再识别隐患 + if (!isDetectingTarget && detectedScene.isNotBlank()) { + isDetectingTarget = true + imageFileViewModel.getRecognizeResult( + context, base64, detectedScene, inspectionId + ) + } } - -// val path = "${createImageFileDir()}/${System.currentTimeMillis()}.png" -// Log.d(kTag, path) -// bitmap.saveImage(path) - val base64 = bitmap.encodeToBase64() - - //先识别场景 - if (!isDetectingScene) { - isDetectingScene = true - imageFileViewModel.getScene(context, base64) - } - - //再识别隐患 - if (!isDetectingTarget && detectedScene.isNotBlank()) { - isDetectingTarget = true - imageFileViewModel.getRecognizeResult( - context, base64, detectedScene, inspectionId - ) - } - //检测完之后close就会继续生成下一帧图片,否则就会被阻塞不会继续生成下一帧 imageProxy.close() }