Плохое качество изображения после изменения размера/масштабирования растрового изображения
Я пишу карточную игру и нуждаюсь в том, чтобы мои карты были разных размеров в разных обстоятельствах. Я сохраняю свои изображения в виде растровых изображений, чтобы их можно было быстро нарисовать и перерисовать (для анимации).
Моя проблема заключается в том, что независимо от того, как я стараюсь масштабировать мои изображения (будь то через matrix.postScale, matrix.preScale или функцию createScaledBitmap), они всегда выглядят неровными и размытыми. Я знаю, что его масштабирование вызывает проблему, потому что изображения выглядят идеально при рисовании без изменения размера.
Я работал над каждым решением, описанным в этих двух потоках:
Качество и качество изображений для Android в режиме реального времени
проблемы с качеством при изменении размера изображения во время выполнения
но все равно ничего не получили.
Я сохраняю свои растровые изображения (в хэш-карту) с помощью этого кода:
cardImages = new HashMap<Byte, Bitmap>();
cardImages.put(GameUtil.hearts_ace, BitmapFactory.decodeResource(r, R.drawable.hearts_ace));
и нарисуйте их с помощью этого метода (в классе карты):
public void drawCard(Canvas c)
{
//retrieve the cards image (if it doesn't already have one)
if (image == null)
image = Bitmap.createScaledBitmap(GameUtil.cardImages.get(ID),
(int)(GameUtil.standardCardSize.X*scale), (int)(GameUtil.standardCardSize.Y*scale), false);
//this code (non-scaled) looks perfect
//image = GameUtil.cardImages.get(ID);
matrix.reset();
matrix.setTranslate(position.X, position.Y);
//These methods make it look worse
//matrix.preScale(1.3f, 1.3f);
//matrix.postScale(1.3f, 1.3f);
//This code makes absolutely no difference
Paint drawPaint = new Paint();
drawPaint.setAntiAlias(false);
drawPaint.setFilterBitmap(false);
drawPaint.setDither(true);
c.drawBitmap(image, matrix, drawPaint);
}
Любое понимание было бы весьма полезным. Благодаря
Ответы
Ответ 1
У меня были красные изображения с низким разрешением экрана, пока я не отключил масштабирование при загрузке растровой графики из ресурсов:
Options options = new BitmapFactory.Options();
options.inScaled = false;
Bitmap source = BitmapFactory.decodeResource(a.getResources(), path, options);
Ответ 2
Использование createScaledBitmap сделает ваше изображение очень плохим.
Я встретил эту проблему, и я решил ее.
Ниже приведен код проблемы:
public Bitmap BITMAP_RESIZER(Bitmap bitmap,int newWidth,int newHeight) {
Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Config.ARGB_8888);
float ratioX = newWidth / (float) bitmap.getWidth();
float ratioY = newHeight / (float) bitmap.getHeight();
float middleX = newWidth / 2.0f;
float middleY = newHeight / 2.0f;
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY);
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
canvas.drawBitmap(bitmap, middleX - bitmap.getWidth() / 2, middleY - bitmap.getHeight() / 2, new Paint(Paint.FILTER_BITMAP_FLAG));
return scaledBitmap;
}
Ответ 3
createScaledBitmap
имеет флаг, в котором вы можете установить, следует ли фильтровать масштабированное изображение или нет. Этот флаг улучшает качество растрового изображения...
Ответ 4
используйте
mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
Paint.FILTER_BITMAP_FLAG
работает для меня
Ответ 5
Я предполагаю, что вы пишете код для версии Android ниже 3.2 (уровень API < 12), поскольку с тех пор поведение методов
BitmapFactory.decodeFile(pathToImage);
BitmapFactory.decodeFile(pathToImage, opt);
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
изменился.
На более старых платформах (уровень API < 12) методы BitmapFactory.decodeFile(..) пытаются вернуть Bitmap с конфигурацией RGB_565 по умолчанию, если они не могут найти альфа, что снижает качество iamge. Это все еще нормально, потому что вы можете использовать растровое изображение ARGB_8888, используя
options.inPrefferedConfig = Bitmap.Config.ARGB_8888
options.inDither = false
Реальная проблема возникает, когда каждый пиксель вашего изображения имеет альфа-значение 255 (т.е. полностью непрозрачно). В этом случае флаг Bitmap 'hasAlpha' имеет значение false, даже если ваш битмап имеет конфигурацию ARGB_8888. Если ваш *.png файл имел по крайней мере один реальный прозрачный пиксель, этот флаг был бы установлен в true, и вам ни о чем не беспокоиться.
Итак, если вы хотите создать масштабированный битмап, используя
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
метод проверяет, установлен ли флаг hasAlpha равным true или false, а в вашем случае он установлен в false, что приводит к получению масштабированного растрового изображения, которое автоматически преобразуется в формат RGB_565.
Поэтому на уровне API >= 12 существует общедоступный метод под названием
public void setHasAlpha (boolean hasAlpha);
который решил бы эту проблему. До сих пор это было просто объяснением проблемы.
Я сделал некоторые исследования и заметил, что метод setHasAlpha существует уже давно, и он публичный, но был скрыт (аннотация @hide). Вот как это определено на Android 2.3:
/**
* Tell the bitmap if all of the pixels are known to be opaque (false)
* or if some of the pixels may contain non-opaque alpha values (true).
* Note, for some configs (e.g. RGB_565) this call is ignore, since it does
* not support per-pixel alpha values.
*
* This is meant as a drawing hint, as in some cases a bitmap that is known
* to be opaque can take a faster drawing case than one that may have
* non-opaque per-pixel alpha values.
*
* @hide
*/
public void setHasAlpha(boolean hasAlpha) {
nativeSetHasAlpha(mNativeBitmap, hasAlpha);
}
Теперь вот мое решение. Это не связано с копированием растровых данных:
-
Проверено во время выполнения с помощью java.lang.Reflect, если текущий
Реализация битового массива имеет общедоступный метод setHasAplha.
(По моим тестам он отлично работает с уровня API 3, и я не тестировал более низкие версии, потому что JNI не работал). У вас могут возникнуть проблемы, если производитель явно сделал это конфиденциальным, защищенным или удалил его.
-
Вызвать метод setHasAlpha для данного объекта Bitmap с использованием JNI.
Это прекрасно работает даже для частных методов или полей. Официально, что JNI не проверяет, нарушаете ли вы правила контроля доступа или нет.
Источник: http://java.sun.com/docs/books/jni/html/pitfalls.html (10.9)
Это дает нам большую силу, которая должна быть разумно использована. Я бы не стал модифицировать последнее поле, даже если бы он работал (просто чтобы привести пример). И обратите внимание, что это всего лишь обходной путь...
Вот моя реализация всех необходимых методов:
ЧАСТЬ JAVA:
// NOTE: this cannot be used in switch statements
private static final boolean SETHASALPHA_EXISTS = setHasAlphaExists();
private static boolean setHasAlphaExists() {
// get all puplic Methods of the class Bitmap
java.lang.reflect.Method[] methods = Bitmap.class.getMethods();
// search for a method called 'setHasAlpha'
for(int i=0; i<methods.length; i++) {
if(methods[i].getName().contains("setHasAlpha")) {
Log.i(TAG, "method setHasAlpha was found");
return true;
}
}
Log.i(TAG, "couldn't find method setHasAlpha");
return false;
}
private static void setHasAlpha(Bitmap bitmap, boolean value) {
if(bitmap.hasAlpha() == value) {
Log.i(TAG, "bitmap.hasAlpha() == value -> do nothing");
return;
}
if(!SETHASALPHA_EXISTS) { // if we can't find it then API level MUST be lower than 12
// couldn't find the setHasAlpha-method
// <-- provide alternative here...
return;
}
// using android.os.Build.VERSION.SDK to support API level 3 and above
// use android.os.Build.VERSION.SDK_INT to support API level 4 and above
if(Integer.valueOf(android.os.Build.VERSION.SDK) <= 11) {
Log.i(TAG, "BEFORE: bitmap.hasAlpha() == " + bitmap.hasAlpha());
Log.i(TAG, "trying to set hasAplha to true");
int result = setHasAlphaNative(bitmap, value);
Log.i(TAG, "AFTER: bitmap.hasAlpha() == " + bitmap.hasAlpha());
if(result == -1) {
Log.e(TAG, "Unable to access bitmap."); // usually due to a bug in the own code
return;
}
} else { //API level >= 12
bitmap.setHasAlpha(true);
}
}
/**
* Decodes a Bitmap from the SD card
* and scales it if necessary
*/
public Bitmap decodeBitmapFromFile(String pathToImage, int pixels_limit) {
Bitmap bitmap;
Options opt = new Options();
opt.inDither = false; //important
opt.inPreferredConfig = Bitmap.Config.ARGB_8888;
bitmap = BitmapFactory.decodeFile(pathToImage, opt);
if(bitmap == null) {
Log.e(TAG, "unable to decode bitmap");
return null;
}
setHasAlpha(bitmap, true); // if necessary
int numOfPixels = bitmap.getWidth() * bitmap.getHeight();
if(numOfPixels > pixels_limit) { //image needs to be scaled down
// ensures that the scaled image uses the maximum of the pixel_limit while keeping the original aspect ratio
// i use: private static final int pixels_limit = 1280*960; //1,3 Megapixel
imageScaleFactor = Math.sqrt((double) pixels_limit / (double) numOfPixels);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap,
(int) (imageScaleFactor * bitmap.getWidth()), (int) (imageScaleFactor * bitmap.getHeight()), false);
bitmap.recycle();
bitmap = scaledBitmap;
Log.i(TAG, "scaled bitmap config: " + bitmap.getConfig().toString());
Log.i(TAG, "pixels_limit = " + pixels_limit);
Log.i(TAG, "scaled_numOfpixels = " + scaledBitmap.getWidth()*scaledBitmap.getHeight());
setHasAlpha(bitmap, true); // if necessary
}
return bitmap;
}
Загрузите свой lib и объявите собственный метод:
static {
System.loadLibrary("bitmaputils");
}
private static native int setHasAlphaNative(Bitmap bitmap, boolean value);
Нативный раздел (папка "jni" )
Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := bitmaputils
LOCAL_SRC_FILES := bitmap_utils.c
LOCAL_LDLIBS := -llog -ljnigraphics -lz -ldl -lgcc
include $(BUILD_SHARED_LIBRARY)
bitmapUtils.c:
#include <jni.h>
#include <android/bitmap.h>
#include <android/log.h>
#define LOG_TAG "BitmapTest"
#define Log_i(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define Log_e(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
// caching class and method IDs for a faster subsequent access
static jclass bitmap_class = 0;
static jmethodID setHasAlphaMethodID = 0;
jint Java_com_example_bitmaptest_MainActivity_setHasAlphaNative(JNIEnv * env, jclass clazz, jobject bitmap, jboolean value) {
AndroidBitmapInfo info;
void* pixels;
if (AndroidBitmap_getInfo(env, bitmap, &info) < 0) {
Log_e("Failed to get Bitmap info");
return -1;
}
if (info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) {
Log_e("Incompatible Bitmap format");
return -1;
}
if (AndroidBitmap_lockPixels(env, bitmap, &pixels) < 0) {
Log_e("Failed to lock the pixels of the Bitmap");
return -1;
}
// get class
if(bitmap_class == NULL) { //initializing jclass
// NOTE: The class Bitmap exists since API level 1, so it just must be found.
bitmap_class = (*env)->GetObjectClass(env, bitmap);
if(bitmap_class == NULL) {
Log_e("bitmap_class == NULL");
return -2;
}
}
// get methodID
if(setHasAlphaMethodID == NULL) { //initializing jmethodID
// NOTE: If this fails, because the method could not be found the App will crash.
// But we only call this part of the code if the method was found using java.lang.Reflect
setHasAlphaMethodID = (*env)->GetMethodID(env, bitmap_class, "setHasAlpha", "(Z)V");
if(setHasAlphaMethodID == NULL) {
Log_e("methodID == NULL");
return -2;
}
}
// call java instance method
(*env)->CallVoidMethod(env, bitmap, setHasAlphaMethodID, value);
// if an exception was thrown we could handle it here
if ((*env)->ExceptionOccurred(env)) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
Log_e("calling setHasAlpha threw an exception");
return -2;
}
if(AndroidBitmap_unlockPixels(env, bitmap) < 0) {
Log_e("Failed to unlock the pixels of the Bitmap");
return -1;
}
return 0; // success
}
Что это. Мы сделали. Я опубликовал весь код для целей копирования и вставки.
Фактический код не такой большой, но все эти проверки параноидальных ошибок делают его намного больше. Надеюсь, это может быть полезно для всех.
Ответ 6
Хороший алгоритм уменьшения масштаба (не похожий на ближайшего соседа, поэтому пикселизация не добавляется) состоит всего из 2 шагов (плюс вычисление точного Rect для обрезки входных/выходных изображений):
- уменьшить масштаб с помощью BitmapFactory.Options :: inSampleSize → BitmapFactory.decodeResource() как можно ближе к разрешению, которое вам нужно, но не меньше его
- получить точное разрешение, немного уменьшив масштаб с помощью Canvas :: drawBitmap()
Вот подробное объяснение того, как SonyMobile решила эту задачу: https://web.archive.org/web/20171011183652/http://developer.sonymobile.com/2011/06/27/how-to-scale-images-for- ваш-андроид-приложение/
Вот исходный код утилит масштаба SonyMobile: https://web.archive.org/web/20170105181810/http://developer.sonymobile.com:80/downloads/code-example-module/image-scaling-code- пример-для-андроид/
Ответ 7
У вас никогда не будет идеального результата, если вы масштабируете растровые изображения вверх.
Вы должны начать с самого высокого разрешения, которое вам нужно, и уменьшить масштаб.
При масштабировании растрового изображения масштабирование не может угадать, что является недостающими точками между каждой существующей точкой, поэтому оно либо дублирует сосед (= > резкое), либо вычисляет среднее значение между соседями (= > размытие).
Ответ 8
Я просто использовал флаг filter=true
в
bitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
для размытия.
Ответ 9
Если вы хотите получить качественный результат, используйте библиотеку [RapidDecoder] [1]. Это просто:
import rapid.decoder.BitmapDecoder;
...
Bitmap bitmap = BitmapDecoder.from(getResources(), R.drawable.image)
.scale(width, height)
.useBuiltInDecoder(true)
.decode();
Не забудьте использовать встроенный декодер, если вы хотите уменьшить масштаб менее 50% и результат HQ. Я тестировал его на API 8.
Ответ 10
private static Bitmap createScaledBitmap(Bitmap bitmap,int newWidth,int newHeight) {
Bitmap scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, bitmap.getConfig());
float scaleX = newWidth / (float) bitmap.getWidth();
float scaleY = newHeight / (float) bitmap.getHeight();
Matrix scaleMatrix = new Matrix();
scaleMatrix.setScale(scaleX, scaleY, 0, 0);
Canvas canvas = new Canvas(scaledBitmap);
canvas.setMatrix(scaleMatrix);
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
paint.setAntiAlias(true);
paint.setDither(true);
paint.setFilterBitmap(true);
canvas.drawBitmap(bitmap, 0, 0, paint);
return scaledBitmap;
}
Ответ 11
Возникла эта проблема при обновлении Android Target Framework с Android 8.1 до Android 9 и появилась на моем ImageEntryRenderer. Надеюсь это поможет
public Bitmap ProcessScaleBitMap(Bitmap bitmap, int newWidth, int newHeight)
{
newWidth = newWidth * 2;
newHeight = newHeight * 2;
Bitmap scaledBitmap = CreateBitmap(newWidth, newHeight, Config.Argb8888);
float scaleDensity = ((float)Resources.DisplayMetrics.DensityDpi / 160);
float scaleX = newWidth / (bitmap.Width * scaleDensity);
float scaleY = newHeight / (bitmap.Height * scaleDensity);
Matrix scaleMatrix = new Matrix();
scaleMatrix.SetScale(scaleX, scaleY);
Canvas canvas = new Canvas(scaledBitmap);
canvas.Matrix = scaleMatrix;
canvas.DrawBitmap(bitmap, 0, 0, new Paint(PaintFlags.FilterBitmap));
return scaledBitmap;
}
Примечание: я разрабатываю под фреймворком Xamarin 3.4.0.10