利用八叉树颜色删减算法进行图片压缩

八叉树删减算法的一些思考以及c++实现


前言

这几天上多媒体技术这门课,老师布置了八叉树删减算法的作业,简单谈谈自己的一点想法。


一、为什么要减色处理?

位图采用RGB颜色模型,是一个二维的像素矩阵的方法显示和存储的图象,矩阵中的每个元素代表一个像素。但是如果直接用这个矩阵表示每一种颜色,就会导致图片很大(R、G、B各八位,共有2^24种颜色,也就是所谓真彩图),所以我们考虑使用八叉树删减算法进行生成一个调色盘以达到删减颜色,压缩文件的目的。

二、思路分析

1.如何用八叉树表示所有颜色

我们可以看到,每种颜色由RGB各8位组成,也就是说,要储存一个颜色,只要存下它的RGB值即可,考虑使用八叉树结构,即不会浪费空间存储不存在的颜色,也便于查找。
具体:将RGB都转换成二进制数,排成三行,共八列,取每一列从上到下组成一个二进制数,这样一个颜色就被分成了八个数,我们只需要记下这八个数即可,分别对应于八叉树的八个子节点的下标,如第一位是3,则选root的下标为3的子节点作为下一个数的存储。

八叉树的具体表示
八叉树最多九层(前八层表示某种颜色的八个数),最后一层页节点,用来保存图片中该颜色的像素数以及各分量总和(总分量/像素数=平均分量),也就是说,一个叶节点代表一个颜色。

2.如何插入一个新的颜色?

对于一个新的颜色,首先要根据其RGB值算出它对应的八个数的值,然后将其插入到八叉树中。在插入时,从root节点开始,每次挑选对应的子节点,直到遇到叶子节点,此时意味着该节点已经被合并(后面会提到),不能再向下扩展,直接像素数+1,各分量加上此颜色的RGB值。

3.如何合并元素?

假设压缩后的文件最多256种颜色,这里考虑一边插入一边压缩,也就是当插入一个颜色后,总颜色大于256后,选择几个叶子节点合并到父节点上,此时父节点变成新的叶子节点,颜色数自然减一。关于如何选择,一个是要考虑节点深度,因为节点深度越大,意味着它在一个颜色的RGB表示中位数越低,影响的值也越小,合并时不会导致两个完全不一样的颜色合并,另外就是要考虑像素数,一个颜色的像素数很少,那么将其删减后影响不大。
方便起见,我们为所有叶节点建立一个数组,然后遍历所有叶节点,找到其上第一个有至少多余两个子节点的祖先节点(这样才能合并),然后再这些祖先节点中找到一个深度最大(深度一样取像素数最小的),合并其子节点。因此,我们需要在节点中存入父节点信息。

代码分析

1.节点结构

struct octNode
{
	long long cnt;//本结点以下的像素总数
	long long rSum, gSum, bSum;//红色分量、绿色分量、蓝色分量灰度值的和
	bool isLeaf;//是否是叶子结点
	int depth;//本节点的深度
	octNode* parent;  //父节点
	int childCount;   //该节点的子节点计数 
	std::vector<int> child_idx;  //非空的子节点下标
	octNode* child[8];//8个子结点的指针数组 
	octNode(){
		cnt = 0;
		isLeaf = 0;
		rSum = gSum = bSum = 0;
		depth = 0;
		parent = NULL;
		childCount = 0;
		for (int i = 0; i < 8; i++)
			child[i] = NULL;
	}
};

2.八叉树类

class octTree
{
public:
	octTree();
	octTree(int maxColorNum) {
		colors = 0;
		maxColors = maxColorNum;
		root = new octNode;
		root->depth = 0;
	}
	~octTree();

	void insertColor(uint8 r, uint8 g, uint8 b);						//插入一个颜色
	uint8 generatePalette(RGBQUAD *pal);						//生成调色板

private:
	octNode *root;														//八叉树的根
	int colors;															//当前的颜色总数
	int maxColors;														//最大颜色数
	void delete_node(octNode* root);									//递归删除所有节点	
	void DEL();															//删除一个节点(保证颜色数量)
	std::vector<octNode*> Leafs;										//储存所有的叶子节点
};

3.插入颜色

对于一个新的颜色,首先获取其八叉树的八个值,然后从根节点开始从上往下递归,如果递归到叶子节点,则不能往下插入,直接加上cnt和rSum 、bSum、gSum的值即可。如果不是叶子节点,但是对应的子节点为NULL,说明是一个新的颜色,这时候该节点的子节点计数++,新建节点插入。我们选择一边插入一边删减,这样可以保证树的节点不至于太多,降低复杂度。

//往八叉树中添加一个像素的颜色
void octTree::insertColor(uint8 r, uint8 g, uint8 b)
{
	uint8 Colors[8];
	for (int i = 0; i < 8; i++) {
		Colors[i] = ((r >> 7 - i) << 2) + ((g >> 7 - i) << 1) + (b >> 7 - i);
	}
	octNode* tmp = root;
	int idx = 0;
	while (!tmp->isLeaf&&tmp->child[Colors[idx]] != NULL) {
		//该节点已经存在
		tmp = tmp->child[Colors[idx]];
		idx++;
	}
	if (tmp->isLeaf) { //该节点是叶子节点,不可以再继续递归了,直接加
		tmp->cnt++;
		tmp->rSum += r;
		tmp->gSum += g;
		tmp->bSum += b;
	}
	else {  //此节点不是叶子节点,继续向下
		for (; idx < 8; idx++) {
			//该节点新建了一个子节点
			tmp->cnt++;
			tmp->childCount++;
			tmp->child_idx.push_back(Colors[idx]);
			octNode* p = new octNode;
			p->depth = tmp->depth + 1;
			p->parent = tmp;
			tmp->child[Colors[idx]] = p;
			tmp = tmp->child[Colors[idx]];
			idx++;
		}
		//新的叶子节点
		tmp->cnt=1;   
		tmp->isLeaf = 1;
		tmp->rSum += r;
		tmp->gSum += g;
		tmp->bSum += b;
		Leafs.push_back(tmp);
		colors++;
	}
	while (colors > maxColors)
		DEL();
}

4.删除节点

我们选择一个至少有两个子节点,并且层数最深(层数相同时取像素计数最大的节点),将其所有的叶子节点合并,其本身成为叶节点(要注意在Leafs里删除相应的叶节点)。

//选择一个至少有两个子节点的节点,将其子节点合并,其成为新的叶节点(找最深层里cnt最小的)
void octTree::DEL() {
	octNode* candicate = NULL;
	for (int i = 0; i < Leafs.size(); i++) {
		octNode* tmp = Leafs[i];
		//找到它的第一个有至少两个子节点的祖先
		while (tmp->childCount < 2)
			tmp = tmp->parent;
		if (candicate == NULL)
			candicate = tmp;
		else if (candicate->depth > tmp->depth || (candicate->depth == tmp->depth && candicate->cnt <= tmp->cnt))
			;
		else
			candicate = tmp;
	}
	if (candicate == NULL)
		return;
	//合并,由于是选的自下而上的第一个有至少两个子节点的节点,而且保证深度是最深的,
	//所以可以保证candidate节点下的所有子节点都是单一路径,也就是说只有一个子节点,故可以一直往下
	for (int i = 0; i < candicate->child_idx.size(); i++) {
		octNode* tmp = candicate->child[candicate->child_idx[i]];
		while (!tmp->isLeaf) {  //递归到叶子节点,每次都删除该节点
			tmp = tmp->child[tmp->child_idx[0]];
			delete tmp->parent;
			tmp->parent = NULL;
		}
		candicate->rSum += tmp->rSum;
		candicate->gSum += tmp->gSum;
		candicate->bSum += tmp->bSum;
		//删除旧的叶子节点
		std::vector<octNode*>::iterator it = Leafs.begin();
		for (; it != Leafs.end();it++) {
			if (*it == tmp) {
				Leafs.erase(it);
				break;
			}
		}
		delete tmp;
		tmp = NULL;
	}
	//candicate成为新的叶节点
	colors -= (candicate->childCount - 1);  //少了所有的子节点,自己变成叶节点
	candicate->isLeaf = 1;
	candicate->childCount = 0;
	Leafs.push_back(candicate);
}

事实上这里存在一个问题,由于是把所有的子节点都合并了,所以单次操作可能删除多个颜色,这样就不能保证最后的颜色数刚好达到maxColors。

5、生成调色板

由于在一开始用了一个vector存放各个叶节点的值,故这一步只要读取各个叶节点就可以了

//根据现有的八叉树,选择256个颜色作为最终的调色板中的颜色
uint8 octTree::generatePalette(RGBQUAD *pal)
{
	//依次读取叶节点即可,忽略为NULL的节点
	int k = 0;
	for (int i = 0; i < Leafs.size(); i++) {
		if (Leafs[i] != NULL) {
			pal[k].rgbRed = Leafs[i]->rSum / Leafs[i]->cnt;
			pal[k].rgbGreen = Leafs[i]->gSum / Leafs[i]->cnt;
			pal[k].rgbBlue = Leafs[i]->bSum / Leafs[i]->cnt;
			k++;
		}
	}
	for (; k < 256; k++) {
		pal[k].rgbRed = 0;
		pal[k].rgbGreen = 0;
		pal[k].rgbBlue = 0;
	}
	return 0;
}

5、颜色匹配

在原图中读取一个颜色,找到其在调色板中的匹配,这里通过计算三维空间距离的方式(平方和即可)来判断距离大小

//从调色板中选出与给定颜色最接近的颜色,采用平方和的计算方法
uint8 selectClosestColor(uint8 r, uint8 g, uint8 b, RGBQUAD *pal)
{	
	int min = 256 * 256 * 3 + 1;
	int min_idx = -1;
	for (int i = 0; i < 256; i++) {
		int x = (int)(pal[i].rgbRed - r) * (int)(pal[i].rgbRed - r) + (int)(pal[i].rgbGreen - g) * (int)(pal[i].rgbGreen - g) + (int)(pal[i].rgbBlue - b) * (int)(pal[i].rgbBlue - b);
		if (x < min) {
			min_idx = i;
			min = x;
		}
	}
	
	return (uint8)min_idx;//给定某颜色,返回其在调色板中最近似颜色的索引值;
}

6、函数主体

注意main函数传入输入输出文件名

int main(int argc, char *argv[])
{
	if (argc < 3)
	{
		printf("using: exe[0], input file[1], output file[2]\n");
		return -1;
	}
	BITMAPFILEHEADER bf, *pbf;//输入、输出文件的文件头
	BITMAPINFOHEADER bi, *pbi;//输入、输出文件的信息头
	RGBQUAD *pRGBQuad;//待生成的调色板指针
	uint8 *pImage;//转换后的图象数据
	DWORD bfSize;//文件大小
	LONG biWidth, biHeight;//图象宽度、高度
	DWORD biSizeImage;//图象的大小,以字节为单位,每行字节数必须是4的整数倍
	unsigned long biFullWidth;//每行字节数必须是4的整数倍

	//打开输入文件
	char *inputName, *outputName;
	FILE *fpIn, *fpOut;
	inputName = argv[1];
	outputName = argv[2];
	printf("Opening %s ... ", inputName);
	if (!(fpIn = fopen(inputName, "rb")))
	{
		printf("\nCan't open %s!\n", inputName);
		return -1;
	}
	printf("Success!\n");

	//创建输出文件
	printf("Creating %s ... ", outputName);
	if (!(fpOut = fopen(outputName, "wb")))
	{
		printf("\nCan't create %s!\n", outputName);
		return -1;
	}
	printf("Success!\n");

	//读取输入文件的文件头、信息头
	fread(&bf, sizeof(BITMAPFILEHEADER), 1, fpIn);
	fread(&bi, sizeof(BITMAPINFOHEADER), 1, fpIn);

	//读取文件信息
	biWidth = bi.biWidth;
	biHeight = bi.biHeight;
	biFullWidth = ceil(biWidth / 4.) * 4;//bmp文件每一行的字节数必须是4的整数倍
	biSizeImage = biFullWidth*biHeight;
	bfSize = biFullWidth*biHeight + 54 + 256 * 4;//图象文件的大小,包含文件头、信息头

	//设置输出文件的BITMAPFILEHEADER
	pbf = new BITMAPFILEHEADER;
	pbf->bfType = 19778;
	pbf->bfSize = bfSize;
	pbf->bfReserved1 = 0;
	pbf->bfReserved2 = 0;
	pbf->bfOffBits = 54 + 256 * 4;
	//写出BITMAPFILEHEADER
	if (fwrite(pbf, sizeof(BITMAPFILEHEADER), 1, fpOut) != 1)
	{
		printf("\nCan't write bitmap file header!\n");
		fclose(fpOut);
		return -1;
	}

	//设置输出文件的BITMAPINFOHEADER
	pbi = new BITMAPINFOHEADER;
	pbi->biSize = 40;
	pbi->biWidth = biWidth;
	pbi->biHeight = biHeight;
	pbi->biPlanes = 1;
	pbi->biBitCount = 8;
	pbi->biCompression = 0;
	pbi->biSizeImage = biSizeImage;
	pbi->biXPelsPerMeter = 0;
	pbi->biYPelsPerMeter = 0;
	pbi->biClrUsed = 0;
	pbi->biClrImportant = 0;
	//写出BITMAPFILEHEADER
	if (fwrite(pbi, sizeof(BITMAPINFOHEADER), 1, fpOut) != 1)
	{
		printf("\nCan't write bitmap info header!\n");
		fclose(fpOut);
		return -1;
	}

	//构建颜色八叉树
	printf("Building Color OctTree ...  ");
	octTree *tree;
	tree = new octTree(256);
	uint8 RGB[3];
	//读取图像中每个像素的颜色,并将其插入颜色八叉树
	for (int i = 0; i < bi.biHeight; i++)
	{
		fseek(fpIn, bf.bfOffBits + i*ceil(biWidth * 3 / 4.) * 4, 0);
		for (int j = 0; j < bi.biWidth; j++)
		{
			//读取一个像素的颜色,并将其插入颜色八叉树
			fread(&RGB, 3, 1, fpIn);
			tree->insertColor(RGB[2], RGB[1], RGB[0]);
		}
	}
	printf("Success!\n");

	//生成并填充调色板
	printf("Generating palette ... ");
	pRGBQuad = new RGBQUAD[256];
	tree->generatePalette(pRGBQuad);

	//输出256色调色板
	if (fwrite(pRGBQuad, 256 * sizeof(RGBQUAD), 1, fpOut) != 1)
	{
		printf("\nCan't write palette!\n");
		fclose(fpOut);
		return -1;
	}
	printf("Success!\n");

	//填充图像数据
	printf("Generating the output image ... ");
	pImage = new uint8[biSizeImage];
	memset(pImage, 0, biSizeImage);
	for (int i = 0; i < bi.biHeight; i++)
	{		
		fseek(fpIn, bf.bfOffBits + i*ceil(biWidth * 3 / 4.) * 4, 0);
		for (int j = 0; j < bi.biWidth; j++)
		{
			//读取一个像素的颜色,并将其转换位颜色索引值
			fread(&RGB, 3, 1, fpIn);
			pImage[i*biFullWidth + j] = selectClosestColor(RGB[2], RGB[1], RGB[0], pRGBQuad);
		}
	}
	//输出图象数据
	if (fwrite(pImage, biSizeImage, 1, fpOut) != 1)
	{
		printf("\nCan't write image data!\n");
		fclose(fpOut);

		return -1;
	}
	printf("Success!\n");


	delete tree;
	delete pbf;
	delete pbi;
	delete[] pRGBQuad;
	delete[] pImage;
	fclose(fpIn);
	fclose(fpOut);
	printf("All done!\n");
	return 0;
}

三、效果对比

原真彩图:
原图
转换后的256色图:
生成图

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
八叉树颜色量化算法是一种用于处理图像颜色算法。该算法通过将原始图像中的颜色值分解为RGB三个分量,将每个分量的值映射到一个[0,255]的范围内。然后,将图像中的像素点分布到一个八叉树数据结构中。 八叉树是一种树状数据结构,每个节点最多可以有八个子节点。在进行颜色量化时,我们通过将颜色空间划分为八个子空间来构建这个八叉树。对于每个节点,我们计算子空间中所有像素点的平均颜色值,并将其用作该节点的颜色。 在构建完成八叉树后,我们可以根据需要对图像进行压缩或减小颜色深度。压缩时,将八叉树中的叶子节点合并为一个新的节点,以减小颜色数量。减小颜色深度时,我们可以对八叉树进行剪切,使得树的深度减少,进而减小颜色级别。 对于每个图像像素点,我们可以使用八叉树来查找最接近的颜色节点,并将其替换为该节点的颜色值。这样一来,就实现了图像的颜色量化。 八叉树颜色量化算法具有如下优点:对于图像较为均匀的地方,可以保留更多的细节,因为这些细节可以对应到八叉树中较为细小的子空间中;同时,对于图像中颜色分布不均匀的地方,可以将颜色量化得更加精确,避免颜色丢失。 总体而言,八叉树颜色量化算法是一种高效且灵活的处理图像颜色算法,可以在保持图像质量的同时减小颜色深度和文件大小。该算法在很多图像处理应用中被广泛使用。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值