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));
}
@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);
final ByteArrayOutputStream os = new ByteArrayOutputStream();
// Encodes bitmap and decodes again using our utility.
private Task<Bitmap> encodeDecodeTask(Bitmap source) {
return encodeDecodeTask(source, 0, 0);
}
// 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.
source.compress(Bitmap.CompressFormat.PNG, 100, os);
final byte[] data = os.toByteArray();
final Task<Bitmap> decode = new Task<>();
decode.listen();
@ -59,9 +61,23 @@ public class CameraUtilsTest extends BaseTest {
ui(new Runnable() {
@Override
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);
assertNotNull(other);
assertEquals(100, w);
@ -73,4 +89,31 @@ public class CameraUtilsTest extends BaseTest {
// 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();
camera.capturePicture();
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
// Which one, depends on factors including device physical orientation
assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());
@ -497,7 +497,7 @@ public class IntegrationTest extends BaseTest {
Size size = camera.getPreviewSize();
camera.captureSnapshot();
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
// Which one, depends on factors including device physical orientation
assertTrue(b.getWidth() == size.getHeight() || b.getWidth() == size.getWidth());

@ -15,7 +15,7 @@ import java.io.IOException;
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 {
@ -27,6 +27,7 @@ public class CameraUtils {
* @param context a valid Context
* @return whether device has cameras
*/
@SuppressWarnings("WeakerAccess")
public static boolean hasCameras(Context context) {
PackageManager manager = context.getPackageManager();
// 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.
* 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 callback a callback to be notified
*/
@SuppressWarnings("WeakerAccess")
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();
WorkerHandler.run(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = decodeBitmap(source);
final Bitmap bitmap = decodeBitmap(source, maxWidth, maxHeight);
ui.post(new Runnable() {
@Override
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;
boolean flip;
InputStream stream = null;
@ -123,12 +144,30 @@ public class CameraUtils {
flip = false;
} finally {
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) {
Matrix matrix = new Matrix();
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 {
/**
* Notifies that the bitmap was succesfully decoded.
* This is run on the UI thread.
*
* @param bitmap decoded bitmap
*/
@UiThread void onBitmapReady(Bitmap bitmap);
}
}

@ -21,7 +21,7 @@ class CropHelper {
// In doing so, EXIF data is deleted.
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);
Bitmap crop = Bitmap.createBitmap(image, cropRect.left, cropRect.top, cropRect.width(), cropRect.height());
image.recycle();

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

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

Loading…
Cancel
Save