OpenCV(开源计算机视觉)是一个主要面向实时计算机视觉的编程函数库,由英特尔位于俄罗斯下诺夫哥罗德的研究中心开发。该库是跨平台的,主要专注于实时图像处理。
OpenCV 在全世界被广泛采用。对最终用户而言,它成熟而强大;对开发者而言,它实现出色、设计良好。OpenCV 的开发者遵循着让代码易于理解和维护的基本准则。
让我们来发掘 OpenCV 的一些设计选择:
模块化
1. 基于库的架构
基于库的架构让所提供的功能更容易、更灵活地被复用和集成到其他项目中。此外,基于库的架构鼓励干净的 API 和关注点分离,让代码更易于开发者理解,因为他们只需专注于大局中的一小部分。
OpenCV 采用了这种方式:定义多个库,每个库承担特定的职责,并且都使用 opencv_core 库。

2. 用命名空间进行模块化
OpenCV 大量使用命名空间来模块化其代码库。例如,下面是 opencv_core 项目中的命名空间:

OpenCV 采用“按特性划分命名空间”(Namespace-by-feature)的方式。按特性划分命名空间,就是用命名空间来反映功能集:把与单个特性相关的所有条目(且仅与该特性相关的条目)放入同一个命名空间。这样得到的命名空间具有高内聚性和高模块性,命名空间之间的耦合也最小。紧密协作的条目被放在一起。
在 OpenCV 中,命名空间的使用主要有三个目的:
- 对库进行模块化。
- 隐藏实现细节,比如 “cv::detail” 命名空间。这种做法向库的使用者明确表明:该命名空间中的类型仅供内部使用,不应直接使用。在 C# 中,“internal” 关键字可以做到这一点,但在 C++ 中没有办法对库的使用者隐藏公有类型。
- 匿名命名空间:没有名字的命名空间。它避免了对全局 static 变量的需求。您创建的匿名命名空间只在创建它的文件内可访问。
把数据模型定义为 POD 类型
每个项目都有自己的数据模型,建议把这些数据定义为POD类型。
让我们在 OpenCV 代码库中搜索没有方法、只有字段的结构体。为此,CQLinq将被用来查询代码库。

该查询的结果覆盖了 OpenCV 项目中定义的类型中的 25%。OpenCV 几乎把整个数据模型都定义为只有字段的结构体。
避免多重继承
多重继承会让设计复杂化,也让调试更加困难;因此,许多 C++ 专家建议避免使用它。
让我们在 OpenCV 代码库中搜索继承自多个具体基类的类。

只有测试项目中的少数类使用了多重继承;在整个 OpenCV 代码库中,这个概念都被避免使用。
避免定义复杂函数
有许多度量可用于检测复杂函数。代码行数(NBLinesOfCode)、参数数量和局部变量数量是其中最基本的几个。
还有其他一些有趣的度量可用于检测复杂函数:
- 圈复杂度是一种流行的过程式软件度量,反映一个过程中可能发生的决策数量。
- 嵌套深度是一种方法级度量,表示方法体内嵌套作用域的最大深度。
- 最大嵌套循环数等于函数中循环嵌套的最大层数。
这些度量可容忍的最大值主要取决于团队的选择;没有标准值。
让我们在 OpenCV 代码库中搜索可以被认为是复杂的方法。

只有 1% 的方法是为降低复杂度而重构的候选。
耦合
低耦合是可取的,因为应用程序某一处的变更,需要整个应用程序中其他地方的改动也会更少。从长远来看,这可以节省与修改应用程序和添加新功能相关的大量时间、精力和成本。
低耦合可以通过使用抽象类来实现。下面是使用抽象类的三个主要好处:
- 抽象类提供了一种定义契约的方式,可以促进复用。如果一个对象实现了一个抽象类,那么该对象就必须符合一个标准。使用另一个对象的对象称为消费方(consumer)。抽象类就是对象与其消费方之间的契约。
- 抽象类还提供了一层抽象,让程序更容易理解。抽象类让开发者能够就代码的一般行为进行推理,而无需深入研究实现细节。
- 抽象类在组件之间保持了低耦合,这使得保护抽象类的消费方、使其免受实现类任何变更的影响变得很容易。
让我们搜索 OpenCV 定义的所有抽象类:

如果我们的首要目标是保持低耦合,那么使用抽象类时有一个常见错误可能会让使用抽象类的目的落空:使用具体类而不是抽象类。为了说明这个问题,让我们看下面的例子:
类 A 实现了抽象类 IA,后者包含 calculate() 方法;消费方类 C 是这样实现的:
public class C
{
….
public:
void calculate()
{
…..
m_a->calculate();
….
}
A* m_a;
};类 C 引用的不是抽象类 IA,而是类 A。在这种情况下,我们失去了低耦合的好处。这个实现有两个主要缺点:
- 如果我们决定使用 IA 的另一种实现,就必须修改类 C 的代码。
- 如果 A 中添加了一些 IA 中不存在的方法,而 C 又使用了它们,我们同时也会失去使用接口带来的契约好处。
C# 在语言中引入了显式接口实现能力,以确保来自 IA 的方法永远不会通过具体类的引用被调用,而只能通过接口的引用被调用。这种技术对于防止开发者失去使用接口的好处非常有用。
内聚
单一职责原则指出,一个类不应有超过一个变更理由。这样的类被称为内聚的。较高的 LCOM 值通常表明类的内聚性较差。LCOM 度量有好几种:LCOM 的取值范围是 [0-1];LCOM HS(HS 代表 Henderson-Sellers)的取值范围是 [0-2]。LCOM HS 值高于 1 就应视为警讯。下面是 LCOM 度量的计算方法:
LCOM = 1 – (sum(MF)/M*F) LCOM HS = (M – sum(MF)/F)(M-1)
其中:
- M 是类中方法的数量(静态方法和实例方法都计入;还包括构造函数、属性的 getter/setter 以及事件的 add/remove 方法)。
- F 是类中实例字段的数量。
- MF 是访问某个特定实例字段的类方法数量。
- Sum(MF) 是类中所有实例字段的 MF 之和。
这些公式背后的基本思想可以这样表述:如果一个类的所有方法都使用它的所有实例字段,那么这个类就是完全内聚的,也就是说 sum(MF)=M*F,此时 LCOM = 0 且 LCOMHS = 0。
LCOMHS 值高于 1 就应视为警讯。

只有少数类型是非内聚的。
结语
如果您看一看 OpenCV 的源代码,会惊讶于它实现的简洁:没有高深的设计概念,也没有过度工程——只有一以贯之的几条基本原则。
