Почему оператор звезды (*) быстрее, чем умножение доступа к указателю при масштабировании матрицы с использованием OpenCV?

Я пытаюсь научиться манипулировать значениями матрицы 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;
}

Между первым подходом и остальными есть большая разница - первый насыщает, а остальные 3 переполняют.

Dan Mašek 09.04.2019 20:35

Спасибо за ваш комментарий. Я немного изучил то, что вы упомянули, но я не мог полностью понять, что вы имели в виду под насыщенностью и переполнением в этом случае. Не могли бы вы объяснить немного больше?

D_Tiger 11.04.2019 18:24
docs.opencv.org/4.1.0/db/de0/… | Вы умножаете 8-битное целое число без знака на число с плавающей запятой и сохраняете результат как 8-битное целое число без знака (т.е. значения могут попадать в диапазон 0-255). Представьте себе сценарий, такой как 128 * 2.0f -- результат 256.0f, преобразованный в целое число, это 256 (или 0x100). Мы можем хранить только 8 бит, поэтому при обычном приведении старшие биты игнорируются. Следовательно, вы получаете 0. Однако при насыщении значения выше, чем могут быть представлены, уменьшаются до максимально возможного значения — в данном случае 255.
Dan Mašek 11.04.2019 19:25

Спасибо за ясное объяснение. Это имеет смысл. Тем не менее, я пытаюсь выяснить, как это приведение вступает в игру и влияет на производительность в приведенном выше сценарии? Как вы думаете, это сильно влияет на производительность здесь?

D_Tiger 11.04.2019 22:28

Я в основном указывал на это, чтобы вы не стали сравнивать яблоки и апельсины. Кроме того, во втором методе вы не определяете время распределения матрицы результатов, тогда как в других местах вы делаете это. | Во многих случаях насыщение займет больше времени, поскольку это дополнительные операции, поэтому первый метод здесь находится в невыгодном положении. Может объяснить, почему LUT имеет такое большое преимущество... (поиск не так хорош, когда у вас есть SIMD) | Как вы обнаружили, делать это на месте намного быстрее, что говорит о том, что распределение результата приводит к заметным накладным расходам. (Правда из опыта)

Dan Mašek 11.04.2019 23:07

Я ценю ваш исчерпывающий ответ. Это было очень полезно. Дело в таблице поиска заключается в том, что я не смогу использовать их все время, так как во многих случаях окончательный вывод нельзя заранее вычислить и сохранить в таблице. Я считаю, что основным преимуществом поиска здесь является то, что я выполняю умножение только 256 раз, а не для каждого отдельного пикселя.

D_Tiger 11.04.2019 23:35
За пределами сигналов Angular: Сигналы и пользовательские стратегии рендеринга
За пределами сигналов Angular: Сигналы и пользовательские стратегии рендеринга
TL;DR: Angular Signals может облегчить отслеживание всех выражений в представлении (Component или EmbeddedView) и планирование пользовательских...
Sniper-CSS, избегайте неиспользуемых стилей
Sniper-CSS, избегайте неиспользуемых стилей
Это краткое руководство, в котором я хочу поделиться тем, как я перешел от 212 кБ CSS к 32,1 кБ (сокращение кода на 84,91%), по-прежнему используя...
0
6
122
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Оператор *() использовал 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, я включу его в свой эксперимент и проверю прирост производительности.

D_Tiger 10.04.2019 16:44

Я реализовал то же самое, используя Mat::forEach, и он работает на 2 ms, что даже быстрее, чем оператор *. Тем не менее, самым быстрым на сегодняшний день является то, когда я делаю image *= sc, который работает очень быстро примерно со скоростью 0.0014 ms, и причина может заключаться в том, что нет выделения памяти или копирования данных, и операция выполняется с собственными значениями пикселей.

D_Tiger 11.04.2019 22:31

Другие вопросы по теме