Blog 8 min read

OpenCV: The Art of Using the KISS and YAGNI Principles

Share this article
OpenCV: The Art of Using the KISS and YAGNI Principles

As programmers, we're often tempted to leverage design patterns, language idioms, advanced language features, and well-known libraries, which is certainly advisable. However, it’s essential to view these techniques through the lens of the KISS and YAGNI principles before diving in.

"KISS" stands for "Keep It Simple, Stupid". It is a design principle that states that simplicity should be a key goal and that unnecessary complexity should be avoided. The idea is that simple solutions are easier to understand, maintain, and troubleshoot. The KISS principle is widely applied in various fields, including engineering, software development, user interface design, and project management.

YAGNI stands for "You Ain't Gonna Need It." It's a principle in software development and agile methodologies that suggests developers should not add functionality or features to their codebase until those features are actually needed to solve a specific problem or fulfill a requirement.

The YAGNI principle is based on the idea that adding unnecessary features or functionality prematurely can lead to several potential issues.

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

OpenCV is a vast project encompassing numerous intricate features. Nonetheless, OpenCV’s developers have embraced fundamental principles that have made the codebase significantly easier to understand and maintain.

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

Modularity

1 - Library-based architecture

A library-based architecture makes the provided functionality easier to reuse and integrate into other projects. In addition, a library-based architecture encourages clean APIs and separation of concerns, making the system easier to understand because developers can focus on small parts of the bigger picture.

OpenCV adopts this approach and defines many libraries; each one has a specific responsibility, and all of them use the opencv_core library.

opencv1

2 - Modularize by namespaces

OpenCV employs namespaces extensively to organize its codebase effectively. Here are some examples of namespaces used within 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 the case of OpenCV, namespaces are used for three main reasons:

  • Modularize the libraries.
  • Hide details, like for the “cv::detail” namespace. This approach could be very interesting if we want to inform the library user that he doesn’t need to directly use types inside this namespace, as it’s only for internal use. 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 making global static variables. The “anonymous” namespace you have created will only be accessible within the file you created it in.

Defining the data model as POD types

Each project has its data model, and we can define this model using the plain old data (POD) types. POD type is a data structure that is represented only as passive collections of field values (instance variables), without using object-oriented features. The advantages of using Plain Old Data (POD) types in programming are numerous and include:

  1. Efficiency: POD types typically have a simple memory layout, which often leads to more efficient memory usage and faster performance. They avoid the overhead associated with complex data structures and member functions.
  2. Compatibility: POD types are compatible with low-level programming constructs and data interchange formats, making them suitable for interfacing with external systems and languages.
  3. Interoperability: POD types can be easily passed between different modules or components of a system, as well as between different systems or programming languages, facilitating interoperability.
  4. Ease of Use: POD types are straightforward to work with and understand, as they typically represent basic data types or aggregates of such types.
  5. Performance: POD types often lead to better performance in terms of both execution speed and memory usage compared to more complex data types.
  6. Predictability: Since POD types have a simple and well-defined structure, their behavior is generally more predictable, which can make debugging and optimization easier.

Overall, using POD types can contribute to simpler, more efficient, and more maintainable code, especially in performance-critical or resource-constrained environments.

Let’s search the OpenCV codebase for structs that have no methods and contain only fields.

opencv3

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

Avoid multiple inheritance

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

Let’s find the classes in the OpenCV codebase that inherit from more than one concrete base class.

opencv4

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

Avoid defining complex functions

Many metrics exist to detect complex functions; NBLinesOfCode, number of parameters, and number of local variables are the basic ones.

There are other interesting metrics to detect complex functions:

  • Cyclomatic complexity is a popular procedural software metric equal to the number of decisions that can be taken in a procedure.
  • Nesting Depth is a metric defined on methods that is relative to the maximum depth of the most nested scope in a method body.
  • Max Nested loop equals the maximum level of loop nesting in a function.

The maximum acceptable values for these metrics depend on the team’s choices; there are no universal thresholds.

Let’s search for methods that could be considered as 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 can save considerable time, effort, and cost when modifying an application or adding new features.

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

  • An abstract class provides a way to define a contract that promotes reuse. 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 start talking about the general way that code behaves without having to get into a lot of detailed specifics.
  • 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 classes.

Let’s search for all 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 kill the utility of using them: using concrete classes instead of abstract ones. To better explain this problem, let’s take the following example:

The 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;
 };

The class C, instead of referencing the abstract class IA, references the class A. In this case, we lose the low coupling benefit. 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 to protect developers from losing the benefit 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]. A LCOM HS value higher than 1 should be considered alarming. Here is how to compute LCOM metrics:

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

Where:

  • M is the number of methods in 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 overall 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 LCOM HS 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: there are no unnecessarily advanced design concepts and no over-engineering—just sound basic principles applied consistently.

Share this article