颜色量子化,又可以理解为图像主要颜色提取或者由图像生成调色板。归根结底,就是对一组颜色进行筛选处理,进而选择出其中具有代表性的N个颜色。
下面我们从两个应用场景来对该主题进行讲述:
一、图像主要颜色提取
假设场景
现在假设一种场景:从一张图片中提取n个主要颜色。
该场景下颜色的量子化可以使用聚合算法,集合算法是一种用于把一组数据进行分类为几个分组的算法。在我们设想的场景中,就是将一组颜色数据分类为几个分组,并获取到每个分组所代表的颜色。
解决方案
聚合算法有很多种实现方式,其中比较经典的一种实现方式是K-Means聚类算法,K-Means算法的基本思想是:以空间中k个点为质点进行聚类,对最靠近他们的对象归类。通过迭代的方法,逐次更新各簇的质点的值,直至得到最好的聚类结果。
大致流程如下:
以下是一个简单的示意图:
其中,对于分类时所用到的距离,我们可以计算数据点到质点的欧式距离。
欧式距离
欧氏距离是一个通常采用的距离定义,指在m维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。在二维和三维空间中的欧氏距离就是两点之间的实际距离。
欧式距离二维空间表达式
欧式距离三维空间表达式
欧式距离n维空间表达式
算法实现
类比到我们假设的场景中,就是输入一组以RGB表示的颜色数据,然后对这些颜色数据进行归类,接下来我们用代码来实现该算法:
1. 首先定义一个结构体来表示颜色
typedef struct MDMColor {
NSUInteger red;
NSUInteger green;
NSUInteger blue;
} MDMColor;
2. 定义一个表示点的结构体
typedef struct MDMNode {
MDMColor color;
NSUInteger i;
} MDMNode;
该结构体有两个用途:
- 表示颜色数据所在的点,其中color就是颜色数据,i表示所属质点。
- 表示质点数据,其中color表示质点类下所有颜色数据中RGB颜色的总量,i表示分类下颜色数据的个数。
3. 算法方法入口
/**
通过k-means算法来进行颜色量子化
@param colorArray 需要进行量子化的颜色数据
@param count 需要进行量子化的颜色数据个数
@param maxCount 需要量子化得到的颜色个数
@return 量子化之后的颜色数据
*/
+ (MDMColor *)calculateColorTableWithColorArray:(MDMColor *)colorArray count:(NSUInteger)count maxCount:(NSUInteger)maxCount;
4. 随机生成第一次聚合所需要的质点
//生成所需质点
for (NSUInteger i = 0; i < maxCount; i++) {
NSUInteger index = arc4random() % count;
MDMColor tmpColor = colorArray[index];
MDMColor color;
color.red = tmpColor.red;
color.green = tmpColor.green;
color.blue = tmpColor.blue;
colorTable[i] = color;
}
5. 遍历数据,对数据进行分类
for (NSUInteger i = 0; i < count; i++) {
MDMColor color = colorArray[i];
NSUInteger index = 0;
CGFloat minDis = CGFLOAT_MAX;
for (NSUInteger j = 0; j < maxCount; j++) {
MDMColor anotherColor = colorTable[j];
CGFloat dis = [self calculateDisWithColor:color anotherColor:anotherColor];
index = minDis > dis ? j : index;
minDis = minDis > dis ? dis : minDis;
if (minDis < __FLT_EPSILON__) {
break;
}
}
materialPoint[index].color.red += color.red;
materialPoint[index].color.green += color.green;
materialPoint[index].color.blue += color.blue;
materialPoint[index].i += 1;
}
其中,colorTable表示此次迭代中所有质点所在位置;materialPoint表示此次迭代中,每个质点所包含的分类数据(所有所属颜色数据RGB的总量以及颜色数据数量)。
6. 更新质点数据
for (NSUInteger i = 0; i < maxCount; i++) {
MDMNode node = materialPoint[i];
if (node.i != 0) {
colorTable[i].red = node.color.red / node.i;
colorTable[i].green = node.color.green / node.i;
colorTable[i].blue = node.color.blue / node.i;
}
}
获取该质点所属分类下所有颜色数据,并求取所有RGB总量的平均值来作为新质点的位置。
7. 重复迭代步骤5、6
8. 结束条件
CGFloat maxDis = 0;
for (NSUInteger i = 0; i < maxCount; i++) {
MDMNode node = materialPoint[i];
MDMColor color;
color.red = colorTable[i].red;
color.green = colorTable[i].green;
color.blue = colorTable[i].blue;
if (node.i != 0) {
colorTable[i].red = node.color.red / node.i;
colorTable[i].green = node.color.green / node.i;
colorTable[i].blue = node.color.blue / node.i;
}
CGFloat dis = [self calculateDisWithColor:colorTable[i] anotherColor:color];
maxDis = maxDis < dis ? dis : maxDis;
}
return maxDis < 5;
此处采用当所有质点相对于上次迭代位置偏移量最大值小于5时结束迭代。
完整代码附在博客的最尾端
应用
将算法应用到设想的场景中,对于下面图片:
在执行K-Means聚合算法获取主题色后,结果为:
编号 | 颜色代码 | 颜色 | 备注 |
---|---|---|---|
1 | FEFCFC | 此处图片接近白色,可能肉眼无法辨别 | |
2 | 7B5B50 | ||
3 | B3A6C3 |
总结
总结一下K-Means算法的特点:
- 该算法简单,只是单纯的迭代分类并优化质点位置。
- 算法是否成功受到初次质点位置影响较大,若初次质点位置距离较近,可能造成聚合失败。
- 该算法对独立点很敏感,若聚合数据中有某个点相较于其他点很独立,那么算法可以很敏感的提取出该点数据。应用场景可以根据K-Means这一特点来选择是否使用该算法。
二、由图像生成调色板
我们知道,在GIF文件中,一般只是用一个全局调色板来保存GIF中所有帧图片的颜色,但是由于全局调色板最多只能保存256种颜色,所以在帧图片所含颜色较多的情况下会丢失部分颜色,造成GIF中帧图片相较于原图来说变模糊。
对于这种情况,一种解决方法是为GIF中每一帧图片指定一个局部调色板,该局部调色板只对对应帧起作用,这样对于每一帧图片就可以较大程度的保存颜色数据,提高清晰度。
图片颜色范围
我们都知道,颜色一般都是使用R、G、B三个颜色分量来表示,而且通常情况下,都是使用8位颜色,即使用0-255来表示一个颜色分量。
如果我们将颜色对应到三维数组中,那么就可以形成一个256 * 256 * 256大小的颜色立方体,这个立方体可以划分为256 * 256 * 256个大小为1 * 1的小立方体,每个小立方体代表一种颜色。
8叉树
8叉树的每个节点最多可以含有8个子节点,这个特性可以类比到一个大立方体可以等分切割为8个大小相同的小立方体,如下图所示:
当8叉树有8层时,就可以表示8的8次方个小立方体,即256 * 256 * 256个小立方体,那么此时可以把颜色数据一一对应到8叉树的所有叶子上了。
建树
那么如何把颜色数据建造为8叉树呢?在1988年,由M. Gervautz 和 W. Purgathofer 发表的论文《A Simple Method for Color Quantization: Octree Quantization》就提出了建造方法,该方法的大致思路为:
1.将每个颜色的RGB分类使用二进制表示,因为采用的是8位颜色,所以使用8位二进制就可以表示所有情况,例如颜色0369CF
03: 0000 0011
69: 0110 1001
CF: 1100 1111
2.将R、G、B三个颜色分量的第n为组合为一个3位二进制数字,用来表示该颜色在树的第n层中的位置。
例如,颜色0369CF在第1层中的位置为001(1)位,在第2层中的位置为011(3)位,在第3层中的位置为010(2)位,以此类推。
这样就把所有颜色对应到8叉树第8层的所有叶子节点上了。
场景应用
我们可以用图片中所有的颜色数据构造一颗8层的8叉树,此时要生成最大颜色数为256的配色表,我们就需要从8叉树中减少表示颜色的叶子,一直减少的只有256个叶子为止。
那么如何减少8叉树的叶子,并且在减少叶子的过程中不会过多的丢失颜色数据?
由建树的过程可知,每一个节点的子节点所表示的颜色都包含在同一色域下,例如颜色0369CF所在节点的父节点就可以表示为:
R: 0000 001X
G: 0110 100X
B: 1100 111X
该父节点可以表示8个颜色所在色域。我们在减少叶子的过程中,可以将叶子保存的颜色交付于父节点保存,然后剔除子节点,最后使用父节点颜色总量的平均值来表示该色域的颜色,这样可以在减少叶子的同时最大程度的聚拢颜色,如图:
如此从树的最深层开始减少叶子节点,直到8叉树的叶子数小于等于我们所需要256数量,那么此时就可以用所有叶子来构建一副配色表了。
完整代码附在博客的最尾端
三、代码
1. K-Means聚合算法
- KMeansTool.h文件
#import <UIKit/UIKit.h>
typedef struct MDMColor {
NSUInteger red;
NSUInteger green;
NSUInteger blue;
} MDMColor;
typedef struct MDMNode {
MDMColor color;
NSUInteger i;
} MDMNode;
@interface KMeansTool : NSObject
/**
通过k-means算法来进行颜色量子化
@param colorArray 需要进行量子化的颜色数据
@param count 需要进行量子化的颜色数据个数
@param maxCount 需要量子化得到的颜色个数
@return 量子化之后的颜色数据
*/
+ (MDMColor *)calculateColorTableWithColorArray:(MDMColor *)colorArray count:(NSUInteger)count maxCount:(NSUInteger)maxCount;
@end
- KMeansTool.m文件
#import "KMeansTool.h"
static NSUInteger kmeansCount = 0;
@implementation KMeansTool
+ (MDMColor *)calculateColorTableWithColorArray:(MDMColor *)colorArray count:(NSUInteger)count maxCount:(NSUInteger)maxCount {
MDMColor *colorTable = (MDMColor *)malloc(sizeof(MDMColor) * maxCount);
//如果所需量子化的数据少于总数据,则无需计算,直接返回
if (count <= maxCount) {
for (NSUInteger i = 0; i < count; i++) {
MDMColor tmpColor = colorArray[i];
MDMColor color;
color.red = tmpColor.red;
color.green = tmpColor.green;
color.blue = tmpColor.blue;
colorTable[i] = color;
}
return colorTable;
}
//生成所需质点
for (NSUInteger i = 0; i < maxCount; i++) {
NSUInteger index = arc4random() % count;
MDMColor tmpColor = colorArray[index];
MDMColor color;
color.red = tmpColor.red;
color.green = tmpColor.green;
color.blue = tmpColor.blue;
colorTable[i] = color;
}
//执行k-means算法,直到聚合数据趋于稳定
BOOL finish = NO;
kmeansCount = 0;
do {
finish = [self doClusterWithColorArray:colorArray count:count maxCount:maxCount colorTable:colorTable];
} while (finish == NO);
printf("聚合结束\n-----------------\n");
return colorTable;
}
+ (BOOL)doClusterWithColorArray:(MDMColor *)colorArray count:(NSUInteger)count maxCount:(NSUInteger)maxCount colorTable:(MDMColor *)colorTable {
printf("-----------------\n第%zd次聚合\n质点:\n", ++kmeansCount);
for (NSUInteger i = 0; i < maxCount; i++) {
printf("第%zd个质点:%zd %zd %zd\n", i, colorTable[i].red, colorTable[i].green, colorTable[i].blue);
}
//此处的materialPoint用来记录质点中rgb总量以及所属质点颜色点的数量
MDMNode *materialPoint = (MDMNode *)malloc(sizeof(MDMNode) * maxCount);
if (materialPoint == NULL) {
return NO;
}
for (NSUInteger i = 0; i < maxCount; i++) {
MDMColor color;
color.red = 0;
color.green = 0;
color.blue = 0;
MDMNode node;
node.color = color;
node.i = 0;
materialPoint[i] = node;
}
for (NSUInteger i = 0; i < count; i++) {
MDMColor color = colorArray[i];
NSUInteger index = 0;
CGFloat minDis = CGFLOAT_MAX;
for (NSUInteger j = 0; j < maxCount; j++) {
MDMColor anotherColor = colorTable[j];
CGFloat dis = [self calculateDisWithColor:color anotherColor:anotherColor];
index = minDis > dis ? j : index;
minDis = minDis > dis ? dis : minDis;
if (minDis < __FLT_EPSILON__) {
break;
}
}
materialPoint[index].color.red += color.red;
materialPoint[index].color.green += color.green;
materialPoint[index].color.blue += color.blue;
materialPoint[index].i += 1;
}
CGFloat maxDis = 0;
for (NSUInteger i = 0; i < maxCount; i++) {
MDMNode node = materialPoint[i];
MDMColor color;
color.red = colorTable[i].red;
color.green = colorTable[i].green;
color.blue = colorTable[i].blue;
if (node.i != 0) {
colorTable[i].red = node.color.red / node.i;
colorTable[i].green = node.color.green / node.i;
colorTable[i].blue = node.color.blue / node.i;
}
CGFloat dis = [self calculateDisWithColor:colorTable[i] anotherColor:color];
maxDis = maxDis < dis ? dis : maxDis;
}
free(materialPoint);
printf("第%zd次聚合完成\n质点:\n", kmeansCount);
for (NSUInteger i = 0; i < maxCount; i++) {
printf("第%zd个质点:%zd %zd %zd\n", i, colorTable[i].red, colorTable[i].green, colorTable[i].blue);
}
return maxDis < 5;
}
+ (CGFloat)calculateDisWithColor:(MDMColor)color anotherColor:(MDMColor)anotherColor {
long long red = color.red - anotherColor.red;
long long green = color.green - anotherColor.green;
long long blue = color.blue - anotherColor.blue;
return red * red + green * green + blue * blue;
}
@end
2. 使用8叉树为图片生成调色板
- OctreeQuantizer.h文件
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#import <malloc/malloc.h>
static const int kMAXLEVEL = 8;
typedef struct OctreeNode {
int redSum;
int greenSum;
int blueSum;
int count;
int level;
struct OctreeNode *children[kMAXLEVEL];
} OctreeNode;
#pragma mark - public
static malloc_zone_t *my_zone = NULL;
OctreeNode* buildRootNode(void);
void initOctreeNode(OctreeNode *node, int redSum, int greenSum, int blueSum, int count, int level);
void addColor(OctreeNode *rootNode, int red, int green, int blue);
int calculateIndex(int level, int red, int green, int blue);
int getLeafCount(OctreeNode *node);
BOOL isLeaf(OctreeNode *node);
void makePalette(OctreeNode *node, int maxColorCount);
void reduceNode(OctreeNode *node, int level, int maxColorCount, int *currentColorCount);
UIImage* drawPalette(OctreeNode *node);
void drawPoint(OctreeNode *node, CGContextRef *context, int *x, int *y, int level);
void freeNode(OctreeNode *node);
void clean(void);
#pragma mark - private
OctreeNode* buildRootNode(void) {
clean();
my_zone = malloc_create_zone(0, 0);
OctreeNode *rootNode = my_zone->malloc(my_zone, sizeof(OctreeNode));
initOctreeNode(rootNode, 0, 0, 0, 0, -1);
return rootNode;
}
void initOctreeNode(OctreeNode *node, int redSum, int greenSum, int blueSum, int count, int level) {
node->redSum = redSum;
node->greenSum = greenSum;
node->blueSum = blueSum;
node->count = count;
node->level = level;
for (int i = 0; i < kMAXLEVEL; i++) {
node->children[i] = NULL;
}
}
void addColor(OctreeNode *rootNode, int red, int green, int blue) {
OctreeNode *node = rootNode;
for (int i = 0; i < kMAXLEVEL; i++) {
int index = calculateIndex(i, red, green, blue);
OctreeNode *childrenNode = node->children[index];
if (node->children[index] == NULL) {
childrenNode = my_zone->malloc(my_zone, sizeof(OctreeNode));
initOctreeNode(childrenNode, 0, 0, 0, 0, i);
node->children[index] = childrenNode;
}
node = childrenNode;
}
node->redSum += red;
node->greenSum += green;
node->blueSum += blue;
node->count++;
}
int calculateIndex(int level, int red, int green, int blue) {
int index = 0;
int mask = 0x80 >> level;
if (red & mask) index |= 4;
if (green & mask) index |= 2;
if (blue & mask) index |= 1;
return index;
}
int getLeafCount(OctreeNode *node) {
if (isLeaf(node) == YES) {
return 1;
} else {
int count = 0;
for (int i = 0; i < kMAXLEVEL; i++) {
OctreeNode *childrenNode = node->children[i];
if (childrenNode != NULL) {
count += getLeafCount(childrenNode);
}
}
return count;
}
}
BOOL isLeaf(OctreeNode *node) {
BOOL isLeaf = YES;
for (int i = 0; i < kMAXLEVEL; i++) {
OctreeNode *childrenNode = node->children[i];
if (childrenNode != NULL) {
isLeaf = NO;
break;
}
}
return isLeaf;
}
void makePalette(OctreeNode *node, int maxColorCount) {
int colorCount = getLeafCount(node);
if (colorCount <= maxColorCount) {
return;
}
for (int i = kMAXLEVEL - 1; i >= 0; i--) {
reduceNode(node, i, maxColorCount, &colorCount);
if (colorCount <= maxColorCount) {
break;
}
}
}
void reduceNode(OctreeNode *node, int level, int maxColorCount, int *currentColorCount) {
if (node == NULL) {
return;
}
if (*currentColorCount <= maxColorCount) {
return;
}
if (node->level < level - 1) {
for (int i = kMAXLEVEL - 1; i >= 0; i--) {
OctreeNode *childrenNode = node->children[i];
reduceNode(childrenNode, level, maxColorCount, currentColorCount);
}
} else {
for (int i = 0; i < kMAXLEVEL; i++) {
OctreeNode *childrenNode = node->children[i];
if (childrenNode != NULL) {
node->redSum += childrenNode->redSum;
node->greenSum += childrenNode->greenSum;
node->blueSum += childrenNode->blueSum;
node->count += childrenNode->count;
node->children[i] = NULL;
(*currentColorCount)--;
freeNode(childrenNode);
}
}
(*currentColorCount) += 1;
if (*currentColorCount <= maxColorCount) {
return;
}
}
}
UIImage* drawPalette(OctreeNode *node) {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(16, 16), YES, 1.0);
CGContextRef context = UIGraphicsGetCurrentContext();
int x = 0, y = 0;
for (int i = kMAXLEVEL; i >= 0; i--) {
drawPoint(node, &context, &x, &y, i);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
void drawPoint(OctreeNode *node, CGContextRef *context, int *x, int *y, int level) {
if (node == NULL) {
return;
}
if (node->level < level) {
for (int i = kMAXLEVEL - 1; i >= 0; i--) {
OctreeNode *childrenNodel = node->children[i];
drawPoint(childrenNodel, context, x, y, level);
}
} else {
if (isLeaf(node) == YES) {
UIColor *color = [UIColor colorWithRed:(node->redSum / (double)(node->count)) / 255.0 green:(node->greenSum / (double)(node->count)) / 255.0 blue:(node->blueSum / (double)(node->count)) / 255.0 alpha:1.0];
CGContextSetFillColorWithColor(*context, color.CGColor);
CGContextFillRect(*context, CGRectMake(*x, *y, 1, 1));
if (node->level == 7 && node->blueSum == 1) {
NSLog(@"%@", color);
}
if (*x == 15) {
*x = 0;
*y += 1;
} else {
*x += 1;
}
}
}
}
void freeNode(OctreeNode *node) {
if (node == NULL) {
return;
}
for (int i = 0; i < kMAXLEVEL; i++) {
OctreeNode *childrenNode = node->children[i];
freeNode(childrenNode);
node->children[i] = NULL;
}
my_zone->free(my_zone, node);
}
void clean(void) {
if (my_zone) {
malloc_destroy_zone(my_zone);
my_zone = NULL;
}
}