Как правильно сбрасывать изображения?

Фон

Создав приложение с большим количеством высококачественных изображений, я решил уменьшить масштаб изображения до необходимого размера (это означает, что если изображение больше экрана, я уменьшаю его).

Проблема

Я заметил, что на некоторых устройствах, если изображения уменьшены, они становятся размытыми/пиксельными, но на тех же устройствах, для одного и того же целевого размера изображения, если изображения не масштабируются, они выглядят просто отлично.

Что я пробовал

Я решил еще раз проверить эту проблему и создал небольшое приложение 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), похоже, исправляет его, но я не уверен, что его установить. Я думаю, что для внешних изображений (например, из Интернета) я могу установить его на плотность экрана, умноженную на размер выборки, которую я хочу использовать.

Но это даже хорошее решение? что я должен делать в том случае, если изображения находятся внутри папки ресурсов (я не думаю, что есть функция, чтобы получить, какая папка плотности имеет растровое изображение)? почему это работает при использовании рекомендуемого способа (о здесь) не работает?


EDIT: я нашел трюк, чтобы получить, какая плотность используется для выталкиваемого вами из ресурсов (здесь). однако это не является будущим доказательством, поскольку вам нужно быть конкретным для определения плотности.

Ответ 1

ok, я нашел хорошую альтернативу, которая, я думаю, должна работать для любого растрового декодирования.

не только это, но и позволяет вам масштабировать, используя любой размер выборки, который вы хотите, а не только мощность 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, потому что inSampleSize пропускает пиксели и потому, что метод плотности выполняет дополнительные вычисления на пропущенных пикселях.

Однако, я думаю, что какой-то андроид запускает оба метода примерно с той же скоростью.

Я думаю, что сравнение двух методов аналогично сравнению между ближайшей соседней понижающей дискретизацией и билинейная интерполяционная понижающая дискретизация.

EDIT: я нашел один недостаток метода, который я показал здесь, по сравнению с тем, который был у Google. память, используемая во время процесса, может быть довольно высокой, и я думаю, что это зависит от самого изображения. это означает, что вы должны использовать его только в тех случаях, которые, по вашему мнению, имеют смысл.


EDIT: я сделал объединенное решение (как решение Google, так и мое) для тех, кто хочет преодолеть проблему с памятью. это не идеально, но это лучше, чем то, что я делал раньше, потому что он не будет использовать столько памяти, сколько нужно исходному растровому изображению во время понижающей дискретизации. вместо этого он будет использовать память, используемую в решении Google.

здесь код:

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

    // if google 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).

Мой способ (с использованием плотности) более точен, дает более качественные изображения и использует меньше памяти на растровом изображении результата. Тем не менее, он может использовать много памяти во время декодирования (зависит от входа), и он немного медленнее.


EDIT: еще одно улучшение, так как я обнаружил, что в некоторых случаях выходное изображение не соответствует требуемому ограничению размера, и вы не хотите слишком многого использовать с помощью 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

Ответ 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);

Надеюсь, что это поможет.

Ответ 3

Для меня только масштабирование с использованием inSampleSize выполнено хорошо (но не похоже на алгоритм ближайшего соседа). Но, к сожалению, это не позволяет получить точное разрешение, которое нам нужно (точно точно целое число меньше оригинала).

Итак, я обнаружил, что решение этой проблемы для SonyMobile работает лучше всего для такой задачи.

Вкратце, он состоит из двух шагов:

  • с помощью BitmapFactory.Options:: inSampleSize- > BitmapFactory.decodeResource() как можно ближе к требуемому разрешению, но не менее
  • получить точное разрешение, уменьшив масштаб немного, используя 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/