OpenCV is a library built for solving a large number of computer vision tasks. It is packed with lots of basic and advanced features, very easy to pickup and available for several programming languages.

In this article we are going to apply some basic image transformation techniques in order to obtain image filters. For those of you accustomed with all the image editing software out there, these filters may seem very basic to you. But I think they are great for a first plunge into OpenCV because they allow us to learn some basic principles without writing tons of code. 😀

White cup and MacBook
Photo by Alex Knight / Unsplash

Introduction

In this section I am going to explain the logic behind how RGB images are stored and manipulated by computers. If you are already familiar with this stuff, please skip to the next section where we are going to jump to more advanced details.

The basic unit data in an image is the pixel. A pixel is just a single point in the image and is stored as a number withing the range of [0, 256]. The RGB model stands for Red-Green-Blue and tells us that for every pixel we store the intensity of red, green and blue(we will call these channels) as a number from 0 to 256. The pixel is white if all 3 channels are 256 and black if all 3 channels are 0. A pixel is fully red/green/blue if the respective channel is 256 and all other channels are 0. You get the idea, every color will be represented as a mix of these 3 channels.

So an image is a collection of pixels. If, let's say, our image is 300x200, then we will store it as a 2D array of 300 lines and 200 columns where every cell is a pixel. But we know from above that for every pixel we will store the information about all 3 channels, so that gives us actually a 3D array.

OpenCV Convolutions and Kernels

This is the last section which consists entirely of theory. If you want to jump straight to the practical part, please see next section.

Now that we know how images are stored as pixels, we can learn to apply transformations on those pixels.

A convolution is the operation of transforming and image by splitting it into small parts called windows and applying an operator called kernel for every part.

The kernel is usually a fixed, small size 2D array containing numbers. An important part in the kernel is the center which is called anchor point.

OpenCV Filters - Kernel example
OpenCV Filters - Kernel example

A convolution is performed by following these steps:

  1. Place the kernel on top of an image, with the kernel anchor point on top of a predetermined pixel.
  2. Perform a multiplication between the kernel numbers and the pixel values overlapped by the kernel, sum the multiplication results and place the result on the pixel that is below the anchor point.
  3. Repeat the process by sliding the kernel on top of the image for every possible position on the image.

If you are wondering how to choose values for the kernels, please note that the most popular kernels are results of lots of research from image processing scientists. You can of course try to choose your own kernels, but for the most basic transformations we already have good kernels which offer us great results.

Project setup

We need to install 2 python packages and then we are good to go.

pip3 install opencv-python
pip3 install scipy

We are going to import a single image(it's an image I personally took) and for every transformation we are going to make a copy of that image. Then we apply the transformation thorugh a separate method so that we can keep our code clean. At the end we are going to save the result as a separate image. Here's how the basic flow looks:

    initialImage = cv2.imread("image1.jpg")
    blurredImage = gaussianBlur(copy.deepcopy(initialImage))
    cv2.imwrite("blurred.jpg", blurredImage)

OpenCV Kernel and Convolution Transformations

This is the section where we are going to apply convolutions with a set of predefined kernels to obtain beautiful effects. For other types of transformations, please skip to the next section. For every transformation I'm going to show you the kernel and the code and at then end of this section I'm going to display a gallery with the initial image and the results.

Image sharpening

Python OpenCV Filters - image sharpening
Python OpenCV Filters - image sharpening

This is the kernel used to sharpen the details on a picture. We are going to use the filter2D method from OpenCV library which will perform the convolution for us.

def sharpen(image):
    kernel = np.array([[-1, -1, -1], [-1, 9, -1], [-1, -1, -1]])
    return cv2.filter2D(image, -1, kernel)
Image Sharpening Kernel and Convolution

Sepia effect

Python OpenCV Filters - Sepia
Python OpenCV Filters - Sepia
def sepia(image):
    kernel = np.array([[0.272, 0.534, 0.131],
                       [0.349, 0.686, 0.168],
                       [0.393, 0.769, 0.189]])
    return cv2.filter2D(image, -1, kernel)

Blurring effect

For this effect we can use a basic kernel like all of the above, but the results are pretty lame. Luckly, OpenCV has a gaussian blur implemented which will do the job for us. All we need to do is:

def gaussianBlur(image):
    return cv2.GaussianBlur(image, (35, 35), 0)

Emboss effect

Python OpenCV Filters - Emboss
Python OpenCV Filters - Emboss
def emboss(image):
    kernel = np.array([[0,-1,-1],
                            [1,0,-1],
                            [1,1,0]])
    return cv2.filter2D(image, -1, kernel)

And here is a gallery of our kernel transformation results.

OpenCV - transformations based on pixel values

Next up we are going to increase the brightness of an image. All we need to do for this is navigate to every pixel of the image and then to every channel of a specific pixel. Then we are going to increase the value for every chanel by a specific value. This is going to give us a nice brightness effect.

To spare us from all this code, the OpenCV framework has a method implemented that can do exactly this.

def brightnessControl(image, level):
    return cv2.convertScaleAbs(image, beta=level)

Here's the result of our brightness control method.

OpenCV - lookup table transformations

We are going to use this type of transformations to make an image appear warmer and colder, but first let's see how we can use lookup tables for that.

A lookup table is just a simple collection of value pairs looking like this: [value1, value2, ...., valueN], [modifiedValue1, modifiedValue2, ..., modifiedValueN] and the logic behind this is simple - we are going to take every pixel value and replace it with the corresponding value from the lookup table(meaning replace value1 with modifiedValue1 and so on).

Now the problem with this is that there are lots of values to be replaced(from 0 to 256) and creating a lookup table for this is a painful process. But luck is on our side again, because there's a tool we can use for that.

The UnivariateSpline smoothing method from the scipy package is here to help us. This method only takes a few reference values and tries to find a method to modify all the other values in the range with respect to the reference values we have provided.

Basically, we are going to define a short lookup table like this

[0, 64, 128, 256], [0, 80, 160, 256]

and apply the UnivariateSpline transformation which will fill up the rest of the values in the [0, 256] range.

Having our lookup tables built, we only need to apply them to the specific channels. For obtaining a warm image, we are going to increase the values of the red channel and decrease the values of the blue channel for all the pixels in the image. For obtaining a cold image, we are going to do the opposite: increase the values for the blue channel and decrease the values for the red channel. The green channel removes untouched in both cases.

def spreadLookupTable(x, y):
  spline = UnivariateSpline(x, y)
  return spline(range(256))

def warmImage(image):
    increaseLookupTable = spreadLookupTable([0, 64, 128, 256], [0, 80, 160, 256])
    decreaseLookupTable = spreadLookupTable([0, 64, 128, 256], [0, 50, 100, 256])
    red_channel, green_channel, blue_channel = cv2.split(image)
    red_channel = cv2.LUT(red_channel, increaseLookupTable).astype(np.uint8)
    blue_channel = cv2.LUT(blue_channel, decreaseLookupTable).astype(np.uint8)
    return cv2.merge((red_channel, green_channel, blue_channel))

def coldImage(image):
    increaseLookupTable = spreadLookupTable([0, 64, 128, 256], [0, 80, 160, 256])
    decreaseLookupTable = spreadLookupTable([0, 64, 128, 256], [0, 50, 100, 256])
    red_channel, green_channel, blue_channel = cv2.split(image)
    red_channel = cv2.LUT(red_channel, decreaseLookupTable).astype(np.uint8)
    blue_channel = cv2.LUT(blue_channel, increaseLookupTable).astype(np.uint8)
    return cv2.merge((red_channel, green_channel, blue_channel))

And the results here are as expected.

This was fun! We saw a bit of math here, a bit of code and learned a lot about how images are stored and manipulated by computers and how we can use this to obtain beautiful transformations for images. If you enjoyed this article and want to learn more, then make sure you follow me on Twitter because soon I'll post another article about building an Instagram-like mask using OpenCV.

Thank you so much for reading this. Interested in more stories like this? Follow me on Twitter at @b_dmarius and I'll post there every new article.