65.9K
CodeProject 正在变化。 阅读更多。
Home

FreeImage 显示演示

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.98/5 (19投票s)

2008年2月26日

公共领域

17分钟阅读

viewsIcon

99562

downloadIcon

10785

如何使用FreeImage在MFC SDI应用程序中显示位图。考虑了各种重缩放算法。

fi_test_demo.gif

引言

本文灵感来自另一篇Code Project文章 - 使用FreeImage加载、显示和转换各种文件格式,作者是 Markus Loibl

本文的主要目的是回答一个简单的问题:“如果我使用FreeImage库,如何在我的MFC应用程序中显示位图?”

在Windows下使用FreeImage库时,这仍然是一个关键问题。它甚至被发布在 FreeImage FAQ页面上。

使用代码

Markus Loibl的文章围绕使用StretchDIBitsSetDIBitsToDevice函数显示位图的想法。我的想法是改用CreateDIBSection函数。

这是在书籍《Windows编程/Charles Petzold. 第5版. 1998》中描述的CreateDIBSection位图显示方法的进一步发展。在该书中,CreateDIBSection函数用于显示BMP文件 - 我只是将这个想法扩展到了FreeImage库。碰巧这是可能的,因为FIBITMAP结构在程序上恰好与BMP内部文件结构完全相同!因此,FreeImage库的理念是读取任何流行的图形格式位图,并将其解包到内存中,作为“BMP内部文件结构”(例如,“FIBITMAP”)。

使用CreateDIBSection将比任何其他位图显示选择都有许多优点 - 因为我们现在处理的是DIB段 - 而不是处理DIB或DDB。

代码的主要部分非常简单。我们将图形文件加载到FIBITMAP结构中,并将指向其BITMAPINFO头的指针传递给CreateDIBSection函数。该函数创建一个空的HBITMAP并返回它。

我们将这个空的HBITMAP GDI结构附加到一个CBitmap MFC对象上,并将后者选入一个先前创建的内存设备上下文。

在我们使用CreateDIBSection提供的m_pBits指针将位图数据从我们的FIBITMAP结构复制到CreateDIBSection分配的内存缓冲区后 - 最后将我们准备好的CBitmap位图块blit到屏幕上。就这样。

//CMyDoc::DisplayBitmap()

FIBITMAP* m_dib;
BYTE* m_pBits;
CBitmap m_OffscreenBitmap, *m_pOldBitmap;
CDC m_dcOffscreen;
BITMAPINFO* m_pbi;
CString FileName; // containing the path of the to-be-opened file

CClientDC dc (NULL);

m_dcOffscreen.CreateCompatibleDC(&dc);

m_dib = GenericLoader(FileName.GetBuffer(FileName.GetLength()), 0);
// This function is described in the FreeImage PDF Help - see there the "FreeImage_GetFileType"
// function description

m_pbi = FreeImage_GetInfo(m_dib);

HBITMAP hBitmap = CreateDIBSection (NULL, m_pbi, DIB_RGB_COLORS, (void**)&m_pBits, NULL, 0);

m_OffscreenBitmap.Attach(hBitmap);

m_pOldBitmap = m_dcOffscreen.SelectObject(&m_OffscreenBitmap);
	
// Fill the DIB Section bitmap buffer (e.g. content to be displayed)
	
CopyMemory(m_pBits, FreeImage_GetBits(m_dib),
	FreeImage_GetPitch(m_dib) * FreeImage_GetHeight(m_dib));
...
 
//CMyScrollView::OnDraw(CDC* pDC)

CRect rcClip;

pDC->GetClipBox(rcClip);
  
pDC->BitBlt(rcClip.left, rcClip.top, rcClip.Width(), rcClip.Height(),
&pDoc->m_dcOffscreen, rcClip.left, rcClip.top, SRCCOPY);

关注点

演示程序的源代码注释很详细。一些有趣的要点是:

  • 不使用CDC::StretchBlt函数进行缩放。程序使用FreeImage_Rescale函数 - 因为它更灵活,质量更高。提示:切勿使用所有这些标准的“拉伸”函数!否则,您将获得糟糕质量的缩放图像。
  • 支持拖放
  • 主程序窗口以最大化模式打开(尽管这是一个SDI应用程序 - 这可能会有问题)。
  • 启用了双缓冲和无闪烁位图滚动

此演示程序已尽可能保持简单。因此,程序的性能尚未达到最佳。一些改进的方法是:

  • FreeImage_Rescale替换为一个类似的函数 - 该函数还可以接受DIB段位的指针并用重采样图像填充它 - 而不是简单地返回一个新的FIBITMAP。这种方法可以消除过度的位复制(从FIBITMAP到DIB段位的缓冲区),从而显著减少缩放时间。现在缩放过程太长(这是完全不合适的)。
  • 为显示的位图创建一个子窗口 - 而不是像现在这样直接显示在CMyScrollView中。这将允许无闪烁缩放 - 因为程序不会遇到确定图像和背景在哪里(像现在这样)的问题。然而,图像在缩放过程中会闪烁(这也完全不合适)。

版本 2.0

此版本有2项主要改进:

  • 引入了背景内存设备上下文和背景位图。图像在缩放过程中不再闪烁。图像和背景现在清晰区分。
  • FreeImage_Rescale函数已按版本1.0中计划的方式替换。

使用代码

1. 新的背景对象

//CMyDoc::DisplayBitmap()

CDC m_dcBitmapHolder;  // a new memory DC - to hold our image
CPoint m_OffscreenSize;     // background bitmap size
CBitmap m_Bitmap, *m_pOldBitmap; // a new CBitmap - to hold our image

CClientDC dc (NULL);

m_dcBitmapHolder.CreateCompatibleDC(&dc); // a new CDC - to hold our image

HBITMAP hBitmap = CreateDIBSection (NULL, m_pbi, DIB_RGB_COLORS, (void**)&m_pBits, NULL, 0) ;
	
if (hBitmap == NULL) return false;
	
m_Bitmap.Attach(hBitmap); // our image in a MFC CBitmap

// Put the image into the CDC-holder
m_pOldBitmap = m_dcBitmapHolder.SelectObject(&m_Bitmap);

// Find out the maximum viewable sizes - for the future background bitmap
// Choose max between client area sizes and zoomed image sizes
m_OffscreenSize.x = max(m_view_init_width, m_zdib_width);	
m_OffscreenSize.y = max(m_view_init_height, m_zdib_height);
	
// Create a background bitmap
// ATTENTION: It is made here compatible with CDC-holder!!!
m_OffscreenBitmap.CreateCompatibleBitmap(&m_dcBitmapHolder,
    m_OffscreenSize.x, m_OffscreenSize.y);

// Select the background bitmap into the background CDC
m_pOldBtmp = m_dcOffscreen.SelectObject(&m_OffscreenBitmap);
	
// Fill the background bitmap with a backcolor
m_dcOffscreen.FillSolidRect(0, 0, m_OffscreenSize.x,
    m_OffscreenSize.y, RGB(160, 160, 160));
	
// Put the image-bitmap onto the center of the background bitmap
// (all this - in the memory device context yet)
m_dcOffscreen.BitBlt((m_OffscreenSize.x - m_zdib_width)/2,		
(m_OffscreenSize.y - m_zdib_height)/2, m_zdib_width, m_zdib_height,
    &m_dcBitmapHolder, 0, 0, SRCCOPY);
...

总体思路很简单:我们创建一个背景位图 - 大小为可视图形区域的最大值 - 并将其选入一个新的(第二个)内存设备上下文。然后,我们将图像从旧的内存设备上下文blit到新的设备上下文 - 然后我们在那里将其用作旧CDC的替代品。

我从 WinDjView Project 的源代码中借用了这项技术。它允许轻松地“擦除”背景而不会闪烁。我称之为“双重内存”(Double memoring) - 因为使用2个内存设备上下文来显示一个图像(而不是版本1.0中的一个CDC)。

这种方法有一个性能缺陷 - 它需要额外的一次显示图像的位图blit。这发生在缩放过程中。尽管如此,我认为完全消除缩放闪烁的可能性是值得的。

2. 重新安排的重采样函数

//CMyDoc::DisplayBitmap()

// Calculate the zoomed image width
m_zdib_width = (int)(FreeImage_GetWidth(m_dib)/zoom);
	
// Calculate the zoomed image height
m_zdib_height = (int)(FreeImage_GetHeight(m_dib)/zoom);
	
// Allocate the "empty" FIBITMAP - just as a BITMAPINFO holder
m_dib2 = FreeImage_AllocateT(FreeImage_GetImageType(m_dib),
    1, 1, // this is a one-by-one pixel sized bitmap ("empty")
    bpp_dst,
    FreeImage_GetRedMask(m_dib),
    FreeImage_GetGreenMask(m_dib),
    FreeImage_GetBlueMask(m_dib));

if (!m_dib2) return false;	

m_pbih = FreeImage_GetInfoHeader(m_dib2);	

m_pbih->biWidth = m_zdib_width;  // replace 1 by the real rescaled width

m_pbih->biHeight = m_zdib_height;  // replace 1 by the real rescaled height

m_pbi = FreeImage_GetInfo(m_dib2);  // now we have a full-fledged
// BITMAPINFO structure for our rescaled image

...

// Fill the DIB Section bitmap buffer (e.g. content to be displayed)

// This function is remade out of FreeImage_Rescale. 
// It rescales a src FIBITMAP placing the result into the provided 
// BYTE* m_pBits buffer
	
FI_Rescale2(m_dib, m_zdib_width, m_zdib_height,
   
    FILTER_BILINEAR, // the best choice here probably
		
    bpp_dst, m_pBits);
...		

这里的主要思想是将目标FIBITMAP分成两部分:头(BITMAPINFO结构)和数据位(m_pBits指针)。

这样做是为了消除重缩放函数返回后的一次额外位复制。

我重新排列了标准的FreeImage_Rescale函数 - 使其接受一个m_pBits指针 - 函数直接将重缩放后的图像写入其中。为此,我不得不深入研究FreeImage_Rescale - 请参阅项目源代码。

首先,我们创建一个“空”的伪FIBITMAP(大小为1x1,但我们在其中写入实际位图的宽度和高度) - 它仅作为CreateDIBSection函数的BITMAPINFO容器。

然后,我们调用FI_Rescale2函数 - 它使用m_pBits指针填充CreateDIBSection分配的缓冲区 - 这是该函数的参数之一。

关于ver.2的结论

1. 演示程序现在可以完全无闪烁地工作 - 无论从哪个方面来看。

2. 引入FI_Rescale2函数实际上并没有带来任何显著的缩放性能提升。问题在于FILTER_BILINEAR重采样方法本身就太慢了。它应该被替换为更快的算法(但不是丑陋的FILTER_BOX)。

3. 我已将默认项目配置从Debug更改为Release。原因是FI_Rescale2函数在Debug配置中运行速度极慢 - 很可能是由于一些不明显的、耗时的ASSERT检查。

版本 2.1

此子版本实现了另一种重缩放模式:**PNM固定缩放**。

这是 NetPBM Project 的重缩放算法。我从 WinDjView Project 的源代码(Scaling.cpp)中获取了其源代码,并将其应用于FreeImage库。目前它只支持1位、8位和24位位图缩放 - 尽管它可以轻松修改以支持所有FreeImage支持的位深度。

在WinDjView程序中,此重缩放算法是可选的 - 当选中“低缩放级别的锐化缩放(较慢)”复选框时使用。通常,WinDjView使用由DjVuLibre提供的自然重缩放算法。

PNM固定重缩放算法比FreeImage的Bilinear(甚至Box)重缩放算法快得多 - 并提供高质量的缩放图像。

但是它有一个很大的缺陷:它在大的缩放图像的缩放值时会卡住。我怀疑该算法只是内存消耗过大。

版本2.1中的演示程序包含3种缩放模式:PNM、Bilinear和Box。您可以直观地比较所有这3种算法的性能。

关于ver.2.1的结论

PNM固定缩放算法对于一个好的位图显示程序来说不幸地完全不合适。而Bilinear和Box算法(来自FreeImage)对于这个目的来说更是完全不合适。

现在的目标是 - 找到并实现一个好的、**快速**、低内存消耗和高质量的位图重缩放算法。

版本 2.2

此子版本实现了另外两种重缩放算法:**线性插值**和**最近像素采样**。我从 Leptonica Project 的源代码(scalelow.c)中获取了它们的源代码,并将其应用于FreeImage库。目前它们只支持1位、8位和24位位图缩放 - 尽管可以轻松修改以支持所有FreeImage支持的位深度。

这些重缩放算法比**PNM固定缩放**(来自上一个子版本)快约两倍。但不幸的是,它们几乎不平滑重缩放后的图像。它们也会在大缩放时卡住(与其他算法一样) - 但好消息是,它们实际卡住的缩放级别比PNM固定缩放高得多。

我还找到了维基百科上的一篇相关文章 - 图像缩放

关于ver.2.2的结论

这两种新的缩放算法对于一个好的位图显示程序来说也最有可能不合适。但它们已经**看起来像**我正在寻找的算法了。如果它们能像PNM固定缩放算法那样平滑目标图像,并且在大缩放时不会卡住 - 那就完美了。

因此,对一个好的重缩放算法的搜索仍在继续。

版本 3.0

此版本将缩放时的单次位图blit带回来 - 但已达到新的水平。重缩放后的图像现在直接写入背景位图表面 - 而不是临时位图(稍后blit到背景位图表面)。

这是通过一种棘手的方式完成的:每个重缩放的扫描线分别写入背景位图的指定位置(大致在中心)。

这种方法消除了另一次整个图像复制 - 这绝对提高了缩放过程的速度。

我选择了上一个版本中最快的重缩放算法 - **最近邻缩放**(来自 Leptonica Project)的源代码。此版本中已删除所有其他重缩放算法(可能暂时)。

关于ver.3.0的结论

如前所述,此版本的重缩放算法有一个缺陷:它不会平滑图像。所以我计划寻找一种额外的、足够好的平滑算法。

另一个问题是,此算法(像所有其他算法一样)在大缩放时会卡住。所以这个问题也需要解决。

尽管如此,此版本在研究领域取得了更多进展。

版本 3.1

此子版本带回了所有先前提到的重缩放算法 - 但应用于缩放时的单次位图blit(首次在ver.3.0中引入)。它还引入了一种新的重缩放算法:**低通滤波** - 取自 Leptonica Project 的源代码。

您可以直观地比较它们各自的质量和性能。我猜其中一些算法在逻辑上甚至会相互翻倍 - 因为它们都来自不同的来源。其中一些速度快,一些视觉质量高 - 但不是两者兼备。

关于ver.3.1的结论

主要改进目标仍未达到 - 最佳的“速度+质量”组合尚未找到。但现在比以前更容易寻找了 - 我们现在有了很多现成的材料可以深入研究。

版本 3.2

此子版本引入了一种新的重缩放算法 - **Areamap** 重缩放。它来自 Leptonica Project 的源代码,并应用于FreeImage。

该算法既快速又具有出色的视觉质量。是的,它不如**最近邻**算法快 - 但比**Bilinear**算法快得多 - 并且提供几乎相同的视觉质量。

但是它有一个很大的缺陷:当位图逐级最大化时 - 在某个时刻,您会看到一个由垂直和水平线组成的网格突然出现在整个图像上。

我猜这只是一个实现不佳的算法 - 可能相应的数学算法足够好,并且可以很好地实现。

关于ver.3.2的结论

如果不是上述缺陷 - 这将是我正在寻找的算法。足够快且视觉效果出色。但是! - 仍然无能为力......所以搜索仍在继续。

版本 3.3

这里没什么特别的。只是修复了一个bug。我必须将显示的图片位图对齐到背景位图的6字节边界(顺便说一句,我不知道为什么,这是通过实验发现的)。

我过去将其对齐到中心右侧 - 这在许多图像上导致了内存访问异常(所有早期的ver.3.x都受此影响)。现在我将其对齐到中心左侧 - 这可以防止错误的内存访问(在较大的缩放级别)。

我向本文添加了一个“有用链接”部分。

版本 3.4

此子版本引入了一种新的重缩放算法。它只是**Areamap**和**Linear**重缩放算法的组合(两者均取自 Leptonica Project 的源代码并应用于FreeImage)。

缩小后的图像使用**Areamap**显示,放大后的图像使用**Linear**显示。这些算法是即时切换的 - 当用户旋转鼠标滚轮越过1.0缩放阈值时,无论朝哪个方向。我称这种“算法”为**AreaLinear**(只是给它起个名字)。

我为什么要这样做?答案是:**速度**。**AreaLinear**相当快 - 但它也提供了足够的光滑度 - 无论缩放值如何。所以这是所有先前考虑过的算法中最好的折衷。

Areamap和**Linear**在平滑位图方面的差异非常小,几乎在视觉上无法察觉。如果您不知道实际上存在2种重缩放算法,而不是只有一种 - 您不会注意到。在此子版本中,我在状态栏上显示当前的缩放值 - 因此您可以自己观察一下,当您越过1.0缩放阈值时,平滑度在视觉上变化多么小 - 主要在某些24位彩色图像上才能勉强看到。

是的,标准的FreeImage重缩放算法提供了更好的平滑质量。但它们速度极慢。主要原因是它们都是两阶段算法,而所有 Leptonica 重缩放算法都是单阶段的。

我尝试寻找更快的两阶段重缩放算法的实现。我 lấy 了 Image Resampler 的源代码并将其应用于FreeImage。这是Dale Schumacher通过Ray Gardener修订的Graphic Gems III Filtered Image Rescaling的代码,Daylon Graphics Ltd. 请参阅我源代码中的FilterRcg.cpp。然后我 lấy 了 Dale Schumacher 的Graphic Gems III Filtered Image Rescaling的原始代码 - 请参阅我源代码中的FilterRcg2.cpp

不幸的是,这两种实现都有质量问题。缩放时会产生各种视觉瑕疵。我努力修复它们 - 但尚未成功(尽管我已经消除了大约一半的瑕疵)。

至少有一点是清楚的:所有这些两阶段算法的实现(包括FreeImage中的)基本上都是相同的 - Paul Heckbert,用于缩放光栅图像的C代码,带漂亮的滤波。因此,很难期望通过“找到更好的实现”来获得一些加速。

关于ver.3.4的结论

我认为引入的**AreaLinear**重缩放模式已经足够好了 - 可以用于实际的基于FreeImage的MFC图像查看器。唯一尚未解决的问题是 - 它像所有其他算法一样,在大缩放时会卡住。这个问题仍有待解决。

**AreaLinear**有一个小缺陷:在非常小的缩放级别下,图像会出现一些视觉瑕疵 - 它们看起来像边框上的黑色锯齿。但这不是很恼人。

此外,寻找更快的两阶段重缩放算法的实现是一个好主意。但这现在(不像以前那样)不那么关键了 - **AreaLinear**已经是一个足够好的FreeImage位图查看器基础。

4.0 版本

此版本带来了多线程位图显示机制。它使用一个用户界面MFC线程(它接受消息并拥有自己的消息循环)。

当打开一个位图文件时,会启动一个新线程。当文件关闭时,该线程会关闭。该线程在屏幕上显示位图。

进行这种多线程处理的原因是为了使缩放更加视觉化舒适。当用户旋转鼠标滚轮时,位图图像会分多步重绘,反映中间的缩放值。而之前只显示最终缩放的图像 - 缩放过程看起来更卡顿。现在它看起来更“流畅”。

线程的构造函数在线程创建时接受一些参数。其中两个最有趣:指向CMainFrame对象的句柄和指向CSrollView对象的指针。句柄用于向主程序窗口发送控制消息,指针用于控制CSrollView派生视图窗口中的位图显示。更多细节请参见源代码 - 它们像往常一样注释得很详细。

我还对**AreaLinear**重缩放算法进行了少量优化,使其对黑白位图工作 - 现在与它们一起工作时比以前明显更快(视觉上)。6字节边界对齐被替换为3字节对齐 - 并且只为彩色图像保留(对于其他彩色模式则不必要)。

版本 5.0

此版本解决了“大缩放卡死”问题。为了实现这一点,我不得不再次重新设计图像显示机制。

在之前的版本中,图像被放置在CDC内存设备上下文中,该上下文的大小会随着每次新的缩放而改变 - 以包含最大的可能滚动范围。这种方法很简单 - 但会产生卡死,因为缩放级别越大,CDC对象创建得越大 - 并且在某个点,任何进一步的大规模缩放都变得不可能。

为了解决这个问题,我将CDC对象的大小固定 - 例如,大小为客户区域 - 无论缩放值如何。图像现在被精确裁剪到CDC对象的大小,结果是不会再出现大规模缩放卡死。

为了补偿CDC对象的固定(而非可变)大小 - 我引入了一个新的CDC辅助对象 - 用于仅显示图像滚动后出现的那些部分。这个新的CDC辅助对象的大小可变 - 它会在每次滚动移动时创建,并根据当前无效矩形的大小进行调整。

此外,此版本将最大滚动大小限制为32767 - 以考虑Win98的最大滚动大小限制。

再一次,我为黑白位图稍微优化了**AreaLinear**重缩放算法 - 现在它与B/W一起工作的速度比上一个版本更快(视觉上)。顺便说一句,所有其他重缩放算法在此版本中都已移除(可能暂时)。

关于ver.5的结论

此版本已经看起来像“真实图像查看器”的原型了。但我特别强调 - 它还不是一个可供使用的程序。许多问题仍然存在。例如,在极大的缩放级别滚动时存在一些小的视觉瑕疵。

参考文献

  1. 使用FreeImage加载、显示和转换各种文件格式
  2. 电子书《Windows编程/Charles Petzold. 第5版. 1998》(CHM 英文)
  3. 电子书《MFC Windows编程/Jeff Prosise. 第2版. 1999》(CHM 英文)
  4. 电子书《Windows图形编程:Win32 GDI和DirectDraw/Feng Yuan. 2000》(CHM 英文)
  5. WinDjView GPL项目源代码
  6. 图像缩放 - Wikipedia
  7. 通用缩放 - Leptonica

有用链接

  1. Delphi的位图重采样插值
  2. Graphics Gems 存储库
  3. 图像重采样器
  4. Google回答:缩放图像的算法
注意:所有提到的电子书都可以在互联网上免费下载。

历史

版本1.0. 发布于2008年2月26日。

版本2.0. 发布于2008年3月2日。

版本2.1. 发布于2008年3月5日。

版本2.2. 发布于2008年3月6日。

版本3.0. 发布于2008年3月9日。

版本3.1. 发布于2008年3月10日。

版本3.2. 发布于2008年3月13日。

版本3.3. 发布于2008年3月14日。

版本3.4. 发布于2008年3月17日。

版本4.0 发布于2008年4月3日。

版本5.0 发布于2008年6月4日。

© . All rights reserved.