Различные статьи по Qt и c++

Mat — основная структура данных OpenCV: хранение, копирование, ROI

Введение

Статья 561 уже создала cv::Mat без подробного объяснения — в этой статье разберём эту фундаментальную структуру данных OpenCV систематически: как она хранит изображение в памяти, особенности её семантики копирования (отличающейся от интуитивно ожидаемой для большинства C++ типов), и концепцию ROI (region of interest) для эффективной работы с частью изображения без копирования.

Концепция

cv::Mat представляет многомерный, типизированный массив (чаще всего двумерный, для представления изображения), хранящий данные в непрерывном блоке памяти (статья 521 цикла про синтаксис C/C++ — аналогично обычному массиву) с управлением памятью через подсчёт ссылок (концептуально похожий на shared_ptr, статья 547 цикла синтаксиса C/C++) — это означает, что операция присваивания (Mat b = a;) по умолчанию НЕ копирует реальные данные пикселей, а лишь создаёт второй заголовок Mat, указывающий на ТЕ ЖЕ данные, с увеличением внутреннего счётчика ссылок, и реальное, «глубокое» копирование данных требует явного вызова .clone(). ROI (region of interest) — представление прямоугольной области исходного изображения как отдельного объекта Mat, без копирования данных этой области — изменения, сделанные через ROI, напрямую отражаются на соответствующей части исходного изображения, поскольку оба объекта Mat физически указывают на одну и ту же область памяти.

Пример кода: Mat — основная структура данных OpenCV

#include <opencv2/opencv.hpp>
#include <iostream>

int main()
{
    cv::Mat original(480, 640, CV_8UC3, cv::Scalar(0, 0, 255)); // красное изображение 640x480

    cv::Mat shallowCopy = original; // ПОВЕРХНОСТНОЕ копирование — НЕ копирует данные пикселей!
    shallowCopy.at<cv::Vec3b>(0, 0) = cv::Vec3b(255, 0, 0); // изменение через shallowCopy...

    // ...отразится и на original, поскольку оба Mat указывают на ОДНИ И ТЕ ЖЕ данные в памяти!
    std::cout << "original в (0,0): " << original.at<cv::Vec3b>(0, 0) << std::endl; // [255, 0, 0], а не [0, 0, 255]!

    cv::Mat deepCopy = original.clone(); // .clone() — РЕАЛЬНОЕ, "глубокое" копирование данных
    deepCopy.at<cv::Vec3b>(0, 0) = cv::Vec3b(0, 255, 0);
    std::cout << "original после deepCopy изменения: " << original.at<cv::Vec3b>(0, 0) << std::endl; // НЕ изменилось

    return 0;
}
// ROI — работа с областью изображения без копирования данных
#include <opencv2/opencv.hpp>

int main()
{
    cv::Mat image(480, 640, CV_8UC3, cv::Scalar(0, 0, 0));

    cv::Rect roiRect(100, 100, 200, 150); // прямоугольник: x=100, y=100, ширина=200, высота=150
    cv::Mat roi = image(roiRect); // ROI — НЕ копия, а "окно" в исходные данные

    roi.setTo(cv::Scalar(255, 255, 255)); // заполнение белым — изменяет СООТВЕТСТВУЮЩУЮ часть ИСХОДНОГО image!

    // image теперь содержит белый прямоугольник именно в области (100,100)-(300,250)
    cv::imwrite("result.png", image); // статья 563 — сохранение изображения с изменённым ROI

    return 0;
}

Пояснения к коду

cv::Mat shallowCopy = original; с последующим изменением через shallowCopy, отражающимся на original, наглядно демонстрирует ключевую, неинтуитивную для разработчиков, ожидающих обычной C++ семантики копирования, особенность — присваивание Mat создаёт лишь дополнительную «ссылку» на те же данные (аналогично копированию shared_ptr, а не глубокому копированию объекта, на который он указывает), и для получения реальной, независимой копии нужен явный вызов .clone(), как демонстрирует deepCopy, чьё изменение уже НЕ влияет на original. Пример с ROI показывает практичное применение этой же модели общих данных — image(roiRect) создаёт Mat, представляющий лишь прямоугольную подобласть исходного изображения, но физически указывающий на ту же память, что и соответствующая область image, и любое изменение через этот ROI-объект (setTo()) напрямую модифицирует именно эту область исходного изображения, что эффективно избегает необходимости копировать данные только для того, чтобы изменить небольшую часть большого изображения.

Подводные камни

  • Ожидание обычной, «глубокой» семантики копирования при простом присваивании Mat, особенно для разработчиков, переносящих интуицию из обычных C++ классов (статья 553 цикла синтаксиса C/C++ про правило пяти/нуля) — забытое использование .clone() там, где реально нужна независимая копия, может привести к неожиданному, непреднамеренному изменению исходного изображения через то, что разработчик считал отдельной, независимой копией, что является одним из самых частых, специфичных для OpenCV источников ошибок для начинающих работать с этой библиотекой.
  • Изменение данных через ROI без понимания, что это напрямую модифицирует исходное изображение — аналогично предыдущему пункту, но применительно конкретно к ROI: код, создающий ROI для «временного», изолированного эксперимента с частью изображения, без понимания, что ROI — это не копия, может непреднамеренно повредить исходные данные, если целью было лишь временное, не влияющее на оригинал исследование конкретной области.
  • Удержание ROI (или любого Mat, использующего общие данные с другим Mat) дольше, чем реально нужно, без понимания влияния на управление памятью через подсчёт ссылок — поскольку реальные данные пикселей освобождаются только тогда, когда ВСЕ Mat-объекты, ссылающиеся на эти данные (включая любые ROI), уничтожены, удержание неиспользуемого, но «забытого» ROI-объекта может продлить время жизни даже значительно большего исходного изображения, потенциально дольше, чем реально необходимо для текущей задачи.
  • Игнорирование разницы в производительности между поверхностным копированием (присваивание) и глубоким копированием (.clone()) для крупных изображений — поверхностное копирование практически бесплатно (лишь обновление счётчика ссылок и метаданных), тогда как .clone() для большого изображения требует реального копирования значительного объёма данных, и явное, осознанное решение о том, действительно ли нужна реальная, независимая копия (оправдывающая стоимость .clone()), важно для производительности кода, интенсивно работающего с большими изображениями.