如何使用OpenCV减少图像中的颜色数量?

32

我有一组图像文件,我希望将它们的颜色数量减少到64个。我该如何使用OpenCV实现这个目标?
我需要这么做是为了能够处理一个大小为64的图像直方图。我正在实现CBIR技术。
我的需求是将颜色量化到一个4位调色板中。

如果您想要64种颜色,则需要一个6位调色板。请参阅我的回答,以获得更好的解释和代码,以使用每个颜色通道的2位来构建6位调色板图像。 - Moacir Ponti
@Felipe 添加了一个新答案,你可能会觉得它很有趣。 - karlphillip
12个回答

19
这个主题在OpenCV 2计算机视觉应用编程食谱一书中得到了很好的涵盖:

第二章介绍了一些缩减操作,其中一个在此用C++演示,稍后再用Python演示:
#include <iostream>
#include <vector>

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>


void colorReduce(cv::Mat& image, int div=64)
{    
    int nl = image.rows;                    // number of lines
    int nc = image.cols * image.channels(); // number of elements per line

    for (int j = 0; j < nl; j++)
    {
        // get the address of row j
        uchar* data = image.ptr<uchar>(j);

        for (int i = 0; i < nc; i++)
        {
            // process each pixel
            data[i] = data[i] / div * div + div / 2;
        }
    }
}

int main(int argc, char* argv[])
{   
    // Load input image (colored, 3-channel, BGR)
    cv::Mat input = cv::imread(argv[1]);
    if (input.empty())
    {
        std::cout << "!!! Failed imread()" << std::endl;
        return -1;
    } 

    colorReduce(input);

    cv::imshow("Color Reduction", input);   
    cv::imwrite("output.jpg", input);   
    cv::waitKey(0);

    return 0;
}

以下是此操作的输入图像(左侧)和输出(右侧):

Python中等价的代码如下所示: (感谢@eliezer-bernart

import cv2
import numpy as np

input = cv2.imread('castle.jpg')

# colorReduce()
div = 64
quantized = input // div * div + div // 2

cv2.imwrite('output.jpg', quantized)

我知道这是一篇旧帖子,但我想问一下你能否详细解释一下这个“量化” data[i] = data[i] / div * div + div / 2; 是如何工作的。 - ksyrium
1
@ksyrium 我认为主要问题在于,如果你只是将像素颜色除以 div 并结束计算,得到的图像会太暗。这种方法将量化后的颜色向上移动一些,以帮助保留一点原始颜色强度。至少从查看这个10年前的答案来看,这是我的理解。 - karlphillip
1
@ksyrium 但这大致是书中试图解释的方式:首先将像素除以 div,然后再乘以 div,以获得小于像素值的 div 的倍数。然后,加上 div / 2,以获得相邻的 div 倍数之间的中心位置。对每个 RGB 通道重复此过程,您将获得总共 256/div x 256/div x 256/div 种可能的颜色值。 - karlphillip

16

你可以考虑使用K-means,但在这种情况下,它可能会非常慢。一种更好的方法是自己"手动"处理。假设你有一幅类型为CV_8UC3的图像,即每个像素由来自0到255的3个RGB值(Vec3b)表示。你可以将这256个值中的四个特定值"映射"到相应的颜色中,这将产生4 x 4 x 4=64种可能的颜色。

我曾经有一个数据集,需要确保黑色=黑色,白色=白色,并减少其他所有颜色的数量。这是我的做法(C++):

inline uchar reduceVal(const uchar val)
{
    if (val < 64) return 0;
    if (val < 128) return 64;
    return 255;
}

void processColors(Mat& img)
{
    uchar* pixelPtr = img.data;
    for (int i = 0; i < img.rows; i++)
    {
        for (int j = 0; j < img.cols; j++)
        {
            const int pi = i*img.cols*3 + j*3;
            pixelPtr[pi + 0] = reduceVal(pixelPtr[pi + 0]); // B
            pixelPtr[pi + 1] = reduceVal(pixelPtr[pi + 1]); // G
            pixelPtr[pi + 2] = reduceVal(pixelPtr[pi + 2]); // R
        }
    }
}

[0,64)变为0[64,128)变为64[128,255)变为255,得到27种颜色:

enter image description here enter image description here

对我来说,这似乎很整洁、非常清晰,而且比其他答案中提到的任何解决方案都要快。

您还可以考虑将这些值减少到某个数字的倍数之一,比如:

inline uchar reduceVal(const uchar val)
{
    if (val < 192) return uchar(val / 64.0 + 0.5) * 64;
    return 255;
}

这将产生一组5个可能的值:{0、64、128、192、255},即125种颜色。


11

有许多方法可以实现。jeff7建议的方法是可以的,但一些缺点是:

  • 方法1需要选择参数N和M,还必须将其转换为另一种颜色空间。
  • 方法2的计算量可能很大,因为您需要计算1670万个bin的直方图,并按频率对其进行排序(以获取64个较高的频率值)。

我喜欢使用基于最高有效位的算法来使用RGB颜色并将其转换为64色图像。如果您正在使用C/OpenCV,则可以使用下面的函数之类的东西。

如果您正在使用灰度图像,则建议使用OpenCV 2.3的LUT()函数,因为它速度更快。有一个关于如何使用LUT减少颜色数量的教程。请参见:教程:如何扫描图像,查找表...但是,如果您正在使用RGB图像,则我发现它更加复杂。

void reduceTo64Colors(IplImage *img, IplImage *img_quant) {
    int i,j;
    int height   = img->height;   
    int width    = img->width;    
    int step     = img->widthStep;

    uchar *data = (uchar *)img->imageData;
    int step2 = img_quant->widthStep;
    uchar *data2 = (uchar *)img_quant->imageData;

    for (i = 0; i < height ; i++)  {
        for (j = 0; j < width; j++)  {

          // operator XXXXXXXX & 11000000 equivalent to  XXXXXXXX AND 11000000 (=192)
          // operator 01000000 >> 2 is a 2-bit shift to the right = 00010000 
          uchar C1 = (data[i*step+j*3+0] & 192)>>2;
          uchar C2 = (data[i*step+j*3+1] & 192)>>4;
          uchar C3 = (data[i*step+j*3+2] & 192)>>6;

          data2[i*step2+j] = C1 | C2 | C3; // merges the 2 MSB of each channel
        }     
    }
}

1
你不能在非灰度图像上仍然使用LUT吗?如果你在这里给出的OpenCV教程链接中运行代码,可以通过将64作为输入参数提供给他们的代码来轻松地将颜色空间量化到64种颜色。这本质上与在你的示例中提取2个最高有效位相同。或者我错过了什么? - t2k32316
@Moacir 这个函数不应该返回任何值,因为它的签名声明了返回 void - karlphillip

10
这是一个使用K-Means聚类和cv2.kmeans的Python颜色量化实现。其思想是尽可能地减少图像中不同颜色的数量,同时尽可能地保留图像的颜色外观。这是结果:

输入 -> 输出

代码

import cv2
import numpy as np

def kmeans_color_quantization(image, clusters=8, rounds=1):
    h, w = image.shape[:2]
    samples = np.zeros([h*w,3], dtype=np.float32)
    count = 0

    for x in range(h):
        for y in range(w):
            samples[count] = image[x][y]
            count += 1

    compactness, labels, centers = cv2.kmeans(samples,
            clusters, 
            None,
            (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10000, 0.0001), 
            rounds, 
            cv2.KMEANS_RANDOM_CENTERS)

    centers = np.uint8(centers)
    res = centers[labels.flatten()]
    return res.reshape((image.shape))

image = cv2.imread('1.jpg')
result = kmeans_color_quantization(image, clusters=8)
cv2.imshow('result', result)
cv2.waitKey()     

5
以下提供的答案真的很好。我想我也可以加入我的想法。我遵循这里许多评论的公式,其中说在RGB图像中每个通道的2位可以表示64种颜色。
下面的代码函数将图像和所需量化的比特数作为输入。它使用位操作来“丢弃”LSB位并仅保留所需数量的位。结果是一种灵活的方法,可以将图像量化到任意数量的位。
#include "include\opencv\cv.h"
#include "include\opencv\highgui.h"

// quantize the image to numBits 
cv::Mat quantizeImage(const cv::Mat& inImage, int numBits)
{
    cv::Mat retImage = inImage.clone();

    uchar maskBit = 0xFF;

    // keep numBits as 1 and (8 - numBits) would be all 0 towards the right
    maskBit = maskBit << (8 - numBits);

    for(int j = 0; j < retImage.rows; j++)
        for(int i = 0; i < retImage.cols; i++)
        {
            cv::Vec3b valVec = retImage.at<cv::Vec3b>(j, i);
            valVec[0] = valVec[0] & maskBit;
            valVec[1] = valVec[1] & maskBit;
            valVec[2] = valVec[2] & maskBit;
            retImage.at<cv::Vec3b>(j, i) = valVec;
        }

        return retImage;
}


int main ()
{
    cv::Mat inImage;
    inImage = cv::imread("testImage.jpg");
    char buffer[30];
    for(int i = 1; i <= 8; i++)
    {
        cv::Mat quantizedImage = quantizeImage(inImage, i);
        sprintf(buffer, "%d Bit Image", i);
        cv::imshow(buffer, quantizedImage);

        sprintf(buffer, "%d Bit Image.png", i);
        cv::imwrite(buffer, quantizedImage);
    }

    cv::waitKey(0);
    return 0;
}

下面是在上述函数调用中使用的图片:

这里输入图片描述

每个RGB通道量化为2位(总共64种颜色):

这里输入图片描述

每个通道量化为3位:

这里输入图片描述

每个通道量化为4位...

这里输入图片描述


3
在OpenCV库中已经有K-means聚类算法可用。简单来说,它确定最佳质心以聚集用户定义的k值(=聚类数)的数据。因此,在您的情况下,您可以找到质心以聚集给定值k = 64的像素值。如果您需要了解详细信息,请搜索相关内容。这里是关于k-means的简短介绍:链接
类似于您可能正在尝试的内容,这里在Stack Overflow上使用k-means提出了一个类似的问题,希望对您有所帮助:链接
另一种方法是使用OpenCV中的金字塔均值漂移滤波器函数。它产生了一些“扁平化”的图像,即颜色数量较少,因此可能能够帮助您。这里是关于该函数的链接:链接

2
如果您想要一个快速而简单的C++方法,只需1行代码:
capImage &= cv::Scalar(0b11000000, 0b11000000, 0b11000000);

所以,它的作用是保留每个R、G、B分量的前2位,并且舍弃后6位,因此得到0b11000000。
由于RGB中有3个通道,最多可以获得4 R x 4 B x 4 B = 最多64种颜色。这样做的优点是可以在任意数量的图像上运行,同样的颜色将被映射。
请注意,这可能会使您的图像变暗,因为它舍弃了一些位。
对于灰度图像,您可以执行以下操作:
capImage &= 0b11111100;

这将保留前6位,也就是说你可以从256种颜色中获得64种灰度,因此图像可能会变得稍微暗一些。
以下是一个例子,原始图像有251424种独特的颜色。 enter image description here 生成的图像只有46种颜色: enter image description here

1

使用适当的位掩码进行简单的按位与运算即可解决问题。

对于64种颜色,Python中的方法为:

img = img & int("11000000", 2)

RGB图像的颜色数量应该是一个完美的立方体(在3个通道中相同)。
对于这种方法,通道可能的值的数量应该是2的幂。(代码忽略了这个检查,并取下一个较低的2的幂)。
import numpy as np
import cv2 as cv


def is_cube(n):
    cbrt = np.cbrt(n)
    return cbrt ** 3 == n, int(cbrt)


def reduce_color_space(img, n_colors=64):
    n_valid, cbrt = is_cube(n_colors)

    if not n_valid:
        print("n_colors should be a perfect cube")
        return

    n_bits = int(np.log2(cbrt))

    if n_bits > 8:
        print("Can't generate more colors")
        return

    bitmask = int(f"{'1' * n_bits}{'0' * (8 - n_bits)}", 2)

    return img & bitmask


img = cv.imread("image.png")

cv.imshow("orig", img)
cv.imshow("reduced", reduce_color_space(img))

cv.waitKey(0)

1

假设您想为所有图像使用相同的64种颜色(即调色板不针对每个图像进行优化),那么我可以想到至少有几种选择:

1)将其转换为Lab或YCrCb颜色空间,并使用N位用于亮度和M位用于每个颜色通道进行量化,其中N应大于M。

2)在所有训练图像上计算颜色值的3D直方图,然后选择具有最大bin值的64种颜色。通过将每个像素分配给来自训练集中最接近的bin的颜色来量化您的图像。

方法1是最通用且最易于实现的方法,而方法2可以更好地适应您的特定数据集。

更新: 例如,32种颜色为5位,因此将3位分配给亮度通道和1位分配给每个颜色通道。要进行量化,请将亮度通道除以2 ^ 8 / 2 ^ 3 = 32,将每个颜色通道除以2 ^ 8 / 2 ^ 1 = 128。现在只有8种不同的亮度值和每种2种不同的颜色通道。通过位移或数学运算将这些值重新组合成单个整数(量化的颜色值=亮度* 4 + color1 * 2 + color2);


1
使用N位对亮度进行量化,每个色彩通道使用M位,其中N应大于M。 - 我该如何完成这个部分? - Felipe Hummel

1

img = numpy.multiply(img//32, 32)


好的,那么它应该是一个答案。在这种情况下... - Yunnosch
2
虽然这段代码可能解决了问题,但是包括解释它如何以及为什么解决了问题将有助于提高您的帖子质量,并可能导致更多的赞。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。请[编辑]您的答案以添加解释并指出适用的限制和假设。 - Yunnosch

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接