Search code examples
c++opencvcluster-analysisk-means

OpenCV - How to apply Kmeans on a grayscale image?


I am trying to cluster a grayscale image using Kmeans.

First, I have a question:

Is Kmeans the best way to cluster a Mat or are there newer more efficient approaches?

Second, when I try this:

Mat degrees = imread("an image" , IMREAD_GRAYSCALE);
const unsigned int singleLineSize = degrees.rows * degrees.cols;
Mat data = degrees.reshape(1, singleLineSize);
data.convertTo(data, CV_32F);
std::vector<int> labels;
cv::Mat1f colors;
cv::kmeans(data, 3, labels, cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 10, 1.), 2, cv::KMEANS_PP_CENTERS, colors);
for (unsigned int i = 0; i < singleLineSize; i++) {
    data.at<float>(i) = colors(labels[i]);
}

Mat outputImage = data.reshape(1, degrees.rows);
outputImage.convertTo(outputImage, CV_8U);
imshow("outputImage", outputImage);

The result (outputImage) is empty.

When I try to multiply colors in the for loop like data.at<float>(i) = 255 * colors(labels[i]); I get this error:

Unhandled exception : Integer division by zero.

How can I cluster a grayscale image properly?


Solution

  • It looks to me that you are wrongly parsing the labels and colors info to your output matrix.

    K-means returns this info:

    • Labels - This is an int matrix with all the cluster labels. It is a "column" matrix of size TotalImagePixels x 1.

    • Centers - This what you refer to as "Colors". This is a float matrix that contains the cluster centers. The matrix is of size NumberOfClusters x featureMean.

    In this case, as you are using BGR pixels as "features" consider that Centers has 3 columns: One mean for the B channel, one mean for the G channel and finally, a mean for the R channel.

    So, basically you loop through the (plain) label matrix, retrieve the label, use this value as index in the Centers matrix to retrieve the 3 colors.

    One way to do this is as follows, using the auto data specifier and looping through the input image instead (that way we can index each input label easier):

        //prepare an empty output matrix
        cv::Mat outputImage( inputImage.size(), inputImage.type() );
    
        //loop thru the input image rows...
        for( int row = 0; row != inputImage.rows; ++row ){
    
            //obtain a pointer to the beginning of the row
            //alt: uchar* outputImageBegin = outputImage.ptr<uchar>(row);            
            auto outputImageBegin = outputImage.ptr<uchar>(row);
    
            //obtain a pointer to the end of the row
            auto outputImageEnd = outputImageBegin + outputImage.cols * 3;
    
            //obtain a pointer to the label:
            auto labels_ptr = labels.ptr<int>(row * inputImage.cols);
    
            //while the end of the image hasn't been reached...
            while( outputImageBegin != outputImageEnd ){
    
                //current label index:
                int const cluster_idx = *labels_ptr;
    
                //get the center of that index:
                auto centers_ptr = centers.ptr<float>(cluster_idx);
    
                //we got an implicit VEC3B vector, we must map the BGR items to the
                //output mat:
                clusteredImageBegin[0] = centers_ptr[0];
                clusteredImageBegin[1] = centers_ptr[1];
                clusteredImageBegin[2] = centers_ptr[2];
    
                //increase the row "iterator" of our matrices:
                clusteredImageBegin += 3; ++labels_ptr;
            }
        }