Convert a bitmap to a fixed size - android

For example, I have a 10mb image; which I want to convert to 300kb. I have been through many examples
used
bmp.compress(Bitmap.CompressFormat.JPEG, 100, stream);
(here changing 100 to a lesser value will decrease the size but how would it result in a size close to 300-350kb)
and
BitmapFactory.decodeFile(filePath, options);
where I provided
options.inSampleSize = 5 /*sample*/;
But somehow I am missing something.
UPDATE
Settled with conversion 11mb to 2mb. Will update if I find a better way.

I think because PNG is lossless, the quality parameter has no effect. It's not going to "crunch" your PNGs. However this approach would work for jpg:
Trial and error, with a binary search will get you close very quickly, 3-4 attempts probably depending on the size of the acceptable range.
int minQuality = 10;
int maxQuality = 100;
long size = 0;
while(true) {
int mid = (maxQuality + minQuality)/2;
long size = compress(mid);
if (size > minSize) { //too large
if (maxQuality == minQuality){
throw new RuntimeException("Cannot compress this image down in to this size range.");
}
maxQuality = mid - 1;
continue;
}
if (size < maxSize) { //too small
if(maxQuality == 100){
break; //this means the image is smaller than the acceptable range even at 100
}
minQuality = mid + 1;
continue;
}
break;//done, falls in range
}

Two options available
Decrease the contrast of the image using image processing or some image processing api for android or do sampling using image processing api
Repeat the bmp.compress(Bitmap.CompressFormat.PNG, 100, stream); for several times by storing the image outside and again reading in each time

Related

Programmatic Image Resizing in Android, Memory Issues

Days, I've spent working on this. Weeks, perhaps. Literally. :(
So I've got an image on an SD card that more than likely came out of the built-in camera. I want to take that image and downsample it to an arbitrary size (but always smaller and never larger). My code uses standard Android Bitmap methods to decode, resize, recompress, and save the image. Everything works fine as long as the final image is smaller than 3MP or so. If the image is larger, or if I try to do several of these at once, the application crashes with an OutOfMemoryError. I know why that's happening, and I know it's happening for a perfectly legitimate reason, I just want it to not happen anymore.
Look, I'm not trying to launch a rocket here. All I want to do is resize a camera image and dump it to an OutputStream or even a temporary file. Surely someone out there must have done such a thing. I don't need you to write my code for me, and I don't need my hand held. But between my various programming abortions and days of obsessed Googling, I don't even know which direction to head in. Roughly speaking, does anyone know how to decode a JPEG, downsample it, re-compress it in JPEG, and send it out on an OutputStream without allocating a massive amount of memory?
Ok I know it's a little bit late but, I had this problem and I found solution. It is actually easy and I am sure it supports back to api 10(I have no idea about before 10). I tried this with my phone. It is a samsung galaxy s2 with an 8mp camera and the code perfectly resized camera images to the 168x168 as well as images i found on web. I checked the images by using file manager too. I never tried resizing images to bigger resoulation.
private Bitmap resize(Bitmap bp, int witdh, int height){
return Bitmap.createScaledBitmap(bp, width, height, false);
}
you can save it like this
private void saveBitmap(Bitmap bp) throws FileNotFoundException{
String state = Environment.getExternalStorageState();
File folder;
//if there is memory card available code choose that
if (Environment.MEDIA_MOUNTED.equals(state)) {
folder=Environment.getExternalStorageDirectory();
}else{
folder=Environment.getDataDirectory();
}
folder=new File(folder, "/aaaa");
if(!folder.exists()){
folder.mkdir();
}
File file=new File(folder, (int)(Math.random()*10000)+".jpg");
FileOutputStream os=new FileOutputStream(file);
bp.compress(Bitmap.CompressFormat.JPEG, 90, os);
}
thanks to this link
The following code is from my previous project. Key point is "options.inSampleSize".
public static Bitmap makeBitmap(String fn, int minSideLength, int maxNumOfPixels) {
BitmapFactory.Options options;
try {
options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fn, options);
if (options.mCancel || options.outWidth == -1
|| options.outHeight == -1) {
return null;
}
options.inSampleSize = computeSampleSize(
options, minSideLength, maxNumOfPixels);
options.inJustDecodeBounds = false;
//Log.e(LOG_TAG, "sample size=" + options.inSampleSize);
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeFile(fn, options);
} catch (OutOfMemoryError ex) {
Log.e(LOG_TAG, "Got oom exception ", ex);
return null;
}
}
private static int computeInitialSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
(int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
(int) Math.min(Math.floor(w / minSideLength),
Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
// return the larger one when there is no overlapping zone.
return lowerBound;
}
if ((maxNumOfPixels == UNCONSTRAINED) &&
(minSideLength == UNCONSTRAINED)) {
return 1;
} else if (minSideLength == UNCONSTRAINED) {
return lowerBound;
} else {
return upperBound;
}
}

Android: bitmap.getByteCount() in API lesser than 12

When i searched for how to find the size of an image before saving it on the SD card, i found this:
bitmap.getByteCount();
but that method is added in API 12 and i am using API 10. So again i found out this:
getByteCount() is just a convenience method which does exactly what you have placed in the else-block. In other words, if you simply rewrite getSizeInBytes to always return "bitmap.getRowBytes() * bitmap.getHeight()"
here:
Where the heck is Bitmap getByteCount()?
so, by calculating this bitmap.getRowBytes() * bitmap.getHeight() i got the value 120000 (117 KB).
where as the image size on the SD card is 1.6 KB.
What am i missing? or doing wrong?
Thank You
You are doing it correctly!
A quick way to know for sure if the values are valid, is to log it like this:
int numBytesByRow = bitmap.getRowBytes() * bitmap.getHeight();
int numBytesByCount = bitmap.getByteCount();
Log.v( TAG, "numBytesByRow=" + numBytesByRow );
Log.v( TAG, "numBytesByCount=" + numBytesByCount );
This gives the result:
03-29 17:31:10.493: V/ImageCache(19704): numBytesByRow=270000
03-29 17:31:10.493: V/ImageCache(19704): numBytesByCount=270000
So both are calculating the same number, which I suspect is the in-memory size of the bitmap. This is different than a JPG or PNG on disk as it is completely uncompressed.
For more info, we can look to AOSP and the source in the example project. This is the file used in the example project BitmapFun in the Android developer docs Caching Bitmaps
AOSP ImageCache.java
/**
* Get the size in bytes of a bitmap in a BitmapDrawable.
* #param value
* #return size in bytes
*/
#TargetApi(12)
public static int getBitmapSize(BitmapDrawable value) {
Bitmap bitmap = value.getBitmap();
if (APIUtil.hasHoneycombMR1()) {
return bitmap.getByteCount();
}
// Pre HC-MR1
return bitmap.getRowBytes() * bitmap.getHeight();
}
As you can see this is the same technique they use
bitmap.getRowBytes() * bitmap.getHeight();
References:
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
http://code.google.com/p/adamkoch/source/browse/bitmapfun/
For now i am using this:
ByteArrayOutputStream bao = new ByteArrayOutputStream();
my_bitmap.compress(Bitmap.CompressFormat.PNG, 100, bao);
byte[] ba = bao.toByteArray();
int size = ba.length;
to get total no.of bytes as size. Because the value i get here perfectly matches the size(in bytes) on the image on SD card.
Nothing is missing! Your codesnippet shows exact the implementation from Android-Source:
http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.1.1_r1/android/graphics/Bitmap.java#Bitmap.getByteCount%28%29
I think the differences in size are the result of image-compressing (jpg and so on).
Here is an alternative way:
public static int getBitmapByteCount(Bitmap bitmap) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1)
return bitmap.getRowBytes() * bitmap.getHeight();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
return bitmap.getByteCount();
return bitmap.getAllocationByteCount();
}
A statement from the IDE for getAllocationByteCount():
This can be larger than the result of getByteCount() if a bitmap is
reused to decode other bitmaps of smaller size, or by manual
reconfiguration. See reconfigure(int, int, Bitmap.Config),
setWidth(int), setHeight(int), setConfig(Bitmap.Config), and
BitmapFactory.Options.inBitmap. If a bitmap is not modified in this
way, this value will be the same as that returned by getByteCount().
may u can try this code
int pixels = bitmap.getHeight() * bitmap.getWidth();
int bytesPerPixel = 0;
switch(bitmap.getConfig()) {
case ARGB_8888:
bytesPerPixel = 4;
break;
case RGB_565:
bytesPerPixel = 2;
break;
case ARGB_4444:
bytesPerPixel = 2;
break;
case ALPHA_8 :
bytesPerPixel = 1;
break;
}
int byteCount = pixels / bytesPerPixel;
the image on the sd card has a different size because it's compressed. on the device it will depend on the width/height
Why don't you try dividing it between 1024? To get the KB instead of Bytes.

Buffer not large enough for pixels

I get the error that my buffer is not large enough for pixels. Any recommendations? The Bitmap b should be the same size as the gSaveBitmap that I'm trying to place its pixels into.
if(gBuffer == null)
{
Bitmap b = Bitmap.createScaledBitmap(gBitmap, mWidth, mHeight, false);
//gBuffer = ByteBuffer.allocateDirect(b.getRowBytes()*b.getHeight()*4);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
b.compress(Bitmap.CompressFormat.PNG, 100, stream);
gBuffer = ByteBuffer.wrap(stream.toByteArray());
b.recycle();
}
gSaveBitmap.copyPixelsFromBuffer(gBuffer);
Update: The below code gives the exact same error without any compression involved.
if(gBuffer == null)
{
Bitmap b = Bitmap.createScaledBitmap(gBitmap, mWidth, mHeight, false);
int bytes = b.getWidth()*b.getHeight()*4;
gBuffer = ByteBuffer.allocate(bytes);
b.copyPixelsToBuffer(gBuffer);
b.recycle();
}
gSaveBitmap.copyPixelsFromBuffer(gBuffer);
Update: Solved the issue by doubling the size of gBuffer. Perhaps someone can tell me why this is the correct size. Also ... the picture is in the wrong rotation, needs rotated 90 degrees. Any ideas how the data would need to be rearranged in gBuffer?
gBuffer = ByteBuffer.allocate(b.getRowBytes()*b.getHeight()*2);
I think I might have solved this one-- take a look at the source (version 2.3.4_r1, last time Bitmap was updated on Grepcode prior to 4.4) for Bitmap::copyPixelsFromBuffer()
public void copyPixelsFromBuffer(Buffer src) {
checkRecycled("copyPixelsFromBuffer called on recycled bitmap");
int elements = src.remaining();
int shift;
if (src instanceof ByteBuffer) {
shift = 0;
} else if (src instanceof ShortBuffer) {
shift = 1;
} else if (src instanceof IntBuffer) {
shift = 2;
} else {
throw new RuntimeException("unsupported Buffer subclass");
}
long bufferBytes = (long)elements << shift;
long bitmapBytes = (long)getRowBytes() * getHeight();
if (bufferBytes < bitmapBytes) {
throw new RuntimeException("Buffer not large enough for pixels");
}
nativeCopyPixelsFromBuffer(mNativeBitmap, src);
}
The wording of the error is a bit unclear, but the code clarifies-- it means that your buffer is calculated as not having enough data to fill the pixels of your bitmap.
This is because they use the buffer's remaining() method to figure the capacity of the buffer, which takes into account the current value of its position attribute. If you call rewind() on your buffer before you invoke copyPixelsFromBuffer(), you should see the runtime exception disappear.
I found the answer for this issue :
You should always set the buffer size > bit map size since The Bitmap on different Android version always changed.
You can log the codes in below to see the buffer size & bitmap size (Android API should >= 12 to use following log)
Log.i("", "Bitmap size = " + mBitmap.getByteCount());
Log.i("", "Buffer size = " + mBuffer.capacity());
It should be worked.
Thanks,

How do I scale a streaming bitmap in-place without reading the whole image first?

I have an Android application that is very image intensive. I'm currently using Bitmap.createScaledBitmap() to scale the image to a desired size. However, this method requires that I already have the original bitmap in memory, which can be quite sizable.
How can I scale a bitmap that I'm downloading without first writing the entire thing out to local memory or file system?
This method will read the header information from the image to determine its size, then read the image and scale it to the desired size in place without allocating memory for the full original sized image.
It also uses BitmapFactory.Options.inPurgeable, which seems to be a sparsely documented but desirable option to prevent OoM exceptions when using lots of bitmaps. UPDATE: no longer uses inPurgeable, see this note from Romain
It works by using a BufferedInputStream to read the header information for the image before reading the entire image in via the InputStream.
/**
* Read the image from the stream and create a bitmap scaled to the desired
* size. Resulting bitmap will be at least as large as the
* desired minimum specified dimensions and will keep the image proportions
* correct during scaling.
*/
protected Bitmap createScaledBitmapFromStream( InputStream s, int minimumDesiredBitmapWith, int minimumDesiredBitmapHeight ) {
final BufferedInputStream is = new BufferedInputStream(s, 32*1024);
try {
final Options decodeBitmapOptions = new Options();
// For further memory savings, you may want to consider using this option
// decodeBitmapOptions.inPreferredConfig = Config.RGB_565; // Uses 2-bytes instead of default 4 per pixel
if( minimumDesiredBitmapWidth >0 && minimumDesiredBitmapHeight >0 ) {
final Options decodeBoundsOptions = new Options();
decodeBoundsOptions.inJustDecodeBounds = true;
is.mark(32*1024); // 32k is probably overkill, but 8k is insufficient for some jpgs
BitmapFactory.decodeStream(is,null,decodeBoundsOptions);
is.reset();
final int originalWidth = decodeBoundsOptions.outWidth;
final int originalHeight = decodeBoundsOptions.outHeight;
// inSampleSize prefers multiples of 2, but we prefer to prioritize memory savings
decodeBitmapOptions.inSampleSize= Math.max(1,Math.min(originalWidth / minimumDesiredBitmapWidth, originalHeight / minimumDesiredBitmapHeight));
}
return BitmapFactory.decodeStream(is,null,decodeBitmapOptions);
} catch( IOException e ) {
throw new RuntimeException(e); // this shouldn't happen
} finally {
try {
is.close();
} catch( IOException ignored ) {}
}
}
Here is my version, based on #emmby solution (thanks man!)
I've included a second phase where you take the reduced bitmap and scale it again to match exactly your desired dimensions.
My version takes a file path rather than a stream.
protected Bitmap createScaledBitmap(String filePath, int desiredBitmapWith, int desiredBitmapHeight) throws IOException, FileNotFoundException {
BufferedInputStream imageFileStream = new BufferedInputStream(new FileInputStream(filePath));
try {
// Phase 1: Get a reduced size image. In this part we will do a rough scale down
int sampleSize = 1;
if (desiredBitmapWith > 0 && desiredBitmapHeight > 0) {
final BitmapFactory.Options decodeBoundsOptions = new BitmapFactory.Options();
decodeBoundsOptions.inJustDecodeBounds = true;
imageFileStream.mark(64 * 1024);
BitmapFactory.decodeStream(imageFileStream, null, decodeBoundsOptions);
imageFileStream.reset();
final int originalWidth = decodeBoundsOptions.outWidth;
final int originalHeight = decodeBoundsOptions.outHeight;
// inSampleSize prefers multiples of 2, but we prefer to prioritize memory savings
sampleSize = Math.max(1, Math.max(originalWidth / desiredBitmapWith, originalHeight / desiredBitmapHeight));
}
BitmapFactory.Options decodeBitmapOptions = new BitmapFactory.Options();
decodeBitmapOptions.inSampleSize = sampleSize;
decodeBitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565; // Uses 2-bytes instead of default 4 per pixel
// Get the roughly scaled-down image
Bitmap bmp = BitmapFactory.decodeStream(imageFileStream, null, decodeBitmapOptions);
// Phase 2: Get an exact-size image - no dimension will exceed the desired value
float ratio = Math.min((float)desiredBitmapWith/ (float)bmp.getWidth(), (float)desiredBitmapHeight/ (float)bmp.getHeight());
int w =(int) ((float)bmp.getWidth() * ratio);
int h =(int) ((float)bmp.getHeight() * ratio);
return Bitmap.createScaledBitmap(bmp, w,h, true);
} catch (IOException e) {
throw e;
} finally {
try {
imageFileStream.close();
} catch (IOException ignored) {
}
}
}

Android -- Map markers displaying differently (dip/px or decompression)

I'm having an odd problem with my map pin sizes. To preserve dynamic-ness, the map pins for different categories are stored on a site's server so that they can be changed at any point even after the app is published.
I'm caching the pins every time I download them and I only ever re-download them if the server sends back a bit saying that one has changed since last I downloaded it. The first time I grab the pins, I use the bitmaps before I save them to files and the map markers are the correct size. Every time after that I'm loading a saved version of the pins straight from the image file. These are displaying considerably smaller than they are when using the bitmaps from the first download.
At first, I thought it was a problem with the way I'm saving the PNGs, but their sizes are correct (64 x 64). Is this a dip/px issue or do I need to decompress the image files with some sort of option?
Here's how I grab the images the first time:
public static Bitmap loadMapPin(String category, int width, int height) {
URL imageUrl;
category = category.toLowerCase().replace(" ", "");
try {
imageUrl = new URL(PIN_URL+category+".png");
InputStream is = (InputStream) imageUrl.getContent();
Options options = new Options();
options.inJustDecodeBounds = true; //Only find the dimensions
//Decode without downloading to find dimensions
BitmapFactory.decodeStream(is, null, options);
boolean scaleByHeight = Math.abs(options.outHeight - height) >= Math.abs(options.outWidth - width);
if(options.outHeight * options.outWidth >= width * height){
// Load, scaling to smallest power of 2 that'll get it <= desired dimensions
double sampleSize = scaleByHeight
? options.outHeight / height
: options.outWidth / width;
options.inSampleSize =
(int)Math.pow(2d, Math.floor(
Math.log(sampleSize)/Math.log(2d)));
}
options.inJustDecodeBounds = false; //Download image this time
is.close();
is = (InputStream) imageUrl.getContent();
Bitmap img = BitmapFactory.decodeStream(is, null, options);
return img;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
And here's how I'm loading them from the cached file:
BitmapFactory.decodeFile(filepath);
Thanks in advance!
I've found that, by default, decompressing an image to a bitmap doesn't scale with high density screens. You have to set the density to none. In other words, you specify that the image is meant for an unknown density.
Solution:
Bitmap b = BitmapFactory.decodeFile(filepath);
b.setDensity(Bitmap.DENSITY_NONE);

Categories

Resources