一.
颜色量化介绍(Color Quantization
)
计算机图形学中,常采用的一种方法是把颜色看成是基于红、绿、蓝三种颜色的混合,也可以采用色度、彩度、亮度等描述颜色,用多种不同的描述符来表示颜色,就称为颜色模型(Color Model),如果有人能量化这三种不同的描述符的数值,就可以用一个三元组来表示一种颜色,例如(r,g,b),这就形成了一个描述颜色的三维坐标系统,选择不同的颜色模型能形成不同坐标系统,坐标系统上所有颜色的集合就称为颜色空间(Color Spaces)。
在图形学中,颜色量化是为了减少一张图像中的颜色数并且使用它尽可能的与原始图像一样,在一些由于内存限制只能显示有限颜色的设备上,颜色量化就显得特别的重要,根据参考【5】的总结,介绍几种颜色量化方法,最后重点介绍八叉树颜色量化方法。
1.1
统一颜色量化(Uniform Quantization
)
在RGB颜色模型,颜色可以表示成三维空间中的一个坐标,颜色空间可以表示为X,Y,Z轴都在[0,1]范围的值,这样颜色空间就相当于一个正方体。统一颜色量化的基本思想就是独立的看待颜色空间中的每个坐标轴,把它们平均分成N条线段,这样就能形成一个个小方块,每个方块当作一种颜色。有时候把红和绿的坐标轴分成8段,把蓝色的坐标轴分成4段,这样就可以生成256个小方块,即256种颜色;除此之外,当然还有别的划分方法,例如可以把红色和蓝色分成6段,绿色分成7段,这样就能产生252种颜色。每个方块内的颜色值,可以取该方块所有颜色值的平均值。这种方法实现非常的简单快速,但是产生的结果并不好。
1.2
流行色算法(Popularity Algorithm
)
通过某种颜色在图像中发生的频率来进行颜色量化,频率越高的赋予越大的优先级,这种方法不考虑常用的颜色之间是否是相似的,即可能频率较高的几种颜色很相近,但是确实又有不同的颜色坐标。流行色算法基本的做法通过两次扫描来处理:第一次扫描图像,创建一个包括颜色和颜色计数的表,以颜色计数从大到小排序,选择前K种颜色作为这张图像的调色板,别的颜色都丢弃掉;第二次扫描,需要计算图像上的颜色在调色板上的索引值,例如颜色(red,green,blue),通过计算它与调色板上所有颜色距离的平方值dist,找到dist值取最小时的索引i值,就是图像上该种颜色的索引值。
dist=(red – red [i])^2 + (green – green [i])^2 + (blue – blue [i])^2
1.3
中值切割法(Median Cut Algorithm
)
与统一颜色量化方法有些相似,也是把颜色空间分成K个子块,使得每个了块尽可能的估计出相同数量的颜色。为了简化说明,这里用二维的平面来解释这个过程,如下图所示。原始图像上的颜色值放到颜色坐标系统上,沿着它最长的维度的中位数位置分成两半,剩下的子块按照类似的方法进行处理,得到右边那张图所示的结果。注意:中位数跟中点不是相同的概念。
算法的基本过程如下所示:
(1) 找到包含所有图像颜色的最小方块
(2) 把密闭在小方块内的颜色,沿着方块最长坐标轴方向进行排序,例如有4种颜色(0,0,0),(1,0,0),(0.2,0.4,0.5),(0.6,0.4,0.7)确定一个密闭的小方块,该方块在x轴方向的长度是1,Y轴和Z轴方的长度分别是0.4,0.7,所以沿着X轴方向进行排序。
(3) 取出中位数,把方块划分成两半
(4) 重复2~3过程,直到颜色被划分成256个区域
二.
八叉树颜色量化(Octree
)
最后介绍八叉树颜色量化方法,该算法最早见于文章最早是在1988, M. Gervautz 和 W. Purgathofer 发表的论文《A Simple Method for Color Quantization: Octree Quantization》,算法的最大优点是效率高,占用内存少(仅需要不超过(颜色数量+1)个节点,加上一些中间节点所占用的内存),选出的调色板最合理,显示效果最好。
代码实现参考了【3】作者写的代码,对他的代码进行了简单的修改和封装。网上有很多八叉树进行颜色量化的理论介绍,参考【1】,【3】,【4】,接下来,结合具体的代码实现,来解释下算法的基本思想。
实现中采用的八叉树节点,设计成如下结构:
typedef struct _OctreeNode
{
bool blIsLeaf; // TRUE if node has no children
unsigned char inColorIndex; // Index of the pallette
unsigned int uiPixelCount; // Number of pixels represented by this leaf
unsigned int uiRedSum; // Sum of red components
unsigned int uiGreenSum; // Sum of green components
unsigned int uiBlueSum; // Sum of blue components
_OctreeNode* ptrChild[8]; // Pointers to child nodes
_OctreeNode* ptrNext; // Pointer to next reducible node
}OctreeNode;
算法过程的伪码如下所示:
void buildOctree(array,numArray,maxColors)
{
//把图像的像素保存在数组array中,大小是numArray
for( color=array[i] ; i<numArray ; i++ )
{
addColor( color ); //往八叉树中加入一个颜色
while(leafNum> maxColors) //如果八叉树叶节点数超过最大允许的颜色数,合并八叉树,以减小叶节点数
reduceTree();
}
}
当往八叉树中增加一个新的结点的时候,调用addColor()方法,该方法的实现代码如下所示:
bool addColor(OctreeNode*& ptrTreeNode,unsigned char byRed,unsigned char byGreen,unsigned char byBlue,int inLevel)
{
int nIndex, shift;
static unsigned char mask[8] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
// 结点不存在,创建一个新的结点
if (ptrTreeNode== NULL)
ptrTreeNode = createNode (inLevel);
if( ptrTreeNode==NULL )
return false;
//如果是叶结点,更新颜色信息
if (ptrTreeNode->blIsLeaf)
{
ptrTreeNode->uiPixelCount++;
ptrTreeNode->uiRedSum += byRed;
ptrTreeNode->uiGreenSum += byGreen;
ptrTreeNode->uiBlueSum += byBlue;
if(ptrTreeNode->uiPixelCount > m_inMaxPixelCount)
m_inMaxPixelCount = ptrTreeNode->uiPixelCount;
}
//如果不是叶结点,递归下一层
else
{
shift = 7 - inLevel;
nIndex = (((byRed & mask[inLevel]) >> shift) << 2) |
(((byGreen & mask[inLevel]) >> shift) << 1) |
((byBlue & mask[inLevel]) >> shift);
if( !addColor (ptrTreeNode->ptrChild[nIndex], byRed, byGreen,byBlue,inLevel + 1) )
return false;
}
return true;
}
这里参考【4】中的图片,来演示下这个方法的基本思想。当插入颜色值(109,204,170)的时候,插入第0层,取R、G、B颜色值的最高位,通过或操作,得到一个小于8的值3(011),作为下一次递归的位置;进入第2层,继续进行或操作,得到值6(110),第6个孩子节点就是下一层要递归的位置。依次类推,直到找到叶子节点,把颜色计算值uiPixelCount加1,同时把(109,204,170)加到OctreeNode结构(uiRedSum ,uiGreenSum,uiBlueSum)中。下图简化了过程,只演示了几层,在实现的算法中每种颜色分量占8位,所有共有9层。
图2. 往八叉树插入颜色
如果叶节点的数量超过了允许的最大颜色数,则需要进行合并操作,以减少叶节点数,如下所示:
void reduceTree ()
{
int i;
OctreeNode* pNode;
unsigned int nRedSum = 0, nGreenSum = 0 , nBlueSum = 0;
int nChildren = 0;
// Find the deepest level containing at least one reducible node
for ( i=7 ; (i>0) && (m_ptrReducibleNodes[i] == NULL); i--);
// Reduce the node most recently added to the list at level i
pNode = m_ptrReducibleNodes[i];
m_ptrReducibleNodes[i] = pNode->ptrNext;
for (i=0; i<8; i++)
{
if (pNode->ptrChild[i] != NULL)
{
nRedSum += pNode->ptrChild[i]->uiRedSum;
nGreenSum += pNode->ptrChild[i]->uiGreenSum;
nBlueSum += pNode->ptrChild[i]->uiBlueSum;
pNode->uiPixelCount += pNode->ptrChild[i]->uiPixelCount;
free(pNode->ptrChild[i]);
pNode->ptrChild[i] = NULL;
nChildren++;
}
}
pNode->blIsLeaf = true;
pNode->uiRedSum = nRedSum;
pNode->uiGreenSum = nGreenSum;
pNode->uiBlueSum = nBlueSum;
//更新节点的最大像素数量。
if(pNode->uiPixelCount > m_inMaxPixelCount)
m_inMaxPixelCount = pNode->uiPixelCount;
//更新叶节点数
m_inLeafCount -= (nChildren - 1);
}
合并的基本思想,用参考【4】中的图片演示,在第n层的节点上有两个叶节点,现在完成的合并操作就是把叶节点的颜色分量和计数值都加到它们的父节点上,同时裁剪掉两个叶节点,这步操作就减少了一个叶节点。
图3. 合并八叉树
程序中有一个设计技巧,就是一层的所有节点都用一个链表存储起来,m_ptrReducibleNodes[i]记录了链表的头节点,每一次合并操作都是从最底层开始,从下到上依次进行的。
颜色量化类
/*
/brief 颜色量化类
通过八叉树算法来实现颜色量化,给定一个允许的颜色数上限MaxColors,量化后的颜色数一定要小于这个值
可以创建量化后调色板,使用原始颜色可以快速的检索出对应调色板的索引值
*/
class SrColorQuant
{
protected:
#pragma pack(push)
#pragma pack(1)
typedef struct _OctreeNode
{
bool blIsLeaf; // TRUE if node has no children
unsigned char inColorIndex; // Index of the pallette
unsigned int uiPixelCount; // Number of pixels represented by this leaf
unsigned int uiRedSum; // Sum of red components
unsigned int uiGreenSum; // Sum of green components
unsigned int uiBlueSum; // Sum of blue components
_OctreeNode* ptrChild[8]; // Pointers to child nodes
_OctreeNode* ptrNext; // Pointer to next reducible node
}OctreeNode;
#pragma pack(pop)
public:
SrColorQuant( );
~SrColorQuant();
/*
\brief 创建八叉树
\param[in] btPtrRgbData 需要量化的颜色数组,每个颜色占三个字节,依次为红、绿、蓝,每种颜色分量以1个字节保存。
\param[in] inPixelCount 需要量化的颜色个数
\param[in] nMaxColors 规定量化后最大允许的颜色数,它的值必需[1,256]范围内。
\return 采用八叉树量化的颜色数个数。如果返回0,则八叉树创建失败
*/
int buildOctree( unsigned char* btPtrRgbData,int inPixelCount,int nMaxColors);
/*
\brief 根据颜色RGB,从已创建出的八叉树中获得相近颜色的索引
\param[in] (byRed,byGreen,byBlue) RGB颜色值
*/
unsigned char indexOctree(unsigned char byRed,unsigned char byGreen,unsigned char byBlue)const;
/*
\brief 叶节点的最大计数值
*/
int getMaxPixelCount() const;
/*
\brief 叶节点的数量,即量化后的颜色种数
*/
int getLeafNodeCount()const;
/*
\brief 如果八叉树为空,返回true
*/
bool isEmpty()const;
/*
\brief 获得量化后的调色板,每个颜色占三个字节,依次为红、绿、蓝,每种颜色分量以1个字节保存。
\param[in] ptrColorPal 不能为空,内存需要提前分配,至少有getLeafNodeCount()*3大小的内存空间。
*/
void getColorPallette(unsigned char* ptrColorPal )const;
protected:
void getColorPallette(OctreeNode*ptrTree, unsigned char* ptrColorPal)const;
OctreeNode* createNode (int inLevel);
/*
\brief 合并八叉树节点,降低叶节点数
*/
void reduceTree ();
/*
\brief 递归的往八叉树中插入颜色值
\param[in] inLevel表示当前插入的层数,初始值是0,每递归一层,值加1
*/
bool addColor(OctreeNode*& ptrTreeNode,unsigned char byRed,unsigned char byGreen,unsigned char byBlue,int inLevel);
/*
\brief 计算每个叶节点的颜色值同时把颜色计数值设置为1.
*/
void setColorIndex(OctreeNode* ptrTree,int& inIndex);
/*
\brief 析构八叉树内存
*/
void freeOctree(OctreeNode*& ocPtrTree);
/*
\brief 清空八叉树
*/
void empty();
unsigned int m_inMaxPixelCount;
int m_inLeafCount;
OctreeNode* m_ptrReducibleNodes[9];
OctreeNode* m_ptrTree;
};
<h4 align="center" style="border: 0px; font-family: merienda, sans-serif; font-size: 18px; font-weight: normal; font-stretch: normal; margin: 0px 0px 0.813em; padding: 0px; vertical-align: baseline; word-wrap: break-word;"><span style="border: 0px; font-family: inherit;font-size:undefined; font-style: inherit; font-variant: inherit; font-weight: 700; font-stretch: inherit; line-height: inherit; margin: 0px; padding: 0px; vertical-align: baseline; word-wrap: break-word;">BMP文件的解析类</span></h4><div><span style="border: 0px; font-family: inherit;font-size:undefined; font-style: inherit; font-variant: inherit; font-weight: 700; font-stretch: inherit; line-height: inherit; margin: 0px; padding: 0px; vertical-align: baseline; word-wrap: break-word;"></span><pre name="code" class="cpp">/*
\brief BMP文件的读写解析类
一个BMP类对象只能进行BMP文件的读取或者写入。
读BMP文件:解析BMP文件必需满足如下条件:
(1)支持像素比特数是1,4,8,16,24,32,图像压缩类型为BI_RGB
(2)压缩类型为BI_RLE4和BI_RLE8,对应的像素比特数分别是4和8
(3)BMP文件的宽和高必需大于0,biPlanes必需是1,必需是"BM"类型
写BMP文件:支持写入像素比特数是1,4,8,16,24,图像压缩类型为BI_RGB的BMP文件
*/
class SrImageBmp: public SrImage
{
protected:
//一些BMP文件中文件头和文件信息头即使有错误也不影响正确使用BMP中的图像信息
//是否检测文件头中位图数据的起始位置
#define SR_IMAGE_BMP_CHECK_OFFBITS 0
//是否检测文件头中文件大小
#define SR_IMAGE_BMP_CHECK_FILESIZE 0
#pragma pack(push)
#pragma pack(1)
/*
BMP文件由四部分组成:BMP文件头(14字节)、位图信息头(40字节)、颜色表、位图数据。
*/
/*
BMP文件头(14字节)
BMP文件头数据结构含有BMP文件的类型、文件大小和位图起始位置等信息。
*/
typedef struct tagBITMAPFILEHEADER
{
WORD bfType; // 位图文件的类型,必须为BMP(0-1字节)
DWORD bfSize; // 位图文件的大小,以字节为单位(2-5字节)
WORD bfReserved1; // 位图文件保留字,必须为0(6-7字节)
WORD bfReserved2; // 位图文件保留字,必须为0(8-9字节)
DWORD bfOffBits; // 位图数据的起始位置,以相对于位图文件头的偏移量表示,以字节为单位(10-13字节)
} BITMAPFILEHEADER;
/*
位图信息头(40字节)
BMP位图信息头数据用于说明位图的尺寸等信息。
*/
typedef struct tagBITMAPINFOHEADER
{
DWORD biSize; // 本结构所占用字节数,通是40个字节(14-17字节)
LONG biWidth; // 位图的宽度,以像素为单位(18-21字节)
LONG biHeight; // 位图的高度,以像素为单位(22-25字节)
WORD biPlanes; // 目标设备的级别,必须为1(26-27字节)
WORD biBitCount; // 每个像素所需的位数,必须是1(双色)、(28-29字节)、4(16色)、8(256色)或24(真彩色)之一
DWORD biCompression; // 位图压缩类型,必须是 0(不压缩),(30-33字节)
// 1(BI_RLE8压缩类型)或2(BI_RLE4压缩类型)之一
DWORD biSizeImage; // 位图的大小,以字节为单位(34-37字节)
LONG biXPelsPerMeter; // 位图水平分辨率,每米像素数(38-41字节),解析中未使用该信息
LONG biYPelsPerMeter; // 位图垂直分辨率,每米像素数(42-45字节),解析中未使用该信息
DWORD biClrUsed; // 位图实际使用的颜色索引表中的颜色数(46-49字节)
DWORD biClrImportant; // 位图显示过程中重要的颜色数(50-53字节),解析中未使用该信息
}BITMAPINFOHEADER;
/*
颜色表
颜色表用于说明位图中的颜色,它有若干个表项,每一个表项是一个RGBQUAD类型的结构,定义一种颜色。
*/
typedef struct tagRGBQUAD
{
BYTE rgbBlue; // 蓝色的亮度(值范围为0-255)
BYTE rgbGreen; // 绿色的亮度(值范围为0-255)
BYTE rgbRed; // 红色的亮度(值范围为0-255)
BYTE rgbReserved; // 保留,必须为0
}RGBQUAD;
/*
位图信息头和颜色表组成位图信息
*/
typedef struct tagBITMAPINFO
{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
}BITMAPINFO;
#pragma pack(pop)
public:
SrImageBmp(int isReadOnly);
~SrImageBmp();
/*
\brief 从BMP文件中读取数据,并且返回数据
\param[in] chPtrImageFile 读取的BMP文件名
\param[out] ptrOutData 如果inRGBType等于IMAGE_RGB,则返回的RGB颜色数据,每个颜色占三个字节,依次为红、绿、蓝,每种颜色分量以1个字节保存;
如果inRGBType等于IMAGE_RGBA,则返回的RGBA颜色数据,每个颜色占四个字节,依次为红、绿、蓝,Alpha,每种颜色分量以1个字节保存;
不可以析构数据ptrOutDatak中的内存,它由BMP对象进行管理。
\param[out] lgPixelCount 返回的RGB颜色数
\param[out] inRGBType 只能是IMAGE_RGBA和IMAGE_RGB中的一个值,表示返回的数据格式是RGBA还是RGB
\return true,读文件成功;false,读文件失败,通过getErrorId()方法,获得错误代号。
*/
bool readFile(const char* chPtrImageFile,unsigned char*& ptrOutData , int& lgPixelCount,int& inRGBType);
/*
\brief 将RGB颜色数据保存进BMP对象中,以便后续写文件操作。
\param[in] ucPtrRgbData RGB颜色数据,要保存进BMP对象中的数据,依次为红、绿、蓝,每种颜色分量以1个字节保存
\param[in] inWidth 指定写入BMP对象的宽,必需大于0
\param[in] inHeight 指定写入BMP对象的高,必需大于0
\param[in] usBitCount 指定BMP对象中每种颜色占的位数,BMP文件只支持1,4,8,16,24,32这6个数值
\param[in] ulCompression 这里只支持BI_RGB类型的写入文件操作
\return true,装载文件成功;false,装载文件失败,通过getErrorId()方法,获得错误代号。
*/
bool loadImageData(unsigned char* ucPtrRgbData ,long inWidth,long inHeight,unsigned short usBitCount=8 );
/*
\brief 将保存在BMP对象中的数据写入BMP文件中
\param[in] chPtrImageFile 写入的BMP文件名
\return true,写文件成功;false,写文件失败,通过getErrorId()方法,获得错误代号。
*/
bool writeFile(const char* chPtrImageFile);
virtual bool isValid()const;
virtual int getWidth()const;
virtual int getHeight()const;
/*
\brief 返回BMP对象中的RGB数据或者RGBA数据
*/
virtual unsigned char* getImageData()const;
unsigned long getFileSize()const;
/*
\brief 判断BMP对象中BMP的压缩类型,只能是BI_RGB或者BI_RGBA
*/
unsigned long getCompression()const;
/*
\brief 每个像素的比特数
*/
unsigned short getPixelDepth()const;
/*
\brief 判断BMP对象中存储的是RGB数据还是RGBA数据
*/
bool getIsRGB()const;
private:
/*
\brief 析构BMP对象中的所有数据
*/
void empty();
/*
\brief 计算BMP文件一行的字节数
*/
long getLineBytes(int imgWidth,int bitCount)const;
/*
\brief 判断读取的BMP文件格式是否正确
*/
bool checkReadFileFormat(FILE* flFileHandle);
bool readUncompression(FILE* filePtrImage,unsigned char*& ptrOutData);
bool decodeRLE(FILE* flPtrImage,BYTE*& btPtrImageData);
/*
\brief 从文件中读取BMP文件头和文件信息头
*/
bool readHeader(FILE* flPtrImage);
bool readColorMap(FILE* flPtrImage);
bool decodeFile(BYTE* ptrImageData,unsigned char*& rgbPtrOutData);
bool initHeader(long inWidth,long inHeight,unsigned short usBitCount);
bool writeHeader(FILE* flPtrImage);
bool writeColorMap(FILE* filePtrImage,SrColorQuant& colorQuant);
bool writeImageData(FILE* flPtrFile,SrColorQuant& colorQuant);
bool writeBinary(FILE* filePtrImage);
bool writeColorMapImage(FILE* filePtrImage,const SrColorQuant& colorQuant);
bool writeNoColorMapImage(FILE* filePtrImage);
bool checkWriteFileFormat();
public:
#ifdef _DEBUG
#ifdef _CONSOLE
void printFileHeader(BITMAPFILEHEADER *bmfh);
void printFileHeader(BITMAPINFOHEADER *bmih);
#endif
#endif
private:
BITMAPINFOHEADER* m_ptrInfoHeader;
BITMAPFILEHEADER* m_ptrFileHeader;
RGBQUAD* m_ptrColorMap;
BYTE* m_ptrImageData;
int m_inIsReadOnly;
};