[非原创] 用于将真彩色图像降级为索引图像的八叉树算法

简介:     本文介绍的内容,是用八叉树法降级一个真彩色图像(BPP=16以上)。这也是某公司在今年校园招聘中的笔试中的最后一道题目。我参考了 Jeff Prosise 所写的这篇文章(见参考资料),然后修改了他提供的范例的源码,使能够更好的演示该算法,同时我也添加了绘制八叉树的代码,可以直观的看到八叉树的形态。

    本文介绍的内容,是用八叉树法降级一个真彩色图像(BPP=16以上)。这也是某公司在今年校园招聘中的笔试中的最后一道题目。我参考了 Jeff Prosise 所写的这篇文章(见参考资料),然后修改了他提供的范例的源码,使能够更好的演示该算法,同时我也添加了绘制八叉树的代码,可以直观的看到八叉树的形态。

    索引图像的尺寸比真彩色图像可以大幅降低,保存为256色索引图像大约只有真彩色图像的1/3 (bpp = 24) 或 1/4 (bpp = 32),因为表示每个像素的数据从3或4个字节减少到1个字节。如果保存为16色索引图像则还能减小一半尺寸,不过16色图像的质量相比真彩图像实在过低,一般除非硬件限制(比如TC那样的图形模式下),在现在的条件下是很少再会用到的。它可以用在需要降级图像质量的场合,例如,如果我们生成一个图标,根据提供真彩色图像,然后根据此图像创建出的其他质量等级的图标图像。

 

    【原创性声明】

    本文提到的算法和原始范例的源码来自参考文献。以下部分属于我增加的代码:

    (a)对八叉树的绘制以及生成动画;

    (b)在不需要修改显示器模式的条件下展示八叉树算法生成的索引图像(原范例需要先把显示器调整为256色模式才能看到效果);

    (c)把真彩色位图通过八叉树算法保存为 BMP 索引位图文件。

 

    对该算法的介绍请参考 Jeff Prosise 的文档,是英文的,那篇文章中讲解了该算法的核心思想,我在这里不做重复介绍。根据文档的内容,八叉树算法最早是在1988, M. Gervautz 和 W. Purgathofer 发表的论文《A Simple Method for Color Quantization: Octree Quantization.》中首次介绍的。这个算法非常简洁优美,其优美之处在于从一副具有很多色彩的图像中,提取出最能完美表达该图像颜色的调色板的过程。这个过程就是一个八叉树的生长和节点合并的过程。该算法的最大优点是效率高,占用内存少(仅需要不超过 颜色数量+1 个节点,加上一些中间节点所占用的内存),选出的调色板最合理,显示效果最好。比流行方法(扫描图像然后取最多像素数量的颜色组成调色板)好很多。下图就是从我修改后的范例的展示出来的结果而来,我重新排列了一下图像:

 

    

 

    为了我想要的展示,我调整了原来范例的代码,提供了一个根据真彩色位图,创建出八叉树的代码,该代码主要来源于原来的范例,该算法的核心是用RGB通道的相应的位组成子结点的索引(从高位开始提取RGB分量中的位),来让树生长。遍历图像的像素,根据当前像素的RGB分量导航到一个叶子节点(如果不存在则创建它),然后把当前像素的RGB分量累加到该节点的相应Sum值,并使节点代表的像素数量递增。每当叶子节点数量(代表一种颜色)超出最大颜色数量时,就选出一个最深的非叶子节点,将其收缩为叶子节点,最终的颜色值是用RGB分量的总和除以该节点代表的像素数量来得到的。可见,在多个相近的不同颜色降级成用同一个调色板颜色来表示时,调色板颜色是在它们之间取了一个加权平均(这种加权平均使原图中的一些“平滑渐变”变成了色带,例如上图中女模特面部的“等高线”效果),以达到表达图像本质的最佳效果。该算法中不需要统计整个图像的直方图也不需要排序,所以不会产生较大的内存需求。尽管真彩色图像可能具有大量的像素,大量的颜色数量,但它的颜色信息被集中统计在一颗较小的树的叶子节点上,该树的动态调整策略使得它总是反应着当前时刻的最佳选择,遍历像素时,结果以非常完美的方式进行“积累”(你不必担心统计结果被覆盖而不得不申请更多独立空间),这正是算法的优美之处。(代码中引用到的某些辅助方法在此省略):

 

 

code_BuildOctree
 
  
// 根据一个图像,创建一个八叉树,hoodlum1980 add;
NODE * BuildOctree(HANDLE hImage, UINT nMaxColors, UINT nColorBits)
{
DIBSECTION ds;
int i, j, nPad;
BYTE
* pbBits;
WORD
* pwBits;
DWORD
* pdwBits;
DWORD rmask, gmask, bmask;
int rright, gright, bright;
int rleft, gleft, bleft;
BYTE r, g, b;
WORD wColor;
DWORD dwColor;
NODE
* pTree;
UINT nLeafCount, nIndex;
NODE
* pReducibleNodes[ 9 ];

// Initialize octree variables
g_nMaxPixelCount = 0 ;

pTree
= NULL;
nLeafCount
= 0 ;
if (nColorBits > 8 ) // Just in case
return NULL;
for (i = 0 ; i <= ( int ) nColorBits; i ++ )
pReducibleNodes[i]
= NULL;

// Scan the DIB and build the octree
GetObject (hImage, sizeof (ds), & ds);
nPad
= ds.dsBm.bmWidthBytes - (((ds.dsBmih.biWidth *
ds.dsBmih.biBitCount)
+ 7 ) / 8 );

switch (ds.dsBmih.biBitCount) {

case 16 : // One case for 16-bit DIBs
if (ds.dsBmih.biCompression == BI_BITFIELDS) {
rmask
= ds.dsBitfields[ 0 ];
gmask
= ds.dsBitfields[ 1 ];
bmask
= ds.dsBitfields[ 2 ];
}
else {
rmask
= 0x7C00 ;
gmask
= 0x03E0 ;
bmask
= 0x001F ;
}

rright
= GetRightShiftCount (rmask);
gright
= GetRightShiftCount (gmask);
bright
= GetRightShiftCount (bmask);

rleft
= GetLeftShiftCount (rmask);
gleft
= GetLeftShiftCount (gmask);
bleft
= GetLeftShiftCount (bmask);

pwBits
= (WORD * ) ds.dsBm.bmBits;
for (i = 0 ; i < ds.dsBmih.biHeight; i ++ ) {
for (j = 0 ; j < ds.dsBmih.biWidth; j ++ ) {
wColor
= * pwBits ++ ;
b
= (BYTE) (((wColor & (WORD) bmask) >> bright) << bleft);
g
= (BYTE) (((wColor & (WORD) gmask) >> gright) << gleft);
r
= (BYTE) (((wColor & (WORD) rmask) >> rright) << rleft);
AddColor (
& pTree, r, g, b, nColorBits, 0 , & nLeafCount,
pReducibleNodes);
while (nLeafCount > nMaxColors)
ReduceTree (nColorBits,
& nLeafCount, pReducibleNodes);
}
pwBits
= (WORD * ) (((BYTE * ) pwBits) + nPad);
}
break ;

case 24 : // Another for 24-bit DIBs
pbBits = (BYTE * ) ds.dsBm.bmBits;
for (i = 0 ; i < ds.dsBmih.biHeight; i ++ ) {
for (j = 0 ; j < ds.dsBmih.biWidth; j ++ ) {
// *pbBits = 0xff;
b = * pbBits ++ ;
g
= * pbBits ++ ;
r
= * pbBits ++ ;
AddColor (
& pTree, r, g, b, nColorBits, 0 , & nLeafCount,
pReducibleNodes);
while (nLeafCount > nMaxColors)
ReduceTree (nColorBits,
& nLeafCount, pReducibleNodes);
}
pbBits
+= nPad;
}
break ;

case 32 : // And another for 32-bit DIBs
if (ds.dsBmih.biCompression == BI_BITFIELDS) {
rmask
= ds.dsBitfields[ 0 ];
gmask
= ds.dsBitfields[ 1 ];
bmask
= ds.dsBitfields[ 2 ];
}
else {
rmask
= 0x00FF0000 ;
gmask
= 0x0000FF00 ;
bmask
= 0x000000FF ;
}

rright
= GetRightShiftCount (rmask);
gright
= GetRightShiftCount (gmask);
bright
= GetRightShiftCount (bmask);

pdwBits
= (DWORD * ) ds.dsBm.bmBits;
for (i = 0 ; i < ds.dsBmih.biHeight; i ++ ) {
for (j = 0 ; j < ds.dsBmih.biWidth; j ++ ) {
dwColor
= * pdwBits ++ ;
b
= (BYTE) ((dwColor & bmask) >> bright);
g
= (BYTE) ((dwColor & gmask) >> gright);
r
= (BYTE) ((dwColor & rmask) >> rright);
AddColor (
& pTree, r, g, b, nColorBits, 0 , & nLeafCount,
pReducibleNodes);
while (nLeafCount > nMaxColors)
ReduceTree (nColorBits,
& nLeafCount, pReducibleNodes);
}
pdwBits
= (DWORD * ) (((BYTE * ) pdwBits) + nPad);
}
break ;

default : // DIB must be 16, 24, or 32-bit!
return NULL;
}

// 现在第二次遍历像素,把它复成调色板的值~
// 得到调色板
nIndex = 0 ;
GetRGBQUADs (pTree, g_pColors,
& nIndex);
g_nColorCount_Real
= nIndex; // 实际调色板数量(小于等于最大颜色数量)

return pTree;
}

void AddColor (NODE ** ppNode, BYTE r, BYTE g, BYTE b, UINT nColorBits,
UINT nLevel, UINT
* pLeafCount, NODE ** pReducibleNodes)
{
int nIndex, shift;
static BYTE mask[ 8 ] = { 0x80 , 0x40 , 0x20 , 0x10 , 0x08 , 0x04 , 0x02 , 0x01 };

// If the node doesn't exist, create it
if ( * ppNode == NULL)
* ppNode = CreateNode (nLevel, nColorBits, pLeafCount,
pReducibleNodes);

// Update color information if it's a leaf node
if (( * ppNode) -> bIsLeaf) {
(
* ppNode) -> nPixelCount ++ ;
(
* ppNode) -> nRedSum += r;
(
* ppNode) -> nGreenSum += g;
(
* ppNode) -> nBlueSum += b;

if (( * ppNode) -> nPixelCount > g_nMaxPixelCount)
g_nMaxPixelCount
= ( * ppNode) -> nPixelCount;
}

// Recurse a level deeper if the node is not a leaf
else {
shift
= 7 - nLevel;
nIndex
= (((r & mask[nLevel]) >> shift) << 2 ) |
(((g
& mask[nLevel]) >> shift) << 1 ) |
((b
& mask[nLevel]) >> shift);
AddColor (
& (( * ppNode) -> pChild[nIndex]), r, g, b, nColorBits,
nLevel
+ 1 , pLeafCount, pReducibleNodes);
}
}

NODE
* CreateNode (UINT nLevel, UINT nColorBits, UINT * pLeafCount,
NODE
** pReducibleNodes)
{
NODE
* pNode;

if ((pNode = (NODE * ) HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY,
sizeof (NODE))) == NULL)
return NULL;

pNode
-> bIsLeaf = (nLevel == nColorBits) ? TRUE : FALSE;
if (pNode -> bIsLeaf)
(
* pLeafCount) ++ ;
else { // Add the node to the reducible list for this level
pNode -> pNext = pReducibleNodes[nLevel];
pReducibleNodes[nLevel]
= pNode;
}
return pNode;
}

void ReduceTree (UINT nColorBits, UINT * pLeafCount, NODE ** pReducibleNodes)
{
int i;
NODE
* pNode;
UINT nRedSum, nGreenSum, nBlueSum, nChildren;

// Find the deepest level containing at least one reducible node
for (i = nColorBits - 1 ; (i > 0 ) && (pReducibleNodes[i] == NULL); i -- );

// Reduce the node most recently added to the list at level i
pNode = pReducibleNodes[i];
pReducibleNodes[i]
= pNode -> pNext;

nRedSum
= nGreenSum = nBlueSum = nChildren = 0 ;
for (i = 0 ; i < 8 ; i ++ ) {
if (pNode -> pChild[i] != NULL) {
nRedSum
+= pNode -> pChild[i] -> nRedSum;
nGreenSum
+= pNode -> pChild[i] -> nGreenSum;
nBlueSum
+= pNode -> pChild[i] -> nBlueSum;
pNode
-> nPixelCount += pNode -> pChild[i] -> nPixelCount;
HeapFree (GetProcessHeap (),
0 , pNode -> pChild[i]);
pNode
-> pChild[i] = NULL;
nChildren
++ ;
}
}

pNode
-> bIsLeaf = TRUE;
pNode
-> nRedSum = nRedSum;
pNode
-> nGreenSum = nGreenSum;
pNode
-> nBlueSum = nBlueSum;

// 更新节点的最大像素数量。
if (pNode -> nPixelCount > g_nMaxPixelCount)
g_nMaxPixelCount
= pNode -> nPixelCount;

* pLeafCount -= (nChildren - 1 );
}

 

    备注:上面代码中,树节点的定义如下:

    
    typedef struct _NODE {
        BOOL bIsLeaf;               // TRUE if node has no children (是否是叶子节点)
        UINT nPixelCount;           // Number of pixels represented by this leaf (代表的像素数量)
        UINT nRedSum;               // Sum of red components
        UINT nGreenSum;             // Sum of green components
        UINT nBlueSum;              // Sum of blue components (B通道分量总和)
        struct _NODE* pChild[8];    // Pointers to child nodes  (子结点)
        struct _NODE* pNext;        // Pointer to next reducible node

        BYTE nColorIndex;             // 仅对叶子节点有效,表示此节点代表的颜色在调色板中的索引!存储到BMP文件时有用。
    } NODE;

  

    上面的方法返回一个八叉树的根节点指针,然后我们就可以根据这棵树,生成索引图像和调色板,方法是重新遍历图像,根据RGB的值,在树之间向更深的方向导航,直到遇到叶子节点,那么叶子节点上可以计算出该像素在索引图像的中的RGB值。在另存为 BMP 时,这个RGB值被放到索引图像的调色板中。下面的方法,我用一个24BPP的像素数据去模拟出索引图像的显示效果,因此 g_lpBits 是一块24bpp的图像处理,但是我将它设置成降级后的颜色,并在窗口中展示出来:

 

 

Code_CreateOctreeBitmap
 
  
// 生成八叉树降级后的位图
void CreateOctreeBitmap (NODE * pTree, UINT nMaxColors, UINT nColorBits)
{
int bpp = g_BmInfo.bmiHeader.biBitCount;
static BYTE mask[ 8 ] = { 0x80 , 0x40 , 0x20 , 0x10 , 0x08 , 0x04 , 0x02 , 0x01 };

// 现在第二次遍历像素,把它复成调色板的值~
int i, j;
UINT nIndex, nLevel, shift;
NODE
* pNode = NULL;
BYTE
* pbBits, r, g, b;
int stride = (g_BmInfo.bmiHeader.biWidth * bpp + 31 ) / 32 * 4 ;
int nPad = stride - (g_BmInfo.bmiHeader.biWidth * bpp + 7 ) / 8 ;

if (bpp == 24 || bpp == 32 )
{
pbBits
= (BYTE * ) g_lpBits;
for (i = 0 ; i < g_BmInfo.bmiHeader.biHeight; i ++ )
{
for (j = 0 ; j < g_BmInfo.bmiHeader.biWidth; j ++ )
{
// *pbBits = 0xff;
b = * pbBits;
g
= * (pbBits + 1 );
r
= * (pbBits + 2 );

pNode
= pTree;
nLevel
= 0 ;
while ( ! pNode -> bIsLeaf)
{
shift
= 7 - nLevel;
nIndex
= (((r & mask[nLevel]) >> shift) << 2 ) |
(((g
& mask[nLevel]) >> shift) << 1 ) |
((b
& mask[nLevel]) >> shift);
pNode
= pNode -> pChild[nIndex];
nLevel
++ ;
}
// 取颜色
* pbBits = (BYTE)(pNode -> nBlueSum / pNode -> nPixelCount);
* (pbBits + 1 ) = (BYTE)(pNode -> nGreenSum / pNode -> nPixelCount);
* (pbBits + 2 ) = (BYTE)(pNode -> nRedSum / pNode -> nPixelCount);

pbBits
+= (bpp / 8 );
}
pbBits
+= nPad;
}
}
return ;
}

 

     该算法如果选择的颜色数量太低(比如低于4),则很可能最终整个图像只有一种颜色,这是因为算法为了满足不超过颜色数量的要求,在收缩子结点时,使一次收缩多个子结点的(最多可以从 8 个叶子结点收缩成1个叶子节点,即 8 个调色板颜色降低为 1 个),可见这种节点收缩使得调色板颜色数量的变化是“跳跃性”的,因此实际调色板的颜色数量不一定总是等于设置的最大颜色数量,有时实际颜色数量会比设定的最大颜色数量小。所以通常也不能期待八叉树法能生成只有两种颜色数量的二元位图(从某个最低颜色数量开始可能会“突变”到全图都被一种颜色填充)。二元位图通常是通过选定一个阈值去重设像素得到的。

 

    最后是用我的代码绘制出的八叉树效果,绘制是通过递归函数来完成的,这个代码并不复杂,这里我就不贴了。在这里假设每个节点具有一个面向的方向,例如根节点是面向下方生长,其角度就是PI/2。然后向八个角度方向生长出子结点,通过递归调用绘制出整个树,中间节点是空白的圆形,叶子节点代表调色板中的一个颜色,并且用该调色板颜色填充。

    通过调整参数(树枝长短,节点占据的角度等)可以绘制出各种形态的八叉树,在源码中我把代表像素数量较多的节点绘制的更大一些,代表像素数量少的绘制小一些。下面是一些早期绘制出来的八叉树,中间的一副因为设置的子结点角度关系,不能看出根节点在何处。右边的一幅图,因为子结点伸展角度小且树枝较长,所以能很清晰的看出节点在树中的深度。下图中我根据我自己的第一印象分别取了名字,现在再看,左边和右边的也好像鸡冠花,菜花,扇子,中间的好像城市地铁交通图,右侧的叫做裙子是因为感觉好像女生跳舞用的舞裙。我还制作出八叉树的生成和合并过程的动画(有这个想法是因为我曾经看到别人写的八皇后算法的树生成动画),使用菜单View中的查看动画,即可看到八叉树算法过程中树的动态变化过程(为了加快动画过程,这部分功能可能并没有完整遍历图像,因此动画中产生的树可能和查看八叉树的树有所不同)。 

 

    

 

 

    源码下载:    

    最后是我修改后的范例的源码下载连接:(该范例可以把降级后的结果保存为BMP格式的索引图像)

    http://files.cnblogs.com/hoodlum1980/Colors_src.rar

 

    这里简单介绍该范例的使用方法,用菜单打开,打开一个真彩色的BMP位图,然后点击Optimized Palette 菜单可以弹出一个对话框,在该对话框中,设置的第一项 SignicantColorBits 表示八叉树的深度可以最大达到多大(1~8,默认值为6),在颜色被降级以后,由于树的深度会减小,所以该值设置的太大的话也没有什么意义。MaxColorCount 表示索引图像最多的颜色数量(1~256)。其他两个为显示值,一个是实际调色板数量,在保存为BMP索引图像时,我将使用八叉树代表的实际的调色板数量(而不是固定为16或者256)。另一个是表示八叉树中的所有叶子节点中,每个叶子节点都有一个属性:所代表的像素数量,g_nMaxPixelCount 表示的它们中的最大值。在生成索引图像以后,即可看到下方显示出索引图像的视觉效果,右侧是其调色板。在View菜单中点击“重新绘制八叉树图”,即可看到我绘制的八叉树的图像。通过View下面的其他两个菜单切换显示位图或者八叉树图。

    

    这里是原始范例的代码:

    http://www.microsoft.com/msj/archive/s3f1a.htm

    注意,如果要使用这个程序之前,必须先把显示器调整到256色模式(XP中隐藏了低级别模式,需要点击监视器的高级按钮),然后点击Option下面的菜单可以看到不同调色板对应的索引图像效果。另外,这个网址仅仅提供的是源码,需要自己创建 Windows 应用程序工程。

 

    参考资料:

    (1)http://www.microsoft.com/msj/archive/S3F1.aspx

      <<wicked code>> -- Jeff Prosise

 

    维护记录:

    (1)增加八叉树动画演示效果。2010-10-29。

    BYTHEWAY:再添加动画功能时,我无意中犯了一个“GDI 对象泄露”的BUG,由于创建的 HBRUSH 没有全部被 Delete,导致输出的帧数达到 140 以上时,窗口开始显示异常。我画了很大精力才定位到这个 BUG 并将其修复。。。

 

 

 

目录
相关文章
|
4月前
|
算法 索引
【算法】二分算法——山脉数组的峰顶索引
【算法】二分算法——山脉数组的峰顶索引
|
2月前
|
机器学习/深度学习 人工智能 自然语言处理
【MM2024】阿里云 PAI 团队图像编辑算法论文入选 MM2024
阿里云人工智能平台 PAI 团队发表的图像编辑算法论文在 MM2024 上正式亮相发表。ACM MM(ACM国际多媒体会议)是国际多媒体领域的顶级会议,旨在为研究人员、工程师和行业专家提供一个交流平台,以展示在多媒体领域的最新研究成果、技术进展和应用案例。其主题涵盖了图像处理、视频分析、音频处理、社交媒体和多媒体系统等广泛领域。此次入选标志着阿里云人工智能平台 PAI 在图像编辑算法方面的研究获得了学术界的充分认可。
【MM2024】阿里云 PAI 团队图像编辑算法论文入选 MM2024
|
2月前
|
机器学习/深度学习 人工智能 算法
【MM2024】面向 StableDiffusion 的多目标图像编辑算法 VICTORIA
阿里云人工智能平台 PAI 团队与华南理工大学合作在国际多媒体顶级会议 ACM MM2024 上发表 VICTORIA 算法,这是一种面向 StableDiffusion 的多目标图像编辑算法。VICTORIA 通过文本依存关系来修正图像编辑过程中的交叉注意力图,从而确保关系对象的一致性,支持用户通过修改描述性提示一次性编辑多个目标。
|
2月前
|
算法 数据安全/隐私保护
织物图像的配准和拼接算法的MATLAB仿真,对比SIFT,SURF以及KAZE
本项目展示了织物瑕疵检测中的图像拼接技术,使用SIFT、SURF和KAZE三种算法。通过MATLAB2022a实现图像匹配、配准和拼接,最终检测并分类织物瑕疵。SIFT算法在不同尺度和旋转下保持不变性;SURF算法提高速度并保持鲁棒性;KAZE算法使用非线性扩散滤波器构建尺度空间,提供更先进的特征描述。展示视频无水印,代码含注释及操作步骤。
|
3月前
|
算法 数据可视化 数据安全/隐私保护
基于LK光流提取算法的图像序列晃动程度计算matlab仿真
该算法基于Lucas-Kanade光流方法,用于计算图像序列的晃动程度。通过计算相邻帧间的光流场并定义晃动程度指标(如RMS),可量化图像晃动。此版本适用于Matlab 2022a,提供详细中文注释与操作视频。完整代码无水印。
|
4月前
|
算法 前端开发 计算机视觉
基于均值坐标(Mean-Value Coordinates)的图像融合算法的优化实现
基于均值坐标(Mean-Value Coordinates)的图像融合算法的优化实现
43 0
|
4月前
|
自然语言处理 并行计算 算法
基于均值坐标(Mean-Value Coordinates)的图像融合算法的具体实现
基于均值坐标(Mean-Value Coordinates)的图像融合算法的具体实现
47 0
|
4月前
|
算法 Java 索引
LeetCode初级算法题:寻找数组的中心索引+x的平方根+三个数的最大乘积+Leetcode 149:直线上最多的点数 Java详解
LeetCode初级算法题:寻找数组的中心索引+x的平方根+三个数的最大乘积+Leetcode 149:直线上最多的点数 Java详解
37 0
|
5月前
|
存储 算法 Java
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之滑动日志算法问题如何解决
|
5月前
|
算法 Java 调度
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之使用Java代码实现令牌桶算法问题如何解决