Является ли класс cv:: Mat неправильным по дизайну?
Я много работаю с интерфейсом OpenCV С++ и разработал ряд классов, которые используют Mat как частные ресурсы.
В последнее время меня беспокоил класс Mat, поскольку он всегда использует данные изображения в качестве общего ресурса, если я явно не вызываю clone. Даже если я пишу const Mat
, я не могу быть уверен, что imagedata не будет изменяться позже извне.
Поэтому мне нужно клонировать, чтобы обеспечить инкапсуляцию. Но проблема с необходимостью явного клонирования Mat заключается в том, что он часто бывает ненужным и дорогостоящим. С другой стороны, я понимаю, что потребность в общих изображениях возникает из селекторов roi и может написать что-то вроде этого:
Mat m_small = m_big(my_roi)
.
Мои вопросы:
1.) Разве класс cv:: Mat не будет клонирован лениво?. Таким образом, пользователь не увидит Mat как общие обработчики ресурсов извне. Должен ли пользователь явно создавать экземпляр класса, называемого как SharedMat
, когда требуется реальная общая информация?
2.) У вас есть стратегия лучше, чем всегда клонирование в случае cv:: Mat как частный ресурс для класса?
UPDATE: "вы не используете Mat::clone()
, если вы не планируете изменять данные". (Вадим Писаревский)
У этой идеи есть проблема.
Рассмотрим ситуацию, когда у вас есть этот класс:
class Res_handler{
public:
const Mat emit_mat(){ return m_treasure; } // I argue you are compelled to clone here.
private:
Mat m_treasure;
};
Если вы не clone
, в этом случае вы можете написать
Mat m_pirate = res_handler.emit_mat(); m_pirate = Scalar(0,0,0);
что приводит к полному отключению в m_treasure
внутри res_handler
через данные общего изображения между m_pirate
и m_treasure
.:) Поэтому, чтобы избежать случайной модификации внутреннего m_treasure
, вам нужно clone
его.
С другой стороны, это решение также ошибочно:
const Mat m_pirate = res_handler.emit_mat();
потому что m_treasure
тоже может быть изменен, поэтому содержимое m_pirate
изменяется в фоновом режиме, вызывая сильную головную боль для пиратского программиста.:)
Ответы
Ответ 1
Да, это плохой дизайн. Поскольку Mat
реализует совместное владение внутри страны, он несовместим со стандартным способом выбора политики собственности, а именно интеллектуальными указателями. Основная проблема заключается в том, что данные и права собственности являются ортогональными и должны быть разделены.
Поскольку он изменен, даже const Mat
больше похож на const shared_ptr<Mat>
, не описывая, что содержащийся Mat
должен быть изменчивым, т.е. shared_ptr<const Mat>
. Это имеет большое сходство с проблемами с final
в Java, если вы знакомы.
Я считаю, что вы можете обойти эти проблемы, обернув Mat
в классе, который предоставляет тот же интерфейс, что и Mat
, но который реализует поведение копирования на запись поверх стандартной реализации по умолчанию.
Ответ 2
[бесстыдное объявление] теперь у нас есть answer.opencv.org, который является своего рода StackOverflow для вопросов, связанных с OpenCV.
Теперь на вопросы:
-
Нет. Мы не видим эффективного способа реализации этого. Если да, обсудите его.
-
Да. Не используйте Mat::clone()
, если вы не планируете изменять данные. Счетчик ссылок заботится о правильном освобождении данных, когда он больше не используется.
Ответ 3
Чтобы ответить на вопрос OPs:
Да, определенно! Как отметили пару человек: OpenCV не дает возможности описать ссылку на изображение const. Это действительно недостаток. "const cv:: Mat &" это не то, что ожидал бы программист на С++, и я часто обнаруживал, что разворачиваю вызовы clone() по всему моему коду до такой степени, когда я теряю выгоду от совместного использования данных.
Отвечать Вадиму вопрос о том, как это сделать эффективно:
Это можно сделать эффективно, хотя не без изменения API. Посмотрите, как Qt отказался от модели явного совместного использования, которую она имела до Qt 4 (аналог текущей модели OpenCV), с ее текущим неявным совместным использованием (копирование при записи) с большим успехом. В принципе, все вызовы функций, которые мутируют объект, или возвращают ссылку, которая впоследствии может мутировать объект, должны "разрезать" его. Это сделать копию, если есть несколько ссылок.
Стоимость этого минимально по сравнению со средней стоимостью операции с изображением. Он становится непомерно высоким, если он должен выполняться на пиксель. Вот почему класс нужно разделить на два. Очень похоже cv:: Mat и cv:: Mat_. Тот, который отвечает за неявное совместное использование и копирование, и тот, который является только шаблонной оболочкой IplImage. Вот пример того, как выглядит API (для ясности я выбрал слишком явные имена):
// The following makes no unnecessary copies. Only a
// couple of atomic increments and decrements.
const cv::Image img = cv::Image("lenna.bmp").toGray().brighter(0.3).inverted();
cv::Image copy(img);// Still no deep copy.
cv::ConstImageRef<char> src = img.constRef<char>();// Still no deep copy.
// This is where the copy(detach) happens.
// "img" is left untouched
cv::MutableImageRef<char> dst = copy.ref<char>();
// The following is as efficient as it has ever been.
for(int y = 0; y<dst.height(); y++)
for(int x = 0; x<dst.width(); x++)
dst.at(x, y) += src.at(x, y);
Я понимаю, что слишком много кода OpenCV, плавающего вокруг, чтобы внести какие-либо радикальные изменения, и окно, чтобы изменить API с помощью OpenCV 3, закрыто, но я не понимаю, почему не должно быть возможно добавить новый улучшенный интерфейс.
Ответ 4
Добавляя и расширяя ответ Вадима, вот некоторые мысли по этой теме.
Я также использовал cv:: Mat во многих отношениях и пользовался его преимуществами.
Общая истина в программировании заключается в том, что вам нужно сбалансировать различные противоположные потребности проектов. Одна из них - производительность и ремонтопригодность. И это было решено раз и навсегда с "Преждевременная оптимизация - это зло". Такой подход велик, но многие программисты просто слепо следуют за ним.
Для обработки изображений производительность имеет первостепенное значение. Без этого многие проекты просто невозможны. Поэтому при обработке изображений никогда не стоит преждевременно оптимизировать. Это одно из тех немногих полей, где миллисекунды считаются, где все, что вы делаете, измеряется как качеством, так и скоростью. И это может быть трудно переварить, если вы исходите из С#, Java или дизайна пользовательского интерфейса, но для этого улучшения скорости стоит пожертвовать некоторыми из установленных практик объектно-ориентированного дизайна.
Если вы воспользуетесь исходным кодом OpenCV, вы увидите невероятный акцент на оптимизации: функции на основе SSE, функции NEON, уловки указателей, все виды ракурсов алгоритмов, реализация графических процессоров, реализации OpenCL, таблицы поиска и многие, многие другие, которые будут считаться излишними, труднодоступными или "преждевременной оптимизацией" в других проектах.
Небольшое изменение в архитектуре вашего приложения (например, стратегия распределения cv:: Mat) может существенно повлиять на производительность. Совместное использование изображения на встроенном устройстве вместо клонирования может сделать разницу между отличным гаджетом и тупиковым доказательством концепции.
Итак, когда Вадим сказал, что они не видят эффективного способа реализовать ваши предложенные изменения, он предположил, что штрафные санкции за эти изменения не будут покрывать выгоды.
Такой проект сложнее писать и поддерживать, но это хорошо. И, как правило, сложная часть проекта визуализации заключается в написании правильного алгоритма. Инкапсуляция - это всего лишь 1% работы в конце.