颜色量子化(K-Means聚合算法以及八叉树的运用)

颜色量子化,又可以理解为图像主要颜色提取或者由图像生成调色板。归根结底,就是对一组颜色进行筛选处理,进而选择出其中具有代表性的N个颜色。

下面我们从两个应用场景来对该主题进行讲述:

一、图像主要颜色提取

假设场景

现在假设一种场景:从一张图片中提取n个主要颜色。

该场景下颜色的量子化可以使用聚合算法,集合算法是一种用于把一组数据进行分类为几个分组的算法。在我们设想的场景中,就是将一组颜色数据分类为几个分组,并获取到每个分组所代表的颜色。

解决方案

聚合算法有很多种实现方式,其中比较经典的一种实现方式是K-Means聚类算法,K-Means算法的基本思想是:以空间中k个点为质点进行聚类,对最靠近他们的对象归类。通过迭代的方法,逐次更新各簇的质点的值,直至得到最好的聚类结果。

大致流程如下:

K-Means聚合算法流程

以下是一个简单的示意图:

K-Means聚合算法示意图

其中,对于分类时所用到的距离,我们可以计算数据点到质点的欧式距离

欧式距离

欧氏距离是一个通常采用的距离定义,指在m维空间中两个点之间的真实距离,或者向量的自然长度(即该点到原点的距离)。在二维和三维空间中的欧氏距离就是两点之间的实际距离。

  1. 欧式距离二维空间表达式

    欧式距离二维空间表达式

  2. 欧式距离三维空间表达式

    欧式距离三维空间表达式

  3. 欧式距离n维空间表达式

    欧式距离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聚合算法素材

在执行K-Means聚合算法获取主题色后,结果为:

编号 颜色代码 颜色 备注
1 FEFCFC K-Means聚合算法主题色 此处图片接近白色,可能肉眼无法辨别
2 7B5B50 K-Means聚合算法主题色
3 B3A6C3 K-Means聚合算法主题色

总结

总结一下K-Means算法的特点:

  1. 该算法简单,只是单纯的迭代分类并优化质点位置。
  2. 算法是否成功受到初次质点位置影响较大,若初次质点位置距离较近,可能造成聚合失败。
  3. 该算法对独立点很敏感,若聚合数据中有某个点相较于其他点很独立,那么算法可以很敏感的提取出该点数据。应用场景可以根据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的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叉树减少叶子

如此从树的最深层开始减少叶子节点,直到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;
    }
}
发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/81745737