如何正确地降采样图片?

16

背景

我正在创建一个包含大量高质量图片的应用程序,因此我决定将图片缩小到所需的大小(也就是说,如果图像比屏幕大,我会将其缩小)。

问题

我注意到在某些设备上,如果图片被缩小,它们会变得模糊/有锯齿,然而在同一设备上,对于相同的目标imageView大小,如果图片没有被缩小,它们看起来就很好。

尝试过的方法

我决定进一步检查这个问题,并创建了一个小型POC应用程序来展示这个问题。

在展示代码之前,这里有一个演示你所讲的:

enter image description here

虽然很难看出差异,但你可以看到第二张图片有点失真。这种情况可以在任何图像上显示。

public class MainActivity extends Activity
  {
  @Override
  protected void onCreate(final Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final ImageView originalImageView=(ImageView)findViewById(R.id.originalImageView);
    final ImageView halvedImageView=(ImageView)findViewById(R.id.halvedImageView);
    final ImageView halvedBitmapImageView=(ImageView)findViewById(R.id.halvedBitmapImageView);
    //
    final Bitmap originalBitmap=BitmapFactory.decodeResource(getResources(),R.drawable.test);
    originalImageView.setImageBitmap(originalBitmap);
    halvedImageView.setImageBitmap(originalBitmap);
    //
    final LayoutParams layoutParams=halvedImageView.getLayoutParams();
    layoutParams.width=originalBitmap.getWidth()/2;
    layoutParams.height=originalBitmap.getHeight()/2;
    halvedImageView.setLayoutParams(layoutParams);
    //
    final Options options=new Options();
    options.inSampleSize=2;
    // options.inDither=true; //didn't help
    // options.inPreferQualityOverSpeed=true; //didn't help
    final Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.test,options);
    halvedBitmapImageView.setImageBitmap(bitmap);
    }
  }

XML:

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  android:layout_height="match_parent" tools:context=".MainActivity"
  android:fillViewport="true">
  <HorizontalScrollView android:layout_width="match_parent"
    android:fillViewport="true" android:layout_height="match_parent">
    <LinearLayout android:layout_width="match_parent"
      android:layout_height="match_parent" android:orientation="vertical">


      <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content" android:text="original" />

      <ImageView android:layout_width="wrap_content"
        android:id="@+id/originalImageView" android:layout_height="wrap_content" />

      <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content" android:text="original , imageView size is halved" />

      <ImageView android:layout_width="wrap_content"
        android:id="@+id/halvedImageView" android:layout_height="wrap_content" />

      <TextView android:layout_width="wrap_content"
        android:layout_height="wrap_content" android:text="bitmap size is halved" />

      <ImageView android:layout_width="wrap_content"
        android:id="@+id/halvedBitmapImageView" android:layout_height="wrap_content" />

    </LinearLayout>
  </HorizontalScrollView>
</ScrollView>

问题

为什么会出现这种情况?

两种方法应该有相同的结果,因为它们都从同一来源采样,使用相同的因素。

我已经尝试过调整降采样方法,但没有任何帮助。

使用inDensity(而不是inSampleSize)似乎可以解决这个问题,但我不确定应该设置什么。我认为对于外部图像(例如来自互联网的图像),我可以将其设置为屏幕密度乘以所需使用的采样大小。

但这真的是一个好的解决方案吗?如果图像在资源文件夹中怎么办(我不认为有一个函数可以获取位图位于哪个密度文件夹中)?为什么使用推荐的方式(在这里讨论)效果不好?


编辑:我发现了一个技巧,可以获取从资源中获取的可绘制对象使用的密度(链接在此处)。然而,它并不具备未来的可扩展性,因为您需要特定的密度才能检测。

3个回答

15

好的,我找到了一个不错的替代方案,我认为它适用于任何类型的位图解码。

不仅如此,它还允许您使用任何样本大小进行缩小,而不仅仅是2的幂。如果您付出更多的努力,甚至可以使用分数而不是整数进行缩小。

下面的代码适用于来自res文件夹的图像,但可以轻松地针对任何类型的位图解码进行操作:

private Bitmap downscaleBitmapUsingDensities(final int sampleSize,final int imageResId)
  {
  final Options bitmapOptions=new Options();
  bitmapOptions.inDensity=sampleSize;
  bitmapOptions.inTargetDensity=1;
  final Bitmap scaledBitmap=BitmapFactory.decodeResource(getResources(),imageResId,bitmapOptions);
  scaledBitmap.setDensity(Bitmap.DENSITY_NONE);
  return scaledBitmap;
  }

我已经测试过了,它可以很好地显示降采样后的图像。在下面的图片中,我展示了原始图像,并使用inSampleSize方法和我的方法对图像进行了缩小。
很难看出差异,但是使用密度的那个不仅跳过像素,而且使用所有像素来考虑。它可能会慢一些,但更精确,并使用更好的插值。

enter image description here

与使用inSampleSize相比,唯一的劣势似乎是速度较慢,因为inSampleSize跳过像素,并且densities方法对跳过的像素进行额外计算。

然而,我认为Android以某种方式在大约相同的速度下运行两种方法。

我认为这两种方法的比较类似于最近邻下采样双线性插值下采样之间的比较。

编辑:我发现这里展示的方法有一个缺点,与Google的方法相比,处理过程中使用的内存可能会相当高,这取决于图像本身。这意味着您应该仅在您认为有意义的情况下使用它。


编辑:我已经为那些希望解决内存问题的人做了一个合并的解决方案(谷歌的解决方案和我的解决方案)。它不是完美的,但比我之前做的更好,因为在缩小采样时不会使用与原始位图需要的内存一样多的内存,而是使用谷歌解决方案中使用的内存。

以下是代码:

    // as much as possible, use google's way to downsample:
    bitmapOptions.inSampleSize = 1;
    bitmapOptions.inDensity = 1;
    bitmapOptions.inTargetDensity = 1;
    while (bitmapOptions.inSampleSize * 2 <= inSampleSize)
        bitmapOptions.inSampleSize *= 2;

    // if google's way to downsample isn't enough, do some more :
    if (bitmapOptions.inSampleSize != inSampleSize) 
      {
      // downsample by bitmapOptions.inSampleSize/originalSampleSize .
      bitmapOptions.inTargetDensity = bitmapOptions.inSampleSize;
      bitmapOptions.inDensity = inSampleSize;
      } 
    else if(sampleSize==1)
      {
      bitmapOptions.inTargetDensity=preferHeight ? reqHeight : reqWidth;
      bitmapOptions.inDensity=preferHeight ? height : width;
      }

所以,简而言之,两种方法的优缺点如下: 谷歌的方法(使用inSampleSize)在解码过程中使用更少的内存,速度更快。但是,有时会导致一些图形伪影,并且仅支持向下采样到2的幂次方,因此结果位图可能比您想要的更大(例如x1/4的大小而不是x1/7)。 我的方法(使用密度)更精确,提供更高质量的图像,并且在结果位图上使用更少的内存。但是,在解码过程中可能会使用大量内存(取决于输入),并且速度较慢。

编辑:另一个改进是,我发现在某些情况下输出图像与所需的大小限制不匹配,并且您不希望使用Google的方法进行过多的降采样:

    final int newWidth = width / bitmapOptions.inSampleSize, newHeight = height / bitmapOptions.inSampleSize;
    if (newWidth > reqWidth || newHeight > reqHeight) {
        if (newWidth * reqHeight > newHeight * reqWidth) {
            // prefer width, as the width ratio is larger
            bitmapOptions.inTargetDensity = reqWidth;
            bitmapOptions.inDensity = newWidth;
        } else {
            // prefer height
            bitmapOptions.inTargetDensity = reqHeight;
            bitmapOptions.inDensity = newHeight;
        }
    }

比如说,将2448x3264的图像降采样至1200x1200,那么它将变成900x1200。


图片尺寸大于2xxx、3xxx时失败。 - Farhan
实际上,通常我使用 opts.inSampleSize = sample。所以这一行(opts.inDensity = sample)真的引起了我的注意,你的代码确实可行,但当我使用高分辨率的图像时,它就无法正确解码。没有异常,但也没有图片。如果你说的话,我可以重新测试并粘贴更详细的结果... - Farhan
@Farhan 奇怪。我本来期望会有些显示...这个图片是从互联网下载到设备上的,还是存在资源文件夹中?如果是的话,在哪个文件夹里并且你可以上传它以便我查看吗? - android developer
@Farhan 我已经更新了我的答案。如果你有一张巨大的图片,在缩小过程中会占用很多内存。但是,一旦处理完成,你将得到精确的预期大小和预期内存使用量。 - android developer
1
@PaulW 是的,图像解码通常有两个阶段:获取有关其信息(分辨率等)以及解码为可以实际显示的实际位图。宽度和高度(从第一步中的bitmapOptions中获取)是原始位图的大小,如果您根本没有对其进行下采样。如果您不进行第一步操作,则最终得到与原始大小相同的结果,这比不将其下采样为较小分辨率要占用更多内存。 - android developer
显示剩余3条评论

2

您应该使用inSampleSize。要确定应该使用的采样大小,请执行以下操作。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap map = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int originalHeight = options.outHeight;
int originalWidth = options.outWidth;
// Calculate your sampleSize based on the requiredWidth and originalWidth
// For e.g you want the width to stay consistent at 500dp
int requiredWidth = 500 * getResources().getDisplayMetrics().density;
int sampleSize = originalWidth / requiredWidth;
// If the original image is smaller than required, don't sample
if(sampleSize < 1) { sampleSize = 1; }
options.inSampleSize = sampleSize;
options.inPurgeable = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);

希望这能帮到你。

正如我在问题中已经写过的(包括代码展示),我使用了inSampleSize,但与imageView的普通调整大小相比,它会导致像素化问题。问题是为什么,是否有解决方法。 - android developer
您所发布的图片无法加载,无法显示差异。 - Aswin Rajendiran
我不知道为什么你看不到这张图片。也许是你屏蔽了它?你尝试过其他浏览器吗?或者在你的手机上试试? - android developer
我现在明白了。当然,如果imageView与图像一样缩小一半,图像看起来会更好。如果你将图像和imageView都缩小一半,那么它将看起来与原始图像相同。原始图像与最后一个图像一样像素化。如果您想要质量好的话,就使用较小的inSampleSize,可以通过增加requiredWidth来实现。将requiredWidth增加20%以上,图像将看起来更好。让我知道进展如何。 - Aswin Rajendiran
inSampleSize不能小于2。实际上,我认为它必须是2的幂(即2、4、8、16等)。这并不是一件显而易见的事情,因为较小的imageView和在底部编写的密度技术都可以使图像看起来更好。请阅读整个问题并考虑它。 - android developer
inSampleSize 可以是 1,事实上这是默认值。而且 1 是 2 的零次幂。 - Lorne Laliberte

0

对我来说,只有使用inSampleSize进行缩小才能表现良好(但不像最近邻算法那样)。但不幸的是,这并不能获得我们需要的精确分辨率(只是比原始分辨率小整数倍)。

因此,我发现SonyMobile的解决方案在这种任务中效果最好。

简而言之,它包括两个步骤:

  1. 使用BitmapFactory.Options :: inSampleSize-> BitmapFactory.decodeResource()尽可能接近所需的分辨率进行缩小,但不少于它
  2. 通过使用Canvas :: drawBitmap()稍微缩小一点来达到精确分辨率

这里是SonyMobile如何解决此任务的详细说明:http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/

这里是SonyMobile比例工具的源代码:http://developer.sonymobile.com/downloads/code-example-module/image-scaling-code-example-for-android/


1
他们只是在缩放解码后得到的位图。这被称为下采样,而不是降采样。这实际上也是我在JNI中使用的方法,在我的库中:https://github.com/AndroidDeveloperLB/AndroidJniBitmapOperations 。无论如何,问题是关于降采样的,意思是你直接从inputStream解码位图到所需大小的位图。不幸的是,我认为我找到的解决方案(使用“inTargetDensity”)也在做同样的事情 - 同时使用2个位图,尽管对于使用它的人来说是隐藏的。 - android developer
是的,你说得对=) 但是当我尝试设置:bitmapOptions.inDensity=sampleSize; 和 bitmapOptions.inTargetDensity=1; 时,看起来Android使用了最近邻下采样算法:( - goRGon
我认为它在这种情况下同时使用了两种方法-首先是最近邻居,然后是另一种方法。顺便说一句,我之所以这样称呼它,是因为我看到的输出图像。如果目标图像大小不是2的幂(例如,缩小5倍),则应该同时使用两种方法。也许你没有注意到?尝试仅使用我的原始方法(当inSampleSize==1时),然后尝试我后来建议的合并方法,看看是否有明显的差异。无论如何,合并方法尝试享受两个世界的优点。 - android developer
但这意味着在Android中没有任何方法可以缩放/降低一个分辨率的图像/位图,而不使用最近邻算法吗?这很奇怪——当他们绘制ImageView时,它的缩小/取样程度到任何分辨率都不错…… - goRGon
谢谢,那太好了!但仍觉得Android没有本地良好的缩放(不是最近邻)和舒适使用的位图工具是一个大遗憾。 - goRGon
显示剩余2条评论

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