Примечание: 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;
Результат такой же, как и раньше.