Blog 6 min read

Some C++ Best Practices from the OpenCV Source Code

Share this article
Some C++ Best Practices from the OpenCV Source Code

OpenCV (Open Source Computer Vision) is a library of programming functions mainly aimed at real-time computer vision, developed by Intel’s research center in Nizhny Novgorod, Russia. The library is cross-platform and focuses mainly on real-time image processing.

OpenCV is widely used and adopted around the world. For end users, it is mature and powerful; for developers, it is well implemented and well designed. The OpenCV developers follow fundamental principles that make the code simple to understand and maintain.

Let’s discover some of OpenCV’s design choices:

Modularity

1- Library-based architecture

A library-based architecture makes the provided functionality easier and more flexible to reuse and integrate into other projects. In addition, the library-based architecture encourages clean APIs and separation of concerns, making the code easier for developers to understand because they only need to focus on small parts of the bigger picture.

OpenCV adopts this approach by defining multiple libraries, each with a specific responsibility, all of which use the opencv_core library.

opencv1

2- Modularize by namespaces

OpenCV makes extensive use of namespaces to modularize its codebase. For example, here are the namespaces in the opencv_core project:

opencv2

OpenCV uses the “Namespace-by-feature” approach. Namespace-by-feature uses namespaces to reflect the feature set. It places all items related to a single feature (and only that feature) into a single namespace. This results in namespaces with high cohesion and high modularity, and with minimal coupling between namespaces. Items that work closely together are placed next to each other.

In OpenCV, namespaces are used for three main reasons:

  • To modularize the libraries.
  • To hide implementation details, as with the “cv::detail” namespace. This approach makes it clear to library users that the types in this namespace are intended for internal use and should not be used directly. In C# the “internal” keyword does the job, but in C++ there’s no way to hide public types from the library user.
  • Anonymous namespace: a namespace with no name. It avoids the need for global static variables. The anonymous namespace you create is only accessible within the file you created it in.

Define the data model as POD types

Every project has its data model, and it’s recommended to define this data as POD types.

Let’s search the OpenCV code base for structs with no methods and only fields. For that, CQLinq will be used to query the code base.

opencv3

The results of this query cover 25% of the types defined in the OpenCV projects. OpenCV defines almost all of its data model as structs with only fields.

Avoid multiple inheritance

Multiple inheritance can complicate a design and make debugging more difficult; therefore, many C++ experts recommend avoiding it.

Let’s search for classes that inherit from more than one concrete base class in the OpenCV code base.

opencv4

Only a few classes from test projects use multiple inheritance; this concept is avoided throughout the entire OpenCV code base.

Avoid defining complex functions

Many metrics can be used to detect complex functions. NBLinesOfCode, the number of parameters, and the number of local variables are among the most basic.

There are other interesting metrics to detect complex functions:

  • Cyclomatic complexity is a popular procedural software metric that reflects the number of decisions that can be made within a procedure.
  • Nesting Depth is a method-level metric that represents the maximum depth of nested scopes within a method body.
  • Max Nested Loops equals the maximum level of loop nesting in a function.

The maximum value tolerated for these metrics depends mostly on the team’s choices; there are no standard values.

Let’s search for methods that could be considered complex in the OpenCV code base.

opencv5

Only 1% are candidates for refactoring to reduce their complexity.

Coupling

Low coupling is desirable because a change in one area of an application will require fewer changes throughout the entire application. In the long run, this could save a lot of time, effort, and cost associated with modifying and adding new features to an application.

Low coupling can be achieved by using abstract classes. Here are three key benefits of using abstract classes:

  • An abstract class provides a way to define a contract that promotes reusability. If an object implements an abstract class, then that object has to conform to a standard. An object that uses another object is called a consumer. An abstract class is a contract between an object and its consumer.
  • An abstract class also provides a level of abstraction that makes programs easier to understand. An abstract class allows developers to reason about the general behavior of code without having to delve into implementation details.
  • An abstract class enforces low coupling between components, which makes it easy to protect the abstract class consumer from any implementation changes in the classes implementing the abstract class.

Let’s search for all the abstract classes defined by OpenCV:

opencv6

If our primary goal is to enforce low coupling, there’s a common mistake when using abstract classes that could defeat the purpose of using them: using the concrete classes instead of the abstract ones. To illustrate this problem, let’s consider the following example:

Class A implements the abstract class IA, which contains the calculate() method; the consumer class C is implemented like this:

public class C
{
   ….
   public:
      void calculate()
      {
        …..
        m_a->calculate();
        ….
       }
       A* m_a;
 };

Instead of referencing the abstract class IA, class C references class A. In this case, we lose the benefit of low coupling. This implementation has two major drawbacks:

  • If we decide to use another implementation of IA, we must change the code of class C.
  • If some methods are added to A that don’t exist in IA, and C uses them, we also lose the contract benefit of using interfaces.

C# introduced the explicit interface implementation capability to the language to ensure that a method from IA will never be called from a reference to a concrete class, but only from a reference to the interface. This technique is very useful for preventing developers from losing the benefits of using interfaces.

Cohesion

The single responsibility principle states that a class should not have more than one reason to change. Such a class is said to be cohesive. A high LCOM value generally pinpoints a poorly cohesive class. There are several LCOM metrics. The LCOM takes its values in the range [0-1]. The LCOM HS (HS stands for Henderson-Sellers) takes its values in the range [0-2]. An LCOM HS value higher than 1 should be considered alarming. Here is how to compute the LCOM metrics:

LCOM = 1 – (sum(MF)/M*F) LCOM HS = (M – sum(MF)/F)(M-1)

Where:

  • M is the number of methods in the class (both static and instance methods are counted, it includes also constructors, properties getters/setters, events add/remove methods).
  • F is the number of instance fields in the class.
  • MF is the number of methods of the class accessing a particular instance field.
  • Sum(MF) is the sum of MF over all instance fields of the class.

The underlying idea behind these formulas can be stated as follows: a class is utterly cohesive if all its methods use all its instance fields, which means that sum(MF)=M*F, and then LCOM = 0 and LCOMHS = 0.

An LCOMHS value higher than 1 should be considered alarming.

opencv8

Only a few types are not cohesive.

Conclusion

If you take a look at the OpenCV source code, you will be surprised by the simplicity of its implementation: no advanced design concepts and no over-engineering—just a few fundamental principles applied consistently.

Share this article