Подтвердить что ты не робот

Сверхбыстрая медиана матрицы в opencv (так же быстро, как и матрица)

Я пишу некоторый код в openCV и хочу найти медианное значение очень большого матричного массива (одноканальное серое, float).

Я попробовал несколько методов, таких как сортировка массива (с использованием std:: sort) и выбор средней записи, но он очень медленный, если сравнивать с медианной функцией в Matlab. Если быть точным - то, что занимает 0,25 секунды в Matlab, занимает более 19 секунд в openCV.

Мое исходное изображение изначально представляет собой 12-битное изображение в оттенках серого с размерами 3840x2748 (~ 10,5 мегапикселей), преобразованное в float (CV_32FC1), где теперь все значения отображаются в диапазон [0,1] и в какой-то момент код я запрашиваю медианное значение, вызывая:

double myMedianValue = medianMat(Input);

Если функция medianMat:

double medianMat(cv::Mat Input){    
    Input = Input.reshape(0,1); // spread Input Mat to single row
    std::vector<double> vecFromMat;
    Input.copyTo(vecFromMat); // Copy Input Mat to vector vecFromMat    
    std::sort( vecFromMat.begin(), vecFromMat.end() ); // sort vecFromMat
        if (vecFromMat.size()%2==0) {return (vecFromMat[vecFromMat.size()/2-1]+vecFromMat[vecFromMat.size()/2])/2;} // in case of even-numbered matrix
    return vecFromMat[(vecFromMat.size()-1)/2]; // odd-number of elements in matrix
}

Я приурочил функцию medinaMat сам по себе, а также различные части - как и ожидалось, узкое место:

  std::sort( vecFromMat.begin(), vecFromMat.end() ); // sort vecFromMat

Есть ли у кого-нибудь эффективное решение?

Спасибо!

ИЗМЕНИТЬ Я попытался использовать std:: nth_element, указанный в ответе Ади Шавита.

Функция medianMat теперь читается как:

double medianMat(cv::Mat Input){    
Input = Input.reshape(0,1); // spread Input Mat to single row
std::vector<double> vecFromMat;
Input.copyTo(vecFromMat); // Copy Input Mat to vector vecFromMat
std::nth_element(vecFromMat.begin(), vecFromMat.begin() + vecFromMat.size() / 2, vecFromMat.end());
return vecFromMat[vecFromMat.size() / 2];}

Время работы снизилось с 19 секунд до 3,5 секунд. Это все еще не так близко к 0,25 секунды в Matlab, используя медианную функцию...

4b9b3361

Ответ 1

OK.

Я действительно пробовал это, прежде чем публиковать вопрос, и из-за некоторых глупых ошибок я дисквалифицировал его как решение... в любом случае вот оно:

Я в основном создаю гистограмму значений для моего исходного ввода с 2 ^ 12 = 4096 бит, вычисляю CDF и нормализую его, чтобы он отображался от 0 до 1 и находил наименьший индекс в CDF, который равен или больше 0,5. Затем я делю этот индекс на 12 ^ 2 и, таким образом, получаю запрашиваемое среднее значение. Теперь он работает через 0,11 секунды (и в режиме отладки без больших оптимизаций), что меньше половины времени, необходимого в Matlab.

Здесь функция (nVals = 4096 в моем случае соответствует 12-битным значениям):

double medianMat(cv::Mat Input, int nVals){

// COMPUTE HISTOGRAM OF SINGLE CHANNEL MATRIX
float range[] = { 0, nVals };
const float* histRange = { range };
bool uniform = true; bool accumulate = false;
cv::Mat hist;
calcHist(&Input, 1, 0, cv::Mat(), hist, 1, &nVals, &histRange, uniform, accumulate);

// COMPUTE CUMULATIVE DISTRIBUTION FUNCTION (CDF)
cv::Mat cdf;
hist.copyTo(cdf);
for (int i = 1; i <= nVals-1; i++){
    cdf.at<float>(i) += cdf.at<float>(i - 1);
}
cdf /= Input.total();

// COMPUTE MEDIAN
double medianVal;
for (int i = 0; i <= nVals-1; i++){
    if (cdf.at<float>(i) >= 0.5) { medianVal = i;  break; }
}
return medianVal/nVals; }

Ответ 2

Сортировка и принятие среднего элемента - не самый эффективный способ найти медиану. Для этого требуются операции O (n log n).

С С++ вы должны использовать std::nth_element() и взять средний итератор. Это операция O (n):

nth_element - это алгоритм частичной сортировки, который упорядочивает элементы в [first, last) таким образом, что:

  • Элемент, на который указывает nth, изменяется на любой элемент, который будет присутствовать в этой позиции , если [first, last) был отсортирован.
  • Все элементы перед этим новым n-м элементом меньше или равны элементам после нового n-го элемента.

Кроме того, ваши исходные данные являются 12-битными целыми числами. Ваша реализация делает несколько вещей, которые делают сравнение с Matlab проблематичным:

  • Вы преобразовали в плавающую точку (CV_32FC1 или double или оба), это дорого и требует времени
  • Код имеет дополнительную копию для vector<double>
  • Операции с поплавком и особенно удвоения стоят больше, чем на целые числа.

Предполагая, что ваше изображение непрерывно в памяти, как и по умолчанию для OpenCV, вы должны использовать CV_16C1 и работать непосредственно с массивом данных после reshape()

Еще одна возможность, которая должна быть очень быстрой, - просто построить гистограмму изображения - это один проход на изображении. Затем, работая на гистограмме, найдите бит, который соответствует половине пикселей с каждой стороны - это максимум один проход над ячейками.

Документы OpenCV имеют несколько tutorials on как построить гистограммы. Когда у вас есть гистограмма, скопируйте значения bin до тех пор, пока не получите пропуск 3840x2748/2. Этот бункер является вашим медианом.

Ответ 3

Вероятно, быстрее найти его из исходных данных.

Поскольку исходные данные имеют 12-битные значения, есть только 4096 различных возможных значений. Это хороший и маленький стол! Пройдите все данные за один проход и подсчитайте, сколько из каждого значения у тебя есть. Это операция O (n). Тогда легко найти медианную, только считать size/2 элементы с любого конца таблицы.