How to make Bitmap compress without change the bitmap size?
Are you sure it is smaller?
Bitmap original = BitmapFactory.decodeStream(getAssets().open("1024x768.jpg"));
ByteArrayOutputStream out = new ByteArrayOutputStream();
original.compress(Bitmap.CompressFormat.PNG, 100, out);
Bitmap decoded = BitmapFactory.decodeStream(new ByteArrayInputStream(out.toByteArray()));
Log.e("Original dimensions", original.getWidth()+" "+original.getHeight());
Log.e("Compressed dimensions", decoded.getWidth()+" "+decoded.getHeight());
Gives
12-07 17:43:36.333: E/Original dimensions(278): 1024 768
12-07 17:43:36.333: E/Compressed dimensions(278): 1024 768
Maybe you get your bitmap from a resource, in which case the bitmap dimension will depend on the phone screen density
Bitmap bitmap=((BitmapDrawable)getResources().getDrawable(R.drawable.img_1024x768)).getBitmap();
Log.e("Dimensions", bitmap.getWidth()+" "+bitmap.getHeight());
12-07 17:43:38.733: E/Dimensions(278): 768 576
Reduce the size of a bitmap to a specified size in Android
I found an answer that works perfectly for me:
/**
* reduces the size of the image
* @param image
* @param maxSize
* @return
*/
public Bitmap getResizedBitmap(Bitmap image, int maxSize) {
int width = image.getWidth();
int height = image.getHeight();
float bitmapRatio = (float)width / (float) height;
if (bitmapRatio > 1) {
width = maxSize;
height = (int) (width / bitmapRatio);
} else {
height = maxSize;
width = (int) (height * bitmapRatio);
}
return Bitmap.createScaledBitmap(image, width, height, true);
}
calling the method:
Bitmap converetdImage = getResizedBitmap(photo, 500);
Where photo
is your bitmap
Android - Scale and compress a bitmap
You need to decide on a limit for either your width or height (not both, obviously). Then replace those fixed image sizes with calculated ones, say:
int targetWidth = 640; // your arbitrary fixed limit
int targetHeight = (int) (originalHeight * targetWidth / (double) originalWidth); // casts to avoid truncating(Add checks and calculation alternatives for landscape / portrait orientation, as needed.)
As @harism also commented: the large size you mentioned is the raw size of that 480x800 bitmap, not the file size, which should be a JPEG in your case. How are you going about saving that bitmap, BTW? Your code doesn't seem to contain the saving part.
See this question here for help on that, with the key being something like:
OutputStream imagefile = new FileOutputStream("/your/file/name.jpg");
// Write 'bitmap' to file using JPEG and 80% quality hint for JPEG:
bitmap.compress(CompressFormat.JPEG, 80, imagefile);
Bitmap.compress doesn't decrease the Byte count
But when I compare the Byte count of the original Bitmap object and the compressed one, I get the same number:
The size of a Bitmap
in memory is based only on its resolution (width and height in pixels) and bit depth (the number of bytes per pixel, for controlling how many colors can be used per pixel).
How can I fix this to have a smaller file?
You do not have a file. You have a Bitmap
object in memory. An image file is usually stored in a compressed form. In particular, this is true for JPEG, PNG, WebP, and GIF, the four major image formats used in Android. So, for example, out.toByteArray()
will be smaller than 23,970,816 bytes.
Moreover, you are not sending a Bitmap
to the server. You are sending an image to the server. You need to read the documentation for the server, or talk to the server developers, to determine what image format(s) they support and how to send the image to the server (ideally, something efficient like an HTTP PUT).
If you want to reduce the in-memory size of the Bitmap
, scale it to a lower-resolution image (e.g., via createScaledBitmap()
).
How to compress Bitmap as JPEG with least quality loss on Android?
After some investigation I found the culprit: Skia's YCbCr conversion. Repro, code for investigation and solutions can be found at TWiStErRob/AndroidJPEG.
Discovery
After not getting a positive response on this question (neither from http://b.android.com/206128) I started digging deeper. I found numerous half-informed SO answers which helped me tremendously in discovering bits and pieces. One such answer was https://stackoverflow.com/a/13055615/253468 which made me aware of YuvImage
which converts an YUV NV21 byte array into a JPEG compressed byte array:
YuvImage yuv = new YuvImage(yuvData, ImageFormat.NV21, width, height, null);
yuv.compressToJpeg(new Rect(0, 0, width, height), 100, jpeg);
There's a lot of freedom going into creating the YUV data, with varying constants and precision. From my question it's clear that Android uses an incorrect algorithm.
While playing around with the algorithms and constants I found online I always got a bad image: either the brightness changed or had the same banding issues as in the question.
Digging deeper
YuvImage
is actually not used when calling Bitmap.compress
, here's the stack for Bitmap.compress
:
- libjpeg/
jpeg_write_scanlines
(jcapistd.c:77) - skia/
rgb2yuv_32
(SkImageDecoder_libjpeg.cpp:913) - skia/
writer(=Write_32_YUV).write
(SkImageDecoder_libjpeg.cpp:961)
[WE_CONVERT_TO_YUV
is unconditionally defined] SkJPEGImageEncoder::onEncode
(SkImageDecoder_libjpeg.cpp:1046)SkImageEncoder::encodeStream
(SkImageEncoder.cpp:15)Bitmap_compress
(Bitmap.cpp:383)Bitmap.nativeCompress
(Bitmap.java:1573)Bitmap.compress
(Bitmap.java:984)app.saveBitmapAsJPEG
()
and the stack for using YuvImage
- libjpeg/
jpeg_write_raw_data
(jcapistd.c:120) YuvToJpegEncoder::compress
(YuvToJpegEncoder.cpp:71)YuvToJpegEncoder::encode
(YuvToJpegEncoder.cpp:24)YuvImage_compressToJpeg
(YuvToJpegEncoder.cpp:219)YuvImage.nativeCompressToJpeg
(YuvImage.java:141)YuvImage.compressToJpeg
(YuvImage.java:123)app.saveNV21AsJPEG
()
By using the constants in rgb2yuv_32
from the Bitmap.compress
flow I was able to recreate the same banding effect using YuvImage
, not an achievement, just a confirmation that it's indeed the YUV conversion that is messed up. I double-checked that the problem is not during YuvImage
calling libjpeg
: by converting the Bitmap's ARGB to YUV and back to RGB then dumping the resulting pixel blob as a raw image, the banding was already there.
While doing this I realized that the NV21/YUV420SP layout is lossy as it samples the color information every 4th pixel, but it keeps the value (brightness) of each pixel which means that some color info is lost, but most of the info for people's eyes are in the brightness anyway. Take a look at the example on wikipedia, the Cb and Cr channel makes barely recognisable images, so lossy sampling on it doesn't matter much.
Solution
So, at this point I knew that libjpeg does the right conversion when it is passed the right raw data. This is when I set up the NDK and integrated the latest LibJPEG from http://www.ijg.org. I was able to confirm that indeed passing the RGB data from the Bitmap's pixels array yields the expected result. I like to avoid using native components when not absolutely necessary, so aside of going for a native library that encodes a Bitmap I found a neat workaround. I've essentially taken the rgb_ycc_convert
function from jcolor.c
and rewrote it in Java using the skeleton from https://stackoverflow.com/a/13055615/253468. The below is not optimized for speed, but readability, some constants were removed for brevity, you can find them in libjpeg code or my example project.
private static final int JSAMPLE_SIZE = 255 + 1;
private static final int CENTERJSAMPLE = 128;
private static final int SCALEBITS = 16;
private static final int CBCR_OFFSET = CENTERJSAMPLE << SCALEBITS;
private static final int ONE_HALF = 1 << (SCALEBITS - 1);
private static final int[] rgb_ycc_tab = new int[TABLE_SIZE];
static { // rgb_ycc_start
for (int i = 0; i <= JSAMPLE_SIZE; i++) {
rgb_ycc_tab[R_Y_OFFSET + i] = FIX(0.299) * i;
rgb_ycc_tab[G_Y_OFFSET + i] = FIX(0.587) * i;
rgb_ycc_tab[B_Y_OFFSET + i] = FIX(0.114) * i + ONE_HALF;
rgb_ycc_tab[R_CB_OFFSET + i] = -FIX(0.168735892) * i;
rgb_ycc_tab[G_CB_OFFSET + i] = -FIX(0.331264108) * i;
rgb_ycc_tab[B_CB_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[R_CR_OFFSET + i] = FIX(0.5) * i + CBCR_OFFSET + ONE_HALF - 1;
rgb_ycc_tab[G_CR_OFFSET + i] = -FIX(0.418687589) * i;
rgb_ycc_tab[B_CR_OFFSET + i] = -FIX(0.081312411) * i;
}
}
static void rgb_ycc_convert(int[] argb, int width, int height, byte[] ycc) {
int[] tab = LibJPEG.rgb_ycc_tab;
final int frameSize = width * height;
int yIndex = 0;
int uvIndex = frameSize;
int index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int r = (argb[index] & 0x00ff0000) >> 16;
int g = (argb[index] & 0x0000ff00) >> 8;
int b = (argb[index] & 0x000000ff) >> 0;
byte Y = (byte)((tab[r + R_Y_OFFSET] + tab[g + G_Y_OFFSET] + tab[b + B_Y_OFFSET]) >> SCALEBITS);
byte Cb = (byte)((tab[r + R_CB_OFFSET] + tab[g + G_CB_OFFSET] + tab[b + B_CB_OFFSET]) >> SCALEBITS);
byte Cr = (byte)((tab[r + R_CR_OFFSET] + tab[g + G_CR_OFFSET] + tab[b + B_CR_OFFSET]) >> SCALEBITS);
ycc[yIndex++] = Y;
if (y % 2 == 0 && index % 2 == 0) {
ycc[uvIndex++] = Cr;
ycc[uvIndex++] = Cb;
}
index++;
}
}
}
static byte[] compress(Bitmap bitmap) {
int w = bitmap.getWidth();
int h = bitmap.getHeight();
int[] argb = new int[w * h];
bitmap.getPixels(argb, 0, w, 0, 0, w, h);
byte[] ycc = new byte[w * h * 3 / 2];
rgb_ycc_convert(argb, w, h, ycc);
argb = null; // let GC do its job
ByteArrayOutputStream jpeg = new ByteArrayOutputStream();
YuvImage yuvImage = new YuvImage(ycc, ImageFormat.NV21, w, h, null);
yuvImage.compressToJpeg(new Rect(0, 0, w, h), quality, jpeg);
return jpeg.toByteArray();
}
The magic key seems to be ONE_HALF - 1
the rest looks an awful lot like the math in Skia. That's a good direction for future investigation, but for me the above is sufficiently simple to be a good solution for working around Android's builtin weirdness, albeit slower. Note that this solution uses the NV21 layout which loses 3/4 of the color info (from Cr/Cb), but this loss is much less than the errors created by Skia's math. Also note that YuvImage
doesn't support odd-sized images, for more info see NV21 format and odd image dimensions.
Related Topics
Trying to Attach a File from Sd Card to Email
How to Pass a Variable from Activity to Fragment, and Pass It Back
How Set Maximum Date in Datepicker Dialog in Android
How to Connect to More Than One Firebase Database from an Android App
...Have You Declared This Activity in Your Androidmanifest.Xml
Nested Recycler View Height Doesn't Wrap Its Content
Differencebetween Min Sdk Version/Target Sdk Version VS. Compile Sdk Version
Failed to Load Appcompat Actionbar with Unknown Error in Android Studio
Android API 21 Toolbar Padding
Android:Difference Between Invisible and Gone
How to Install Trusted Ca Certificate on Android Device
You Have Not Accepted the License Agreements of the Following Sdk Components
Changing the Background Drawable of the Searchview Widget
How to Display Toast in Android
Android "Elevation" Not Showing a Shadow
Using Build Flavors - Structuring Source Folders and Build.Gradle Correctly