Android: Bitmap resizing using better resampling algorithm than bilinear (like Lanczos3)

8k views Asked by At

Is there any way or external library that can resize image using Lanczos (ideally) or at least bicubic alg. under Android? (faster is better of course, but quality is priority, a processing time is secondary)

Everything what I've got so far is this:

Bitmap resized = Bitmap.createScaledBitmap(yourBitmap, newWidth, newHeight, true);

However it uses bilinear filter and the output quality is terrible. Especially if you want to preserve details (like thin lines or readable texts).

There are many good libraries for Java as discussed for example here: Java - resize image without losing quality

However it's always depended on Java awt classes like java.awt.image.BufferedImage, so it can't be used in Android.

Is there a way how to change the default (bilinear) filter in Bitmap.createScaledBitmap() method or some library like Morten Nobel's lib that is able to work with android.graphics.Bitmap class (or with some raw representation, as @Tron in the comment has pointed out)?

5

There are 5 answers

7
Sergio On BEST ANSWER

The most promising IMO is to use libswscale (from FFmpeg), it offers Lanczos and many other filters. To access Bitmap buffer from native code you can use jnigraphics. This approach guarantees nice performance and reliable results.

EDIT

Here you can find rough demo app, that uses proposed approach. At the moment performance is frustratingly bad, so it should be investigated to decide if we to do something to improve it.

0
user2288580 On

If you just want to resample the image in a way that is optimized for display purposes you can use this nifty little one liner that has served me well.

Bitmap bitmap = new BitmapDrawable(getResources(), yourBitmap).getBitmap();

This line of code may seem strange because you're converting a bitmap to a BitmapDrawable and back to a bitmap again but a BitmapDrawable defaults to the device's pixel density (unless you use a different constructor).

If you need to resize also then just separate it into two lines and use setBounds before converting the BitmapDrawable back to a bitmap like so:

BitmapDrawable bitmapDrawable = new BitmapDrawable(getResources(), yourBitmap);
bitmapDrawable.setBounds(left, top, right, bottom); //Make it a new size in pixels.
yourBitmap = bitmapDrawable.getBitmap(); //Convert it back to a bitmap optimised for display purposes.

Bitmap drawable may be listed as depricated but it's not, only certain constructors are depricated and the constructor in this above example is not depricated. Also this will work with API 4

Alternativly the android docs have a downloadable sample for this here: https://developer.android.com/topic/performance/graphics/load-bitmap.html

Hope this helps.

3
Pwnstar On

I recently wrote this to scale/crop an image to a specific resolution and compress it with quality:

public static void scaleImageToResolution(Context context, File image, int dstWidth, int dstHeight) {
    if (dstHeight > 0 && dstWidth > 0 && image != null) {

        Bitmap result = null;
        try {
            //Get Image Properties
            BitmapFactory.Options bmOptions = new BitmapFactory.Options();
            bmOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(image.getAbsolutePath(), bmOptions);
            int photoH = bmOptions.outHeight;
            int photoW = bmOptions.outWidth;

            bmOptions.inJustDecodeBounds = false;
            bmOptions.inPurgeable = true;
            //Smaller Image Size in Memory with Config
            bmOptions.inPreferredConfig = Bitmap.Config.RGB_565;

            //Is resolution not the same like 16:9 == 4:3 then crop otherwise fit
            ScalingLogic scalingLogic = getScalingLogic(photoW, photoH,dstWidth, dstHeight);
            //Get Maximum automatic downscaling that it's still bigger then this requested resolution
            bmOptions.inSampleSize = calculateScalingSampleSize(photoW, photoH, dstWidth, dstHeight, scalingLogic);

            //Get unscaled Bitmap
            result = BitmapFactory.decodeFile(image.getAbsolutePath(), bmOptions);

            //Scale Bitmap to requested Resolution
            result = scaleImageToResolution(context, result, scalingLogic);

            if (result != null) {
                //Save Bitmap with quality
                saveImageWithQuality(context, result, image);
            }
        } finally {
            //Clear Memory
            if (result != null)
                result.recycle();
        }
    }
}


public static void saveImageWithQuality(Bitmap bitmap, String path, int compressQuality) {
    try {
        FileOutputStream fOut;
        fOut = new FileOutputStream(path);
        bitmap.compress(Bitmap.CompressFormat.JPEG, compressQuality, fOut);
        fOut.flush();
        fOut.close();
    } catch (IOException ex) {
        if (Logger.getRootLogger() != null)
            Logger.getRootLogger().error(ex);
        else
            Log.e("saveImageWithQuality", "Error while saving compressed Picture: " + ex.getMessage() + StringUtils.newLine() + ex.getStackTrace().toString());
    }
}

public static void saveImageWithQuality(Context context, Bitmap bitmap, File file) {
    saveImageWithQuality(bitmap, file.getAbsolutePath(), getCompressQuality());
}

public static void saveImageWithQuality(Context context, Bitmap bitmap, String path) {
    saveImageWithQuality(bitmap, path, getCompressQuality());
}

private static int calculateScalingSampleSize(int srcWidth, int srcHeight, int dstWidth, int dstHeight, ScalingLogic scalingLogic) {
    if (scalingLogic == ScalingLogic.FIT) {
        final float srcAspect = (float) srcWidth / (float) srcHeight;
        final float dstAspect = (float) dstWidth / (float) dstHeight;

        if (srcAspect > dstAspect) {
            return srcWidth / dstWidth;
        } else {
            return srcHeight / dstHeight;
        }
    } else {
        final float srcAspect = (float) srcWidth / (float) srcHeight;
        final float dstAspect = (float) dstWidth / (float) dstHeight;

        if (srcAspect > dstAspect) {
            return srcHeight / dstHeight;
        } else {
            return srcWidth / dstWidth;
        }
    }
}

private static Bitmap scaleImageToResolution(Context context, Bitmap unscaledBitmap, ScalingLogic scalingLogic, int dstWidth, int dstHeight) {
    //Do Rectangle of original picture when crop
    Rect srcRect = calculateSrcRect(unscaledBitmap.getWidth(), unscaledBitmap.getHeight(), dstWidth, dstHeight, scalingLogic);
    //Do Rectangle to fit in the source rectangle
    Rect dstRect = calculateDstRect(unscaledBitmap.getWidth(), unscaledBitmap.getHeight(), dstWidth, dstHeight, scalingLogic);
    //insert source rectangle into new one
    Bitmap scaledBitmap = Bitmap.createBitmap(dstRect.width(), dstRect.height(), Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(scaledBitmap);
    canvas.drawBitmap(unscaledBitmap, srcRect, dstRect, new Paint(Paint.FILTER_BITMAP_FLAG));
    //Recycle the unscaled Bitmap afterwards
    unscaledBitmap.recycle();

    return scaledBitmap;
}

private static Rect calculateSrcRect(int srcWidth, int srcHeight, int dstWidth, int dstHeight, ScalingLogic scalingLogic) {
    if (scalingLogic == ScalingLogic.CROP) {
        if (srcWidth >= srcHeight) {
            //Horizontal
            final float srcAspect = (float) srcWidth / (float) srcHeight;
            final float dstAspect = (float) dstWidth / (float) dstHeight;

            if (srcAspect < dstAspect || isResolutionEqual(srcAspect, dstAspect)) {
                final int srcRectHeight = (int) (srcWidth / dstAspect);
                final int scrRectTop = (srcHeight - srcRectHeight) / 2;
                return new Rect(0, scrRectTop, srcWidth, scrRectTop + srcRectHeight);
            } else {
                final int srcRectWidth = (int) (srcHeight * dstAspect);
                final int srcRectLeft = (srcWidth - srcRectWidth) / 2;
                return new Rect(srcRectLeft, 0, srcRectLeft + srcRectWidth, srcHeight);
            }
        } else {
            //Vertikal
            final float srcAspect = (float) srcHeight / (float) srcWidth;
            final float dstAspect = (float) dstWidth / (float) dstHeight;

            if (srcAspect < dstAspect || isResolutionEqual(srcAspect, dstAspect)) {
                final int srcRectWidth = (int) (srcHeight / dstAspect);
                final int srcRectLeft = (srcWidth - srcRectWidth) / 2;
                return new Rect(srcRectLeft, 0, srcRectLeft + srcRectWidth, srcHeight);
            } else {
                final int srcRectHeight = (int) (srcWidth * dstAspect);
                final int scrRectTop = (srcHeight - srcRectHeight) / 2;
                return new Rect(0, scrRectTop, srcWidth, scrRectTop + srcRectHeight);
            }
        }
    } else {
        return new Rect(0, 0, srcWidth, srcHeight);
    }
}

private static Rect calculateDstRect(int srcWidth, int srcHeight, int dstWidth, int dstHeight, ScalingLogic scalingLogic) {
    if (scalingLogic == ScalingLogic.FIT) {
        if (srcWidth > srcHeight) {
            //Vertikal
            final float srcAspect = (float) srcWidth / (float) srcHeight;
            final float dstAspect = (float) dstWidth / (float) dstHeight;

            if (srcAspect < dstAspect || isResolutionEqual(srcAspect, dstAspect)) {
                return new Rect(0, 0, (int) (dstHeight * srcAspect), dstHeight);
            } else {
                return new Rect(0, 0, dstWidth, (int) (dstWidth / srcAspect));
            }
        } else {
            //Horizontal
            final float srcAspect = (float) srcHeight / (float) srcWidth;
            final float dstAspect = (float) dstWidth / (float) dstHeight;

            if (srcAspect < dstAspect || isResolutionEqual(srcAspect, dstAspect)) {
                return new Rect(0, 0, (int) (dstHeight / srcAspect), dstHeight);
            } else {
                return new Rect(0, 0, dstWidth, (int) (dstWidth * srcAspect));
            }
        }
    } else {
        if (srcWidth >= srcHeight)
            return new Rect(0, 0, dstWidth, dstHeight);
        else
            return new Rect(0, 0, dstHeight, dstWidth);
    }
}

private static ScalingLogic getScalingLogic(int imageWidth, int imageHeight, int dstResolutionWidth, int dstResolutionHeight) {
    if (imageWidth >= imageHeight) {
        //Bild horizontal
        final float srcAspect = (float) imageWidth / (float) imageHeight;
        final float dstAspect = (float) dstResolutionWidth / (float) dstResolutionHeight;
        if (!isResolutionEqual(srcAspect, dstAspect)) {
            return ScalingLogic.CROP;
        } else {
            return ScalingLogic.FIT;
        }
    } else {
        //Bild vertikal
        final float srcAspect = (float) imageHeight / (float) imageWidth;
        final float dstAspect = (float) dstResolutionWidth / (float) dstResolutionHeight;
        if (!isResolutionEqual(srcAspect, dstAspect)) {
            return ScalingLogic.CROP;
        } else {
            return ScalingLogic.FIT;
        }
    }
}

public enum PictureQuality {
    High,
    Medium,
    Low
}

public enum ScalingLogic {
    CROP,
    FIT
}

//Does resolution match
private static boolean isResolutionEqual(float v1, float v2) {
    // Falls a 1.999999999999 and b = 2.000000000000
    return v1 == v2 || Math.abs(v1 - v2) / Math.max(Math.abs(v1), Math.abs(v2)) < 0.01;
}

public int getCompressQuality() {
    if (Quality == PictureQuality.High)
        return 100;
    else if (Quality == PictureQuality.Medium)
        return 50;
    else if (Quality == PictureQuality.Low)
        return 25;
    else return 0;
}

it's not using the libraries you mentioned but it works and I am happy with it. Maybe you are, too.

0
k3b On

Unfortunately Android uses android.graphics.Bitmap which does not exist in java while java uses java.awt.image.BufferedImage that does not exist in android :-(

I donot have a ready to use library for android but a path how to port a java-awt specific lib to a platform independat java lib with platfrom specific handlers for android and awt/j2se

In the java rescale lib you have to hide all java-awt specific classes (like BufferedImage) behind an interface IBitmap and implement that interface for j2se and independantly for Android.

I have done this successfully for exif/icc/ipc metadata processing and implemented interface pixymeta-lib/.../IBitmap.java with implementation for j2se pixymeta-j2se-lib/.../j2se/BitmapNative.java and android pixymeta-android-lib/.../android/BitmapNative.java

So I have these packages

  • pixymeta-lib
    • transformed platform independant lib where all awt-references are replaced by IBitmap interface
  • pixymeta-j2se-lib
    • awt/j2se implementation of IBitmap
  • pixymeta-android-lib
    • android implementation of IBitmap
0
Preet_Android On

Here is code that I've used for resize the image..

Bitmap photo1 ;
private byte[] imageByteArray1 ;


BitmapFactory.Options opt1 = new BitmapFactory.Options();
opt1.inJustDecodeBounds=true;
BitmapFactory.decodeFile(imageUrl.get(imgCount).toString(),opt1);

// The new size we want to scale to
final int REQUIRED_SIZE=320;

// Find the correct scale value. It should be the power of 2.
int width_tmp=opt1.outWidth,height_tmp=opt1.outHeight;
int scale=2;
while(true){
    if(width_tmp>REQUIRED_SIZE||height_tmp>REQUIRED_SIZE)
        break;
    width_tmp/=2;
    height_tmp/=2;
    scale*=2;
}
// Decode with inSampleSize
BitmapFactory.Options o2=new BitmapFactory.Options();
o2.inSampleSize=scale;
o2.inJustDecodeBounds=false;
photo1=BitmapFactory.decodeFile(imageUrl.get(imgCount).toString(),o2);

ByteArrayOutputStream baos1=new ByteArrayOutputStream();
photo1.compress(Bitmap.CompressFormat.JPEG,60,baos1);
imageByteArray1=baos1.toByteArray();

Hope it will help you..