分析android图片的抖动处理

       android下图片资源从源图到显示需要经历三个步骤:使用aapt工具处理,图片解码,绘制。让我们按顺序一一探讨。

android开发指南对位图资源有如下描述:

Bitmap files may be automatically optimized with lossless image compression by the aapt tool during the build process. For example, a true-color PNG that does not require more than 256 colors may be converted to an 8-bit PNG with a color palette. This will result in an image of equal quality but which requires less memory. So be aware that the image binaries placed in this directory can change during the build. If you plan on reading an image as a bit stream in order to convert it to a bitmap, put your images in the res/raw/ folder instead, where they will not be optimized.

这段描述很模糊,我们关注的抖动处理更是完全没有提及,但它隐约提到这里的处理是无损压缩。

google搜索发现国外有位老大对此问题进行了探讨,它说android根据图片是否带alpha通道进行不同处理,没有alpha通道的24位图片转换成565格式,并在转换过程中进行抖动处理,带alpha通道的图片则统一转换为32位格式。

人言不可尽信,何况那位老大完全没有提到他这信息是哪来的,也没提到他在哪个版本上测试的,咱们还是自己研究一下aapt源码。

aapt工具源码位于frameworks/base/tools/aapt目录下,其中images.cpp负责处理图片,具体处理函数是preProcessImage,它目前只处理png图片,analyze_image函数对源图片进行格式分析并设定目标格式,它的工作在注释中描述得很清楚,如下:

    // Scan the entire image and determine if:
    // 1. Every pixel has R == G == B (grayscale)
    // 2. Every pixel has A == 255 (opaque)
    // 3. There are no more than 256 distinct RGBA colors

很明显,这里的操作都是为压缩服务的,设置好相关参数后,它还是调用libpng的相关函数重新压缩成png格式,而png格式本身并不支持565格式,也没有看到哪里设置了抖动选项,而libpng并不是专门为aapt服务的,说它默认设置抖动选项未免太过牵强附会。至此基本可以确定至少我这个版本的aapt并不会进行那位老大说的抖动处理和格式转换,但write_png函数中这段注释还是让人有点不安。

    // If the image is a 9-patch, we need to preserve it as a ARGB file to make
    // sure the pixels will not be pre-dithered/clamped until we decide they are

为以防万一,我祭出了最后法宝,写一段测试程序进行实际测试。

首先用photoshop制作一张渐变的图片,以保证抖动处理会有明显的变化,存为24位bmp格式,再转存一张png格式的,放入android工程中作为目标资源,再用openRawResource函数将该图片资源取出来,存入sd卡,因png格式中不同压缩方案对同一张图会产生不同结果,无法直接比较,我们再在pc上用画图程序将它转存为bmp格式,然后跟最初的bmp文件比较。果然,结果一字不差。

反正接下来要研究解码和绘制,顺便用下述代码将上述图片解码出来,

[java]  view plain copy
  1. java.io.InputStream is = context.getResources().openRawResource(  
  2.         R.drawable.bk);  
  3. BitmapFactory.Options options = new BitmapFactory.Options();  
  4. options.inDither = false;  
  5. mBitmap = BitmapFactory.decodeStream(is, null, options);  
再绘制到界面上,
[java]  view plain copy
  1. canvas.drawBitmap(mBitmap, 00, mPaint);  

其中mPaint完全采用默认参数创建。我的机器是16位的,结果带状效果很明显,绘制速度也明显经历了格式转换,比使用565格式位图慢了近一个数量级。

下面研究第二步,解码。老规矩,先看开发文档。没发现系统描述这个问题的文档,只好看看解码参数BitmapFactory.Options,相关参数有两个,inDither决定是否进行抖动,inPreferredConfig决定了采用565格式还是ARGB,具体描述如下:

If this is non-null, the decoder will try to decode into this internal configuration. If it is null, or the request cannot be met, the decoder will try to pick the best matching config based on the system's screen depth, and characteristics of the original image such as if it has per-pixel alpha (requiring a config that also does). Image are loaded with the ARGB_8888 config by default.

看来,最佳的解码方式应该是像下面这样,让系统决定最佳格式:

[java]  view plain copy
  1. mBitmap = BitmapFactory.decodeStream(context.getResources().openRawResource(R.drawable.bk));  
在我的16位机器上一试,效果不错,加载时进行了抖动和格式转换,显示又快又好。
文档看过了,该看源码了,android的文档不太完善,没源码还真不行。先看看BitmapFactory.java,首先是Options,inDither默认为false,inPreferredConfig默认为ARGB_8888。再看decodeStream函数,它调用了nativeDecodeStream函数,一搜,它在BitmapFactory.cpp中定义的,这个函数很简单,主要是调用doDecode,该函数定义了各参数的默认值,如下:
[cpp]  view plain copy
  1. SkBitmap::Config prefConfig = SkBitmap::kNo_Config;  
  2. bool doDither = true;  
先看看抖动,它调用setDitherImage保存在fDitherImage中,该成员是私有的,仅在getDitherImage函数中有使用。用getDitherImage一搜,在SkImageDecoder_libpng.cpp的onDecode函数中有使用,它取出抖动设置后,又调用getBitmapConfig根据图片格式进行了调整,如果源图低于565格式,则不进行抖动,调整好后就交给了SkScaledBitmapSampler::begin函数,该函数根据抖动和格式设置选取不同的采样函数,抖动选项至此真正使用起来。图片格式的使用稍微复杂一些,搜索过程咱就不啰嗦了,直接讲结果。getBitmapConfig函数中抖动调整之后就是格式设置,它调用了getPrefConfig函数,并将位深和是否透明作为参数传递过去,该函数代码如下:
[cpp]  view plain copy
  1. SkBitmap::Config SkImageDecoder::getPrefConfig(SrcDepth srcDepth,  
  2.                                                bool srcHasAlpha) const {  
  3.     SkBitmap::Config config;  
  4.   
  5.     if (fUsePrefTable) {  
  6.         int index = 0;  
  7.         switch (srcDepth) {  
  8.             case kIndex_SrcDepth:  
  9.                 index = 0;  
  10.                 break;  
  11.             case k16Bit_SrcDepth:  
  12.                 index = 2;  
  13.                 break;  
  14.             case k32Bit_SrcDepth:  
  15.                 index = 4;  
  16.                 break;  
  17.         }  
  18.         if (srcHasAlpha) {  
  19.             index += 1;  
  20.         }  
  21.         config = fPrefTable[index];  
  22.     } else {  
  23.         config = fDefaultPref;  
  24.     }  
  25.   
  26.     if (SkBitmap::kNo_Config == config) {  
  27.         config = SkImageDecoder::GetDeviceConfig();  
  28.     }  
  29.     return config;  
  30. }  

它优先使用表格方式,而表格仅仅在处理.9.png格式时会设置,fDefaultPref即最早的doDecode函数中设置的,默认为SkBitmap::kNo_Config,这时取用系统配置,系统配置可在C++中通过SkImageDecoder::SetDeviceConfig设置,也可在JAVA中通过BitmapFactory.setDefaultConfig(隐藏方法)设置。在我的系统中,仅仅AndroidRuntime初始化时有调用,设成了565格式。

小结一下,系统定制者应该在AndroidRuntime初始化时根据系统位深和内存配置调用SkImageDecoder::SetDeviceConfig设置好系统默认配置,并在BitmapFactory.cpp的doDecode函数中设置好抖动的默认值。系统内置应用可以在启动时调用BitmapFactory.setDefaultConfig修改本应用的默认配置。应用程序在decodeStream时不传递或传递null参数将使用系统默认配置,传递一个new BitmapFactory.Options()则使用不抖动的ARGB_8888格式。

其实,我还有一个疑问,比如一张红色的渐变图,因为它的颜色总数少于256,在大小足够时会被aapt压缩成使用调色板的256色图片,在getBitmapConfig时会否因颜色低于565而不进行抖动?这个问题需要时我再行验证,哪位同学知道或有空验证后还望不吝赐教。

第三步,绘制,文档只有Paint的DITHER_FLAG,控制是否进行抖动,还是直接看源码吧。skia的绘制流程相当复杂,我看过很多次了,但没有系统整理过,依然没有任何头绪。不过没关系,咱们还是定点突破。很容易就可以发现Paint.java中的DITHER_FLAG通过setFlags最终传递给了SkPaint.h中的SkPaint类的fFlags成员,对应的标识为kDither_Flag,搜索一下,SkBlitter_RGB16.cpp的SkRGB16_Shader_Blitter函数有使用,它根据该标识设置自己的flags,然后传递给SkBlitRow::Factory,该函数根据flags查表选择不同的具体绘制函数,表格为gDefault_565_Procs,其内容明显地分为抖动和不抖动两组。问题很快理清了,没有任何干扰,Paint中的DITHER_FLAG完全决定了绘制时抖动与否。而只有SkBlitter_RGB16.cpp和SkBlitter_4444.cpp使用了抖动标识,可见只有这两种目标格式支持抖动。

接下来,我们再探讨一下常见的应用程序使用图片资源的方式。探讨过程就省略了,不外乎那三板斧。还是看结论吧,有不完整的请同学们提出。

[java]  view plain copy
  1. BitmapDrawable drawable = (BitmapDrawable)context.getResources().getDrawable(R.drawable.bk);  
  2. mBitmap = drawable.getBitmap();  

[java]  view plain copy
  1. mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.bk);  

[java]  view plain copy
  1. BitmapFactory.Options options = new BitmapFactory.Options();  
  2. mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.bk, options);  

取得不抖动的ARGB_8888格式。

[java]  view plain copy
  1. mBitmap = BitmapFactory.decodeStream(context.getResources().openRawResource(R.drawable.bk));  

[java]  view plain copy
  1. mBitmap = BitmapFactory.decodeFile(bitmapPath);  

[java]  view plain copy
  1. mBitmap = BitmapFactory.decodeFileDescriptor(bitmapFD);  

采用系统默认配置,通常是抖动的565格式。

[java]  view plain copy
  1. mView.setBackgroundResource(R.drawable.bk);  

解码为不抖动的ARGB_8888格式,绘制时进行抖动。直接在xml中指定背景也是如此。

总结一下,系统默认采用ARGB_8888格式,并在绘制时进行抖动,只有直接使用BitmapFactory才可能按机器配置自动选择合适的方式。

基本工作已经完成了,但问题还有很多。Android中的View为了保证动画流畅,设置了灵活的cache,以避免不必要的重画,这张图片又是什么格式呢?它对动画性能的影响可不一般。Android的显示管理基于Surface,普通窗口的Surface又是什么格式的呢?它决定了最终用户所见的画质。

View的cache比较简单,直接看源码,它取决于View是否透明和mUse32BitDrawingCache参数,而此参数仅仅在窗口透明时设置,可见大部分情况下cache都是565格式的,在16位机器和32位机器上分别测试,结果都是一样。

窗口的Surface理论上应该跟系统位深一致,否则修改系统位深就成了一个笑话,起不到任何作用,速度测试也间接证实了这一点。咱们再从代码中确认一下,我在窗口管理的文章中提到过,Surface的创建在WindowSession的relayoutWindow中进行,找到这个函数,它调用WindowState的createSurfaceLocked,最终通过Surface的构造函数创建,(其实分析一下Surface方面也知道,这是Java中唯一的入口)格式参数由WindowManager.LayoutParams确定,它的默认值是OPAQUE。继续分析Surface的创建,它通过ISurfaceComposerClient::createSurface创建,搜索createSurface,可以发现它的真正实现在SurfaceFlinger中,它最终调用createNormalSurface创建,对于OPAQUE格式,它会根据DisplayHardware的格式决定用565还是8888。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会尽力回答您的问题。 Floyd-Steinberg抖动算法是一种常用的图像处理算法,可以用来将一张高色彩深度的图片转换为低色彩深度的图片,适用于墨水屏等设备。在QT中,可以通过QImage类来读取和处理图片。 以下是使用Floyd-Steinberg抖动算法处理墨水屏图片的基本步骤: 1. 打开图片 使用QImage类的load()函数打开需要处理图片,例如: ``` QImage image; image.load("image.png"); ``` 2. 转换为灰度图像 将彩色图片转换为灰度图像,可以使用QImage类的convertToFormat()函数实现: ``` image = image.convertToFormat(QImage::Format_Grayscale8); ``` 3. 进行抖动处理 使用Floyd-Steinberg抖动算法对灰度图像进行处理,将每个像素点的值转换为0或1,然后将误差传递给周围的像素点。这里可以用一个二维数组来存储每个像素点的值: ``` int width = image.width(); int height = image.height(); int** pixels = new int*[width]; for(int i = 0; i < width; i++) { pixels[i] = new int[height]; } for(int y = 0; y < height; y++) { for(int x = 0; x < width; x++) { int oldPixel = qGray(image.pixel(x, y)); int newPixel = oldPixel > 127 ? 255 : 0; pixels[x][y] = newPixel; int error = oldPixel - newPixel; if(x < width - 1) { pixels[x+1][y] += (int)(error * 7 / 16.0); } if(x > 0 && y < height - 1) { pixels[x-1][y+1] += (int)(error * 3 / 16.0); } if(y < height - 1) { pixels[x][y+1] += (int)(error * 5 / 16.0); } if(x < width - 1 && y < height - 1) { pixels[x+1][y+1] += (int)(error * 1 / 16.0); } } } ``` 4. 保存处理后的图片处理后的像素点重新转换为QImage格式,并使用save()函数保存为图片文件: ``` QImage result(width, height, QImage::Format_Grayscale8); for(int y = 0; y < height; y++) { for(int x = 0; x < width; x++) { result.setPixel(x, y, qRgb(pixels[x][y], pixels[x][y], pixels[x][y])); } } result.save("result.png"); ``` 以上就是使用Floyd-Steinberg抖动算法处理墨水屏图片用QT实现的基本步骤,希望对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值