Я пытаюсь научиться манипулировать значениями матрицы OpenCV наиболее оптимизированным способом. Я попытался масштабировать большое изображение, хранящееся в матрице OpenCV, четырьмя разными способами.
1) С помощью оператора звезды *
2) Использование функции at
и циклов for
3) Использование доступа к указателю и циклов for
4) Использование таблицы поиска
Округленные результаты этого эксперимента были следующими:
*
оператор ------- 3 ms
at
функция ------- 12 ms
доступ к указателю ---- 9 ms
таблица поиска -------- 1 ms
Теперь совершенно очевидно, почему таблицы поиска являются самыми быстрыми. Но я не всегда смогу ими воспользоваться. В случаях, когда я не могу использовать таблицу поиска, мне нужно понять, как OpenCV реализует масштабирование с помощью оператора *
, чтобы я мог использовать этот метод в качестве ссылки в других моих манипуляциях со значениями матрицы.
Я был бы очень признателен, если бы кто-нибудь мог сказать мне, что происходит за операцией *
, которая делает ее быстрее, чем метод доступа к указателю?
Пожалуйста, найдите следующий код для справки.
Спасибо,
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <chrono>
typedef std::chrono::system_clock Timer;
typedef std::chrono::duration<double> Duration;
using std::cout;
using std::endl;
using std::vector;
double profile(Timer::time_point start, Timer::time_point end) {
Duration span = end - start;
return span.count() * 1000;
}
int main() {
cv::Mat image = cv::imread("../data/large.jpg", 0);
float sc = 1;
while (true) {
//=================== first method ====================
Timer::time_point s1 = Timer::now();
cv::Mat mine = image * sc;
Timer::time_point s2 = Timer::now();
//=================== second method ====================
cv::Mat yours(image.size(), image.type());
Timer::time_point s3 = Timer::now();
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++) {
yours.at<uchar>(i, j) = image.at<uchar>(i, j) * sc;
}
}
Timer::time_point s4 = Timer::now();
//=================== third method ====================
if (!image.isContinuous()) {
std::cerr << "ERROR: image matrix isn't stored as a 1D array" << endl;
exit(-1);
}
Timer::time_point s5 = Timer::now();
cv::Mat result(image.size(), image.type());
for (int i = 0; i < image.rows; i++) {
for (int j = 0; j < image.cols; j++) {
result.data[i * image.cols + j] = image.data[i * image.cols + j] * sc;
}
}
Timer::time_point s6 = Timer::now();
//=================== fourth method ====================
Timer::time_point s7 = Timer::now();
cv::Mat lookupTable(1, 256, image.type());
for (int i = 0; i < 256; i++)
lookupTable.data[i] = i * sc;
cv::Mat his;
cv::LUT(image, lookupTable, his);
Timer::time_point s8 = Timer::now();
cout << "first = " << profile(s1, s2) << endl;
cout << "second = " << profile(s3, s4) << endl;
cout << "third = " << profile(s5, s6) << endl;
cout << "fourth = " << profile(s7, s8) << endl;
cout << "=============== " << endl;
}
return 0;
}
Спасибо за ваш комментарий. Я немного изучил то, что вы упомянули, но я не мог полностью понять, что вы имели в виду под насыщенностью и переполнением в этом случае. Не могли бы вы объяснить немного больше?
128 * 2.0f
-- результат 256.0f
, преобразованный в целое число, это 256 (или 0x100). Мы можем хранить только 8 бит, поэтому при обычном приведении старшие биты игнорируются. Следовательно, вы получаете 0. Однако при насыщении значения выше, чем могут быть представлены, уменьшаются до максимально возможного значения — в данном случае 255.
Спасибо за ясное объяснение. Это имеет смысл. Тем не менее, я пытаюсь выяснить, как это приведение вступает в игру и влияет на производительность в приведенном выше сценарии? Как вы думаете, это сильно влияет на производительность здесь?
Я в основном указывал на это, чтобы вы не стали сравнивать яблоки и апельсины. Кроме того, во втором методе вы не определяете время распределения матрицы результатов, тогда как в других местах вы делаете это. | Во многих случаях насыщение займет больше времени, поскольку это дополнительные операции, поэтому первый метод здесь находится в невыгодном положении. Может объяснить, почему LUT имеет такое большое преимущество... (поиск не так хорош, когда у вас есть SIMD) | Как вы обнаружили, делать это на месте намного быстрее, что говорит о том, что распределение результата приводит к заметным накладным расходам. (Правда из опыта)
Я ценю ваш исчерпывающий ответ. Это было очень полезно. Дело в таблице поиска заключается в том, что я не смогу использовать их все время, так как во многих случаях окончательный вывод нельзя заранее вычислить и сохранить в таблице. Я считаю, что основным преимуществом поиска здесь является то, что я выполняю умножение только 256 раз, а не для каждого отдельного пикселя.
Оператор *() использовал parallel_for! Так что это будет быстрее, чем однопоточная функция. И вы можете написать #pragma omp parallel для цикла перед строками и сравнить результаты. Или вы можете использовать image.forEach — это тоже параллельно и быстро.
cv::LUT: https://github.com/opencv/opencv/blob/master/modules/core/src/lut.cpp#L359
Эта функция имеет множество реализаций: opencl, openvx, ipp и простые с parallel_for. Я думаю, что в вашем случае был использован ipp_lut, очень-очень оптимизированная версия от Intel для своих процессоров. И это быстро!
Большое спасибо за ваш проницательный ответ. Это имеет смысл. Кроме того, я не знал о функционале Mat::forEach
, я включу его в свой эксперимент и проверю прирост производительности.
Я реализовал то же самое, используя Mat::forEach
, и он работает на 2 ms
, что даже быстрее, чем оператор *
. Тем не менее, самым быстрым на сегодняшний день является то, когда я делаю image *= sc
, который работает очень быстро примерно со скоростью 0.0014 ms
, и причина может заключаться в том, что нет выделения памяти или копирования данных, и операция выполняется с собственными значениями пикселей.
Между первым подходом и остальными есть большая разница - первый насыщает, а остальные 3 переполняют.