cv::Mat 类是不是存在设计缺陷?
Posted
技术标签:
【中文标题】cv::Mat 类是不是存在设计缺陷?【英文标题】:Is cv::Mat class flawed by design?cv::Mat 类是否存在设计缺陷? 【发布时间】:2012-12-04 23:09:31 【问题描述】:我经常使用 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_pirate
和m_treasure
之间共享的图像数据导致res_handler
内部的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>
。如果您熟悉的话,这与 Java 中 final
的问题非常相似。
我相信您可以通过将Mat
包装在一个公开与Mat
相同接口但在默认共享实现之上实现写入时复制行为的类中来绕过这些问题。
【讨论】:
是的,我有这个意见,但我似乎无法找到一种方法来安全地转弯带包装的垫子,正是由于这种行为,你描述了。我正在编写自己的包装器并重新使用 IplImages。【参考方案2】:[无耻广告]我们现在有了 answers.opencv.org,它是针对 OpenCV 特定问题的 ***。
现在回答问题:
没有。我们没有看到一种有效的方法来实现这一点。如果有,我们来讨论一下。
是的。不要使用Mat::clone()
,除非您打算修改数据。引用计数负责在数据不再使用时正确释放数据。
【讨论】:
等一下,我认为根据 *** 规则,Vadim 应该提到他是一个 OpenCV 首席开发人员。 他刚刚注册,可能还没有正确设置个人资料。 @RuiMarques 尽管他似乎昨天才登录,但他仍然不是活跃成员(除了这个答案,他没有任何活动)。【参考方案3】:回答 OP 问题:
是的,绝对是!正如一些人指出的那样:OpenCV 使您无法描述对图像的 const 引用。这确实是一个缺陷。 “const cv::Mat&”不是 c++ 程序员所期望的,我经常发现自己在我的代码中到处调用 clone(),以至于我失去了数据共享的好处。
回答 Vadims 关于如何有效地做到这一点的问题:
绝对有可能有效地做到这一点,尽管并非没有 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 代码浮动,无法进行任何根本性的更改,并且使用 OpenCV 3 进行 API 更改的窗口已关闭,但我不明白为什么不能添加新的改进界面。
【讨论】:
赞成与 Qt 进行比较。 Qt 的按需复制方法非常直观且易于使用。【参考方案4】:添加和扩展 Vadim 的答案,这里有一些关于该主题的想法。
我还以多种方式广泛使用了 cv::Mat,并享受了它的好处。
编程中的一个普遍事实是,您必须平衡项目的不同对立需求。其中之一是性能与可维护性。并且通过“过早的优化是邪恶的”一劳永逸地解决了这个问题。这种方法很棒,但是很多程序员只是盲目地遵循它。
对于图像处理,性能至关重要。没有它,许多项目根本不可行。因此,在处理图像时进行优化永远不会为时过早。它是为数不多的以毫秒计的领域之一,您所做的一切都是通过质量和速度来衡量的。而且,如果您来自 C#、Java 或用户界面设计,可能很难消化它,但是为了提高速度,牺牲一些面向对象设计的既定实践是值得的。
如果您浏览 OpenCV 的源代码,您会看到对优化的难以置信的强调:基于 SSE 的函数、NEON 函数、指针技巧、各种算法奇观、图形处理器实现、OpenCL 实现、查找表等等,还有许多其他类型的项目会被认为是矫枉过正、难以维护或“过早优化”。
应用架构中的一个小改动(如 cv::Mat 分配策略)可以在性能方面产生巨大的差异。在嵌入式设备上共享图像,而不是克隆,可能会在一个伟大的小工具和一个死胡同的概念验证之间产生差异。
因此,当 Vadim 说他们没有看到实施您建议的更改的有效方法时,他建议这些更改的性能损失不会涵盖收益。
这样的项目是更难编写和维护,但它是好的。通常,成像项目的困难部分是编写正确的算法。封装它只是最后 1% 的工作。
【讨论】:
嗯,您的大部分答案似乎并没有真正添加到 Vadim 的答案中,因为 cv::Mat 背后的优化没有受到质疑。我希望你了解懒惰复制对象的概念,只有在真正需要的时候。这 - 或其他可能的方法 - 不会影响其他好处,如果他们这样做,请在你的答案中写下。 这里不可能进行懒惰复制。这将意味着完全封装(不能直接访问 Mat 数据),这种方法将扼杀 OpenCV 中的大部分优化以及您可能想要添加到应用程序中的任何自定义优化。大多数优化和大多数 OpenCV 算法实现都使用直接指针访问矩阵数据。不使用它们会立即使任何算法慢 10 倍。 我不确定你见过哪些惰性复制实现。请注意,您可以直接访问 Mat 数据,但仍然有惰性副本。以上是关于cv::Mat 类是不是存在设计缺陷?的主要内容,如果未能解决你的问题,请参考以下文章
AsyncTask 真的在概念上存在缺陷还是我只是遗漏了啥?