Android:使用比双线性更好的重采样算法(如Lanczos3)调整位图大小

25

有没有任何方法或外部库可以在 Android 下使用 Lanczos(最好)或至少是双立方算法来调整图像大小?(当然,更快的处理速度更好,但质量是首要考虑因素,处理时间是次要的)

到目前为止,我所拥有的一切就是这些:

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

然而,它使用双线性过滤器,输出质量很差。特别是如果您想要保留细节(如细线或可读文本)。

有许多优秀的Java库,例如在此处讨论的: Java - 调整图像大小而不失去质量

但是它总是依赖于Java awt类,例如java.awt.image.BufferedImage,因此无法在Android中使用。

是否有一种方法可以更改Bitmap.createScaledBitmap()方法中的默认(双线性)滤波器或类似于 Morten Nobel的库的库,该库能够与android.graphics.Bitmap类(或某些原始表示形式,正如@Tron在评论中所指出的)一起使用?


1
拥有一个独立的解决方案,基于原始格式(整数数组 - 像素数组)将是非常棒的。 - Quark
也许可以尝试使用RenderScript。它可以高效地操作位图数据。不过文档有点匮乏。 - David Medenjak
你的使用场景是什么?是为了缩放视图还是进行后期处理照片?通常情况下,你希望在移动设备上(如Android)获得最快速、最节省内存的缩放解决方案,质量通常只是次要考虑因素。尽管如此,我建议采用一种渐进式缩放变体,它a) 可以轻松地实现甚至适用于Android b) 具有高度的内存效率和速度。稍后我会发布一个答案。 - Patrick
1
@for3st 主要是用来将各种图表和截图缩小(这些图片上的文本应该是可读的,细线条也应该被保留)。渐进式缩放确实是一个很大的改进,但 Lanczos 输出仍然更好。处理时间并不重要(太多)。 - Jolinar
1
你有没有任何文件证明“createScaledBitmap”使用双线性缩放?我需要它来证明使用该方法的合理性。 - SamuelPS
@SamuelPS https://dev59.com/SHE85IYBdhLWcg3wMQlm#2895140 Bitmap 使用 Paint: https://developer.android.com/reference/android/graphics/Paint (查找 FILTER_BITMAP_FLAG - 启用缩放位图的双线性采样的 Paint 标志。 - Jolinar
5个回答

7
我认为最有前途的方法是使用libswscale(来自FFmpeg),它提供了Lanczos和许多其他滤镜。要从本地代码访问Bitmap缓冲区,您可以使用jnigraphics。这种方法保证了良好的性能和可靠的结果。
编辑 在这里您可以找到粗略的演示应用程序,使用了所提出的方法。目前性能令人沮丧,因此应该进行调查以决定是否需要采取措施改进它。

1
我决定颁发这个答案,因为我喜欢NDK方法来完成这种任务。在Android中,Java堆大小受限,并且即使是ART,Android虚拟机的性能与PC上的Sun/Oracle VM或本地代码相比仍然很差。此外,libswscale使用 uint8_t *src_data[4], *dst_data[4]; 来进行数据处理,因此可以使用 Bitmap.getPixels()+.setPixels()(在Android中)和java.awt.image.PixelGrabber(在awt环境中)轻松实现。这是一个悖论,但在Java环境中,最好的“平台无关”解决方案是在这里用C编写的。 - Quark
1
好的,我会在几天内提供完整的代码。关于FFmpeg:肯定有构建它用于Android的指南,或者至少已经构建好的二进制文件(如libavutil.so、libswscale.so等)。 - Sergio
@Tron 通用解决方案,既适用于AWT又适用于Android,将是非常好的,但不幸的是我完全不熟悉AWT,我认为直接从本地代码中直接操作Bitmap缓冲区比通过setPixels()复制/转换更有效,并且涉及较少的JNI特定代码。因此,它应该更容易阅读和理解。无论如何,其他人可能能够适应AWT的代码,甚至可以制作统一的代码。 - Sergio
@Serhio,我很满意你的回答。如果你能制作一个安卓版本,那将是一个巨大的加分项。安卓内置的缩放速度很快,但对于某些类型的图像来说,效果真的很丑陋。我认为很多人会感激这一点。 - Quark
@Jolinar 不这么认为 :) 更好的说法是没有足够的空闲时间... - Sergio
显示剩余2条评论

5

很不幸,Android使用android.graphics.Bitmap,而在Java中不存在该类,而Java使用java.awt.image.BufferedImage,而在Android中不存在 :-(

我没有一个可以直接使用的Android库,但我有一条路径,可以将一个特定于java-awt的库移植到平台无关的java库,并为Android和awt/j2se实现平台特定的处理程序。

在Java重缩放库中,您必须将所有特定于java-awt的类(如BufferedImage)隐藏在接口IBitmap后面,并为j2se和Android分别实现该接口。

我已经成功地完成了exif / icc / ipc元数据处理,并实现了接口pixymeta-lib/.../IBitmap.java,并为j2se pixymeta-j2se-lib/.../j2se/BitmapNative.java和android pixymeta-android-lib/.../android/BitmapNative.java提供了实现。
所以我有这些软件包。
  • pixymeta-lib
    • 将所有AWT引用替换为IBitmap接口的平台无关库
  • pixymeta-j2se-lib
    • IBitmap的AWT/J2SE实现
  • pixymeta-android-lib
    • IBitmap的Android实现

0

我最近写了一个程序将图像缩放/裁剪到特定分辨率并进行压缩处理:

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;
}

它没有使用你提到的库,但它可以工作,我很满意。也许你也会喜欢。


谢谢,但它似乎使用与Bitmap.createScaledBitmap相同的内部机制...(双线性重采样,只是过程不同)。 - Jolinar
2
如何在Android上使用双三次插值绘制和缩放位图?使用双三次插值将图像缩小,使用双线性插值将其放大。 - Pwnstar
插值FILTER_BITMAP_FLAG为false时使用最近邻,为true时使用双线性。你怎么知道它在降采样时使用双三次插值? - Jolinar

-1

这里是我用来调整图像大小的代码...

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();

希望能对你有所帮助。。

-1

如果您只是想以优化显示为目的重采样图像,您可以使用这个小巧玲珑的一行代码,它为我服务良好。

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

这行代码可能看起来很奇怪,因为你正在将位图转换为BitmapDrawable,然后再次转换回位图,但是BitmapDrawable默认为设备的像素密度(除非您使用不同的构造函数)。

如果您还需要调整大小,则只需将其分成两行,并在将BitmapDrawable转换回位图之前使用setBounds,如下所示:

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 可能被列为已弃用,但实际上只有某些构造函数被弃用了,而上面示例中的构造函数并没有被弃用。此外,这将与 API 4 兼容。

或者,Android 文档中有一个可下载的示例,您可以在此处找到:https://developer.android.com/topic/performance/graphics/load-bitmap.html

希望这可以帮助您。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接