OpenCV Mat UMat GpuMat Matx HostMem InputArray等设计哲学

发布于:2025-07-24 ⋅ 阅读:(41) ⋅ 点赞:(0)

一、概览:

GpuMat对应于cuda;HostMem 可以看作是一种特殊的Mat,其存储对应cuda在主机分配的锁页内存,可以不经显示download upload自动转变成GpuMat(但是和GpuMat并无继承关系);UMat对应于opencl的存储 Matx指代常量Mat,编译时即确定:InputArray则是一种代理模式。 注意,InputAray和Mat UMat GpuMat Matx等无继承关系!!

二、然后我们通过几个点来深入了解一下opencv为何这么设计,以及一些细节。

一、为何一些数据结构之间有时候可以转换有时候不可以

首先要知道opencv的数据结构本质是要管理一块存储,也许是主机内存也许是cuda显存 也许是opencl存储 那么,无论是Mat Matx 还是其他数据结构,本质上都是一个header+一个数据指针,不同数据结构之间并无继承关系。那么有一个情况需要解释,比如HostMem和Mat同样是主机内存,那么可以HostMem就会有从HostMem转变为Mat的构造函数,同时因为HostMem是cuda分配的,如果是带有deviceMapped的主机内存(opencv管这叫shared HostMem),也可以调用转变为GpuMat的构造函数,需要强调,这些构造函数本质上是转移了data指针并创造了一个新的header。

二、为何InputArray不是一个基类?

对于许多OpenCV的C++开发者来说,第一次在函数签名中遇到 cv::InputArraycv::OutputArray 时,心中难免会产生疑问:“这到底是什么类型?为什么我不直接传递 cv::Mat?” 当我们进一步发现,MatGpuMat 甚至 std::vector 都可以被传递给一个 InputArray 参数时,这种好奇心会变得更加强烈。

这背后,隐藏着OpenCV设计者们关于性能、灵活性和扩展性的深刻思考。本文将结合我们对OpenCV数据结构的理解,深入探讨这个看似“奇怪”却极其精妙的设计选择。

1、舞台上的演员们:OpenCV的数据江湖

在深入探讨设计哲学之前,我们必须先认识一下舞台上的主要“演员们”。它们的核心任务都是管理一块内存,但这块内存的“家”却各不相同。

  • cv::Mat: 最家喻户晓的明星。它是一个通用的N维数组容器,主要负责管理主机(CPU)内存。它是OpenCV图像处理的基石。
  • cv::cuda::GpuMat: CUDA阵营的先锋。它专门管理在NVIDIA GPU显存中的数据,是进行CUDA加速运算的主体。
  • cv::cuda::HostMem: GpuMat 的得力助手。它可以看作是一种特殊的Mat,其数据存储在由CUDA分配的**主机端锁页内存(Pinned Memory)**中。这种内存的特殊之处在于,它可以被GPU直接访问(DMA),从而极大地加速了主机与设备之间的数据传输,甚至可以实现数据流的并发。
  • cv::UMat: OpenCL阵营的代表,透明计算的未来。它是一个更为抽象的容器,其管理的内存可能在CPU上,也可能在GPU、DSP或其他OpenCL设备上。UMat 的美妙之处在于它能根据计算上下文自动处理数据同步,对开发者隐藏了复杂的内存迁移操作。
  • cv::Matx: 轻量级的“便签条”。它是一个小尺寸、固定大小的矩阵,其内存通常直接在**栈(Stack)**上分配。由于大小在编译时就已确定,避免了堆内存分配的开销,非常适合用于表示3D点、像素值等小型数据。
  • std::vector: 来自C++标准库的“外援”。无论是std::vector<Point>还是std::vector<float>,它们都是OpenCV算法中常见的数据结构。

关键点:这些数据结构,尤其是 MatGpuMatUMatstd::vector,彼此之间并无继承关系。它们是独立的、为了不同目的而设计的类。

2、核心挑战:如何让一个函数“通吃”所有数据类型?

现在,问题来了。假设我们要写一个函数,比如计算数组的均值。我们希望这个函数既能处理CPU上的Mat,也能处理GPU上的GpuMat,甚至还能处理一个std::vector<float>

一个遵循传统面向对象(OOP)思路的开发者可能会立刻想到:继承!

我们可以设计一个抽象基类 Array,然后让 MatGpuMat 等都公有继承自它:

// 一个看似很美的“继承”方案(但OpenCV没有采纳)
class Array {
public:
    virtual ~Array() {}
    virtual int getRows() const = 0;
    // ... 其他通用接口
};

class Mat : public Array { /*...*/ };
class GpuMat : public Array { /*...*/ };

// 函数签名
void calculateMean(const Array& arr);

然而,这个方案存在三个对于高性能计算库而言几乎是致命的缺陷。

  1. 性能的枷锁——虚函数开销:为了实现多态,基类中的函数必须是虚函数。这意味着每个对象都需要额外存储一个虚函数表指针,并且每次函数调用都需要一次间接寻址。在像素级的海量循环中,这种微小的开销会被无限放大,违背了OpenCV追求极致性能的初衷。

  2. 灵活性的噩梦——侵入式设计:这个方案最大的问题是,它要求所有被处理的类型都必须从Array继承。我们不可能去修改C++标准库,让std::vector继承自我们的Array!我们也无法让一个C风格的原始数组指针继承一个类。这种“侵入式”的设计会极大地限制库的通用性。

  3. 稳定性的隐患——脆弱的ABI:对于一个被全球开发者使用的库,保持二进制接口(ABI)的稳定至关重要。一旦基类Array的结构(如增删虚函数)发生改变,所有依赖它的、已编译的程序都可能需要重新编译,这是一场灾难。

3、OpenCV的答案:优雅的代理模式(Proxy Pattern)

面对上述挑战,OpenCV的设计者们给出了一个非凡的答案:代理模式InputArrayOutputArray 就是这个模式的实现者。

InputArray 不是一个基类,而是一个轻量级的“代理”或“适配器”。

它本身不拥有数据,而是像一个经纪人一样,持有对“真正”数据(Mat, GpuMat, vector…)的引用或指针,并对外提供一个统一的接口。

这种设计是如何工作的呢?

模式一:转发共同能力

当函数需要执行一个通用操作时(比如获取尺寸),它会调用InputArray的接口,例如arr.size()InputArray内部会判断自己当前代理的是哪位“明星”(Mat? GpuMat?),然后将这个调用转发给实际对象的对应方法。

// 函数实现者视角
void myFunction(cv::InputArray arr) {
    // 无需关心 arr 到底是 Mat 还是 GpuMat
    // InputArray 会自动将调用转发给它代理的对象的 .size() 方法
    cv::Size sz = arr.size(); 
    // ...
}```

这实现了多态的好处,却没有虚函数的性能开销,也无需修改任何原始类。

#### 模式二:直接获取特有能力

当需要执行某个特定类型才有的操作时(比如将`GpuMat`传入一个自定义的CUDA核函数),`InputArray`也提供了一个“逃生通道”。你可以从它那里获取到原始对象的引用。

```cpp
// 函数实现者视角
void myCudaFunction(cv::InputArray arr) {
    // 确认代理的是GpuMat后,获取其可写引用
    cv::cuda::GpuMat& d_mat = arr.getGpuMatRef(); 
    // 现在可以调用 GpuMat 的所有特有方法了
    my_cuda_kernel<<<...>>>(d_mat.ptr<float>(), ...);
}

这保证了设计的灵活性和功能的完整性,我们不会因为使用了代理而丢失对底层对象的完全控制。

结论:一场工程智慧的胜利

现在,我们可以清晰地回答最初的问题了。

OpenCV之所以不采用传统的继承体系,而是设计出InputArray这样的代理类,是为了在一套API中,同时实现三个看似矛盾的目标:

  1. 极致的性能:避免了虚函数带来的开销。
  2. 无与伦比的灵活性:通过非侵入式的设计,使其能够适配MatGpuMatUMatstd::vector等众多类型,而无需它们做出任何改变。
  3. 坚如磐石的稳定性:代理类本身结构稳定,易于扩展以支持新类型,而不会破坏二进制兼容性。

InputArray的设计哲学,是典型的用组合(代理是一种组合形式)优于继承的工程实践。它或许在初学时带来一丝困惑,但一旦理解其背后的深意,你便会由衷地赞叹这种设计的优雅与强大。它不仅仅是一个技术选择,更是OpenCV作为一个高性能、高通用性计算库的立身之本。

InputArray除了可以作为通用接口接受不同数据结构外,还有什么作用?

两个层面:抽象接口的转发具体对象的直接访问。这两种模式是相辅相成的。

模式一:转发/代理 (Forwarding/Delegation) - 处理“共同能力”

当一个操作是所有或大多数数组类型(Mat, UMat, GpuMat, vector…)都应该具备的通用能力时,_OutputArray 类会为这个操作提供一个自己的成员函数。

最典型的“共同能力”就是:

  • 创建/分配内存 (create, createSameSize)
  • 释放/清空 (release, clear)
  • 赋值 (setTo, assign, move)

工作流程:

  1. 函数实现者调用 OutputArray 的方法,例如 dst.create(size, type)
  2. _OutputArray 内部会检查它当前“代理”的是哪种具体对象(Mat? GpuMat?)。
  3. 然后,它将这个调用**转发(Forward)**给它所代理的那个具体对象的相应方法。
    • 如果 dst 包裹的是一个 Mat,它内部会调用 the_mat.create(size, type)
    • 如果 dst 包裹的是一个 GpuMat,它内部会调用 the_gpumat.create(size, type)

这么做的好处是多态性代码复用。函数实现者无需写 if-else 来判断 dst 的具体类型,只需面向 OutputArray 这个统一的抽象接口编程即可。这使得一个函数(如 cv::cvtColor)可以无缝地同时支持 MatUMatGpuMat 作为输出。


模式二:直接获取 (Direct Access) - 处理“特有能力”

当一个操作是某个具体类(比如 GpuMat特有的能力,而其他类(如 Mat)没有这个能力时,_OutputArray 接口中就不会包含这个操作。

例如:

  • 直接访问 GpuMatstep 成员进行指针运算。
  • 调用 Mat 特有的 push_back() 方法。
  • GpuMat 传递给一个需要 cudaStream_t 参数的自定义CUDA核函数。

在这种情况下,函数实现者就必须先“揭开”OutputArray 的代理面纱,拿到它背后包裹的那个原始对象。

工作流程:

  1. 函数实现者首先需要知道或判断 OutputArray 代理的是哪种类型。
  2. 然后调用 get...Ref() 方法,如 dst.getMatRef()dst.getGpuMatRef(),来获取一个可写的引用
  3. 拿到这个引用后,就可以像操作一个普通的 MatGpuMat 对象一样,调用它所有特有的方法和成员。
    这么做的好处是灵活性完整性。它提供了一个“逃生通道”,确保了即使 OutputArray 的抽象接口没有覆盖某个功能,开发者依然可以使用具体类的全部能力,不会因为使用了代理类而丢失功能。

总结对比

行为模式 调用方式 适用场景 设计目的
转发/代理 直接调用 dst.create(...), dst.setTo(...)OutputArray 的方法。 处理所有数组类型都支持的通用操作 抽象与多态:隐藏具体实现,让函数可以处理多种数据类型。
直接获取 先调用 dst.getMatRef() 等获取具体引用,再调用该引用的特有方法。 处理某个特定数组类型才有的专属操作 灵活性与完整性:不限制开发者使用具体类的全部功能。

OpenCV 的 Input/Output 代理类设计,是一个在高度抽象(为了易用和通用)和完全控制(为了性能和功能完整性)之间取得精妙平衡的典范。


网站公告

今日签到

点亮在社区的每一天
去签到