【问题标题】:How to downsample images correctly?如何正确下采样图像?
【发布时间】:2013-04-30 18:49:43
【问题描述】:

背景

创建一个包含大量高质量图像的应用程序,我决定将图像缩小到所需的大小(这意味着如果图像大于屏幕,我会缩小它)。

问题

我注意到在某些设备上,如果图像被缩小,它们会变得模糊/像素化,但在相同的设备上,对于相同的目标 imageView 大小,如果图像没有缩小,它们看起来还不错。

我尝试过的

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

在向您展示代码之前,这里是我正在谈论的内容的演示:

有点难以看出区别,但你可以看到第二个有点像素化。这可以显示在任何图像上。

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)似乎可以修复它,但我不确定要为它设置什么。我认为对于外部图像(例如来自互联网),我可以将其设置为屏幕密度乘以我希望使用的样本大小。

但它甚至是一个好的解决方案吗?如果图像位于资源文件夹中,我该怎么办(我认为没有功能可以获取位图所在的密度文件夹)?为什么它在使用推荐的方式(谈到here)时效果不佳?


编辑:我发现了一个技巧,可以从资源 (link here) 中获取用于绘制的可绘制对象的密度。但是,这不是未来的证据,因为您需要特定于要检测的密度。

【问题讨论】:

    标签: android bitmap decode downsampling pixelate


    【解决方案1】:

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

    不仅如此,它还允许您使用您希望的任何样本大小进行缩减,而不仅仅是 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 方法缩小图像,并使用我的方法。

    很难看出区别,但是使用密度的那个实际上不只是跳过像素,而是使用所有像素来考虑。它可能会慢一些,但它更精确并且使用更好的插值。

    与使用 inSampleSize 相比,唯一的缺点似乎是速度,这在 inSampleSize 上更好,因为 inSampleSize 跳过了像素,并且因为 densities 方法对跳过的像素进行了额外的计算。

    但是,我认为不知何故,android 以大致相同的速度运行这两种方法。

    我认为这两种方法的比较类似于nearest-neighbor downsamplingbilinear-interpolation downsampling之间的比较。

    编辑:与谷歌的方法相比,我发现了我在这里展示的方法的一个缺点。在此过程中使用的内存可能非常高,我认为这取决于图像本身。这意味着你应该只在你认为有意义的情况下使用它。


    编辑:我为那些希望克服内存问题的人制作了一个合并的解决方案(谷歌的解决方案和我的解决方案)。它并不完美,但它比我以前做的要好,因为它不会在下采样期间使用原始位图所需的内存。相反,它将使用谷歌解决方案中使用的内存。

    代码如下:

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

    所以,简而言之,两种方法的优缺点:

    Google 的方式(使用 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;
            }
        }
    

    所以,比如从 2​​448x3264 的图像下采样到 1200x1200,就会变成 900x1200

    【讨论】:

    • 无法获取尺寸大于 2xxx,3xxx 的图像
    • @Farhan 你能描述一下失败吗?当您尝试这样做时究竟会发生什么?你得到 OOM 了吗?
    • 实际上,通常我使用 opts.inSampleSize = sample.. 这样 line(opts.inDensity = sample) 真的引起了我的注意,您的代码确实有效,但是当我使用高分辨率图像时,它只是没有正确解码。没有例外,但没有图像。如果你说,我可以重新测试并粘贴更详细的结果...
    • @Farhan 奇怪。我希望能显示一些东西...是从互联网下载到设备的图像,还是它驻留在资源文件夹中?如果是这样,在哪个文件夹中,你能以某种方式上传它以便我检查吗?
    • @PaulW 是的,图像解码通常需要两个阶段:获取有关它的信息(分辨率等...),然后解码为您可以实际显示的真实位图。如果您根本不进行下采样,宽度和高度(从第一步中的 bitmapOptions 获取)是原始位图的。如果你不采取第一步,你最终会得到与原始大小相同的大小,这比不下采样到更小的分辨率需要更多的内存。
    【解决方案2】:

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

    所以我发现 SonyMobile 解决此问题的方法最适合此类任务。

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

    1. 使用 BitmapFactory.Options::inSampleSize->BitmapFactory.decodeResource() 尽可能接近您需要的分辨率,但不能低于它
    2. 使用 Canvas::drawBitmap() 稍微缩小一点以达到精确的分辨率

    这里是索尼移动如何解决这个任务的详细解释:http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for-your-android-application/

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

    【讨论】:

    • 他们只是缩放你解码后得到的位图。这称为缩减,而不是缩减采样。这实际上也是我在我的库中的 JNI 中所做的:github.com/AndroidDeveloperLB/AndroidJniBitmapOperations。无论如何,问题是关于下采样的,这意味着您将位图直接从 inputStream 解码为所需大小的位图。可悲的是,我认为我找到的解决方案(使用“inTargetDensity”)也在做同样的事情——同时使用 2 个位图,尽管它对任何使用它的人都是隐藏的。
    • 是的,你是对的=) 但是当我尝试设置时:bitmapOptions.inDensity=sampleSize;和 bitmapOptions.inTargetDensity=1;看起来 Android 使用了最近邻缩减/采样算法 :(
    • 我认为它在这种情况下同时使用 - 首先是最近的邻居,然后是另一个。顺便说一句,我之所以这样称呼它,是因为我看到的是输出图像。如果目标图像大小不是 2 的幂(例如,下采样 5),它应该使用这两种方法。也许你只是没有注意到它?尝试仅使用我的原始方法(当 inSampleSize==1 时),然后尝试我稍后建议的合并方法,看看是否有明显差异。在任何情况下,合并的方法都试图享受这两个世界。
    • 但这意味着Android现在可以在不使用最近邻算法的情况下将图像/位图从一种分辨率缩放/缩减采样到更小的分辨率?太奇怪了 - 至少当他们绘制 ImageView 时,他们肯定有这样的方式,它被缩小/采样到任何分辨率都不是那么糟糕......
    • 您始终可以使用自己的算法。我在大学时已经实现了双线性插值,它是用 Java 编写的。也许我会把它放到我做的JNI项目中。希望它仍然有效。这应该可以恢复任何人都无法使用的代码......
    【解决方案3】:

    您应该使用 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 的正常调整大小相比,它在使用它时会导致像素化问题。问题是为什么,以及是否有办法解决它。
    • 您发布的显示差异的图片未加载。
    • 我不知道为什么您看不到图像。也许你阻止它?您是否尝试过其他网络浏览器?也许在你的手机上?
    • 我现在看到了。当然,如果 imageView 与图像相同,图像会更好看。如果你一半的图像和一半的 imageView,它将看起来与原始图像相同。原始图像与最后一张图像一样像素化。如果您希望质量好,请使用较小的 inSampleSize,这可以通过增加 requiredWidth 来实现。将 requiredWidth 比您需要的多 20%,图像会更好看。告诉我进展如何。
    • inSampleSize 不能小于 2。事实上我认为它必须是 2 的幂(意思是 2,4,8,16,...)。这并不是一件显而易见的事情,因为较小的 imageView 和使用我在底部编写的密度技术使图像看起来更好。请阅读整个问题并考虑一下。
    猜你喜欢
    • 2017-03-23
    • 2011-09-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多