Problèmes de qualité lors du redimensionnement d'une image à l'exécution
J'ai un fichier image sur le disque et je redimensionne le fichier et le sauvegarde sur le disque en tant que nouveau fichier image. Pour le bien de cette question, Je ne les mets pas en mémoire afin de les afficher à l'écran, seulement pour les redimensionner et les resaver. Tout cela fonctionne très bien. Cependant, les images mises à l'échelle ont des artefacts sur eux comme montré ici: android: qualité des images redimensionnées dans l'exécution
Ils sont enregistrés avec cette distorsion, car je peux les retirer du disque et les regarder sur mon ordinateur et ils ont toujours le même problème.
J'utilise un code similaire à celui-ci étrange problème de mémoire lors du chargement d'une image dans un objet Bitmap pour décoder le bitmap en mémoire:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imageFilePathString, options);
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
int scale = 1;
while(srcWidth / 2 > desiredWidth){
srcWidth /= 2;
srcHeight /= 2;
scale *= 2;
}
options.inJustDecodeBounds = false;
options.inDither = false;
options.inSampleSize = scale;
Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(imageFilePathString, options);
Ensuite, je fais la mise à l'échelle réelle avec:
Bitmap scaledBitmap = Bitmap.createScaledBitmap(sampledSrcBitmap, desiredWidth, desiredHeight, false);
Enfin, la nouvelle image redimensionnée est enregistrée sur le disque avec:
FileOutputStream out = new FileOutputStream(newFilePathString);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
Ensuite, comme je l'ai mentionné, si je retire ce fichier du disque et le regarde, il a ce problème de qualité lié ci-dessus et regarde terrible. Si je saute le createScaledBitmap et enregistre simplement le sampledSrcBitmap sur le disque, il n'y a pas de problème, cela ne semble se produire que si la taille change.
J'ai essayé, comme vous pouvez le voir dans le code, de définir inDither à false comme mentionné ici http://groups.google.com/group/android-developers/browse_thread/thread/8b1abdbe881f9f71 et comme mentionné dans le tout premier post lié ci-dessus. Qui n'a pas changé quoi que ce soit. Aussi, dans le premier post que j'ai lié, Romain Guy a dit:
Au lieu de redimensionner au moment du dessin (qui va être très coûteux), essayez de redimensionner dans un bitmap hors écran et assurez vous que Bitmap est 32 bits (ARGB888).
Cependant, je n'ai aucune idée de comment m'assurer que le Bitmap reste 32 bits tout au long du processus.
J'ai aussi lu quelques autres articles comme celui-ci http://android.nakatome.net/2010/04/bitmap-basics.html mais ils semblaient tous aborder le dessin et l'affichage du Bitmap, je veux juste redimensionner et enregistrer sur le disque sans ce problème de qualité.
Merci beaucoup
7 réponses
Après avoir expérimenté, j'ai finalement trouvé un moyen de le faire avec des résultats de bonne qualité. Je vais écrire ceci pour tous ceux qui pourraient trouver cette réponse utile à l'avenir.
Pour résoudre le premier problème, les artefacts et le tramage étrange introduits dans les images, vous devez vous assurer que votre image reste en tant QU'image ARGB_8888 32 bits. En utilisant le code dans ma question, vous pouvez simplement ajouter cette ligne aux options avant le deuxième décodage.
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Après avoir ajouté cela, les artefacts étaient disparu, mais les bords à travers les images sont venus à travers déchiqueté au lieu de croustillant. Après plus d'expérimentation, j'ai découvert que redimensionner le bitmap en utilisant une matrice au lieu de Bitmap.createScaledBitmap a produit des résultats beaucoup plus nets.
Avec ces deux solutions, les images sont maintenant redimensionnées parfaitement. Voici le code que j'utilise au cas où cela profiterait à quelqu'un d'autre qui rencontrerait ce problème.
// Get the source image's dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(STRING_PATH_TO_FILE, options);
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
// Only scale if the source is big enough. This code is just trying to fit a image into a certain width.
if(desiredWidth > srcWidth)
desiredWidth = srcWidth;
// Calculate the correct inSampleSize/scale value. This helps reduce memory use. It should be a power of 2
// from: https://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/823966#823966
int inSampleSize = 1;
while(srcWidth / 2 > desiredWidth){
srcWidth /= 2;
srcHeight /= 2;
inSampleSize *= 2;
}
float desiredScale = (float) desiredWidth / srcWidth;
// Decode with inSampleSize
options.inJustDecodeBounds = false;
options.inDither = false;
options.inSampleSize = inSampleSize;
options.inScaled = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(STRING_PATH_TO_FILE, options);
// Resize
Matrix matrix = new Matrix();
matrix.postScale(desiredScale, desiredScale);
Bitmap scaledBitmap = Bitmap.createBitmap(sampledSrcBitmap, 0, 0, sampledSrcBitmap.getWidth(), sampledSrcBitmap.getHeight(), matrix, true);
sampledSrcBitmap = null;
// Save
FileOutputStream out = new FileOutputStream(NEW_FILE_PATH);
scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
scaledBitmap = null;
EDIT: après un travail continu sur ce point, j'ai constaté que les images ne sont toujours pas à 100% parfait. Je ferai une mise à jour si je peux l'améliorer.
Update: Après avoir révisé ceci, j'ai trouvé cette question sur SO et il y avait une réponse qui mentionnait l'option inScaled. Cela a aidé avec la qualité aussi bien donc j'ai ajouté mis à jour la réponse ci - dessus pour l'inclure. J'ai aussi maintenant null les bitmaps après qu'ils ont été utilisés.
Aussi, comme une note de côté, si vous utilisez ces images dans un WebView, assurez-vous que vous prenez ce post dans considération.
REMARQUE: Vous devez également ajouter une vérification pour vous assurer que la largeur et la hauteur sont des nombres valides (pas -1). Si c'est le cas, la boucle inSampleSize deviendra infinie.
Dans Ma situation, je dessine l'image à l'écran. Voici ce que j'ai fait pour que mes images soient correctes (une combinaison de la réponse de littleFluffyKitty, plus quelques autres choses).
Pour mes options lorsque je charge réellement l'image (en utilisant decodeResource), je définis les valeurs suivantes:
options.inScaled = false;
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Quand je dessine réellement l'image, je configure mon objet paint comme ceci:
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
Espérons que quelqu'un d'autre trouve cela utile aussi. Je souhaite qu'il y avait juste des options pour " oui, laissez mon les images redimensionnées ressemblent à des ordures " et "non, Ne forcez pas mes utilisateurs à s'arracher les yeux avec des cuillères" au lieu de toute la myriade d'options différentes. Je sais qu'ils veulent nous donner beaucoup de contrôle, mais peut-être que certaines méthodes d'assistance pour les paramètres courants pourraient être utiles.
J'ai créé une bibliothèque simple basée sur la réponse littleFluffyKitty
qui redimensionne et fait d'autres choses comme la culture et la rotation, alors veuillez l'utiliser et l'améliorer - Android-ImageResizer.
" Cependant, je n'ai aucune idée de comment m'assurer que le Bitmap reste 32 bits à travers l'ensemble du processus."
Je voulais poster une solution alternative, qui prend soin de garder la configuration ARGB_8888 intacte. Remarque: ce code décode uniquement les bitmaps et doit être étendu, de sorte que vous pouvez stocker un Bitmap.
Je suppose que vous écrivez du code pour une version D'Android inférieure à 3.2 (niveau API
BitmapFactory.decodeFile(pathToImage);
BitmapFactory.decodeFile(pathToImage, opt);
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
A changé.
Sur les anciennes plates-formes (niveau API
options.inPrefferedConfig = Bitmap.Config.ARGB_8888
options.inDither = false
Le vrai problème vient quand chaque pixel de votre image a une valeur alpha de 255 (c'est-à-dire complètement opaque). Dans ce cas, le drapeau du Bitmap 'hasAlpha' est défini sur false, même si votre Bitmap a ARGB_8888 config. Si votre *.png-file avait au moins un vrai pixel transparent, ce drapeau aurait été défini sur true et vous n'auriez à vous soucier de rien.
Donc, lorsque vous voulez créer un bitmap mis à l'échelle en utilisant
bitmapObject.createScaledBitmap(bitmap, desiredWidth, desiredHeight, false /*filter?*/);
La méthode vérifie si l'indicateur 'hasAlpha' est défini sur true ou false, et dans votre cas, il est défini sur false, ce qui entraîne l'obtention d'un bitmap mis à l'échelle, qui a été automatiquement converti en RGB_565 format.
Par conséquent, au niveau de L'API > = 12, Il existe une méthode publique appelée
public void setHasAlpha (boolean hasAlpha);
, Qui aurait résolu ce problème. Jusqu'à présent c'était juste une explication du problème. J'ai fait quelques recherches et j'ai remarqué que la méthode setHasAlpha existait depuis longtemps et qu'elle était publique, mais qu'elle était cachée (@hide annotation). Voici comment il est défini sur 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);
}
Maintenant, voici ma proposition de solution. Cela n'implique aucune copie de bitmap données:
Vérifié à l'exécution en utilisant java.lang.Refléter si le courant L'implémentation Bitmap a une méthode publique 'setHasAplha'. (Selon mes tests, cela fonctionne parfaitement depuis le niveau API 3, et je n'ai pas testé les versions inférieures, car JNI ne fonctionnerait pas). Vous pouvez avoir des problèmes si un fabricant l'a explicitement Rendu privé, protégé ou supprimé.
Appelez la méthode 'setHasAlpha' pour un objet Bitmap donné en utilisant JNI. Cela fonctionne parfaitement, même pour méthodes ou champs privés. Il est officiel que JNI ne vérifie pas si vous violez les règles de contrôle d'accès ou non. Source: http://java.sun.com/docs/books/jni/html/pitfalls.html (10,9) Cela nous donne un grand pouvoir, qui devrait être utilisé à bon escient. Je n'essaierais pas de modifier un champ final, même si cela fonctionnerait (juste pour donner un exemple). Et Veuillez noter que c'est juste une solution de contournement...
Voici ma mise en œuvre de toutes les méthodes nécessaires:
JAVA Partie:
// 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;
}
Chargez votre lib et déclarez la méthode native:
static {
System.loadLibrary("bitmaputils");
}
private static native int setHasAlphaNative(Bitmap bitmap, boolean value);
Section Native (dossier '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
}
C'est ça. Nous avons terminé. J'ai posté tout le code à des fins de copier-coller. Le code réel n'est pas si grand, mais faire toutes ces vérifications d'erreurs paranoïaques le rend beaucoup plus grand. J'espère que cela pourrait être utile à tout le monde.
onScreenResults = Bitmap.createScaledBitmap(tempBitmap, scaledOSRW, scaledOSRH, true); <----
Définir le filtre sur true a fonctionné pour moi.
Ainsi, createScaledBitmap et createBitmap (avec une matrice qui évolue) sur un bitmap immuable (comme lorsqu'il est décodé) ignoreront le Bitmap d'origine.Config et créer bitmap avec Bitmap.Config.ARGB_565 si original n'a aucune transparence (hasAlpha = = false). Mais il ne le fera pas sur bitmap mutable. Donc, si votre bitmap décodé est b:
Bitmap temp = Bitmap.createBitmap(b.getWidth(), b.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(temp);
canvas.drawBitmap(b, 0, 0, null);
b.recycle();
Maintenant, vous pouvez redimensionner temp et il devrait conserver Bitmap.Config.ARGB_8888.
La mise à l'échelle de L'Image peut également être réalisée par ce moyen sans aucune perte de qualité!
//Bitmap bmp passed to method...
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.JPEG, 100, stream);
Image jpg = Image.getInstance(stream.toByteArray());
jpg.scalePercent(68); // or any other number of useful methods.