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

5

25

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)?

Almeida answered 11/6, 2016 at 11:36 Comment(6)
It would be great to have an independent solution on a raw format (array of integers - pixel array).In
Maybe try something with RenderSript. It lets you manipulate bitmap data with high performance. The documentation is somewhat scarce, thoughPastorate
What is your use case? Scaling for a view or e.g. post processing fotos? Usally you want the fastest most memory efficient solution for scaling on mobile devices like Android, quality is usually only second thought. Never the less Id recommend a progressive scaling variant which a) can be easily implemented even for android b) is highly memory efficient and fast. I will later post an answer on thatDensimeter
@for3st Mainly for various diagrams and screenshots downscaling (texts on these images should be readable and thin lines should be preserved). A progressive scaling is really a great improvement, but Lanczos output is still better. A processing time is not important (much).Almeida
Do you have any documentation pointing the fact that "createScaledBitmap" uses bilinear downscaling? I need it to justify the use of the methodTotalitarian
@Totalitarian #2895565 Bitmap is using Paint: developer.android.com/reference/android/graphics/Paint (look for FILTER_BITMAP_FLAG - Paint flag that enables bilinear sampling on scaled bitmaps.)Almeida
L
7

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.

Lawabiding answered 1/7, 2016 at 19:38 Comment(7)
I have decided to award this answer because I like the NDK approach for this type of task. Java heap size is limited in Android and the Android VM performance (even ART) is still pathetic in comparison with Sun/Oracle VM on PCs or with a native code. Furthermore, libswscale uses uint8_t *src_data[4], *dst_data[4]; for data processing, so the implementation will be easy using Bitmap.getPixels()+.setPixels() (in Android) and java.awt.image.PixelGrabber (in awt environment). It's paradox, but the best "platform independent" solution (for me) in Java environment is written in C here.In
Okay, I'll provide complete code in few days. About FFmpeg: there are surely should be guides for building it for Android, or at least already built binaries (libavutil.so, libswscale.so, so on)Lawabiding
@Tron Universal solution that will work both for AWT and Android would be really nice, but unfortunately I'm not familiar with AWT at all and IMO manipulating with Bitmap buffer directly from native code would be more effective than copying/translating via setPixels() and involves less JNI specific code. So it should be easier to read and understand. And anyway someone else may be able to adapt code for AWT or even make an unified code.Lawabiding
@Serhio I'm happy with your answer as it is. If you make an Android version, it will be a great bonus. The Android build-in scaling is fast, but it has really ugly results for some type of images. I think many people will appreciate that.In
@Almeida Don't think so :) Better to say that there is not enough of spare time...Lawabiding
@Almeida You can touch the demo. And I'm going to try to do something with performance.Lawabiding
It's great and the performance isn't so bad (ok, the Android default is faster, but it uses GPU acceleration and the quality is poor). ThanksAlmeida
S
5

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
Shantelleshantha answered 20/6, 2016 at 14:10 Comment(0)
T
0

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.

Titivate answered 20/6, 2016 at 13:30 Comment(3)
Thank you, but it seems it uses the same internal mechanisms as Bitmap.createScaledBitmap... (bilinear resampling, only a procedure is different).Almeida
#2518141 Bicubic for making an image smaller Billinear for making it biggerTitivate
The interpolation FILTER_BITMAP_FLAG false is nearest neighbor and true is bilinear. How do you know that it uses bicubic for downsampling?Almeida
C
-1

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..

Connally answered 2/7, 2016 at 6:31 Comment(0)
P
-1

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.

Preteritive answered 26/5, 2017 at 7:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.