Improve CameraUtils.decodeBitmap (#83)

pull/86/head
Mattia Iavarone 7 years ago committed by GitHub
parent e40f93acfb
commit 2685f47472
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 57
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  2. 4
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java
  3. 78
      cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java
  4. 2
      cameraview/src/main/utils/com/otaliastudios/cameraview/CropHelper.java
  5. 18
      demo/src/main/java/com/otaliastudios/cameraview/demo/PicturePreviewActivity.java
  6. 8
      demo/src/main/res/layout/activity_picture_preview.xml

@ -36,15 +36,17 @@ public class CameraUtilsTest extends BaseTest {
assertFalse(CameraUtils.hasCameras(context)); assertFalse(CameraUtils.hasCameras(context));
} }
@Test // Encodes bitmap and decodes again using our utility.
public void testDecodeBitmap() { private Task<Bitmap> encodeDecodeTask(Bitmap source) {
int w = 100, h = 200, color = Color.WHITE; return encodeDecodeTask(source, 0, 0);
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); }
source.setPixel(0, 0, color);
final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Encodes bitmap and decodes again using our utility.
private Task<Bitmap> encodeDecodeTask(Bitmap source, final int maxWidth, final int maxHeight) {
final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Using lossy JPG we can't have strict comparison of values after compression. // Using lossy JPG we can't have strict comparison of values after compression.
source.compress(Bitmap.CompressFormat.PNG, 100, os); source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray();
final Task<Bitmap> decode = new Task<>(); final Task<Bitmap> decode = new Task<>();
decode.listen(); decode.listen();
@ -59,9 +61,23 @@ public class CameraUtilsTest extends BaseTest {
ui(new Runnable() { ui(new Runnable() {
@Override @Override
public void run() { public void run() {
CameraUtils.decodeBitmap(os.toByteArray(), callback); if (maxWidth > 0 && maxHeight > 0) {
CameraUtils.decodeBitmap(data, maxWidth, maxHeight, callback);
} else {
CameraUtils.decodeBitmap(data, callback);
}
} }
}); });
return decode;
}
@Test
public void testDecodeBitmap() {
int w = 100, h = 200, color = Color.WHITE;
Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
source.setPixel(0, 0, color);
Task<Bitmap> decode = encodeDecodeTask(source);
Bitmap other = decode.await(800); Bitmap other = decode.await(800);
assertNotNull(other); assertNotNull(other);
assertEquals(100, w); assertEquals(100, w);
@ -73,4 +89,31 @@ public class CameraUtilsTest extends BaseTest {
// TODO: improve when we add EXIF writing to byte arrays // TODO: improve when we add EXIF writing to byte arrays
} }
@Test
public void testDecodeDownscaledBitmap() {
int width = 1000, height = 2000;
Bitmap source = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Task<Bitmap> task;
Bitmap other;
task = encodeDecodeTask(source, 100, 100);
other = task.await(800);
assertNotNull(other);
assertTrue(other.getWidth() <= 100);
assertTrue(other.getHeight() <= 100);
task = encodeDecodeTask(source, Integer.MAX_VALUE, Integer.MAX_VALUE);
other = task.await(800);
assertNotNull(other);
assertTrue(other.getWidth() == width);
assertTrue(other.getHeight() == height);
task = encodeDecodeTask(source, 6000, 6000);
other = task.await(800);
assertNotNull(other);
assertTrue(other.getWidth() == width);
assertTrue(other.getHeight() == height);
}
} }

@ -459,7 +459,7 @@ public class IntegrationTest extends BaseTest {
Size size = camera.getCaptureSize(); Size size = camera.getCaptureSize();
camera.capturePicture(); camera.capturePicture();
byte[] jpeg = waitForPicture(true); byte[] jpeg = waitForPicture(true);
Bitmap b = CameraUtils.decodeBitmap(jpeg); Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
// Result can actually have swapped dimensions // Result can actually have swapped dimensions
// Which one, depends on factors including device physical orientation // Which one, depends on factors including device physical orientation
assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth()); assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());
@ -497,7 +497,7 @@ public class IntegrationTest extends BaseTest {
Size size = camera.getPreviewSize(); Size size = camera.getPreviewSize();
camera.captureSnapshot(); camera.captureSnapshot();
byte[] jpeg = waitForPicture(true); byte[] jpeg = waitForPicture(true);
Bitmap b = CameraUtils.decodeBitmap(jpeg); Bitmap b = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
// Result can actually have swapped dimensions // Result can actually have swapped dimensions
// Which one, depends on factors including device physical orientation // Which one, depends on factors including device physical orientation
assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth()); assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());

@ -15,7 +15,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
/** /**
* Static utilities for dealing with camera I/O, orientation, etc. * Static utilities for dealing with camera I/O, orientations, etc.
*/ */
public class CameraUtils { public class CameraUtils {
@ -27,6 +27,7 @@ public class CameraUtils {
* @param context a valid Context * @param context a valid Context
* @return whether device has cameras * @return whether device has cameras
*/ */
@SuppressWarnings("WeakerAccess")
public static boolean hasCameras(Context context) { public static boolean hasCameras(Context context) {
PackageManager manager = context.getPackageManager(); PackageManager manager = context.getPackageManager();
// There's also FEATURE_CAMERA_EXTERNAL , should we support it? // There's also FEATURE_CAMERA_EXTERNAL , should we support it?
@ -60,18 +61,34 @@ public class CameraUtils {
* is that this cares about orientation, reading it from the EXIF header. * is that this cares about orientation, reading it from the EXIF header.
* This is executed in a background thread, and returns the result to the original thread. * This is executed in a background thread, and returns the result to the original thread.
* *
* This ignores flipping at the moment.
* TODO care about flipping using Matrix.scale()
*
* @param source a JPEG byte array * @param source a JPEG byte array
* @param callback a callback to be notified * @param callback a callback to be notified
*/ */
@SuppressWarnings("WeakerAccess")
public static void decodeBitmap(final byte[] source, final BitmapCallback callback) { public static void decodeBitmap(final byte[] source, final BitmapCallback callback) {
decodeBitmap(source, Integer.MAX_VALUE, Integer.MAX_VALUE, callback);
}
/**
* Decodes an input byte array and outputs a Bitmap that is ready to be displayed.
* The difference with {@link android.graphics.BitmapFactory#decodeByteArray(byte[], int, int)}
* is that this cares about orientation, reading it from the EXIF header.
* This is executed in a background thread, and returns the result to the original thread.
*
* The image is also downscaled taking care of the maxWidth and maxHeight arguments.
*
* @param source a JPEG byte array
* @param maxWidth the max allowed width
* @param maxHeight the max allowed height
* @param callback a callback to be notified
*/
@SuppressWarnings("WeakerAccess")
public static void decodeBitmap(final byte[] source, final int maxWidth, final int maxHeight, final BitmapCallback callback) {
final Handler ui = new Handler(); final Handler ui = new Handler();
WorkerHandler.run(new Runnable() { WorkerHandler.run(new Runnable() {
@Override @Override
public void run() { public void run() {
final Bitmap bitmap = decodeBitmap(source); final Bitmap bitmap = decodeBitmap(source, maxWidth, maxHeight);
ui.post(new Runnable() { ui.post(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -83,7 +100,11 @@ public class CameraUtils {
} }
static Bitmap decodeBitmap(byte[] source) { // TODO ignores flipping
@SuppressWarnings({"SuspiciousNameCombination", "WeakerAccess"})
/* for tests */ static Bitmap decodeBitmap(byte[] source, int maxWidth, int maxHeight) {
if (maxWidth <= 0) maxWidth = Integer.MAX_VALUE;
if (maxHeight <= 0) maxHeight = Integer.MAX_VALUE;
int orientation; int orientation;
boolean flip; boolean flip;
InputStream stream = null; InputStream stream = null;
@ -123,12 +144,30 @@ public class CameraUtils {
flip = false; flip = false;
} finally { } finally {
if (stream != null) { if (stream != null) {
try { stream.close(); } catch (Exception e) {} try { stream.close(); } catch (Exception ignored) {}
} }
} }
Bitmap bitmap;
if (maxWidth < Integer.MAX_VALUE || maxHeight < Integer.MAX_VALUE) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(source, 0, source.length, options);
int outHeight = options.outHeight;
int outWidth = options.outWidth;
if (orientation % 180 != 0) {
outHeight = options.outWidth;
outWidth = options.outHeight;
}
options.inSampleSize = computeSampleSize(outWidth, outHeight, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeByteArray(source, 0, source.length, options);
} else {
bitmap = BitmapFactory.decodeByteArray(source, 0, source.length);
}
Bitmap bitmap = BitmapFactory.decodeByteArray(source, 0, source.length);
if (orientation != 0 || flip) { if (orientation != 0 || flip) {
Matrix matrix = new Matrix(); Matrix matrix = new Matrix();
matrix.setRotate(orientation); matrix.setRotate(orientation);
@ -141,7 +180,30 @@ public class CameraUtils {
} }
private static int computeSampleSize(int width, int height, int maxWidth, int maxHeight) {
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
int inSampleSize = 1;
if (height > maxHeight || width > maxWidth) {
while ((height / inSampleSize) >= maxHeight
|| (width / inSampleSize) >= maxWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
/**
* Receives callbacks about a bitmap decoding operation.
*/
public interface BitmapCallback { public interface BitmapCallback {
/**
* Notifies that the bitmap was succesfully decoded.
* This is run on the UI thread.
*
* @param bitmap decoded bitmap
*/
@UiThread void onBitmapReady(Bitmap bitmap); @UiThread void onBitmapReady(Bitmap bitmap);
} }
} }

@ -21,7 +21,7 @@ class CropHelper {
// In doing so, EXIF data is deleted. // In doing so, EXIF data is deleted.
static byte[] cropToJpeg(byte[] jpeg, AspectRatio targetRatio, int jpegCompression) { static byte[] cropToJpeg(byte[] jpeg, AspectRatio targetRatio, int jpegCompression) {
Bitmap image = CameraUtils.decodeBitmap(jpeg); Bitmap image = CameraUtils.decodeBitmap(jpeg, Integer.MAX_VALUE, Integer.MAX_VALUE);
Rect cropRect = computeCrop(image.getWidth(), image.getHeight(), targetRatio); Rect cropRect = computeCrop(image.getWidth(), image.getHeight(), targetRatio);
Bitmap crop = Bitmap.createBitmap(image, cropRect.left, cropRect.top, cropRect.width(), cropRect.height()); Bitmap crop = Bitmap.createBitmap(image, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
image.recycle(); image.recycle();

@ -27,8 +27,8 @@ public class PicturePreviewActivity extends Activity {
setContentView(R.layout.activity_picture_preview); setContentView(R.layout.activity_picture_preview);
final ImageView imageView = findViewById(R.id.image); final ImageView imageView = findViewById(R.id.image);
final MessageView nativeCaptureResolution = findViewById(R.id.nativeCaptureResolution); final MessageView nativeCaptureResolution = findViewById(R.id.nativeCaptureResolution);
final MessageView actualResolution = findViewById(R.id.actualResolution); // final MessageView actualResolution = findViewById(R.id.actualResolution);
final MessageView approxUncompressedSize = findViewById(R.id.approxUncompressedSize); // final MessageView approxUncompressedSize = findViewById(R.id.approxUncompressedSize);
final MessageView captureLatency = findViewById(R.id.captureLatency); final MessageView captureLatency = findViewById(R.id.captureLatency);
final long delay = getIntent().getLongExtra("delay", 0); final long delay = getIntent().getLongExtra("delay", 0);
@ -40,25 +40,25 @@ public class PicturePreviewActivity extends Activity {
return; return;
} }
CameraUtils.decodeBitmap(b, new CameraUtils.BitmapCallback() { CameraUtils.decodeBitmap(b, 1000, 1000, new CameraUtils.BitmapCallback() {
@Override @Override
public void onBitmapReady(Bitmap bitmap) { public void onBitmapReady(Bitmap bitmap) {
imageView.setImageBitmap(bitmap); imageView.setImageBitmap(bitmap);
approxUncompressedSize.setTitle("Approx. uncompressed size"); // approxUncompressedSize.setTitle("Approx. uncompressed size");
approxUncompressedSize.setMessage(getApproximateFileMegabytes(bitmap) + "MB"); // approxUncompressedSize.setMessage(getApproximateFileMegabytes(bitmap) + "MB");
captureLatency.setTitle("Capture latency"); captureLatency.setTitle("Approx. capture latency");
captureLatency.setMessage(delay + " milliseconds"); captureLatency.setMessage(delay + " milliseconds");
// ncr and ar might be different when cropOutput is true. // ncr and ar might be different when cropOutput is true.
AspectRatio nativeRatio = AspectRatio.of(nativeWidth, nativeHeight); AspectRatio nativeRatio = AspectRatio.of(nativeWidth, nativeHeight);
AspectRatio finalRatio = AspectRatio.of(bitmap.getWidth(), bitmap.getHeight());
nativeCaptureResolution.setTitle("Native capture resolution"); nativeCaptureResolution.setTitle("Native capture resolution");
nativeCaptureResolution.setMessage(nativeWidth + "x" + nativeHeight + " (" + nativeRatio + ")"); nativeCaptureResolution.setMessage(nativeWidth + "x" + nativeHeight + " (" + nativeRatio + ")");
actualResolution.setTitle("Actual resolution"); // AspectRatio finalRatio = AspectRatio.of(bitmap.getWidth(), bitmap.getHeight());
actualResolution.setMessage(bitmap.getWidth() + "x" + bitmap.getHeight() + " (" + finalRatio + ")"); // actualResolution.setTitle("Actual resolution");
// actualResolution.setMessage(bitmap.getWidth() + "x" + bitmap.getHeight() + " (" + finalRatio + ")");
} }
}); });

@ -20,15 +20,15 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<com.otaliastudios.cameraview.demo.MessageView <!-- com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/actualResolution" android:id="@+id/actualResolution"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/-->
<com.otaliastudios.cameraview.demo.MessageView <!-- com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/approxUncompressedSize" android:id="@+id/approxUncompressedSize"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/-->
<com.otaliastudios.cameraview.demo.MessageView <com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/captureLatency" android:id="@+id/captureLatency"

Loading…
Cancel
Save