OpenCV, как работают преобразования элементов Matrix - PullRequest
1 голос
/ 29 мая 2019

У меня проблемы с пониманием внутренней работы OpenCV. Рассмотрим следующий код:

Scalar getAverageColor(Mat img, vector<Rect>& rois) {

    int n = static_cast<int>(rois.size());
    Mat avgs(1, n, CV_8UC3);
    for (int i = 0; i < n; ++i) {
        // What is the correct way to assign the color elements in 
        // the matrix?
        avgs.at<Scalar>(i) = mean(Mat(img, rois[i]));
        /*
        This seems to always work, but there has to be a better way.
        avgs.at<Vec3b>(i)[0] = mean(Mat(img, rois[i]))[0];
        avgs.at<Vec3b>(i)[1] = mean(Mat(img, rois[i]))[1];
        avgs.at<Vec3b>(i)[2] = mean(Mat(img, rois[i]))[2];
        */
    }
    // If I access the first element it seems to be set correctly.
    Scalar first = avgs.at<Scalar>(0);
    // However mean returns [0 0 0 0] if I did the assignment above using scalar, why???
    Scalar avg = mean(avgs);
    return avg;
}

Если я использую avgs.at<Scalar>(i) = mean(Mat(img, rois[i])) для назначения в цикле, первый элемент выглядит правильным, но тогда вычисление среднего всегда возвращает ноль (даже если первый элемент выглядит правильным). Если я назначу все цветовые элементы вручную с помощью Vec3b, это, похоже, сработает, но почему ???

1 Ответ

1 голос
/ 29 мая 2019

Примечание: cv::Scalar - это typedef для cv::Scalar_<double>, который происходит от cv::Vec<double, 4>, который получается изcv::Matx<double, 4, 1>.Точно так же, cv::Vec3b - это cv::Vec<uint8_t, 3>, который происходит от cv::Matx<uint8_t, 3, 1> - это означает, что мы можем использовать любой из этих 3 в cv::Mat::at и получить идентичное (правильное) поведение.


Важно знать, что cv::Mat::at это в основном reinterpret_cast в базовом массиве данных.Вы должны быть чрезвычайно осторожны, чтобы использовать соответствующий тип данных для аргумента шаблона, который соответствует типу элементов (включая количество каналов) cv::Mat, на котором вы его вызываете.

Документацияупоминает следующее:

Имейте в виду, что идентификатор размера, используемый в операторе at, не может быть выбран случайным образом.Это зависит от изображения, с которого вы пытаетесь получить данные.Таблица ниже дает лучшее понимание этого:

  • Если матрица имеет тип CV_8U, тогда используйте Mat.at<uchar>(y,x).
  • Если матрица имеет тип CV_8S, тогда используйте Mat.at<schar>(y,x).
  • Если матрица имеет тип CV_16U, тогда используйте Mat.at<ushort>(y,x).
  • Если матрица имеет тип CV_16S, тогда используйте Mat.at<short>(y,x).
  • Еслиматрица имеет тип CV_32S, тогда используйте Mat.at<int>(y,x).
  • Если матрица имеет тип CV_32F, тогда используйте Mat.at<float>(y,x).
  • Если матрица имеет тип CV_64F, тогда используйте Mat.at<double>(y,x).

Кажется, там не упоминается, что делать в случае нескольких каналов - в этом случае вы используете cv::Vec<...> (или, скорее, один из предоставленных typedef),cv::Vec<...> - это, по сути, обертка вокруг массива фиксированного размера из N значений данного типа.


В вашем случае матрица avgs равна CV_8UC3 - каждый элемент состоит из 3 беззнаковыхбайтовые значения (т.е. всего 3 байта).Однако, используя avgs.at<Scalar>(i), вы интерпретируете каждый элемент как 4 двойных (всего 32 байта).Это означает, что:

  • Фактический элемент, в который вы пытались записать (при правильной интерпретации), будет содержать только 3 старших байта среднего значения (8-байтовая с плавающей запятой) первого канала, т.е.полный мусор.
  • На самом деле вы перезаписываете следующие 10 элементов (последний частично, третий канал без потерь) с большим количеством мусора.
  • В какой-то момент вы неизбежно переполните буфер и потенциальномусор других структур данных.Эта проблема довольно серьезна.

Мы можем продемонстрировать это с помощью следующей простой программы.

Пример:

#include <opencv2/opencv.hpp>

int main()
{
    cv::Mat test_mat(cv::Mat::zeros(1, 12, CV_8UC3)); // 12 * 3 = 36 bytes of data
    std::cout << "Before: " << test_mat << "\n";

    cv::Scalar test_scalar(cv::Scalar::all(1234.5678));    
    test_mat.at<cv::Scalar>(0, 0) = test_scalar;
    std::cout << "After: " << test_mat << "\n";

    return 0;
}

Вывод:

Before: [  0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0,   0]
After: [173, 250,  92, 109,  69,  74, 147,  64, 173, 250,  92, 109,  69,  74, 147,  64, 173, 250,  92, 109,  69,  74, 147,  64, 173, 250,  92, 109,  69,  74, 147,  64,   0,   0,   0,   0]

Это ясно показывает, что мы пишем намного больше, чем нужно.

В режиме отладки неправильное использование at также вызывает утверждение:

OpenCV(3.4.3) Error: Assertion failed (((((sizeof(size_t)<<28)|0x8442211) >> ((traits::Depth<_Tp>::value) & ((1 << 3) - 1))*4) & 15) == elemSize1()) in cv::Mat::at, file D:\code\shit\so07\deps\include\opencv2/core/mat.inl.hpp, line 1102

Чтобы разрешить присвоение результата из cv::mean (который является cv::Scalar) нашей матрице CV_8UC3, нам нужно сделать две вещи (не обязательно в этом порядке):

  • Преобразование значений из double в uint8_t - OpenCV выполнит saturate_cast, но с учетом того, что среднее значение не превысит мин /макс. входных элементов, мы будем в порядке с обычным приведением.
  • Избавимся от 4-го элемента.

Чтобы удалить 4-й элемент, мы можем использовать cv::Matx::get_minor (документации немного не хватает, но взгляд на реализацию 1126 * довольно хорошо это объясняет).В результате получается cv::Matx, поэтому мы должны использовать его вместо cv::Vec при использовании cv::Mat::at.

Тогда возможны два варианта:

  • Избавьтесь от 4-го элемента, а затем приведите результат для преобразования cv::Matx в uint8_t тип элемента.

  • Cast the *Сначала с 1146 * до cv::Scalar_<uint8_t>, а затем избавиться от 4-го элемента.

Пример:

#include <opencv2/opencv.hpp>

typedef cv::Matx<uint8_t, 3, 1> Mat31b; // Convenience, OpenCV only has typedefs for double and float variants

int main()
{
    cv::Mat test_mat(1, 12, CV_8UC3); // 12 * 3 = 36 bytes of data
    test_mat = cv::Scalar(1, 1, 1); // Set all elements to 1
    std::cout << "Before: " << test_mat << "\n";

    cv::Scalar test_scalar{ 2,3,4,0 };
    cv::Matx31d temp = test_scalar.get_minor<3, 1>(0, 0);
    test_mat.at<Mat31b>(0, 0) = static_cast<Mat31b>(temp);

    // or
    // cv::Scalar_<uint8_t> temp(static_cast<cv::Scalar_<uint8_t>>(test_scalar));
    // test_mat.at<Mat31b>(0, 0) = temp.get_minor<3, 1>(0, 0);


    std::cout << "After: " << test_mat << "\n";

    return 0;
}

NB: Вы можете избавиться от явных временных значений, они здесь просто для удобства чтения.

Вывод:

Обе опции дают следующий вывод:

Before: [  1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1]
After: [  2,   3,   4,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1,   1]

Как мы видим, были изменены только первые 3 байта, поэтому он ведет себя правильно.


Некоторые мысли о производительности.

Трудноугадайте, какой из двух подходов лучше.Сначала приведение означает, что вы выделяете меньший объем памяти для временного, но затем вам нужно сделать 4 saturate_cast с вместо 3. Некоторый бенчмаркинг должен быть выполнен (упражнения для читателя).Вычисление среднего значения значительно перевесит его, поэтому оно, вероятно, не будет иметь значения.

Учитывая, что нам действительно не нужны saturate_cast s, возможно, простой, но более подробный подход (оптимизированная версия вещикоторый работал для вас) может работать лучше в узком цикле.

cv::Vec3b& current_element(avgs.at<cv::Vec3b>(i));
cv::Scalar current_mean(cv::mean(cv::Mat(img, rois[i])));
for (int n(0); n < 3; ++n) {
    current_element[n] = static_cast<uint8_t>(current_mean[n]);
}

Обновление:

Еще одна идея, которая возникла при обсуждении с @ alkasm .Оператор присваивания для cv::Mat векторизуется, когда ему присваивается cv::Scalar (он присваивает одно и то же значение всем элементам), и он игнорирует дополнительные значения канала, которые cv::Scalar может содержать относительно целевого типа cv::Mat.(например, для 3-канального Mat он игнорирует 4-е значение).

Мы могли бы взять 1x1 ROI цели Mat и присвоить ей среднее значение Scalar.Произойдут необходимые преобразования типов, и 4-й канал будет сброшен.Возможно, это не оптимально, но пока что это наименьшее количество кода.

test_mat(cv::Rect(0, 0, 1, 1)) = test_scalar;

Результат такой же, как и раньше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...