parent
6e937843c9
commit
a5e9189101
@ -1,100 +0,0 @@ |
|||||||
package com.otaliastudios.cameraview.internal.utils; |
|
||||||
|
|
||||||
|
|
||||||
import android.graphics.Bitmap; |
|
||||||
import android.graphics.BitmapFactory; |
|
||||||
import android.graphics.Canvas; |
|
||||||
import android.graphics.Color; |
|
||||||
import android.graphics.ImageFormat; |
|
||||||
import android.graphics.Paint; |
|
||||||
import android.graphics.PorterDuff; |
|
||||||
import android.graphics.Rect; |
|
||||||
import android.graphics.YuvImage; |
|
||||||
import android.media.Image; |
|
||||||
import android.media.ImageReader; |
|
||||||
import android.os.Handler; |
|
||||||
import android.os.Looper; |
|
||||||
import android.view.Surface; |
|
||||||
|
|
||||||
import androidx.annotation.NonNull; |
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4; |
|
||||||
import androidx.test.filters.SmallTest; |
|
||||||
|
|
||||||
import com.otaliastudios.cameraview.BaseTest; |
|
||||||
import com.otaliastudios.cameraview.tools.Op; |
|
||||||
import com.otaliastudios.cameraview.tools.SdkExclude; |
|
||||||
|
|
||||||
import org.junit.Test; |
|
||||||
import org.junit.runner.RunWith; |
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream; |
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull; |
|
||||||
|
|
||||||
/** |
|
||||||
* Starting from API 29, surface.lockCanvas() sets the surface format to RGBA_8888: |
|
||||||
* https://github.com/aosp-mirror/platform_frameworks_base/blob/android10-release/core/jni/android_view_Surface.cpp#L215-L217 .
|
|
||||||
* For this reason, acquireLatestImage crashes because we requested a different format. |
|
||||||
*/ |
|
||||||
@SdkExclude(minSdkVersion = 29) |
|
||||||
@RunWith(AndroidJUnit4.class) |
|
||||||
@SmallTest |
|
||||||
public class ImageHelperTest extends BaseTest { |
|
||||||
|
|
||||||
@NonNull |
|
||||||
private Image getImage() { |
|
||||||
ImageReader reader = ImageReader.newInstance(100, 100, ImageFormat.YUV_420_888, 1); |
|
||||||
Surface readerSurface = reader.getSurface(); |
|
||||||
final Op<Image> imageOp = new Op<>(); |
|
||||||
reader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() { |
|
||||||
@Override |
|
||||||
public void onImageAvailable(ImageReader reader) { |
|
||||||
Image image = reader.acquireLatestImage(); |
|
||||||
if (image != null) imageOp.controller().end(image); |
|
||||||
} |
|
||||||
}, new Handler(Looper.getMainLooper())); |
|
||||||
|
|
||||||
// Write on reader surface.
|
|
||||||
Canvas readerCanvas = readerSurface.lockCanvas(null); |
|
||||||
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
|
||||||
paint.setColor(Color.RED); |
|
||||||
readerCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.MULTIPLY); |
|
||||||
readerCanvas.drawCircle(50, 50, 50, paint); |
|
||||||
readerSurface.unlockCanvasAndPost(readerCanvas); |
|
||||||
|
|
||||||
// Wait
|
|
||||||
Image image = imageOp.await(5000); |
|
||||||
assertNotNull(image); |
|
||||||
return image; |
|
||||||
} |
|
||||||
|
|
||||||
@Test |
|
||||||
public void testImage() { |
|
||||||
Image image = getImage(); |
|
||||||
int width = image.getWidth(); |
|
||||||
int height = image.getHeight(); |
|
||||||
int bitsPerPixel = ImageFormat.getBitsPerPixel(ImageFormat.NV21); |
|
||||||
int sizeBits = width * height * bitsPerPixel; |
|
||||||
int sizeBytes = (int) Math.ceil(sizeBits / 8.0d); |
|
||||||
byte[] bytes = new byte[sizeBytes]; |
|
||||||
ImageHelper.convertToNV21(image, bytes); |
|
||||||
image.close(); |
|
||||||
|
|
||||||
// Read the image
|
|
||||||
YuvImage yuvImage = new YuvImage(bytes, ImageFormat.NV21, width, height, null); |
|
||||||
ByteArrayOutputStream jpegStream = new ByteArrayOutputStream(); |
|
||||||
yuvImage.compressToJpeg(new Rect(0, 0, width, height), 100, jpegStream); |
|
||||||
byte[] jpegByteArray = jpegStream.toByteArray(); |
|
||||||
Bitmap bitmap = BitmapFactory.decodeByteArray(jpegByteArray, 0, jpegByteArray.length); |
|
||||||
assertNotNull(bitmap); |
|
||||||
|
|
||||||
// Wanted to do assertions on the color here but it doesn't work. There must be an issue
|
|
||||||
// with how we are drawing the image in this test, since in real camera, the algorithm works well.
|
|
||||||
// So for now let's just test that nothing crashes during this process.
|
|
||||||
// int color = bitmap.getPixel(bitmap.getWidth() - 1, bitmap.getHeight() - 1);
|
|
||||||
// assertEquals(Color.red(color), 255, 5);
|
|
||||||
// assertEquals(Color.green(color), 0, 5);
|
|
||||||
// assertEquals(Color.blue(color), 0, 5);
|
|
||||||
// assertEquals(Color.alpha(color), 0, 5);
|
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,30 @@ |
|||||||
|
package com.otaliastudios.cameraview.frame; |
||||||
|
|
||||||
|
import android.media.Image; |
||||||
|
import android.os.Build; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.RequiresApi; |
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.KITKAT) |
||||||
|
public class ImageFrameManager extends FrameManager<Image> { |
||||||
|
|
||||||
|
public ImageFrameManager(int poolSize) { |
||||||
|
super(poolSize, Image.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onFrameDataReleased(@NonNull Image data, boolean recycled) { |
||||||
|
try { |
||||||
|
data.close(); |
||||||
|
} catch (Exception ignore) {} |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
protected Image onCloneFrameData(@NonNull Image data) { |
||||||
|
throw new RuntimeException("Cannot freeze() an Image Frame. " + |
||||||
|
"Please consider using the frame synchronously in your process() method, " + |
||||||
|
"which also gives better performance."); |
||||||
|
} |
||||||
|
} |
@ -1,100 +0,0 @@ |
|||||||
package com.otaliastudios.cameraview.internal.utils; |
|
||||||
|
|
||||||
import android.graphics.ImageFormat; |
|
||||||
import android.media.Image; |
|
||||||
|
|
||||||
import java.nio.ByteBuffer; |
|
||||||
|
|
||||||
import androidx.annotation.NonNull; |
|
||||||
import androidx.annotation.RequiresApi; |
|
||||||
|
|
||||||
/** |
|
||||||
* Conversions for {@link android.media.Image}s into byte arrays. |
|
||||||
*/ |
|
||||||
@RequiresApi(19) |
|
||||||
public class ImageHelper { |
|
||||||
|
|
||||||
/** |
|
||||||
* From https://stackoverflow.com/a/52740776/4288782 .
|
|
||||||
* The result array should have a size that is at least 3/2 * w * h. |
|
||||||
* This is correctly computed by {@link com.otaliastudios.cameraview.frame.FrameManager}. |
|
||||||
* |
|
||||||
* @param image input image |
|
||||||
* @param result output array |
|
||||||
*/ |
|
||||||
public static void convertToNV21(@NonNull Image image, @NonNull byte[] result) { |
|
||||||
if (image.getFormat() != ImageFormat.YUV_420_888) { |
|
||||||
throw new IllegalStateException("CAn only convert from YUV_420_888."); |
|
||||||
} |
|
||||||
int width = image.getWidth(); |
|
||||||
int height = image.getHeight(); |
|
||||||
int ySize = width * height; |
|
||||||
int uvSize = width * height / 4; |
|
||||||
|
|
||||||
ByteBuffer yBuffer = image.getPlanes()[0].getBuffer(); // Y
|
|
||||||
ByteBuffer uBuffer = image.getPlanes()[1].getBuffer(); // U
|
|
||||||
ByteBuffer vBuffer = image.getPlanes()[2].getBuffer(); // V
|
|
||||||
|
|
||||||
int rowStride = image.getPlanes()[0].getRowStride(); |
|
||||||
|
|
||||||
if (image.getPlanes()[0].getPixelStride() != 1) { |
|
||||||
throw new AssertionError("Something wrong in convertToNV21"); |
|
||||||
} |
|
||||||
|
|
||||||
int pos = 0; |
|
||||||
|
|
||||||
if (rowStride == width) { // likely
|
|
||||||
yBuffer.get(result, 0, ySize); |
|
||||||
pos += ySize; |
|
||||||
} |
|
||||||
else { |
|
||||||
int yBufferPos = width - rowStride; // not an actual position
|
|
||||||
for (; pos<ySize; pos+=width) { |
|
||||||
yBufferPos += rowStride - width; |
|
||||||
yBuffer.position(yBufferPos); |
|
||||||
yBuffer.get(result, pos, width); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
rowStride = image.getPlanes()[2].getRowStride(); |
|
||||||
int pixelStride = image.getPlanes()[2].getPixelStride(); |
|
||||||
|
|
||||||
if (rowStride != image.getPlanes()[1].getRowStride()) { |
|
||||||
throw new AssertionError("Something wrong in convertToNV21"); |
|
||||||
} |
|
||||||
if (pixelStride != image.getPlanes()[1].getPixelStride()) { |
|
||||||
throw new AssertionError("Something wrong in convertToNV21"); |
|
||||||
} |
|
||||||
|
|
||||||
if (pixelStride == 2 && rowStride == width && uBuffer.get(0) == vBuffer.get(1)) { |
|
||||||
// maybe V an U planes overlap as per NV21, which means vBuffer[1]
|
|
||||||
// is alias of uBuffer[0]
|
|
||||||
byte savePixel = vBuffer.get(1); |
|
||||||
vBuffer.put(1, (byte)0); |
|
||||||
if (uBuffer.get(0) == 0) { |
|
||||||
vBuffer.put(1, (byte)255); |
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (uBuffer.get(0) == 255) { |
|
||||||
vBuffer.put(1, savePixel); |
|
||||||
vBuffer.get(result, ySize, uvSize); |
|
||||||
return; // shortcut
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// unfortunately, the check failed. We must save U and V pixel by pixel
|
|
||||||
vBuffer.put(1, savePixel); |
|
||||||
} |
|
||||||
|
|
||||||
// other optimizations could check if (pixelStride == 1) or (pixelStride == 2),
|
|
||||||
// but performance gain would be less significant
|
|
||||||
|
|
||||||
for (int row=0; row<height/2; row++) { |
|
||||||
for (int col=0; col<width/2; col++) { |
|
||||||
int vuPos = col*pixelStride + row*rowStride; |
|
||||||
result[pos++] = vBuffer.get(vuPos); |
|
||||||
result[pos++] = uBuffer.get(vuPos); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
Loading…
Reference in new issue