当前位置: 首页 > news >正文

读源码学算法之Octree color quantization

颜色量化(color quantization)技术通过减少颜色数量实现图像的颜色压缩,旨在用尽可能少的颜色去尽可能逼真的还原图片。如下图所示:输入是一张图像,输出是一个调色板(只包含少量的颜色)+ 该调色板还原的输入图像(输入图像中的所有颜色均来自该调色板),最终使得还原后的图像跟输入图像尽可能接近。这种技术最早用于解决低质量设备上显示高质量图像的质量损失问题。
在这里插入图片描述
八叉树颜色量化算法于1988年由 M. Gervautz 和 W. Purgathofer 发表:A Simple Method for Color Quantization: Octree Quantization,引用近500次,算法的最大优点是设计巧妙,效率高,占用内存少,选出的调色板最合理,显示效果最好。 下面是一张算法示意图。
在这里插入图片描述

论文讲解见:知乎:八叉树颜色压缩图解以及c++实现
我修改了下这篇博客的代码,写了较为详细的注释,故不在进一步解析。

#include <vector>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"  		//https://github.com/nothings/stb/blob/master/stb_image.h
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h" 	//https://github.com/nothings/stb/blob/master/stb_image_write.h
using namespace std;

int mask[8] = { 128,64,32,16,8,4,2,1 };
int max_color_cnt = 16, color_cnt = 0;

//八叉树节点的数据结构
struct Node{
	bool IsLeaf, Reduceable;
	int count, level, rsum, gsum, bsum, global_pos, childnum;
	Node* pChilds[8];

	Node(int lev) {
		level = lev;
		rsum = gsum = bsum = count = childnum = 0;
		global_pos = -1;
		for (int i = 0; i < 8; i++) pChilds[i] = NULL;
		IsLeaf = false;
		Reduceable = true;
	}
};
vector<Node*> AllTreeNodes; //存放所有节点的数组,每个节点也存了其位置:global_pos
vector<int> LayerNodes[8];  //没放每一层节点的二维数组

//递归的将第idx个节点的所有子孙节点设从树上摘除,(这里实现:将所有后台设定为不可reduce)
void RemoveChildNodesOf(int idx) {
	if (AllTreeNodes[idx]->IsLeaf == true)
		return;

	AllTreeNodes[idx]->Reduceable = false; //不可reduce,因为已经从树上“脱离”
	for (int i = 0; i < 8; i++)
		if (AllTreeNodes[idx]->pChilds[i] != NULL)
			RemoveChildNodesOf(AllTreeNodes[idx]->pChilds[i]->global_pos);
}

void MergeColors(){
	//1.自倒数第二层往树根方向,找到具有最少孩子节点的节点(子节点越少,从该节点分出的颜色越少,越不重要)
	int m_idx = -1;
	for (int i = 7; i >= 0; i--){
		int min_son_cnt = 0x7f7f7f;
		for (int j = 0; j < LayerNodes[i].size(); j++){
			int idx = LayerNodes[i][j];
			if (AllTreeNodes[idx]->Reduceable && AllTreeNodes[idx]->childnum >= 2 && min_son_cnt > AllTreeNodes[idx]->childnum){
				min_son_cnt = AllTreeNodes[idx]->childnum;
				m_idx = idx;
			}
		}
		if (m_idx != -1) break;
	}
	//2.将该节点的孩子从树上摘除,将当前节点设为新的树叶节点,颜色总数更新。
	RemoveChildNodesOf(m_idx);
	AllTreeNodes[m_idx]->IsLeaf = true;
	color_cnt -= (AllTreeNodes[m_idx]->childnum - 1);
}

void AddColor(Node*& pNode, int R, int G, int B, bool newNode){
	if (pNode->level < 8 && newNode)	//将该节点的索引放进所在层中,方便之后搜索
		LayerNodes[pNode->level].push_back(pNode->global_pos);
	if (pNode->IsLeaf) return;			//树叶节点直接返回

	pNode->rsum += R;	//R通道颜色 加到 当前节点的rsum上
	pNode->gsum += G;	//G通道颜色 加到 当前节点的gsum上
	pNode->bsum += B;	//B通道颜色 加到 当前节点的bsum上
	pNode->count += 1;	//该节点颜色总数 + 1

	//非叶子节点
	if (pNode->level < 8) {
		int shift = 7 - pNode->level, Idx = 0;
		//将当前颜色的各通道的第k位合成一个三位的二进制数Idx = (R_kG_kB_k),这也是子节点的位置。
		Idx |= (R & mask[pNode->level]) >> shift << 2;
		Idx |= (G & mask[pNode->level]) >> shift << 1;
		Idx |= (B & mask[pNode->level]) >> shift << 0;

		//如果第Idx节点为空,则创建之,并从新的节点开始继续深入八叉树的下一层。
		if (pNode->pChilds[Idx] == NULL) {
			pNode->pChilds[Idx] = new Node(pNode->level + 1);
			AllTreeNodes.push_back(pNode->pChilds[Idx]);
			pNode->pChilds[Idx]->global_pos = AllTreeNodes.size()-1;
			pNode->childnum += 1;		
			AddColor(pNode->pChilds[Idx], R, G, B, true);
		}
		//如果第Idx节点已经创建,从新的节点开始继续深入八叉树的下一层。
		else
			AddColor(pNode->pChilds[Idx], R, G, B, false);
	}

	//叶子节点,如果颜色总数超过调色板颜色数目,则需要合并某些分支以减少颜色数量。
	else {
		pNode->IsLeaf = true;
		pNode->Reduceable = false;
		if (newNode) {
			color_cnt += 1;
			if (color_cnt > max_color_cnt)
				MergeColors();
		}
	}
}

//颜色量化即替换图像中真实像素颜色,只需要从树根开始,按照添加颜色的方式从上到下遍历,直到叶节点,返回位置。
int QueryColor(Node*& pNode, int R, int G, int B) {
	if (pNode->IsLeaf)
		return pNode->global_pos;

	int shift = 7 - pNode->level, Idx = 0;
	Idx |= (R & mask[pNode->level]) >> shift << 2;
	Idx |= (G & mask[pNode->level]) >> shift << 1;
	Idx |= (B & mask[pNode->level]) >> shift << 0;
	return QueryColor(pNode->pChilds[Idx], R, G, B);
}

int main(){
	int width, height, channels;
	unsigned char* img = stbi_load("bread.png", &width, &height, &channels, 0);

	//1. 先创建八叉树的树根节点
	Node* RootNode = new Node(0);
	AllTreeNodes.push_back(RootNode);
	RootNode->global_pos = 0;
	LayerNodes[0].push_back(0);

	//2. 依次添加输入图像的所有像素颜色
	for (int i = 0; i < height; i++){
		for (int j = 0; j < width; j++){
			int idx = (i * width + j) * channels;
			AddColor(RootNode, img[idx], img[idx + 1], img[idx + 2], false);
		}
	}

	//3.1.图像量化即替换输入图像的颜色,先Query当前颜色,找到八叉树对应的叶子节点.
	//3.2.然后用叶节点的各通道的颜色总和 / 颜色数量 -> 替换之后的颜色。
	for (int i = 0; i < height; i++){
		for (int j = 0; j < width; j++){
			int idx = (i * width + j) * channels;
			int pos = QueryColor(RootNode, img[idx], img[idx + 1], img[idx + 2]);
			img[idx + 0] = AllTreeNodes[pos]->rsum / AllTreeNodes[pos]->count;
			img[idx + 1] = AllTreeNodes[pos]->gsum / AllTreeNodes[pos]->count;
			img[idx + 2] = AllTreeNodes[pos]->bsum / AllTreeNodes[pos]->count;
		}
	}

	stbi_write_png("result.png", width, height, channels, img, width * channels);
	stbi_image_free(img);
}

其它参考:
[1] https://web.cs.wpi.edu/~matt/courses/cs563/talks/color_quant/CQindex.html

相关文章:

  • C#调用C++生成的DLL 找不到入口点 以及 尝试读取或写入受保护的内存
  • 品牌是选择KOC还是KOL?抖音KOC如何进行推广投放?
  • css同时设置最大宽度和最小宽度
  • 微信小程序播放视频的时候如果突然插入一个音频视频就会卡顿一下
  • S7协议下,如何搭建触摸屏与PLC之间无线通信?
  • java SpringBoot 静态方法中获取@Value注入的值
  • 以太坊账户私钥管理之导出、导出keystore 文件
  • byte[] 转换为图片并保存
  • opencv中直方图和颜色跟踪相关:calcHist, calcBackProject, Meanshift和Camshift
  • 敏感词过滤实践
  • 【面试题】公平锁和非公平锁/可重入锁
  • 【字体转换】快速实现繁简字体相互转换
  • Jeecg Online代码生成器--单表代码生成
  • 获取一个实时走动的时间
  • 现货黄金的收益怎么样
  • ----------
  • 网络传输文件的问题
  • “Material Design”设计规范在 ComponentOne For WinForm 的全新尝试!
  • 10个确保微服务与容器安全的最佳实践
  • chrome扩展demo1-小时钟
  • CSS居中完全指南——构建CSS居中决策树
  • Linux中的硬链接与软链接
  • SpiderData 2019年2月25日 DApp数据排行榜
  • springMvc学习笔记(2)
  • Travix是如何部署应用程序到Kubernetes上的
  • 函数式编程与面向对象编程[4]:Scala的类型关联Type Alias
  • 技术攻略】php设计模式(一):简介及创建型模式
  • 实现简单的正则表达式引擎
  • 事件委托的小应用
  • 微服务入门【系列视频课程】
  • 云大使推广中的常见热门问题
  • 阿里云ACE认证学习知识点梳理
  • # Swust 12th acm 邀请赛# [ E ] 01 String [题解]
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • (007)XHTML文档之标题——h1~h6
  • (C语言)逆序输出字符串
  • (zhuan) 一些RL的文献(及笔记)
  • (ZT) 理解系统底层的概念是多么重要(by趋势科技邹飞)
  • (博弈 sg入门)kiki's game -- hdu -- 2147
  • (七)理解angular中的module和injector,即依赖注入
  • (三)elasticsearch 源码之启动流程分析
  • (四)库存超卖案例实战——优化redis分布式锁
  • (推荐)叮当——中文语音对话机器人
  • (转)Groupon前传:从10个月的失败作品修改,1个月找到成功
  • (转)甲方乙方——赵民谈找工作
  • * 论文笔记 【Wide Deep Learning for Recommender Systems】
  • .bat批处理(三):变量声明、设置、拼接、截取
  • .gitignore文件_Git:.gitignore
  • .net core 6 使用注解自动注入实例,无需构造注入 autowrite4net
  • .NET Core WebAPI中使用Log4net 日志级别分类并记录到数据库
  • .Net Core缓存组件(MemoryCache)源码解析
  • .Net中的集合
  • .sh 的运行
  • [ vulhub漏洞复现篇 ] Celery <4.0 Redis未授权访问+Pickle反序列化利用
  • [ 云计算 | AWS ] AI 编程助手新势力 Amazon CodeWhisperer:优势功能及实用技巧