Load large images with Picasso and custom Transform object - android

I'm getting an Out Of Memory exception using Picasso when loading "large" images (> 1.5MB) from Android Gallery (using startActivityForResult).
I'm using a custom Target object because I need to preprocess the Bitmap when it is ready and also I'm using a custom Transform object to scale the Bitmap.
The problem is that the method public Bitmap transform(Bitmap source) on my Transform object is never called because the Out Of Memory Exception, so I don't get the oportunity to resample the image.
But, if I use the .resize(maxWidth, maxHeight) method, then it loads the image OK. I supossed that the Transform object was for that purpose too, but it seems that the transform method is called after the resize, and, if I don't call resize, then it will end in an Out of Memory..
The problem is that with resize I need to specify both width and height, but I need to scale and keep the aspect ratio.
Consider that the images will be selected from user Gallery, so they can be bigger or smaller, portrait, squared or landscape, etc, so I need my own Transformation object to perform the logic that needs my application.

I found a solution..
In my Transform object I needed to scale the image (keeping aspect ratio) to 1024 x 768 max.
Transform object was never called unless I call .resize(width, height) to resample down the image.
For keeping aspect ratio and using resize I call .centerInside(). This way image will be scaled resample to fit width, height).
The value that I give to .resize(width, height) is Math.ceil(Math.sqrt(1024 * 768)).
This way I'm sure to have an image higher enough for my custom Transform object, and also avoid Out Of Memory exception
Update: Full example
Following this example you will get a image that fits inside MAX_WIDTH and MAX_HEIGHT bounds (keeping aspect ratio)
private static final int MAX_WIDTH = 1024;
private static final int MAX_HEIGHT = 768;
int size = (int) Math.ceil(Math.sqrt(MAX_WIDTH * MAX_HEIGHT));
// Loads given image
Picasso.with(imageView.getContext())
.load(imagePath)
.transform(new BitmapTransform(MAX_WIDTH, MAX_HEIGHT))
.skipMemoryCache()
.resize(size, size)
.centerInside()
.into(imageView);
And this is my custom BitmapTransform class:
import android.graphics.Bitmap;
import com.squareup.picasso.Transformation;
/**
* Transformate the loaded image to avoid OutOfMemoryException
*/
public class BitmapTransform implements Transformation {
private final int maxWidth;
private final int maxHeight;
public BitmapTransform(int maxWidth, int maxHeight) {
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
}
#Override
public Bitmap transform(Bitmap source) {
int targetWidth, targetHeight;
double aspectRatio;
if (source.getWidth() > source.getHeight()) {
targetWidth = maxWidth;
aspectRatio = (double) source.getHeight() / (double) source.getWidth();
targetHeight = (int) (targetWidth * aspectRatio);
} else {
targetHeight = maxHeight;
aspectRatio = (double) source.getWidth() / (double) source.getHeight();
targetWidth = (int) (targetHeight * aspectRatio);
}
Bitmap result = Bitmap.createScaledBitmap(source, targetWidth, targetHeight, false);
if (result != source) {
source.recycle();
}
return result;
}
#Override
public String key() {
return maxWidth + "x" + maxHeight;
}
}

There's a very good article on the Android Developer website which helped me alot when I had the same problem with loading large images.
This article explains really well what causes the error and how to solve it. There are also other articles (see menu) to, for example, cache images.

Related

Android bitmap scale banner image to portrait

I'm pulling in images from a cms and they are banner style (width = 900, height = 250). I have to use them in a fullscreen portrait mode, so naturally they are badly distorted. I've tried a lot of different things with xml, resizing bitmaps, picasso transforms etc..but not sure what else to try at this point. The only time I've been able to maintain quality is by centercropping, but that isn't an option. I need the image to maintain its aspect ratio. Is this even possible to do without sacrificing the aspect ratio? The images aspect ratio 1.7 while the desired is 0.5625 on my galaxy s5.
Edit for more details:
This is a proprietary project so I can't include the images I'm using but they are banner style. They are being used in an imageview that is screenSizeHeight - toolbarHeight and screensizewidth / 1.2.
this is how I'm trying to resize the bitmap. It's just centercropping though and is much too large.
public static final Bitmap scaleAndCropBitmapByPixelDimensions(final Context context, final Bitmap sourceBitmap, final int targetWidth, final int targetHeight)
{
try
{
if (context != null && sourceBitmap != null)
{
int srcWidth = sourceBitmap.getWidth();
int srcHeight = sourceBitmap.getHeight();
float targetAspectRatio = (float)targetWidth / (float)targetHeight;
float srcAspectRatio = (float)srcWidth / (float)srcHeight;
int scaleWidth = targetWidth;
int scaleHeight = targetHeight;
int x = 0;
int y = 0;
if (srcAspectRatio < targetAspectRatio)
{
scaleWidth = targetWidth;
scaleHeight = (int)((float)(1 / srcAspectRatio) * (float)scaleWidth);
x = 0;
y = (scaleHeight - targetHeight) / 2;
}
else
{
scaleHeight = targetHeight;
scaleWidth = (int)((float)srcAspectRatio * (float)scaleHeight);
x = (scaleWidth - targetWidth) / 2;
y = 0;
}
Bitmap scaledBitmap = Bitmap.createScaledBitmap(sourceBitmap, scaleWidth, scaleHeight, true);
Bitmap croppedBitmap = Bitmap.createBitmap(scaledBitmap, x, y, targetWidth, targetHeight);
return croppedBitmap;
}
}
catch (Exception ex)
{
//Log.e(LOG_TAG, "Error scaling and cropping bitmap", ex);
ex.printStackTrace();
}
return null;
}
Setting up the bitmap transform
#Override
public Bitmap transform(Bitmap sourceBitmap) {
i_bitmap = Utils.scaleAndCropBitmapByPixelDimensions(mContext,sourceBitmap,targetWidth,targetHeight);
if (sourceBitmap != i_bitmap) {
// Same bitmap is returned if sizes are the same
sourceBitmap.recycle();
}
return i_bitmap;
}
imageview xml
<ImageView
android:id="#+id/bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="center" />
and loading the image with picasso
Picasso.with(mContext)
.load(url)
.transform(transformation)
.into(getBg());
Well, since the desired aspect ratio is the opposite of the actual ratio, one thing you can is to simply rotate the banner images (-90º or +90º).
Of course, this is a very naive suggestion. Maybe if you give more details about the contents to be displayed or any other constraints, we'll be able to provide a better solution.

How to reduce image size into 1MB

I want my application to upload image with no size limit, but in the code, I want to resize the image into 1MB if the image size exceeds. I have tried many ways but I couldn't find any code for the requirement I have mentioned above.
For once, I have tried this:
public void scaleDown() {
int width = stdImageBmp.getWidth();
int height = stdImageBmp.getHeight();
Matrix matrix = new Matrix();
float scaleWidth = ((float) MAX_WIDTH) / width;
float scaleHeight = ((float) MAX_HEIGHT) / height;
matrix.postScale(scaleWidth, scaleHeight);
stdImageBmp = Bitmap.createBitmap(stdImageBmp, 0, 0, width, height, matrix, true);
File Image = new File("path");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//compress bmp
stdImageBmp.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
byte[] byteArray = byteArrayOutputStream.toByteArray();
imgViewStd.setImageBitmap(stdImageBmp);
Log.d("resizedBitmap", stdImageBmp.toString());
width = stdImageBmp.getWidth();
height = stdImageBmp.getHeight();
System.out.println("imgWidth" + width);
System.out.println("imgHeight" + height);
}
you can use this code to resize a bitmap and for image size < 1MB i recommend use resolution of 480x640
public Bitmap getResizedBitmap(Bitmap bm, int newWidth, int newHeight) {
int width = bm.getWidth();
int height = bm.getHeight();
float scaleWidth = ((float) newWidth) / width;
float scaleHeight = ((float) newHeight) / height;
// CREATE A MATRIX FOR THE MANIPULATION
Matrix matrix = new Matrix();
// RESIZE THE BIT MAP
matrix.postScale(scaleWidth, scaleHeight);
// "RECREATE" THE NEW BITMAP
return Bitmap.createBitmap(
bm, 0, 0, width, height, matrix, false);
}
If you really want the Bitmap that scales down to the Bitmap that is the closest to a given amount of bytes, heres the method I use. (It does not uses a while loop)
NOTE: This method only works if passed bitmap is in ARGB_8888 configuration.
See: Compress bitmap to a specific byte size in Android for the conversion method.
/**
* Method to scale the Bitmap to respect the max bytes
*
* #param input the Bitmap to scale if too large
* #param maxBytes the amount of bytes the Image may be
* #return The scaled bitmap or the input if already valid
* #Note: The caller of this function is responsible for recycling once the input is no longer needed
*/
public static Bitmap scaleBitmap(final Bitmap input, final long maxBytes) {
final int currentWidth = input.getWidth();
final int currentHeight = input.getHeight();
final int currentPixels = currentWidth * currentHeight;
// Get the amount of max pixels:
// 1 pixel = 4 bytes (R, G, B, A)
final long maxPixels = maxBytes / 4; // Floored
if (currentPixels <= maxPixels) {
// Already correct size:
return input;
}
// Scaling factor when maintaining aspect ratio is the square root since x and y have a relation:
final double scaleFactor = Math.sqrt(maxPixels / (double) currentPixels);
final int newWidthPx = (int) Math.floor(currentWidth * scaleFactor);
final int newHeightPx = (int) Math.floor(currentHeight * scaleFactor);
Timber.i("Scaled bitmap sizes are %1$s x %2$s when original sizes are %3$s x %4$s and currentPixels %5$s and maxPixels %6$s and scaled total pixels are: %7$s",
newWidthPx, newHeightPx, currentWidth, currentHeight, currentPixels, maxPixels, (newWidthPx * newHeightPx));
final Bitmap output = Bitmap.createScaledBitmap(input, newWidthPx, newHeightPx, true);
return output;
}
Where the Sample use would look something like:
// (1 MB)
final long maxBytes = 1024 * 1024;
// Scale it
final Bitmap scaledBitmap = BitmapUtils.scaleBitmap(yourBitmap, maxBytes);
if(scaledBitmap != yourBitmap){
// Recycle the bitmap since we can use the scaled variant:
yourBitmap.recycle();
}
// ... do something with the scaled bitmap
I've tried to comment it but the comment become too big.
So, I've tested many solutions and it seems like there are only TWO solutions for this problem, which give some results.
Lets discuss Koen solution first. What it actually does is creates a scaled JPG
Bitmap.createScaledBitmap(input, newWidthPx, newHeightPx, true)
Seems like it does not compress it at all but just cuts off the resolution.
I've tested this code and when I pass MAX_IMAGE_SIZE = 1024000 it gives me 350kb compressed image out of 2.33Mb original image. Bug?
Also it lacks quality. I was unable to recognize a text on A4 sheet of paper photo made by Google Pixel.
There is another solution to this problem, which gives good quality, but lacks in speed.
A WHILE LOOP!
Basically you just loop through image size, until you get the desired size
private fun scaleBitmap() {
if (originalFile.length() > MAX_IMAGE_SIZE) {
var streamLength = MAX_IMAGE_SIZE
var compressQuality = 100
val bmpStream = ByteArrayOutputStream()
while (streamLength >= MAX_IMAGE_SIZE) {
bmpStream.use {
it.flush()
it.reset()
}
compressQuality -= 8
val bitmap = BitmapFactory.decodeFile(originalFile.absolutePath, BitmapFactory.Options())
bitmap.compress(Bitmap.CompressFormat.JPEG, compressQuality, bmpStream)
streamLength = bmpStream.toByteArray().size
}
FileOutputStream(compressedFile).use {
it.write(bmpStream.toByteArray())
}
}
}
I think that this approach will consume exponential time depending on image resolution.
9mb image takes up to 12 seconds to compress down to 1mb.
Quality is good.
You can tweak this by reducing the original bitmap resolution(which seems like a constant operation), by doing:
options.inSampleSize = 2;
What we need to do is to somehow calculate compressQuality for any image.
There should be a math around this, so we can determinate compressQuality from original image size or width + height.

Picasso - Image downsizing before downloading for better performance

I have a list of images that I have to download from the server and show them in a listView in android app. They are of varying ratio, and since I would to make each ImageView to match the device's width, I cannot fix their height.
I also know that I could improve the performance by downsizing the image before downloading, just as what Picasso is doing with fit() and resize().
I am using Picasso for my project. The problem is, I cannot use Picasso's fit() and resize() methods, which are methods to downsize image before downloading, if I do not know the height of the imageView beforehand.
So I tried with other approaches. I am wondering if transform would do the downsizing as well?
new Transformation() {
#Override
public Bitmap transform(Bitmap source) {
int targetHeight = deviceWidth;
double aspectRatio = (double) source.getHeight() / (double) source.getWidth();
int targetWidth = (int) (targetHeight / aspectRatio);
Bitmap result = Bitmap.createScaledBitmap(source, targetWidth, targetHeight, false);
if (result != source) {
// Same bitmap is returned if sizes are the same
source.recycle();
}
return result;
}
#Override
public String key() {
return "transformation" + " desiredWidth";
}
Many Thanks.
You cannot "downsize the image before downloading".
Picasso (and everything else) downloads the original image as-is, and only then does calculations to scale the image according to the container's dimensions.
If you don't use neither fit() nor resize(), Picasso will load the image at full resolution: that is, the resolution of the original image.
You can use Thumbor service to crop and resize images before downloading. Square has a nice Java client for it - Pollexor. And special Picasso RequestTransformer for Pollexor. That said, you'll get all the preprocessing extracted to the backend.

center Crop Image In proper size to set on ImageView

I am using camera API to take picture i have to open camera in different sizes according to my Image view size. I am following the sample project which we get inside Android sdk/sample/adroid-18 at the name "ApiDemo" the thing i have changed is not set camera on setcontentview. I have set the camera on Frame Layout. at first my camera preview was starched so i got the camera OptimalPreviewSize and make FrameLayout parameter width and height as wrap-content.Now the camera preview is smaller then ImageView (The size i want). If i make the size of FrameLayout parameter as match-parent then camera View is stretch.How to resolve this issue.
find this link for more specification. Android camera preview look strange
UPDATE
My camera preview size is fine now i use the on Layout method the idea was i have the bigger layout then my ImageView and now camera preview is looking good.
Now the Problem I am facing is set the image of proper size for this I have to center crop and scale in same size in like my ImageView.this Image i get by TakePicture method and saved in sdcard.
For this I am using this method:-
public Bitmap scaleCenterCrop(Bitmap source, int newHeight, int newWidth) {
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
// Compute the scaling factors to fit the new height and width, respectively.
// To cover the final image, the final scaling will be the bigger
// of these two.
float xScale = (float) newWidth / sourceWidth;
float yScale = (float) newHeight / sourceHeight;
float scale = Math.max(xScale, yScale);
// Now get the size of the source bitmap when scaled
float scaledWidth = scale * sourceWidth;
float scaledHeight = scale * sourceHeight;
// Let's find out the upper left coordinates if the scaled bitmap
// should be centered in the new size give by the parameters
float left = (newWidth - scaledWidth) / 2;
float top = (newHeight - scaledHeight) / 2;
// The target rectangle for the new, scaled version of the source bitmap will now
// be
RectF targetRect = new RectF(left+50, top, left + scaledWidth, top + scaledHeight+50);
// RectF targetRect = new RectF(0, 0, newWidth, newHeight/2);
// Finally, we create a new bitmap of the specified size and draw our new,
// scaled bitmap onto it.
Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, source.getConfig());
Canvas canvas = new Canvas(dest);
canvas.drawBitmap(source, null, targetRect, null);
return dest;
}
But the result image quality is not good.Height Corners are cutting from top and bottom, and result image quality is not good.Pixels are stretching.
Don't tell me to use scaleType=Center_crop i can't use it in my case,and don't want to show cropping frame to user,this all process should not show on UI.
UPDATE
I used blow method for crop image from center and scale according to my imageView size
Bitmap dstBmp = ThumbnailUtils.extractThumbnail(source, newWidth, newHeight);
But the bitmap i got is not looking same the camera preview shown on FrameLayout. because camera preview is big.I think these code cropped the large area.
I tried to reduce the width and change the height but not getting the same cropped image in which ratio i want.
One more idea i have after picture crop a last image frame set automatically on FrameLayout. can we get that set frame from Frame Layout. How is this possible?
Here is question like this How to retrieve the visible part of a SurfaceView in Android do any one have solution.
I want to achieve this by this line ThumbnailUtils.extractThumbnail(source, newWidth, newHeight);and by this line i am getting src like image described in diagram .
What to change in this line exactly ????
Center crop an image may be help you this.
public Bitmap scaleCenterCrop(Bitmap source, int newHeight, int newWidth) {
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
// Compute the scaling factors to fit the new height and width, respectively.
// To cover the final image, the final scaling will be the bigger
// of these two.
float xScale = (float) newWidth / sourceWidth;
float yScale = (float) newHeight / sourceHeight;
float scale = Math.max(xScale, yScale);
// Now get the size of the source bitmap when scaled
float scaledWidth = scale * sourceWidth;
float scaledHeight = scale * sourceHeight;
// Let's find out the upper left coordinates if the scaled bitmap
// should be centered in the new size give by the parameters
float left = (newWidth - scaledWidth) / 2;
float top = (newHeight - scaledHeight) / 2;
// The target rectangle for the new, scaled version of the source bitmap will now
// be
RectF targetRect = new RectF(left, top, left + scaledWidth, top + scaledHeight);
// Finally, we create a new bitmap of the specified size and draw our new,
// scaled bitmap onto it.
Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, source.getConfig());
Canvas canvas = new Canvas(dest);
canvas.drawBitmap(source, null, targetRect, null);
return dest;
}
#Akanksha Please use this below code, you just need to pass the path of the saved image, and the hight and width of our imageview. This code works perfectly for me.
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class ImageHandler {
/**
* Decode and sample down a bitmap from a file to the requested width and
* height.
*
* #param filename
* The full path of the file to decode
* #param reqWidth
* The requested width of the resulting bitmap
* #param reqHeight
* The requested height of the resulting bitmap
* #return A bitmap sampled down from the original with the same aspect
* ratio and dimensions that are equal to or greater than the
* requested width and height
*/
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filename, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filename, options);
}
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) {
if (width > height) {
inSampleSize = Math.round((float) height / (float) reqHeight);
} else {
inSampleSize = Math.round((float) width / (float) reqWidth);
}
// This offers some additional logic in case the image has a
// strange
// aspect ratio. For example, a panorama may have a much larger
// width than height. In these cases the total pixels might
// still
// end up being too large to fit comfortably in memory, so we
// should
// be more aggressive with sample down the image (=larger
// inSampleSize).
final float totalPixels = width * height;
// Anything more than 2x the requested pixels we'll sample down
// further.
final float totalReqPixelsCap = reqWidth * reqHeight * 2;
while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
inSampleSize++;
}
}
return inSampleSize;
}
}
I call this method inside async task because it may take too much UImemory and time
Here is how I call it.
class Asyncing extends AsyncTask {
private int reqWidth;
private int reqHeight;
private ImageView iv;
private String fileName;
private ProgressDialog pd;
public Asyncing(int reqWidth, int reqHeight, ImageView iv,
String fileName) {
super();
this.reqWidth = reqWidth;
this.reqHeight = reqHeight;
this.fileName = fileName;
this.iv = iv;
}
#Override
protected Bitmap doInBackground(String... params) {
return ImageHandler.decodeSampledBitmapFromFile(params[0],
reqWidth, reqHeight);
}
#Override
protected void onPostExecute(Bitmap result) {
iv.setImageBitmap(result);
if (pd.isShowing()) {
pd.setMessage(getString(R.string.completed));
pd.dismiss();
}
super.onPostExecute(result);
}
#Override
protected void onProgressUpdate(Void... values) {
super.onProgressUpdate(values);
}
#Override
protected void onPreExecute() {
pd = ProgressDialog.show(CustomerDetailsActivity.this, "",
getString(R.string.processing_signature));
super.onPreExecute();
}
}
This is how you need to call the asynctask
signedImagePath = data.getExtras().getString("imagePath");
new Asyncing(signature_img.getWidth(), signature_img.getHeight(),
signature_img, "spenTest.png").execute(signedImagePath);
above code is written according to my requirements,you modify it according to yours.

Android Image Resize basic

I like to resize an bitmap,if it is big make it small and place it on an specific location in the surface view, I need to get device width and height and then bitmap size and place them in an surface view and then take another image resize it and place it in any location has i like.
How to know the location co ordinates first(to place second image - this should be device independent if larger/smaller screen layout should not change)
and how to scale an big bitmap and set as background so i can draw an image over it.
my code :
final int canvasWidth = getWidth();
final int canvasHeight = getHeight();
int imageWidth = img.getWidth();
int imageHeight = img.getHeight();
float scaleFactor = Math.min( (float)canvasWidth / imageWidth,
(float)canvasHeight / imageHeight );
Bitmap scaled = Bitmap.createScaledBitmap( img,
(int)(scaleFactor * imageWidth),
(int)(scaleFactor * imageHeight),
true );
canvas.drawColor(Color.BLACK);
canvas.drawBitmap(scaled, 10, 10, null);
this scale the big image but it doesn't fit the whole screen "img - bitmap is like an background image "
Someone can help me to understand the resize basics (I'm new so finding hard to understand resizing) to resize image to fit screen and resize any image to smaller one and place it in any location i like .
Store your bitmap as a source for manipulation and display it using ImageView:
Bitmap realImage = BitmapFactory.decodeFile(filePathFromActivity.toString());
Bitmap newBitmap = scaleDown(realImage, MAX_IMAGE_SIZE, true);
imageView.setImageBitmap(newBitmap);
//scale down method
public static Bitmap scaleDown(Bitmap realImage, float maxImageSize,
boolean filter) {
float ratio = Math.min(
(float) maxImageSize / realImage.getWidth(),
(float) maxImageSize / realImage.getHeight());
int width = Math.round((float) ratio * realImage.getWidth());
int height = Math.round((float) ratio * realImage.getHeight());
Bitmap newBitmap = Bitmap.createScaledBitmap(realImage, width,
height, filter);
return newBitmap;
}
And set your ImageView's width and height to "fill_parent".

Categories

Resources