Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve CameraUtils.decodeBitmap #83

Merged
merged 1 commit into from
Oct 28, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@ public void testHasCameras() {
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();
Expand All @@ -59,9 +61,23 @@ public void onBitmapReady(Bitmap bitmap) {
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);
Expand All @@ -73,4 +89,31 @@ public void run() {

// 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ public void testCapturePicture_size() throws Exception {
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());
Expand Down Expand Up @@ -497,7 +497,7 @@ public void testCaptureSnapshot_size() throws Exception {
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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
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 {

Expand All @@ -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?
Expand Down Expand Up @@ -60,18 +61,34 @@ public static boolean hasCameraFacing(Context context, Facing facing) {
* 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() {
Expand All @@ -83,7 +100,11 @@ public void run() {
}


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;
Expand Down Expand Up @@ -123,12 +144,30 @@ static Bitmap decodeBitmap(byte[] source) {
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);
Expand All @@ -141,7 +180,30 @@ static Bitmap decodeBitmap(byte[] source) {
}


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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ static byte[] cropToJpeg(YuvImage yuv, AspectRatio targetRatio, int jpegCompress
// 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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);
Expand All @@ -40,25 +40,25 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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 + ")");
}
});

Expand Down
8 changes: 4 additions & 4 deletions demo/src/main/res/layout/activity_picture_preview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down