Самый быстрый способ преобразования изображения в оттенки серого с использованием определенных коэффициентов - PullRequest
2 голосов
/ 10 марта 2019

Мне нужно преобразовать cv::Mat в оттенки серого, используя пользовательскую формулу.Каждый канал входной матрицы должен быть умножен на определенный коэффициент.

Это псевдокод операции:

Y = 0.2126*R + 0.7152*G + 0.0722*B

Матрица ввода равна CV_32FC3, а на выходе должно быть CV_32FC1.

Простое циклическое использование с использованием 2 для циклов ипоследовательное вычисление каждого пикселя кажется недостаточно быстрым.

int rows = src.rows, cols = src.cols;
for (int row = 0; row < rows; row++){
    const float* src_ptr = src.ptr<float>(row);
    float* dst_ptr = dst.ptr<float>(row);

    for (int col = 0; col < cols; col++){
        dst_ptr[col] = ( 0.0722 * src_ptr[0] ) + ( 0.7152 * src_ptr[1] ) + ( 0.2126 * src_ptr[2]);
        src_ptr += 3;
    }
}

Есть ли более эффективный способ сделать это?Я надеялся использовать цикл parallel_for_, но не могу понять это самостоятельно.

Это не рабочее решение, над которым я работал:

void MyOperator::getIntensity(const cv::Mat& src, cv::Mat& dst){
    int nElements = src.cols * src.rows;
    parallel_for_(cv::Range(0,nElements) , BGR2rec709Parallel((float*)src.data, (float*)dst.data));
}

class BGR2rec709Parallel : public cv::ParallelLoopBody
{
private:
    float *src;
    float *dst;
public:
    BGR2rec709Parallel(float* src_ptr, float* dst_ptr) : src(src_ptr), dst(dst_ptr) {}

    virtual void operator()( const cv::Range &r ) const
    {
        for (int i = r.start; i != r.end; ++i)
        {
            dst[i] = ( 0.0722 * src[i] ) + ( 0.7152 * src[i+1] ) + ( 0.2126 * src[i+2]);
        }
    }
    virtual ~BGR2rec709Parallel();
};

1 Ответ

2 голосов
/ 10 марта 2019

Основная проблема заключается в том, что вы не правильно проиндексировали исходные данные.

for (int i = r.start; i != r.end; ++i)
{
    dst[i] = ( 0.0722 * src[i] )
           + ( 0.7152 * src[i+1] )
           + ( 0.2126 * src[i+2]);
}

Давайте представим r.start == 0 и r.end == 2.Этот код эквивалентен:

dst[0] = ( 0.0722 * src[0] ) + ( 0.7152 * src[1] ) + ( 0.2126 * src[2]);
dst[1] = ( 0.0722 * src[1] ) + ( 0.7152 * src[2] ) + ( 0.2126 * src[3]);

Обратите внимание, что значения из src в конечном итоге используются повторно - это, безусловно, нежелательно.Проблема в том, что src содержит в 3 раза больше значений, чем dst, следовательно, его индекс должен расти в 3 раза быстрее.

for (int i = r.start; i != r.end; ++i)
{
    dst[i] = ( 0.0722 * src[i * 3] )
           + ( 0.7152 * src[i * 3 + 1] )
           + ( 0.2126 * src[i * 3 + 2]);
}

Это должно обеспечить правильную работу распараллеленной версии, однако есть и другие способы улучшенияпо.


Заметное улучшение (~ 30% в однопоточном варианте, ~ 10% в параллельном варианте) можно сделать, сделав коэффициенты float вместо double (например, 0.0722f вместо 0.0722).Это требует некоторой точности, но позволяет избежать ненужных преобразований (и потенциально может лучше векторизовать).


Не используйте приведение в стиле C.В BGR2rec709Parallel((float*)src.data, (float*)dst.data) вы должны использовать reinterpret_cast<float>.Или даже лучше, как вы использовали в первой версии, используйте cv::Mat::ptr (то есть src.ptr<float>(), dst.ptr<float>()).


Способ использования parallel_for_ не идеален:

int nElements = src.cols * src.rows;
parallel_for_(cv::Range(0, nElements), /* ... */);

Третий параметр (nstripes) не указан.Исходя из моих наблюдений (OpenCV 3.1.0 / MSVS2013 и 3.4.3 / MSVC2015), результат заключается в том, что operator() вызывается с диапазонами размера 1. Это может вызвать некоторые довольно неприятные издержки, особенно когда диапазон размера 1 соответствуетдо одного пикселя.

Значительное улучшение можно увидеть, установив nstripes в cv::getNumThreads().Это приведет к разделению работы до 1 диапазона на рабочий поток с диапазонами аналогичных размеров.


Параллельная версия больше не может обрабатывать прерывистые Mat с (например, результат взятияROI большого изображения), что и в первой версии.

Чтобы решить эту проблему, parallel_for_ должен работать со строками, а не с пикселями, а его контекст должен быть ссылками на ввод и вывод Mat s вместоуказателей данных.

Разделение работы на количество полос, равное количеству потоков, здесь не так важно, поскольку обработка строки уже включает в себя значительный объем работы, но мы все еще можем это сделать.


О, еще один, чтобы упомянуть.for (int i = r.start; i != r.end; ++i) - != здесь вызывает проблемы в случае, если вы увеличите i более чем на 1. Предпочитаете использовать < здесь.


Окончательная версия выглядит следующим образом:

class BGR2rec709ParallelC
    : public cv::ParallelLoopBody
{
public:
    BGR2rec709ParallelC(cv::Mat const& src, cv::Mat& dst)
        : src(src), dst(dst)
    {
        CV_Assert(src.type() == CV_32FC3);
        CV_Assert(dst.type() == CV_32FC1);
        CV_Assert(src.size() == dst.size());
    }

    virtual void operator()(const cv::Range &r) const
    {
        for (int row(r.start); row < r.end; ++row) {
            convert_row(src.ptr<float>(row), dst.ptr<float>(row));
        }
    }

private:
    void convert_row(float const* src_ptr, float * dst_ptr) const
    {
        for (int i(0); i != src.cols; ++i) {
            dst_ptr[i] = (0.0722f * src_ptr[i * 3])
                + (0.7152f * src_ptr[i * 3 + 1])
                + (0.2126f * src_ptr[i * 3 + 2]);
        }
    }

private:
    cv::Mat const& src;
    cv::Mat& dst;
};

void get_intensity_v4(cv::Mat const& src, cv::Mat& dst)
{
    parallel_for_(cv::Range(0, src.rows)
        , BGR2rec709ParallelC(src, dst)
        , cv::getNumThreads());
}

Полная тестовая программа, сравнивающая производительность различных реализаций:

#include <opencv2/opencv.hpp>

void get_intensity_base(cv::Mat const& src, cv::Mat& dst)
{
    cv::cvtColor(src, dst, cv::COLOR_BGR2GRAY);
}

void get_intensity_v1a(cv::Mat const& src, cv::Mat& dst)
{
    int rows = src.rows, cols = src.cols;
    for (int row(0); row < rows; ++row) {
        float const* src_ptr = src.ptr<float>(row);
        float* dst_ptr = dst.ptr<float>(row);

        for (int col(0); col < cols; ++col, src_ptr += 3) {
            dst_ptr[col] = static_cast<float>((0.0722 * src_ptr[0])
                + (0.7152 * src_ptr[1])
                + (0.2126 * src_ptr[2]));
        }
    }
}

void get_intensity_v1b(cv::Mat const& src, cv::Mat& dst)
{
    int rows = src.rows, cols = src.cols;
    for (int row(0); row < rows; ++row) {
        float const* src_ptr = src.ptr<float>(row);
        float* dst_ptr = dst.ptr<float>(row);

        for (int col(0); col < cols; ++col, src_ptr += 3) {
            dst_ptr[col] = (0.0722f * src_ptr[0])
                + (0.7152f * src_ptr[1])
                + (0.2126f * src_ptr[2]);
        }
    }
}

class BGR2rec709ParallelA
    : public cv::ParallelLoopBody
{
public:
    BGR2rec709ParallelA(float const* src, float* dst) : src(src), dst(dst) {}

    virtual void operator()(cv::Range const& r) const
    {
        for (int i(r.start); i < r.end; ++i) {
            dst[i] = static_cast<float>((0.0722 * src[i * 3])
                + (0.7152 * src[i * 3 + 1])
                + (0.2126 * src[i * 3 + 2]));
        }
    }
private:
    float const* src;
    float* dst;
};

class BGR2rec709ParallelB
    : public cv::ParallelLoopBody
{
public:
    BGR2rec709ParallelB(float const* src, float* dst) : src(src), dst(dst) {}

    virtual void operator()(cv::Range const& r) const
    {
        for (int i(r.start); i < r.end; ++i) {
            dst[i] = (0.0722f * src[i * 3])
                + (0.7152f * src[i * 3 + 1])
                + (0.2126f * src[i * 3 + 2]);
        }
    }
private:
    float const* src;
    float* dst;
};

template <typename LoopBody>
void get_intensity_v2(cv::Mat const& src, cv::Mat& dst)
{
    int nElements = src.cols * src.rows;
    parallel_for_(cv::Range(0, nElements)
        , LoopBody(src.ptr<float>(), dst.ptr<float>()));
}

template <typename LoopBody>
void get_intensity_v3(cv::Mat const& src, cv::Mat& dst)
{
    int nElements = src.cols * src.rows;
    parallel_for_(cv::Range(0, nElements)
        , LoopBody(src.ptr<float>(), dst.ptr<float>())
        , cv::getNumThreads());
}

class BGR2rec709ParallelC
    : public cv::ParallelLoopBody
{
public:
    BGR2rec709ParallelC(cv::Mat const& src, cv::Mat& dst)
        : src(src), dst(dst)
    {
        CV_Assert(src.type() == CV_32FC3);
        CV_Assert(dst.type() == CV_32FC1);
        CV_Assert(src.size() == dst.size());
    }

    virtual void operator()(const cv::Range &r) const
    {
        for (int row(r.start); row < r.end; ++row) {
            convert_row(src.ptr<float>(row), dst.ptr<float>(row));
        }
    }

private:
    void convert_row(float const* src_ptr, float * dst_ptr) const
    {
        for (int i(0); i != src.cols; ++i) {
            dst_ptr[i] = (0.0722f * src_ptr[i * 3])
                + (0.7152f * src_ptr[i * 3 + 1])
                + (0.2126f * src_ptr[i * 3 + 2]);
        }
    }

private:
    cv::Mat const& src;
    cv::Mat& dst;
};

void get_intensity_v4(cv::Mat const& src, cv::Mat& dst)
{
    parallel_for_(cv::Range(0, src.rows)
        , BGR2rec709ParallelC(src, dst)
        , cv::getNumThreads());
}

cv::Mat test(std::string const& name
    , cv::Mat const& input
    , void(*fn)(cv::Mat const&, cv::Mat&))
{
    cv::Mat output(input.size(), CV_32FC1); // pre-allocate

    std::cout << name << "\n";
    int64 min_ticks(0x7FFFFFFFFFFFFFFF);
    for (int i(0); i < 32; ++i) {
        int64 t_start(cv::getTickCount());
        fn(input, output);
        int64 t_stop(cv::getTickCount());
        min_ticks = std::min(min_ticks, t_stop - t_start);
    }
    std::cout << " >= " << min_ticks << " ticks\n";

    return output;
}

cv::Mat3f make_test_data(int rows, int cols)
{
    cv::Mat m(rows, cols, CV_16UC3);
    cv::randu(m, 0, 0x10000);
    cv::Mat3f result;
    m.convertTo(result, CV_32FC3, 1.0 / 0xFFFF);
    return result;
}

int main()
{
    cv::Mat input(make_test_data(4096, 4096));

    test("Base", input, get_intensity_base);

    cv::Mat out_v1a = test("V1A", input, get_intensity_v1a);
    cv::Mat out_v1b = test("V1B", input, get_intensity_v1b);

    cv::Mat out_v2a = test("V2A", input, get_intensity_v2<BGR2rec709ParallelA>);
    cv::Mat out_v2b = test("V2B", input, get_intensity_v2<BGR2rec709ParallelB>);

    cv::Mat out_v3a = test("V3A", input, get_intensity_v3<BGR2rec709ParallelA>);
    cv::Mat out_v3b = test("V3B", input, get_intensity_v3<BGR2rec709ParallelB>);

    cv::Mat out_v4 = test("V4", input, get_intensity_v4);

    std::cout << "Differences V1A vs V2A: " << cv::countNonZero(out_v1a != out_v2a) << "\n";
    std::cout << "Differences V1B vs V2B: " << cv::countNonZero(out_v1b != out_v2b) << "\n";
    std::cout << "Differences V1B vs V3B: " << cv::countNonZero(out_v1b != out_v3b) << "\n";
    std::cout << "Differences V1B vs V4: " << cv::countNonZero(out_v1b != out_v4) << "\n";

    return 0;
}

Консольный вывод (OpenCV 3.1.0 / MSVC2013 / x64 / i7-4930K):

Base
 >= 126365 ticks
V1A
 >= 500890 ticks
V1B
 >= 331197 ticks
V2A
 >= 746851 ticks
V2B
 >= 704011 ticks
V3A
 >= 148181 ticks
V3B
 >= 134176 ticks
V4
 >= 133750 ticks
Differences V1A vs V2A: 0
Differences V1B vs V2B: 0
Differences V1B vs V3B: 0
Differences V1B vs V4: 0

Вывод на консоль (OpenCV 3.4.3 / MSVC2015 / x64 / i7-4930K):

Base
 >= 123620 ticks
V1A
 >= 503707 ticks
V1B
 >= 331801 ticks
V2A
 >= 1768515 ticks
V2B
 >= 1710579 ticks
V3A
 >= 145451 ticks
V3B
 >= 135767 ticks
V4
 >= 131438 ticks
Differences V1A vs V2A: 0
Differences V1B vs V2B: 0
Differences V1B vs V3B: 0
Differences V1B vs V4: 0

Примечание: обратите внимание, насколько хуже точная зернистость parallel_for_ версии здесь!


ОБНОВЛЕНИЕ:

Как предложено Нужный , вот реализация с использованием cv::Mat::forEach вместе с лямбдой.

void get_intensity_v5(cv::Mat const& src, cv::Mat& dst)
{
    CV_Assert(src.type() == CV_32FC3);
    CV_Assert(dst.type() == CV_32FC1);
    CV_Assert(src.size() == dst.size());

    dst.forEach<float>(
        [&](float& pixel, int const* po) -> void
        {
            cv::Vec3f const& in_pixel(src.at<cv::Vec3f>(po));
            pixel = (0.0722f * in_pixel[0])
                + (0.7152f * in_pixel[1])
                + (0.2126f * in_pixel[2]);
        }
    );
}

Дополнительный консольный вывод:

V5
 >= 123071 ticks

Differences V1B vs V5: 0

И на данный момент, я честно не могу объяснить, почему это работает лучше- forEach используется реализация parallel_for_, разделенная на строки ...

...