I'm making an Android app and I need to load an image (bitmap) in a cavas and resize it using the "pinch zoom" gesture. When the image is over a certain size, however, the application crashes (OutOfMemory exception). How do I optimize the loading and manipulation of the image?
To load the image I use:
BitmapFactory.decodeResource (ctx.getResources (), R.drawable.image)
To draw it:
imgCanvas.drawBitmap (image, posX, posY, null),
To change its size:
Bitmap.createScaledBitmap (originalBitmap, neww, NEWH, true);
This is not trivial.
Based on the current scale of the image and the currently visible part of the image, only load a part of that image at the appropriate resolution:
https://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html
When zoomed-out and you want to show the entire image scaled down, use the methods from this BitmapRegionDecoder class that take a BitmapFactory.Options parameter and set it inSampleSize to a value larger than 1 (preferably a value that is a power of 2):
https://developer.android.com/reference/android/graphics/BitmapFactory.Options.html#inSampleSize
When zooming in, first zoom in the lower resolution that is already shown (where you used a value of inSampleSize > 1) and lazily load a higher resolution version (where inSampleSize is smaller than the previous value you used) using the BitmapRegionDecoder and fade in the higher resolution version gradually.
When the user zooms in, keep doing this until your inSampleSize is 1.
Related
I am trying to work on a small requirement which provides users with an option to resize an image to a given percentage. Lets say 75% is the option. Does that mean I should resize the image size to 75% or the resolution of the image to 75%?
Any thoughts on this?
I wanted to use
Bitmap yourBitmap; resized =
Bitmap.createScaledBitmap(yourBitmap,(int)(yourBitmap.getWidth()*0.75),
(int)(yourBitmap.getHeight()*0.75), true);
For the newWidth and newHeight should I blindly pass calculate like above?
I think
image resolution is 100% like 100x100 or 200x200 or 1000x1000
user want to resize it to 75%
I think scaleX, scaleY
so on the end image must be 75% like 75x75 or 150x150 or 750x750
Within the help of Bitmap.createScaledBitmap you can resize your image even without losing your image quality
Bitmap scaledBitmap = Bitmap.createScaledBitmap(unscaledBitmap, wantedWidth, wantedHeight, true);
I hope it will work for you
You can implement as following way.
/* sample code */
Bitmap srcBitmap;
Bitmap destBitmap = Bitmap.createScaledBitmap(srcBitmap, (int)(srcBitmap.getWidth()*0.75), (int)(srcBitmap.getHeight()*0.75), true);
Hopefully...
What you consider to be 75% of an image is up to you. There are several posibilities. A nice variant is to resize to a predetermined size like 1024 for width if landscape and for height if portrait.
All the here suggested techniques can often not be used as they construct a full blown original bitmap first. Often memory for that will not be available.
So scale it down while loading. BitmapFactory will do it for you.
I want to crop image without getting OutOfMemory exception.
it means i have x, y, width and height of cropped image and want to crop original image without bringing it to memory.
Yes i know that BitmapRegionDecoder is good idea but maybe the cropped image would be too large for bringing it to memory.
In fact i don't want copped bitmap, just want to write cropped image from source file to destination file.
EDIT : I want to save cropped image not just showing it in an ImageView
I want to save it in a new file without losing dimensions
This is the example
in this situation cropped image resolution is 20000x20000 and code below wont work cause of OOM:
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);
using inSampleSize to decrease the original picture size is good but the result i save is no longer 20000x20000.
How can i crop the 25000x25000 and save the 20000x20000 part of image in a file?
Simply put, it requires lots of low level programming and optimizations.
as you can see, lots of answers in this region are pointing to generic concepts of bitmap compression, etc which are indeed applicable in most issues but not specifically yours.
Also BitmapRegionDecoder as suggested in answers won’t work well. It sure prevents loading the whole bitmap in RAM but what about the cropped image? after cropping an image it gives you a giant bitmap which no matter what, gives you an OOM.
Because your problem as you described, needs Bitmaps to get written or get read from disk just as they get written or read from memory; something called a BufferedBitmap (or so) which efficiently handles the memory it requires by saving little chunks of a bitmap to disk and using them later, thus, avoiding OOM.
Any other solution which wants to tackle the problem with scaling only do half of the work. why? because cropped image itself can be too big for memory (as you said).
However, solving the problem by scaling isn’t that bad, if you don’t care about the quality of the cropped image compared to the quality user had seen when she was cropping it. that’s what the Google Photos do, it simply reduces the quality of cropped image, very simple!
I haven’t seen any BufferedBitmap classes around (but if there are, it would be awesome). They sure become handy for solving similar problems.
You can check Telegram messaging app which comes with an open-source implementation of image cropping facilities; you guess right, it handles all the similar nasty works with good old C... Hence, we might conclude that a good global solution (or better said, ONE OF THE SEVERAL APPLICABLE SOLUTIONS) appears to be low-level programming to handle disk and memory yourself.
I know my answer failed to give any copy-paste-ish solution to your problem but at least I hope it gave you some ideas my friend.
Did you checked BitmapRegionDecoder? It will extract a rectangle out of the original image.
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);
http://developer.android.com/reference/android/graphics/BitmapRegionDecoder.html
You can solve this using BitmapFactory. To determinate the original bitmap size without putting it in to memory, do the fallowing:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(..., options);
int originalImageWith = options.outWidth;
int originalImageHeight = options.outHeight;
Now you can use options.inSampleSize
If set to a value > 1, requests the decoder to
subsample the original image, returning a smaller image to save
memory. The sample size is the number of pixels in either dimension
that correspond to a single pixel in the decoded bitmap. For example,
inSampleSize == 4 returns an image that is 1/4 the width/height of the
original, and 1/16 the number of pixels. Any value <= 1 is treated the
same as 1. Note: the decoder uses a final value based on powers of 2,
any other value will be rounded down to the nearest power of 2.
Now it's not a perfect solution but you can do math to find what is the closest factor of 2 that you can use on options.inSampleSize to save memory.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
Bitmap bitmap = BitmapFactory.decodeResource(..., options);
BitmapRegionDecoder is the good way to crop big or large Images, but it's available from API 10 and above.
There is a class called BitmapRegionDecoder which might help you, but it's available from API 10 and above.
If you can't use it :
Many image formats are compressed and therefore require some sort of loading into memory.
You will need to read about the best image format that fits your needs, and then read it by yourself, using only the memory that you need.
a little easier task would be to do it all in JNI, so that even though you will use a lot of memory, at least your app won't get into OOM so soon since it won't be constrained to the max heap size that is imposed on normal apps.
Of course, since android is open source, you can try to use the BitmapRegionDecoder and use it for any device.
Reference :
Crop image without loading into memory
Or you can find some other way on below that might be helpful to you:
Bitmap/Canvas use and the NDK
I was reading an article about how to load bitmaps efficiently here. it had suggested using some techniques to load bitmap with a size that is needed not the real size. the only thing is that I didn't get what inSampleSize variable does(which must be a power of 2). if I choose number 1 for that, does it mean that this would be like if i normally loaded a bitmap with its real size?
Rajesh has quoted the explanation from the documentation of what inSampleSize does; that explanation can be expanded on with diagrams.
The important part is:
The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap.
So, if we had this image (where each letter denotes a pixel):
AAAABBBB
AAAABBBB
AAAABBBB
AAAABBBB
CCCCDDDD
CCCCDDDD
CCCCDDDD
CCCCDDDD
And we set inSampleSize = 2, we would get a decoded bitmap that looks like this:
AABB
AABB
CCDD
CCDD
That is, 2 pixels in the original image (AA) correspond to 1 pixel (A) in the decoded image.
If we set inSampleSize = 4, we would get a decoded bitmap that looks like this:
AB
CD
That is, 4 pixels in the original image correspond to 1 pixel in the decoded image.
Notice than an inSampleSize of 2 effectively halves the vertical and horizontal resolutions, but uses 1/4 of the pixels - and therefore only 1/4 of the memory.
Please read the documentation for inSampleSize
If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.
if I choose number 1 for that, does it mean that this would be like if i normally loaded a bitmap with its real size?
Yes, 1 denotes no subsampling.
I have a Canvas that i draw text on.
(this is quite advanced i think hope you can follow me)
See attached image below.
The functionality is that I can tap on the screen to change the textsize.
Tap left screen = smaller Font.
Tap right Screen = bigger Font.
I can then also move the text on the screen.
When textsize is ok and i have moved the text where i want it,
Then I want to save it back to the original Bitmap.
I use options.inSampleSize = 4; to initially load the Bitmap
The ImageView that have the Bitmap is of course smaller then the original Image.
Some kind of calculation is needed.
This tends to be quite difficult to do.
I have the options.inSampleSize = 4 Bitmaps Ratio.
It's 0.59, 0.69 something depending on Landscape or portrait.
Im playing around with that to somehow change the new BitmapsetTextSize
to look the same as the ImageView smaller Bitmap.
What could i do here?
I have a feeling that since one never know what size an image have.
I have to somehow scale/constrain the Loaded Bitmap Ratio to a fixed Ratio.
Then i need to using percentage or something to transfer the text location
to the bigger image.
I can kind of do that when it comes to initial
(red small ball on picture) location. Hence, the starting point of the text.
But i dont know how long the text is so im stuck so so speak and asking for advice
One way i tried was to divide paint.getTextSize() with the Ratio something like 0.59. That looked like a solution at first. But the image ratio is not fixed and the Font size is not fixed something else is needed.
Here are two pictures showing the problem.
On phone Bitmap:
The saved new Bitmap:
I'm not 100% clear that I understand what you mean, but here's a go. It sounds like you were close to the right approach. Instead of using a fixed ratio, you need to calculate the ratio that the image is scaled by to fit in the view on the phone, then you can scale the text by the inverse ratio. So in steps:
Measure the width of the original image (height would do just as well, but we just need one dimension)
Measure the width of the scaled image
Calculate ratio (ratio = original / scaled)
Let the user type their text
You can then get the text size using something like: float paintSize = paint.getTextSize();
For rendering on the final image, use paint.setTextSize(paintSize / ratio);.
Say I have a somewhat large (i.e. not fit in most phones' memory) bitmap on disk. I want to draw only parts of it on the screen in a way that isn't scaled (i.e. inSampleSize == 1)
Is there a way to load/draw just the part I want given a Rect specifying the area without loading the entire bitmap content?
I'm quite confident this is possible since you can load a really large bitmap file into an ImageView without problems so there must be some sort of a built-in way to handle large bitmaps... and after a few attempts, I've found a solution:
Instead of loading the entire bitmap and manually draw it yourself, load it as a Drawable instead:
InputStream mapInput = getResources().openRawResource(
R.drawable.transit_map);
_map = Drawable.createFromStream(mapInput, "transit_map");
_map.setBounds(0, 0, _mapDimension.width(), _mapDimension.height());
I'm using a resource file but since you can use Drawable.createFromStream to load image from any InputStream, it should works with arbitrary bitmap.
Then, use the Drawable.draw method to draw it onto the desired canvas like so:
int left = -(int) contentOffset.x;
int top = -(int) contentOffset.y;
int right = (int) (zoom * _mapDimension.width() - contentOffset.x);
int bottom = (int) (zoom * _mapDimension.height() - contentOffset.y);
_map.setBounds(left, top, right, bottom);
_map.draw(canvas);
As in the above case, You can also scale and translate the bitmap as well by manipulating the drawable's bounds and only the relevant parts of the bitmap will be loaded and drawn onto the Canvas.
The result is a pinch-zoomable view from just one single 200KB bitmap file. I've also tested this with a 22MB PNG file and it still works without any OutOfMemoryError including when screen orientation changes.
Now it's very relevant: BitmapRegionDecoder.
Note: available since Android SDK 10
It can easily be done by using RapidDecoder.
import rapid.decoder.BitmapDecoder;
Rect bounds = new Rect(10, 20, 30, 40);
Bitmap bitmap = BitmapDecoder.from("your-file.png")
.region(bounds)
.decode();
imageView.setImageBitmap(bitmap);
It supports down to Android 2.2 (API Level 8).
Generally speaking, that isn't possible, particularly since most image formats are compressed, so you don't even know which bytes to read until you've extracted the uncompressed form.
Break your image up into small tiles and load just the tiles you need to cover the region you want to display at runtime. To avoid jittery scrolling, you might also want to preload tiles that are just out of sight (the ones that border the visible tiles) on a background thread.