Welcome to this brand new OpenCV series. In this series, we’ll discuss various features and possibilities we can achieve with OpenCV.

OpenCV is a BSD-licensed open-source computer vision library that’s available at opencv.org. You can either download the latest committed source code from their GitHub repository and build it from source or download the pre-built library from your favorite package manager. Further instructions can be found here for Linux and here for Windows.

In this series, we’ll be using OpenCV 4.2.X (currently at 4.2.0 but that may change in the future as the library keeps evolving) and the C++ programming language.

The basics: loading an image

Let’s start from the most basic building block of most of OpenCV applications: actually loading an image from disk. In order to do that, we’ll bring in the Mat class available in the cv namespace.

#include <opencv2/opencv.hpp>
...
using namespace cv;
...
Mat image;
image = imread("image.jpg", IMREAD_GRAYSCALE);
if (!image.data)
{
    cout << "[ERROR] Could not open file image.png" << endl;
    return -1;
}

The method imread will take two parameters: the image path and a flag value. In our case, we’re using the IMREAD_GRAYSCALE flag which indicates OpenCV should open our image in grayscale. If imread fails for some reason, it’ll return an empty matrix, so we do that check right after.

Now that we have our image in memory, we’re ready to process it.

Going deeper: accessing pixels

Pixels are the most basic units of our image. In our specific case, we loaded ours in grayscale, which means every pixel will have a value from 0 to 255 indicating how gray a pixel is. Had we loaded it in RGB color mode, each pixel would be a vector of three components: a red value, a green value, and a blue value.

For us to access a pixel, we’ll need to use the Mat structure we just populated and retrieve the pixel by its row i and column j. We’ll also cast the result to uchar, which is a big enough type to store the shade of gray.

image.at<uchar>(i, j)

The at method can be used to either read the pixel value or set it.

Playing around with pictures

Now let’s put our knowledge into practice by building two OpenCV applications. The first one will take the negative of a region in the picture input by the user, and the other will swap the four regions of a square picture.

Application #1: Extracting the Negative

A negative pixel, mathematically, means that we’ll subtract whatever value that pixel has from 255. The resulting image should have lighter areas appearing darker and darker areas appearing lighter. Let’s build the code.

First of all, we need to read from the standard input the starting and ending points of the region. We’ll read two points, one being the top-left-most point of the rectangular negative region and the other being the bottom-right-most point. We also need to make sure these points are inside of the image region but we’ll take for granted that the first point input will be smaller than the second for simplicity’s sake.

Point2i start;
Point2i end;

cout << "Enter the starting point (x, y): ";
cin >> start.x >> start.y;
cout << "Enter the ending point (x, y): ";
cin >> end.x >> end.y;

// Verify whether negative section is within the image bounds.
if (start.x < 0 || start.x > image.cols || end.x < 0 || end.x > image.cols ||
    start.y < 0 || start.y > image.rows || end.y < 0 || end.y > image.rows)
{
    cout << "[ERROR] Point chosen is out of bounds." << endl;
    return -1;
}

As you can see, the user input is being saved into a structure called Point2i. Don’t let the naming startle you. It simply means it’s a Point structure with 2 coordinates, x and y, of type int.

Now, we need to iterate every pixel of the image within the bounds set by the two points and calculate the new pixel value.

for (int i = start.y; i < end.y; i++)
{
    for (int j = start.x; j < end.x; j++)
    {
        image.at<uchar>(i, j) = 255 - image.at<uchar>(i, j);
    }
}

And now we’re done processing. Let’s take the resulting image and finally display it on the screen.

namedWindow("Negative", WINDOW_AUTOSIZE);
imshow("Negative", image);
waitKey();

When I run the program and input a random region to become negative, this is the result I see.

Figure 01: Image negative in the region (100, 100) through (400, 600).
Figure 01: Image negative in the region (100, 100) through (400, 600).

Now, let’s head into our second piece of software.

Application #2: Swapping Regions

Now let’s take into account we have a square picture and we want to subdivide it into four smaller perfectly square regions A, B, C, and D. Our software will simply swap regions D and A, and C and B. Let’s see how we can take this image and do the region swapping.

Figure 02: The image which we'll swap regions in.
Figure 02: The image which we’ll swap regions in.
Figure 03: Subdivisions used for the region swapping.
Figure 03: Subdivisions used for the region swapping.

First of all, after loading our image into a Mat, we’ll calculate the middle x and y points of our picture.

int xHalf = image.cols / 2;
int yHalf = image.rows / 2;

Now, from the Mat containing the original picture, we’ll build four Mats that will contain the four subregions of our image. We’ll explore now another constructor available in the Mat class, which takes in as parameters another Mat and two Ranges which represent the region that will be extracted from the original image.

Mat A(image, Range(0, yHalf), Range(0, xHalf));
Mat B(image, Range(0, yHalf), Range(xHalf, image.cols));
Mat C(image, Range(yHalf, image.rows), Range(0, xHalf));
Mat D(image, Range(yHalf, image.rows), Range(xHalf, image.cols));

So region A, for example, will read from our original image and extract the whole region ranging from 0 to yHalf on the y axis and from 0 to xHalf on the x axis. Region B, however, will range from 0 to yHalf on the y axis and from xHalf to image.cols on the x axis.

The variables image.cols and image.rows will return the number of columns and rows, respectively, the image has.

Now, we need to find a way to concatenate these regions into a single matrix that will represent our picture put back together with the regions swapped out. For that end, we’ll use the hconcat and vcontact methods, which, as the naming suggests, concatenates matrices vertically and horizontally.

Mat result1;
vconcat(D, B, result1);

Mat result2;
vconcat(C, A, result2);

Mat result;
hconcat(result1, result2, result);

Finally, let’s display the result on the screen.

namedWindow("Swap Regions", WINDOW_AUTOSIZE);
imshow("Swap Regions", result);
waitKey();

And we’re greeted with this rather peculiar picture:

Figure 04: A square image with its four square subregions swapped out.
Figure 04: A square image with its four square subregions swapped out.

And that’s it, folks! I hope you all learned as much as I did and stay tuned for what’s coming on the OpenCV Series.