In the app I'm working on, part of the user's input is a series of images. Some of these might be 4MB large in their raw form. I resize and rotate them, then save them in the app's portion of the device memory for later use. The problem I'm experiencing is that I seem to run out of memory even though I recycle each Bitmap after it's saved.
Here's the main processing
private class SaveImagesTask extends AsyncTask<Long, Void, Void>{
#Override
protected Void doInBackground(Long... ids){
long id = ids[0];
Iterator<ImageButton> itImg = arrBtnImage.iterator();
Iterator<TextView> itLbl = arrLblImage.iterator();
while(itImg.hasNext() && itLbl.hasNext()){
String imgPath = (String) itImg.next().getTag();
String imgLbl = itLbl.next().getText().toString().trim();
String imgName = imgLbl.replace(" ", "_").replace(",", "_");
imgName += ".jpg";
if(imgPath != null){
/* Save resized version of image */
File dir = getApplicationContext().getFilesDir();
dir = new File(dir, "temp/" + Long.toString(plantId));
boolean madeDir = dir.mkdirs();
File path = new File(dir, imgName);
Bitmap toSave = getScaledBitmap(imgPath, IMAGE_MAX_SIDE_LENGTH, IMAGE_MAX_SIDE_LENGTH);
try{
BufferedOutputStream outStream = new BufferedOutputStream(new FileOutputStream(path));
boolean insertSuccess = toSave.compress(Bitmap.CompressFormat.JPEG, 90, outStream);
outStream.close();
}
catch(FileNotFoundException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
toSave.recycle();
}//if
}//while(more images to process)
}// method: doInBackground(params)
}// inner class: saveImages extends AsyncTask
And here's where I resize the image
private Bitmap getScaledBitmap(String picturePath, int newWidth, int newHeight){
/* Size */
BitmapFactory.Options sizeOptions = new BitmapFactory.Options();
sizeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(picturePath, sizeOptions);
int sampleSize = 1;
int rawHeight = sizeOptions.outHeight;
int rawWidth = sizeOptions.outWidth;
if(rawHeight > newHeight || rawWidth > newWidth){
/* Find the dimension that needs to change the most */
int heightRatio = Math.round((float) rawHeight / (float) newHeight);
int widthRatio = Math.round((float) rawWidth / (float) newWidth);
sampleSize = (heightRatio > widthRatio ? heightRatio : widthRatio);
}//if(raw image is wider or taller than it should be){reduce size so neither is too large}
sizeOptions.inJustDecodeBounds = false;//Load pixels for display.
sizeOptions.inSampleSize = sampleSize;//Set shrink factor.
Bitmap scaledBitmap = BitmapFactory.decodeFile(picturePath, sizeOptions);
/* Rotation */
int rotation = 1;
try{
ExifInterface exif = new ExifInterface(picturePath);
rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
}
catch(IOException e){
e.printStackTrace();
}
int rotationInDegrees = 0;
if(rotation == ExifInterface.ORIENTATION_ROTATE_90)
rotationInDegrees = 90;
else if(rotation == ExifInterface.ORIENTATION_ROTATE_180)
rotationInDegrees = 180;
else if(rotation == ExifInterface.ORIENTATION_ROTATE_270)
rotationInDegrees = 270;
Matrix matrix = new Matrix();
if(rotation != 0f)
matrix.preRotate(rotationInDegrees);
return Bitmap.createBitmap(scaledBitmap, 0, 0,
scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
}// method: getScaledBitmap(String, int, int)
Before I start getting comments about this being so common of a question, I'll point out that I'm not displaying these images, so it's not like I'm trying to keep all of these in memory. I need to keep large images because users will want to be able to zoom in on the pictures, but I'm resizing them because they don't need to be ridiculously huge. Pretty much any other solution I've seen on SO for images and OOM errors don't apply to my back-to-back access of multiple images.
So like I said, I'm recycling each Bitmap after it's saved, but they still seem to be using memory. Any idea what I'm missing?
You're not recycling scaledBitmap in getScaledBitmap. Fixing that should help. Change this line:
return Bitmap.createBitmap(scaledBitmap, 0, 0,
scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
to something like:
Bitmap newBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0,
scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
scaledBitmap.recycle();
return newBitmap;
If you have multiple threads working on large bitmaps, you will use a lot of memory on some cases.
What you need is to find the best approach according to your needs. here are some things you can do and/or need to know:
use a single thread for the images handling.
always recycle old bitmaps that you don't need anymore, as soon as possible. it's true that the GC will help you, but that can help it too, and it will work even on pre-honeycomb devices.
do the image manipulations via NDK (so you won't need to have 2 bitmaps for each image manipulation), for example using this.
downsample the image to the minimal size that you need, and never assume that the memory is large enough for any given image (unless you are 100% sure that the images are small).
remember that the requirements for android devices are still very low in terms of RAM per app (heap size) - the bare minimal is still 16MB per app.
you can use android:largeHeap="true" in the manifest, but that doesn't mean anything about how much more you will get, if at all.
Related
Sometimes on some mobile devices, image converting to Base64 String get OutOfMemoryError because of enormous size if base64 string (when the original image & it's resize - not weight too much). Is there any way to get more modest size of string without Outofmemory exception? In my code I resize image & compress... but anyway the size of final Base64 String is large.
public String getBase64Image() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(getFileInst().getAbsolutePath(), options);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int currentWidth = options.outWidth;
int currentHeight = options.outHeight;
int maxSize = Math.max(currentHeight, currentWidth);
double diff = 1;
if (maxSize > maxAcceptableImageSize) {
diff = (double)maxAcceptableImageSize / maxSize;
}
Matrix matrix = new Matrix();
int rotate = getCameraPhotoOrientation(getImage().getContext(), getFileInst().getAbsolutePath());
matrix.preRotate(rotate);
Bitmap image;
Bitmap src = Bitmap.createBitmap(BitmapFactory.decodeFile(getFileInst().getAbsolutePath()), 0, 0,
currentWidth, currentHeight, matrix, false);
if (diff <= 1) {
image = Bitmap.createScaledBitmap(src, (int)(src.getWidth() * diff), (int)(src.getHeight() * diff), false);
} else {
image = src;
}
image.compress(Bitmap.CompressFormat.PNG, 75, baos);
byte[] byteArrayImage = baos.toByteArray();
String base64Str = "data:image/png;base64," + Base64.encodeToString(byteArrayImage, Base64.DEFAULT);
src.recycle();
image.recycle();
return base64Str;
}
You should scale the image before rotating it.
If you scale it down first, there are less pixels that need to be rotated, saving memory.
You should also place .recycle() calls directly after you're done using them, not all at the end of the function.
As a very very VERY last resort, you could add android:largeHeap="true" to your application in the manifest, which will most likely give you more available memory.
For specific details on largeHeap, read the documentation here:
https://developer.android.com/guide/topics/manifest/application-element.html
So I am using the API to detect faces in images, and it is working well for me so far. I have not been able to figure out how to crop the image to the face however. I know how to crop the Bitmap, but it requires getting the top left position of the face in the Bitmap and width and height. When I query for the top left position using
points = face.getPosition();
Bitmap bmp = Bitmap.createBitmap(bit,(int)points.x,(int)(-1.0*points.y),(int)face.getWidth(),(int)face.getHeight());
But when I look at points, I notice that y is -63.5555 and x is 235.6666; I dont understand why there is a negative y coordinate. I did some Debugging and looked inside the face object; I found that it contained a PointF object already that had positive x and y coordinates. So why is a negative y coordinate being returned in this case?
The bounding box estimates the dimensions of the head, even though it may not be entirely visible within the photo. The coordinates may be negative if the face is cropped by the top or left of the image (e.g., the top of the head is cropped off the top of the picture, resulting in a y coordinate above 0).
The difference that you see in debugging is due to that fact that the implementation internally uses the head center position to represent the position (approximately at the mid-point between the eyes), but the API translates this to the top-left position when you call getPosition, for your convenience.
Also note that the bounding box is not necessarily a tight bounds on the face. If you want a tighter fit, you should enable landmark detection and compute your desired level of cropping relative to the returned landmarks.
I have used the same API before and was able to successfully crop the face.
Try
//Crop face option
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
//Bitmap bitmap = BitmapFactory.decodeFile(pictureFile.getAbsolutePath(), options);
Bitmap bitmap = getRotatedImageToUpload(pictureFile.getAbsolutePath());
Bitmap faceBitmap = Bitmap.createBitmap(bitmap, (int) faceCentre.x, (int) faceCentre.y, (int) faceWidth, (int) faceHeight);
FileOutputStream out = null;
try {
out = new FileOutputStream(getOutputMediaFile());
faceBitmap.compress(Bitmap.CompressFormat.PNG, 100, out); // bmp is your Bitmap instance
// PNG is a lossless format, the compression factor (100) is ignored
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//End of Crop face option
And the code for getRotateImageToUpload is
public Bitmap getRotatedImageToUpload(String filePath) {
try {
String file = filePath;
BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file, bounds);
BitmapFactory.Options opts = new BitmapFactory.Options();
Bitmap bm = BitmapFactory.decodeFile(file, opts);
ExifInterface exif = null;
exif = new ExifInterface(file);
String orientString = exif.getAttribute(ExifInterface.TAG_ORIENTATION);
int orientation = orientString != null ? Integer.parseInt(orientString) : ExifInterface.ORIENTATION_NORMAL;
int rotationAngle = 0;
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) rotationAngle = 90;
if (orientation == ExifInterface.ORIENTATION_ROTATE_180) rotationAngle = 180;
if (orientation == ExifInterface.ORIENTATION_ROTATE_270) rotationAngle = 270;
Matrix matrix = new Matrix();
matrix.setRotate(rotationAngle, (float) bm.getWidth() / 2, (float) bm.getHeight() / 2);
Bitmap rotatedBitmap = Bitmap.createBitmap(bm, 0, 0, bounds.outWidth, bounds.outHeight, matrix, true);
return rotatedBitmap;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
BitmapFactory.Options bounds = new BitmapFactory.Options();
bounds.inJustDecodeBounds = true;
BitmapFactory.decodeFile(Common.getRealPathFromURI(selectedImage,rootView.getContext()), bounds);
BitmapFactory.Options opts = new BitmapFactory.Options();
Common.setBitmap(null);
Bitmap bm = BitmapFactory.decodeFile(Common.getRealPathFromURI(selectedImage, rootView.getContext()), opts);
saveToInternalSorage(bm);
ExifInterface exif = new ExifInterface(Common.getRealPathFromURI(selectedImage,rootView.getContext()));
String orientString = exif.getAttribute(ExifInterface.TAG_ORIENTATION);
int orientation = orientString != null ? Integer.parseInt(orientString) : ExifInterface.ORIENTATION_NORMAL;
int rotationAngle = 0;
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) rotationAngle = 90;
if (orientation == ExifInterface.ORIENTATION_ROTATE_180) rotationAngle = 180;
if (orientation == ExifInterface.ORIENTATION_ROTATE_270) rotationAngle = 270;
Matrix matrix = new Matrix();
matrix.postRotate(rotationAngle);
try
{
Bitmap rotatedBitmap = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
bitmap2 = rotatedBitmap;//BitmapFactory.decodeStream(imageStream);
}
catch(OutOfMemoryError e)
{
}
Basically at times, i get my image rotated. So I have implemented that function, the problem lies on createbitmap i get outofmemory error. I want to avoid that error , without modifying the size of the image... and the quality. i want to keep same size and quality.
If you want to use large heap, you should write in AndroidManifest.xml.
android:largeHeap="true"
If you want to check heap size on your device, call ActivityManager.getLargeMemoryClass().
ActivityManager am = ((ActivityManager)getSystemService(Activity.ACTIVITY_SERVICE));
int largeMemory = am.getLargeMemoryClass();
You have far bigger problems, such as the fact that your app will not work on most Android devices. Use your Uri properly and get rid of getRealPathFromURI().
That being said, there is no guarantee that you can allocate a bitmap of the size needed to rotate the image. Whether you can or not will be based on the state of your heap and the resolution of the image. Your choices are:
Switch to using native code via the NDK for the image rotation. This too is not guaranteed to work, but the odds are a lot higher, because you have all system RAM to work with.
Switch to using android:largeHeap="true". This will have no effect on many devices. However, on some, it will increase your heap limit and make it somewhat more likely that your bitmap allocation will succeed.
Settle for resizing the image.
Tell the user "sorry, we do not have enough memory to rotate the image".
My app is an OCR app base on Tesseract. It will do OCR task from camera picture. Users can take many pictures and put them into an OCR queue. To get more accuracy, I want to keep high quality image (I choose min size is 1024 x 768 (maybe larger in future), JPEG, 100% quality). When users take many pictures, there are three things to do:
Save the image data byte[] to file and correct EXIF.
Correct the image orientation base on device's orientation. I know there are some answers that said the image which comes out of the camera is not oriented automatically, have to correct it from file, like here and here. I'm not sure about it, I can setup the camera preview orientation correctly, but the image results aren't correct.
Load bitmap from taken picture, convert it to grayscale and save to another file for OCR task.
And here is my try:
public static boolean saveBitmap(byte[] bitmapData, int orientation, String imagePath, String grayScalePath) throws Exception {
Boolean rotationSuccess = false;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap originalBm = null;
Bitmap bitmapRotate = null;
Bitmap grayScale = null;
FileOutputStream outStream = null;
try {
// save directly from byte[] to file
saveBitmap(bitmapData, imagePath);
// down sample
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, options);
int sampleSize = calculateInSampleSize(options, Config.CONFIG_IMAGE_WIDTH, Config.CONFIG_IMAGE_HEIGHT);
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
originalBm = BitmapFactory.decodeFile(imagePath, options);
Matrix mat = new Matrix();
mat.postRotate(orientation);
bitmapRotate = Bitmap.createBitmap(originalBm, 0, 0, originalBm.getWidth(), originalBm.getHeight(), mat, true);
originalBm.recycle();
originalBm = null;
outStream = new FileOutputStream(new File(imagePath));
bitmapRotate.compress(CompressFormat.JPEG, 100, outStream);
// convert to gray scale
grayScale = UIUtil.convertToGrayscale(bitmapRotate);
saveBitmap(grayScale, grayScalePath);
grayScale.recycle();
grayScale = null;
bitmapRotate.recycle();
bitmapRotate = null;
rotationSuccess = true;
} catch (OutOfMemoryError e) {
e.printStackTrace();
System.gc();
} finally {
if (originalBm != null) {
originalBm.recycle();
originalBm = null;
}
if (bitmapRotate != null) {
bitmapRotate.recycle();
bitmapRotate = null;
}
if (grayScale != null) {
grayScale.recycle();
grayScale = null;
}
if (outStream != null) {
try {
outStream.close();
} catch (IOException e) {
}
outStream = null;
}
}
Log.d(TAG,"save completed");
return rotationSuccess;
}
Save to file directly from byte[]
public static void saveBitmap(byte[] bitmapData, String fileName) throws Exception {
File file = new File(fileName);
FileOutputStream fos;
BufferedOutputStream bos = null;
try {
final int bufferSize = 1024 * 4;
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos, bufferSize);
bos.write(bitmapData);
bos.flush();
} catch (Exception ex) {
throw ex;
} finally {
if (bos != null) {
bos.close();
}
}
}
Calculate scale size
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
When save complete, this image is loaded into thumbnail image view by UIL. The problem is the save task is very slow (wait some second before save complete and load into view), and sometime I got OutOfMemory exception. Is there any ideas to reduce the save task and avoid OutOfMemory exception?
Any help would be appreciated!
P/S: the first time I try to convert byte[] to bitmap instead of save to file, and then rotate and convert to grayscale, but I still got above issues.
Update: here is the grayscale bitmap process:
public static Bitmap convertToGrayscale(Bitmap bmpOriginal) {
int width, height;
height = bmpOriginal.getHeight();
width = bmpOriginal.getWidth();
Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(bmpGrayscale);
Paint paint = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
c.drawBitmap(bmpOriginal, 0, 0, paint);
return bmpGrayscale;
}
The OutOfMemory exception seldom occurred (just a few times) and I can't reproduce it now.
Update:
Since you're still saying that the method takes too long time I would define a callback interface
interface BitmapCallback {
onBitmapSaveComplete(Bitmap bitmap, int orientation);
onBitmapRotateAndBWComlete(Bitmap bitmap);
}
Let your activity implement the above interface and convert the byte[] to bitmap in top of your saveBitmap method and fire the callback, before the first call to save. Rotate the imageView based on the orientation parameter and set a black/white filter on the imageView to fool the user into thinking that the bitmap is black and white (do this in your activity). See to that the calls are done on main thread (the calls to imageView). Keep your old method as you have it. (all steps need to be done anyway) Something like:
public static boolean saveBitmap(byte[] bitmapData, int orientation, String imagePath, String grayScalePath, BitmapCallback callback) throws Exception {
Boolean rotationSuccess = false;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap originalBm = null;
Bitmap bitmapRotate = null;
Bitmap grayScale = null;
FileOutputStream outStream = null;
try {
// TODO: convert byte to Bitmap, see to that the image is not larger than your wanted size (1024z768)
callback.onBitmapSaveComplete(bitmap, orientation);
// save directly from byte[] to file
saveBitmap(bitmapData, imagePath);
.
.
// same as old
.
.
saveBitmap(grayScale, grayScalePath);
// conversion done callback with the real fixed bitmap
callback.onBitmapRotateAndBWComlete(grayScale);
grayScale.recycle();
grayScale = null;
bitmapRotate.recycle();
bitmapRotate = null;
rotationSuccess = true;
How do you setup your camera? What might be causing the long execution time in the first saveBitmap call, could be that you are using the default camera picture size settings and not reading the supported camera picture size and choosing best fit for your 1024x768 image needs. You might be taking big mpixel images and saving such, but in the end need you need < 1 mpixles (1024x768). Something like this in code:
Camera camera = Camera.open();
Parameters params = camera.getParameters();
List sizes = params.getSupportedPictureSizes();
// Loop camera sizes and find best match, larger than 1024x768
This is probably where you will save most of the time if you are not doing this already. And do it only once, during some initialization phase.
Increase the buffer to 8k in saveBitmap, change the 1024*4 to 1024*8, this would increase the performance at least, not save any significant time perhaps.
To save/reuse bitmap memory consider using inBitmap field, if you have a post honeycomb version, of BitmapFactory.Options and set that field to point to bitmapRotate bitmap and send options down to your convertToGrayscale method to not need allocating yet another bitmap down in that method. Read about inBitmap here: inBitmap
I have an app that displays quite a few images for the user, and we've been seeing a lot of error reports with OutOfMemoryError exception.
What we currently do is this:
// Check if image is a landscape image
if (bmp.getWidth() > bmp.getHeight()) {
// Rotate it to show as a landscape
Matrix m = image.getImageMatrix();
m.postRotate(90);
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
}
image.setImageBitmap(bmp);
The obvious problem with this is that we have to recreate the bitmap from the image on memory and rotate the matrix, this is quite expensive for the memory.
My question is simple:
Is there a better way to rotate images without causing OutOfMemoryError?
2 methods of rotating a large image:
using JNI , like on this post.
using a file : it's a very slow way (depending on the input and the device , but still very slow) , which puts the decoded rotated image into the disk first , instead of putting it into the memory .
code of using a file is below:
private void rotateCw90Degrees()
{
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),INPUT_IMAGE_RES_ID);
// 12 => 7531
// 34 => 8642
// 56 =>
// 78 =>
final int height=bitmap.getHeight();
final int width=bitmap.getWidth();
try
{
final DataOutputStream outputStream=new DataOutputStream(new BufferedOutputStream(openFileOutput(ROTATED_IMAGE_FILENAME,Context.MODE_PRIVATE)));
for(int x=0;x<width;++x)
for(int y=height-1;y>=0;--y)
{
final int pixel=bitmap.getPixel(x,y);
outputStream.writeInt(pixel);
}
outputStream.flush();
outputStream.close();
bitmap.recycle();
final int newWidth=height;
final int newHeight=width;
bitmap=Bitmap.createBitmap(newWidth,newHeight,bitmap.getConfig());
final DataInputStream inputStream=new DataInputStream(new BufferedInputStream(openFileInput(ROTATED_IMAGE_FILENAME)));
for(int y=0;y<newHeight;++y)
for(int x=0;x<newWidth;++x)
{
final int pixel=inputStream.readInt();
bitmap.setPixel(x,y,pixel);
}
inputStream.close();
new File(getFilesDir(),ROTATED_IMAGE_FILENAME).delete();
saveBitmapToFile(bitmap); //for checking the output
}
catch(final IOException e)
{
e.printStackTrace();
}
}
you can try:
image.setImageBitmap(null);
// Check if image is a landscape image
if (bmp.getWidth() > bmp.getHeight()) {
// Rotate it to show as a landscape
Matrix m = image.getImageMatrix();
m.postRotate(90);
bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
}
BitmapDrawable bd = new BitmapDrawable(mContext.getResources(), bmp);
bmp.recycle();
bmp = null;
setImageDrawable(bd);
bd = null;
When working with lots of Bitmaps be sure to call recycle() on them as soon as they are not needed. This call will instantly free memory associated with a particular bitmap.
In your case if you do not need the original bitmap after rotation, then recycle it. Something along the lines of:
Bitmap result = bmp;
// Check if image is a landscape image
if (bmp.getWidth() > bmp.getHeight()) {
// Rotate it to show as a landscape
Matrix m = image.getImageMatrix();
m.postRotate(90);
result = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, true);
// rotating done, original not needed => recycle()
bmp.recycle();
}
image.setImageBitmap(result);