Как правильно уменьшить изображение?
Фон
Создавая приложение с большим количеством изображений высокого качества, я решил уменьшить изображение до необходимого размера (то есть, если изображение больше экрана, я уменьшу его) .
Эта проблема
Я заметил, что на некоторых устройствах, если изображения уменьшены, они становятся размытыми / пикселизированными, но на тех же устройствах, с тем же целевым размером 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), кажется, исправляет это, но я не уверен, что установить для него. Я думаю, что для внешних изображений (например, из Интернета) я могу установить плотность экрана, умноженную на размер выборки, который я хочу использовать.
Но это даже хорошее решение? Что мне делать, если изображения находятся внутри папки ресурсов (я не думаю, что есть функция для определения, в какой папке плотности находится растровое изображение)? почему это работает, в то время как использование рекомендуемого способа (о котором говорили здесь) не работает хорошо?
РЕДАКТИРОВАТЬ: Я нашел трюк, чтобы узнать, какая плотность используется для рисования, которое вы получаете из ресурсов ( ссылка здесь) . однако, это не является будущим доказательством, так как вам нужно быть определенным для плотности, чтобы обнаружить.
3 ответа
Хорошо, я нашел хорошую альтернативу, которая, я думаю, должна работать для любого вида растрового декодирования.
не только это, но и позволяет уменьшить масштаб, используя любой размер выборки, а не только степень 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 пропускает пиксели и потому что метод плотностей делает дополнительные вычисления для пропущенных пикселей.
Тем не менее, я думаю, что каким-то образом Android запускает оба метода примерно с одинаковой скоростью.
Я думаю, что сравнение двух методов аналогично сравнению между понижающей дискретизацией ближайшего соседа и понижающей выборкой с билинейной интерполяцией.
РЕДАКТИРОВАТЬ: я нашел один недостаток метода, который я показал здесь, по сравнению с тем, который имеет Google. память, используемая во время процесса, может быть довольно высокой, и я думаю, что это зависит от самого изображения. это означает, что вы должны использовать его только в тех случаях, которые, по вашему мнению, имеют смысл.
РЕДАКТИРОВАТЬ: я сделал объединенное решение (решение Google и мое) для тех, кто хочет преодолеть проблему с памятью. это не идеально, но лучше, чем я делал раньше, потому что он не будет использовать столько памяти, сколько требуется исходному растровому изображению во время понижающей дискретизации. вместо этого он будет использовать память, используемую в решении 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;
}
Итак, вкратце, плюсы и минусы обоих методов:
Способ 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;
}
}
Так, например, при понижении частоты дискретизации с 2448x3264 изображения до 1200x1200, оно станет 900x1200
Вы должны использовать 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 работало хорошо (но не как алгоритм ближайшего соседа). Но, к сожалению, это не позволяет получить точное разрешение, которое нам нужно (только ровно в целое число раз меньше, чем оригинал).
Таким образом, я обнаружил, что решение SonyMobile этой проблемы лучше всего подходит для такой задачи.
Вкратце, он состоит из 2 шагов:
- уменьшить масштаб с помощью 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/