Найти доминирующий цвет | KMeans | Emgu.CV

Как с помощью библиотеки Emgu.CV можно найти доминирующий цвет изображения?

Я знаю про функцию CvInvoke.KMeans(...), в которую можно передать исходное изображение Image<..., float> и количество ожидаемых цветов k. У неё есть 2 аргумента "на выход": bestLables и centers - 2 Mat-объекта. Но я никак не могу понять: как из этих Mat получить массив цветов, по типу MCvScalar[].

Код, для быстрого воспроизведения:

using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Emgu.CV.Util;

using var image   = new Bitmap(filepath).ToImage<Bgr, float>();
using var labels  = new Mat();
using var centers = new Mat();

CvInvoke.Kmeans(resultImage, 5, labels, new MCvTermCriteria(10, 0.0001), 2, KMeansInitType.PPCenters, centers);

Ответы (1 шт):

Автор решения: iKuzmychov

Я, вроде бы, понял как работает KMeans и хочу этим поделиться.

Понимание входных и выходных данных

Предположим, что у нас есть Image<Color, float> размерами width на height, где Color - цветовая схема изображения, а ColorSize - количеством цветов (к примеру, 3 для Bgr). От этого будем отталкиваться.

Матрица labels является int-матрицей размерами 1 × height, значения варьируются от 0 до k-1, где k - количество доминирующих цветов (кластеров).

Матрица centers является float-матрицей размерами k × (width * TColor.Size), значения варьируются от 0f до 255f.

Работа с выходными данными

Получить результирующий цвет пикселя с координатами (x, y) можно по такой схеме:

значение цветового канала = centers[labels[0, y], x * ColorSize + номер цветового канала]

Чтобы было понятнее, что такое номер цветового канала, приведу пример для Bgr:

blue  = centers[labels[0, y], x * 3 + 0]
green = centers[labels[0, y], x * 3 + 1]
red   = centers[labels[0, y], x * 3 + 2]

Последняя проблема, которую осталось решить: как получить элементы матриц? (они не дают такой возможности "из коробки", приведённая схема является псевдокодом). Для этого отлично подойдёт метод расширения GetValue(this Mat mat, int row, int col) из этого ответа (спасибо @aepot за ссылку).

В итоге, код будет выглядить так:

var channelValue = centers.GetValue(labels.GetValue(0, y), x * ColorSize + channelIndex);

Пример

Теперь приведу реальный пример использования, где всё собрано вместе. В этом примере изображение разбивается на 1 доминирующий цвет и выводится этот самый цвет:

using var image   = new Image<Bgr, float>(filepath);
using var labels  = new Mat();
using var centers = new Mat();

// пусть будет 1 доминирующий цвет
const int K = 1;

CvInvoke.Kmeans(image, K, labels, new MCvTermCriteria(10, 1d), 2, KMeansInitType.PPCenters, centers);

// берём нулевую точку, потому это ничего не меняет (доминирующий цвет один, а значит в любой точке один и тот же цвет)
const int X = 0;
const int Y = 0;

// цвет в точке (X, Y)
var color = new Bgr(
    blue:  centers.GetValue((int)labels.GetValue(0, Y), X * 3 + 0),
    green: centers.GetValue((int)labels.GetValue(0, Y), X * 3 + 1),
    red:   centers.GetValue((int)labels.GetValue(0, Y), X * 3 + 2));

MessageBox.Show($"R: {(byte)color.Red}, G: {(byte)color.Green}, B: {(byte)color.Blue}");

Я нашёл в интернете такую картинку:

введите сюда описание изображения

Вышеприведённый код дал мне следующий результат:

введите сюда описание изображения

Если указать этот цвет в "color picker"-е, то можно убедиться в правильности результата:

введите сюда описание изображения

→ Ссылка