How to reduce the number of colors in an image with OpenCV?
Asked Answered
E

12

32

I have a set of image files, and I want to reduce the number of colors of them to 64. How can I do this with OpenCV?

I need this so I can work with a 64-sized image histogram. I'm implementing CBIR techniques

What I want is color quantization to a 4-bit palette.

Emory answered 6/5, 2011 at 4:12 Comment(2)
If you want 64 colors, you want a 6-bit palette. See my answer below to a better explanation and code for using 2 bits from each color channel in order to build a 6-bit palette image.Thickwitted
@Felipe Added a new answer, you might find it interesting.Overrefinement
T
11

There are many ways to do it. The methods suggested by jeff7 are OK, but some drawbacks are:

  • method 1 have parameters N and M, that you must choose, and you must also convert it to another colorspace.
  • method 2 answered can be very slow, since you should compute a 16.7 Milion bins histogram and sort it by frequency (to obtain the 64 higher frequency values)

I like to use an algorithm based on the Most Significant Bits to use in a RGB color and convert it to a 64 color image. If you're using C/OpenCV, you can use something like the function below.

If you're working with gray-level images I recommed to use the LUT() function of the OpenCV 2.3, since it is faster. There is a tutorial on how to use LUT to reduce the number of colors. See: Tutorial: How to scan images, lookup tables... However I find it more complicated if you're working with RGB images.

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
        }     
    }
}
Thickwitted answered 16/4, 2012 at 18:45 Comment(2)
Could you not still use LUT on non-gray-scale images? If you run the code from the OpenCV tutorial in the link you gave here, you can easily quantize the color space down to 64 colors by giving 64 as an input parameter to their code. That essentially does the same thing as extracting the 2 most significant bits in your example. Or am I missing something?Clamper
@Moacir This function shouldn't return a value since it's signature states that it returns void.Overrefinement
O
19

This subject was well covered on OpenCV 2 Computer Vision Application Programming Cookbook:

Chapter 2 shows a few reduction operations, one of them demonstrated here in C++ and later in 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;
}

Below you can find the input image (left) and the output of this operation (right):

The equivalent code in Python would be the following: (credits to @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)
Overrefinement answered 21/12, 2013 at 3:23 Comment(3)
I know this is an old post, but I'd like to ask if you can elaborate a bit on how this "quantization" data[i] = data[i] / div * div + div / 2; works.Misconstruction
@Misconstruction I think the main point is that if you simply divide the pixel color by div and end the calculation there, the resulting image would be too dark. This method shifts the quantized color back up to help preserve a bit of the original color intensity. At least that is my understanding from looking at this 10 year old answer.Overrefinement
@Misconstruction But this is more or less how the book tries to explain it: first divide the pixel by div and then multiply it by div to get a multiple of div that is just below the pixel value. Then, add div / 2 to obtain the central position of the interval between 2 adjacent multiples of div. Repeat this process for each RGB channel and you'll get a total of 256/div x 256/div x 256/div possible color values.Overrefinement
N
16

You might consider K-means, yet in this case it will most likely be extremely slow. A better approach might be doing this "manually" on your own. Let's say you have image of type CV_8UC3, i.e. an image where each pixel is represented by 3 RGB values from 0 to 255 (Vec3b). You might "map" these 256 values to only 4 specific values, which would yield 4 x 4 x 4 = 64 possible colors.

I've had a dataset, where I needed to make sure that dark = black, light = white and reduce the amount of colors of everything between. This is what I did (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
        }
    }
}

causing [0,64) to become 0, [64,128) -> 64 and [128,255) -> 255, yielding 27 colors:

enter image description here enter image description here

To me this seems to be neat, perfectly clear and faster than anything else mentioned in other answers.

You might also consider reducing these values to one of the multiples of some number, let's say:

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

which would yield a set of 5 possible values: {0, 64, 128, 192, 255}, i.e. 125 colors.

Nebulize answered 23/11, 2013 at 1:22 Comment(0)
T
11

There are many ways to do it. The methods suggested by jeff7 are OK, but some drawbacks are:

  • method 1 have parameters N and M, that you must choose, and you must also convert it to another colorspace.
  • method 2 answered can be very slow, since you should compute a 16.7 Milion bins histogram and sort it by frequency (to obtain the 64 higher frequency values)

I like to use an algorithm based on the Most Significant Bits to use in a RGB color and convert it to a 64 color image. If you're using C/OpenCV, you can use something like the function below.

If you're working with gray-level images I recommed to use the LUT() function of the OpenCV 2.3, since it is faster. There is a tutorial on how to use LUT to reduce the number of colors. See: Tutorial: How to scan images, lookup tables... However I find it more complicated if you're working with RGB images.

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
        }     
    }
}
Thickwitted answered 16/4, 2012 at 18:45 Comment(2)
Could you not still use LUT on non-gray-scale images? If you run the code from the OpenCV tutorial in the link you gave here, you can easily quantize the color space down to 64 colors by giving 64 as an input parameter to their code. That essentially does the same thing as extracting the 2 most significant bits in your example. Or am I missing something?Clamper
@Moacir This function shouldn't return a value since it's signature states that it returns void.Overrefinement
A
10

Here's a Python implementation of color quantization using K-Means Clustering with cv2.kmeans. The idea is to reduce the number of distinct colors in an image while preserving the color appearance of the image as much as possible. Here's the result:

Input -> Output

Code

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()     
Allanite answered 30/1, 2020 at 1:57 Comment(0)
P
5

The answers suggested here are really good. I thought I would add my idea as well. I follow the formulation of many comments here, in which it is said that 64 colors can be represented by 2 bits of each channel in an RGB image.

The function in code below takes as input an image and the number of bits required for quantization. It uses bit manipulation to 'drop' the LSB bits and keep only the required number of bits. The result is a flexible method that can quantize the image to any number of bits.

#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;
}

Here is an image that is used in the above function call:

enter image description here

Image quantized to 2 bits for each RGB channel (Total 64 Colors):

enter image description here

3 bits for each channel:

enter image description here

4 bits ...

enter image description here

Psycho answered 10/10, 2016 at 22:28 Comment(0)
J
3

There is the K-means clustering algorithm which is already available in the OpenCV library. In short it determines which are the best centroids around which to cluster your data for a user-defined value of k ( = no of clusters). So in your case you could find the centroids around which to cluster your pixel values for a given value of k=64. The details are there if you google around. Here's a short intro to k-means.

Something similar to what you are probably trying was asked here on SO using k-means, hope it helps.

Another approach would be to use the pyramid mean shift filter function in OpenCV. It yields somewhat "flattened" images, i.e. the number of colors are less so it might be able to help you.

Judaica answered 7/5, 2011 at 1:51 Comment(0)
P
2

If you want a quick and dirty method in C++, in 1 line:

capImage &= cv::Scalar(0b11000000, 0b11000000, 0b11000000);

So, what it does is keep the upper 2 bits of each R, G, B component, and discards the lower 6 bits, hence the 0b11000000.

Because of the 3 channels in RGB, you get maximum 4 R x 4 B x 4 B = max 64 colors. The advantage of doing this is that you can run this on any number of images and the same colors will be mapped.

Note that this can make your image a bit darker since it discards some bits.

For a greyscale image, you can do:

capImage &= 0b11111100;

This will keep the upper 6 bits, which means you get 64 grays out of 256, and again the image can become a bit darker.

Here's an example, original image = 251424 unique colors. enter image description here

And the resulting image has 46 colors: enter image description here

Piperidine answered 29/4, 2022 at 20:3 Comment(0)
D
1

Assuming that you want to use the same 64 colors for all images (ie palette not optimized per image), there are a at least a couple choices I can think of:

1) Convert to Lab or YCrCb colorspace and quantize using N bits for luminance and M bits for each color channel, N should be greater than M.

2) Compute a 3D histogram of color values over all your training images, then choose the 64 colors with the largest bin values. Quantize your images by assigning each pixel the color of the closest bin from the training set.

Method 1 is the most generic and easiest to implement, while method 2 can be better tailored to your specific dataset.

Update: For example, 32 colors is 5 bits so assign 3 bits to the luminance channel and 1 bits to each color channel. To do this quantization, do integer division of the luminance channel by 2^8/2^3 = 32 and each color channel by 2^8/2^1 = 128. Now there are only 8 different luminance values and 2 different color channels each. Recombine these values into a single integer doing bit shifting or math (quantized color value = luminance*4+color1*2+color2);

Deneb answered 6/5, 2011 at 11:42 Comment(1)
"quantize using N bits for luminance and M bits for each color channel, N should be greater than M." - How can I do this part?Emory
H
1

A simple bitwise and with a proper bitmask would do the trick.

python, for 64 colors,

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

The number of colors for an RGB image should be a perfect cube (same across 3 channels).

For this method, the number of possible values for a channel should be a power of 2. (This check is ignored by the code and the next lower power of 2 is taken by it)

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)
Hirundine answered 13/5, 2020 at 9:49 Comment(0)
S
1

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

Shabby answered 28/2, 2022 at 16:14 Comment(2)
OK, so it is meant to be an answer. In that case ...Blinni
While this code may solve the question, including an explanation of how and why this solves the problem would really help to improve the quality of your post, and probably result in more up-votes. Remember that you are answering the question for readers in the future, not just the person asking now. Please edit your answer to add explanations and give an indication of what limitations and assumptions apply.Blinni
A
1

nQuantGpp includes top 10 color quantization algorithms for g++ producing high quality optimized images. It is ported from nQuantCpp which migrates to OpenCV to leverage the deep learning features of such popular library.

Mat source = imread(sourceFile, IMREAD_UNCHANGED);
PnnQuant::PnnQuantizer pnnQuantizer;
int nMaxColors = 64;
vector<uchar> pngBytes;
Mat dest = pnnQuantizer.QuantizeImage(source, pngBytes, nMaxColors, true);
if(nMaxColors > 256)
    imwrite(destPath, dest);
else {
    ofstream outfile(destPath, ios::binary);
    outfile.write(reinterpret_cast<const char*>(pngBytes.data()), pngBytes.size());
    outfile.close();
}

Reduced to 64 colors by Fast PNN based algorithm Reduced to 64 colors by Fast pairwise nearest neighbor based algorithm

Reduced to 64 colors by Fast PNN based algorithm with CIELAB color space Reduced to 64 colors by Fast pairwise nearest neighbor based algorithm with CIELAB color space

Reduced to 64 colors by NeuQuant Neural-Net Quantization Algorithm Reduced to 64 colors by NeuQuant Neural-Net Quantization Algorithm

Reduced to 64 colors by Efficient, Edge-Aware, Combined Color Quantization and Dithering with CIELAB color space Reduced to 64 colors by Efficient, Edge-Aware, Combined Color Quantization and Dithering with CIELAB color space

Reduced to 64 colors by Xialoin Wu's fast optimal color Quantization Algorithm Reduced to 64 colors by Xialoin Wu's fast optimal color Quantization Algorithm

Finally, the most essential method for CBIR techniques should be the OTSU method (OTSU) which is a global adaptive binarization threshold image segmentation algorithm. Then categorization of images into meaningful classes by efficient extraction of feature vectors from image datasets has been dependent on feature selection techniques.

Aster answered 5/6, 2023 at 1:33 Comment(2)
Please make it clear that this is a library that you wrote yourself, and include some code to show how to use the library to get those results. Otherwise it’s an advertisement, not an answer.Necroscopy
Every algorithm in nQuantGpp with my name as Copyright (c) 2023 Miller Cy ChanAster
U
-2

Why don't you just do Matrix multiplication/division? Values will be automatically rounded.

Pseudocode:

convert your channels to unsigned characters (CV_8UC3),
Divide by total colors / desired colors. Mat = Mat / (256/64). Decimal points will be truncated.
Multiply by the same number. Mat = mat * 4

Done. Each channel now only contains 64 colors.

Urbanna answered 29/12, 2012 at 17:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.