cv::Mat类的设计存在缺陷吗?

21

我经常使用OpenCV C++接口,并设计了一些使用Mat作为私有资源的类。

最近,我对Mat类感到担忧,因为它总是将图像数据用作共享资源,除非我显式调用clone。即使我写const Mat,我也无法确定图像数据不会在外部被更改。

因此,我需要克隆来确保封装性。但需要显式克隆Mat的问题在于,它通常是不必要的和昂贵的。另一方面,我理解共享图像数据的需求源自ROI选择器,并且能够编写类似以下内容的代码:

Mat m_small = m_big(my_roi)

我的问题是:

1.) cv::Mat类是否应该采用惰性克隆? 这样用户将不会从外部看到Mat作为共享资源处理程序。当需要真正的共享图像数据时,用户是否应该显式实例化一个名为SharedMat的类?
2.) 对于作为类的私有资源的cv::Mat,您是否有任何比始终克隆更好的策略


更新:“你不使用Mat::clone(),除非你计划修改数据。”(Vadim Pisarevsky)
这个想法有一个问题。 考虑您拥有这个类的情况:

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_piratem_treasure 之间的共享图像数据,在 res_handler 内部导致 m_treasure 完全黑屏。所以,为了避免意外修改内部的 m_treasure你需要对其进行 clone

另一方面,这个解决方案也有缺陷:

const Mat m_pirate = res_handler.emit_mat(); 

因为m_treasure也可以被修改,所以m_pirate的内容会在后台中发生改变,使得海盗的程序员感到很头痛。 :)

4个回答

15

是的,这是一个糟糕的设计,因为Mat在内部实现共享所有权,所以它与选择所有权策略的标准方式即智能指针不兼容。基本问题在于数据和所有权是正交的,应该分开处理。

由于它是可变的,即使是const Mat更像是const shared_ptr<Mat>,没有办法描述所包含的Mat应该是可变的,也就是shared_ptr<const Mat>。如果熟悉Java中final的问题,这非常相似。

我相信你可以通过将Mat封装在一个类中来避免这些问题,该类公开与Mat相同的接口,但在默认共享实现之上实现写时复制行为。


是的,我有这个看法,但我似乎找不到一种安全地用包装器将一个Mat固定在角落的方法,正如你所描述的那样。我已经到了编写自己的包装器并回归使用IplImages的地步。 - Barney Szabolcs

11
< p >< em >< strong >【不要脸地广告】现在我们有answers.opencv.org,它是OpenCV特定问题的类似于StackOverflow的网站。

现在来看问题:

  1. 不,我们没有找到一种有效的方法来实现这个。如果你有,让我们讨论一下。

  2. 是的,请勿使用Mat::clone(),除非您打算修改数据。引用计数会在不再使用时正确释放数据。


2
等一下,我认为根据stackoverflow的规定,Vadim应该提到他是OpenCV的某种主要开发者。 - Barney Szabolcs
他刚刚注册,也许他还没有完全设置好自己的个人资料。 - Rui Marques
@RuiMarques仍然不是一个活跃的成员(除了这个回答之外,他没有任何活动),尽管他昨天似乎已经登录。 - Barney Szabolcs

7
回答OP的问题: 是的,肯定可以!正如几位人指出的那样:OpenCV无法描述图像的常量引用。这确实是一个缺陷。“const cv :: Mat&”不是C ++程序员所期望的,我经常在代码中散布clone()调用,以至于失去了数据共享的好处。
回答Vadim的问题如何高效地做到这一点: 这绝对是可能的,并且没有API更改就无法实现。看看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的代码在使用,所以不太可能做出大的改变。而且随着OpenCV 3的发布,API的修改也已经关闭了。但是我认为增加一个新的、更好的接口应该是可行的。


点赞以与Qt进行比较。Qt的按需复制方法非常直观且易于使用。 - Aleksei Petrenko

3
在Vadim的回答上,我想进一步探讨这个话题。我也广泛使用cv::Mat,并从中受益匪浅。
编程中一个普遍的真理是你必须平衡项目中不同的需求。其中之一是性能与可维护性之间的平衡。而这已经得到了“过早优化是万恶之源”的解决。这种方法很好,但许多程序员只是盲目地跟随它。
对于图像处理来说,性能至关重要。没有它,许多项目根本不可行。因此,在处理图像时从不过早进行优化。这是为数不多的几个领域之一,毫秒计数,你所做的一切都被质量和速度所衡量。如果你来自C#、Java或用户界面设计,可能很难接受这一点,但为了提高速度,值得牺牲一些面向对象设计的成熟实践。
如果你查看OpenCV的源代码,你会看到非常强调优化:基于SSE的函数、NEON函数、指针技巧、各种算法奇思妙想、图形处理器实现、OpenCL实现、查找表等等,这在其他类型的项目中被认为是过度的、难以维护或“过早优化”。
应用架构的小改变(如cv::Mat分配策略)在性能方面可能会产生巨大的差异。在嵌入式设备上共享图像,而不是克隆,可能会使一个伟大的小工具与死胡同的概念证明之间产生区别。
因此,当Vadim说他们没有看到实现你建议的更改的有效方法时,他指出这些更改的性能惩罚不足以弥补其好处。
这样的项目写起来和维护起来都更难,但这是为了好处。通常,图像处理项目的困难部分是编写正确的算法。将其封装只是最后1%的工作。

大部分你的回答似乎并没有真正增加到Vadim的回答中,因为cv::Mat背后的优化并没有受到质疑。我希望你知道“惰性复制”对象的概念,即只有在真正需要时才进行复制。这种方法或其他可能的方法不能抵消其他好处,如果可以,请在你的回答中写出来。 - Barney Szabolcs
1
这里不可能使用惰性复制。这将意味着完全封装(无法直接访问Mat数据),而这种方法将破坏OpenCV中的大部分优化以及您可能想要添加到应用程序中的任何自定义优化。大多数优化和大多数OpenCV算法实现都使用直接指针访问矩阵数据。不使用它们将立即使任何算法变慢10倍。 - Sam
2
我不确定你见过哪些懒复制实现。请注意,您可以直接访问Mat数据并仍然进行懒复制。 - Barney Szabolcs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接