==========================================================
补充说明: -- hoodlum1980 2010年1月28日
==========================================================
我很快发现这篇文章实际上意义不大了。因为这是因为没有设置我们需要的拉伸模式导致的问题。
图像失真是由于 StretchBlt 的默认模式是 BLACKONWHITE:(对产生重叠的像素进行AND操作)导致的。事实上解决这个问题的正确方式是在 StretchBlt 之前调用 SetStretchBltMode 函数设置模式,下文中采用的方法实际上是 COLORONCOLOR 模式(即删除像素),这种模式将完全舍弃那些产生重叠的行列信息。下面解释一下这些模式:(内容来自 MSDN)
BLACKONWHITE
在保留像素和损失像素之间执行逻辑与(AND)操作。如果图片是单色位图,则被舍弃的黑色像素会在被保留的白色像素上保持黑色。
COLORONCOLOR
删除像素。不保留那些被舍弃行列上的像素信息。 (备注:即本文后面采用的下采样方式)
HALFTONE
把源矩形中的像素映射到目标矩形时,使像素的平均值近似相同。使用这种模式,应用程序必须然后调用 SetBrushOrgEx 校正画刷起始点,否则可能产生偏差。
WHITEONBLACK
在保留像素和损失像素之间执行逻辑或(OR)操作。如果图片是单色位图,则被舍弃的白色像素会在被保留的黑色像素上保持白色。
在拉伸绘制时,我们应该先进行模式设置:
hdc = BeginPaint(hWnd, &ps);
SetStretchBltMode( hdc, HALFTONE );
HDC hMemDC = CreateCompatibleDC(hdc);
SelectObject(hMemDC, m_Bitmap);
StretchBlt( hdc, 0, 0, 102, 136, hMemDC, 0, 0, 331,372, SRCCOPY );
DeleteDC(hMemDC);
EndPaint(hWnd, *ps);
以下是原文内容:
======================
本文所提到的问题是一个在实际项目中遇到的问题,在 VC 中,通过 StretchBlt 函数来完成缩小位图,将导致像素堆积(效果可参考下图)。具体体现就是 GDI 可能在 StretchBlt 的实现是比较简单的,导致使用拉伸绘制后的图像分辨率严重失真,以至于不能符合应用的要求。因此我们必须解决这个问题。(PS:在我印象中,可能在 GDI+ 中是不存在这个问题的。)
问题出现时,最开始我以为是在保存过程中的图像压缩质量导致的问题,但我把图像质量设置到 100% 时,图像质量依然没有任何改善,然后我发现其实图像质量的降低是发生在 StretchBlt 这一步,(DestRect 比原图小)一旦做了这个操作 ,则图像就变得面目全非,难以辨认细节。因此我很快的在网上搜索一些资料,也想过是不是要放弃CImage,该用网上的开源的CxImage。最终我采用的是在《CTreeCtrl和CListCtrl复杂控件的综合使用》一文中使用的方法:在matlab中叫做重采样(上采样-updample,下采样-downsample)。
在GDI中,如果是放大的拉伸绘制,产生的结果就是像素被线性放大,将出现明显锯齿,实际上问题不大。(一般的应用程序在放大图像时, 会在像素方格内进行线性插值来柔和图像。)因此本文主要讨论的是缩小的拉伸绘制。
缩小的拉伸绘制的原理非常简单,就是把图像缩小以后,我们把目标图像上的每个像素,按缩放比例去原图中选取相应像素,拷贝到目标图像中。 为了加快操作,我们使用图像的数据块进行操作。(在.NET中对应的大概是Bitmap.LockBits)
代码如下:
void StretchBltFast(CImage * pDest, int xDest, int yDest, int cxDest, int cyDest,
CImage * pSrc, int xSrc, int ySrc, int cxSrc, int cySrc)
{
int i,j,k;
LPBYTE pBitsSrc = (LPBYTE)(pSrc -> GetBits()); // 数据块起始位置
LPBYTE pBitsDest = (LPBYTE)(pDest -> GetBits()); // 数据块起始位置
LPBYTE pixAddrSrc = pBitsSrc;
LPBYTE pixAddrDest = pBitsDest;
int strideSrc = pSrc -> GetPitch(); // pitch有时为负
int strideDest = pDest -> GetPitch();
int bytesPerPixelSrc = pSrc -> GetBPP() / 8 ;
int bytesPerPixelDest = pDest -> GetBPP() / 8 ;
for (j = 0 ; j < cyDest; j ++ )
{
for (i = 0 ; i < cxDest; i ++ )
{
pixAddrSrc = pBitsSrc + (j * cySrc / cyDest) * strideSrc + (i * cxSrc / cxDest) * bytesPerPixelSrc;
pixAddrDest = pBitsDest + strideDest * j + i * bytesPerPixelDest;
// 复制当前像素
for (k = 0 ; k < bytesPerPixelDest; k ++ , pixAddrDest ++ )
{
* pixAddrDest = * pixAddrSrc;
// 是否可以移动到下一个通道?
if (k < bytesPerPixelSrc - 1 ) pixAddrSrc ++ ;
}
}
}
}
使用上面的重采样方法和GDI的StretchBlt方法绘制的图像效果如下:(可见重采样方法的效果是要好过StretchBlt)
补充一些其他讨论:
(1)MSDN中提到CImage可以使用32bpp的图片进行 alpha 合成, 即使用第四个通道作为每个像素的 alpha 值。本质上是通过调用 AlphaBlend 来实现的。根据 AlphaBlend 函数的要求,在绘制(draw)前必须预先把alpha通道应用到位图的RGB通道上(RGB*Alpha/255); 绘制结果如下所示:
预先应用alpha通道将改变RGB通道中的数据,代码如下所示:
void PreMultiplied(CImage * pImg)
{
int i, j;
LPBYTE pPixel;
// 必须是32bpp
if (pImg -> GetBPP() != 32 )
return ;
LPBYTE pBytes = (LPBYTE)pImg -> GetBits();
int stride = pImg -> GetPitch();
int width = pImg -> GetWidth();
int height = pImg -> GetHeight();
for (j = 0 ; j < height; j ++ )
{
for (i = 0 ; i < width;i ++ )
{
pPixel = pBytes + j * stride + i * 4 ;
pPixel[ 0 ] = (BYTE)((UINT)pPixel[ 0 ] * pPixel[ 3 ] / 0xff );
pPixel[ 1 ] = (BYTE)((UINT)pPixel[ 1 ] * pPixel[ 3 ] / 0xff );
pPixel[ 2 ] = (BYTE)((UINT)pPixel[ 2 ] * pPixel[ 3 ] / 0xff );
}
}
}
在上图中,左上角是一个32bpp的图像绘制结果。 在下方我分别绘制了 0,1,2,3 每个通道的图像(转变成灰度图像),其中RGB通道是在应用Alpha通道前的数据。(可以事先创建一个24bpp的同等大小图像去接收某个通道数据)
void GetChannel(CImage * pDest, CImage * pSrc, int channel)
{
int i, j, k;
LPBYTE pPixelDest, pPixelSrc;
LPBYTE pBytesDest = (LPBYTE)pDest -> GetBits();
LPBYTE pBytesSrc = (LPBYTE)pSrc -> GetBits();
int strideDest = pDest -> GetPitch();
int strideSrc = pSrc -> GetPitch();
int width = pDest -> GetWidth();
int height = pSrc -> GetHeight();
int bppDest = pDest -> GetBPP();
int bppSrc = pSrc -> GetBPP();
for (j = 0 ; j < height; j ++ )
{
for (i = 0 ; i < width;i ++ )
{
pPixelSrc = pBytesSrc + j * strideSrc + i * bppSrc / 8 + channel;
pPixelDest = pBytesDest + j * strideDest + i * bppDest / 8 ;
for (k = 0 ; k < bppDest / 8 ; k ++ , pPixelDest ++ )
{
* pPixelDest = * pPixelSrc;
}
}
}
}
本文参考以下资料:
(1) CTreeCtrl和CListCtrl复杂控件的综合使用 ;