This blog post going over the basic image manipulation things you can do with Open CV. [Open CV](https://opencv.org/) is an open-source library of computer vision tools. Open CV is written to be used in conjunction with deep learning frameworks like [TensorFlow](https://www.tensorflow.org/). This tutorial is going to be using Python3, although you can also use Open CV with C++, Java, and [Matlab](https://www.mathworks.com/products/matlab.html) # Reading and Displaying Images The first thing that you want to do when you start playing around with open cv is to import the dependencies required. Most basic computer vision projects with OpenCV will use NumPy and matplotlib. All images in Open CV are represented as NumPy matrices with shape (x, y, 3), with the data type uint8. This essentially means that every image is a 2d matrix with three color channels for BGR where each pixel can have an intensity between 0 and 255. Zero is black where 255 is white in grayscale. ```python # Open cv library import cv2 # numpy library for matrix manipulation import numpy as np # matplotlib for displaying the images from matplotlib import pyplot as plt ``` Reading an image is as easy as using the "cv2.imread" function. If you simply try to print the image with Python's print function, you will flood your terminal with a massive matrix. In this post, we are going to be using the infamous [Lenna](https://en.wikipedia.org/wiki/Lenna) image which has been used in the Computer Vision field since 1973. ```python lenna = cv2.imread('lenna.jpg') # Prints a single pixel value print(lenna[50][50]) # Prints the image dimensions # (width, height, 3 -- BRG) print(lenna.shape) ``` [ 89 104 220] (440, 440, 3) By now you might have noticed that I am saying "BRG" instead of "RGB"; in Open CV colors are in the order of "BRG" instead of "RGB". This makes it particularly difficult when printing the images using a different library like matplotlib because they expect images to be in the form "RGB". Thankfully for us we can use some functions in the Open CV library to convert the color scheme. ```python def printI(img): rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(rgb) printI(lenna) ``` ![png](media/cv1/output_6_0.png) Going a step further with image visualization, we can use matplotlib to view images side by side to each other. This makes it easier to make comparisons when running different algorithms on the same image. ```python def printI3(i1, i2, i3): fig = plt.figure() ax1 = fig.add_subplot(1,3,1) ax1.imshow(cv2.cvtColor(i1, cv2.COLOR_BGR2RGB)) ax2 = fig.add_subplot(1,3,2) ax2.imshow(cv2.cvtColor(i2, cv2.COLOR_BGR2RGB)) ax3 = fig.add_subplot(1,3,3) ax3.imshow(cv2.cvtColor(i3, cv2.COLOR_BGR2RGB)) def printI2(i1, i2): fig = plt.figure() ax1 = fig.add_subplot(1,2,1) ax1.imshow(cv2.cvtColor(i1, cv2.COLOR_BGR2RGB)) ax2 = fig.add_subplot(1,2,2) ax2.imshow(cv2.cvtColor(i2, cv2.COLOR_BGR2RGB)) ``` If we zero out the other colored layers and only left one channel, we can visualize each channel individually. In the following example notice that image.copy() generates a deep-copy of the image matrix -- this is a useful NumPy function. ```python def generateBlueImage(image): b = image.copy() # set the green and red channels to 0 # note images are in BGR b[:, :, 1] = 0 b[:, :, 2] = 0 return b def generateGreenImage(image): g = image.copy() # sets the blue and red channels to 0 g[:, :, 0] = 0 g[:, :, 2] = 0 return g def generateRedImage(image): r = image.copy() # sets the blue and green channels to 0 r[:, :, 0] = 0 r[:, :, 1] = 0 return r def visualizeRGB(image): printI3(generateRedImage(image), generateGreenImage(image), generateBlueImage(image)) ``` ```python visualizeRGB(lenna) ``` ![png](media/cv1/output_11_0.png) # Grayscale Images Converting a color image to grayscale reduces the dimensionality because you are squishing each color layer into one channel. Open CV has a built-in function to do this. ```python glenna = cv2.cvtColor(lenna, cv2.COLOR_BGR2GRAY) printI(glenna) ``` ![png](media/cv1/output_14_0.png) The builtin function works in most applications, however, you sometimes want more control in which color layers are weighted more in generating the grayscale image. To do that you can ```python def generateGrayScale(image, rw = 0.25, gw = 0.5, bw = 0.25): """ Image is the open cv image w = weight to apply to each color layer """ w = np.array([[[ bw, gw, rw]]]) gray2 = cv2.convertScaleAbs(np.sum(image*w, axis=2)) return gray2 ``` ```python printI(generateGrayScale(lenna)) ``` ![png](media/cv1/output_17_0.png) Notice that the sum of the weights is equal to 1 if it above 1, it would brighten the image but if it was below 1, it would darken the image. ```python printI2(generateGrayScale(lenna, 0.1, 0.3, 0.1), generateGrayScale(lenna, 0.5, 0.6, 0.5)) ``` ![png](media/cv1/output_19_0.png) We could also use our function to display the grayscale output of each color layer. ```python printI3(generateGrayScale(lenna, 1.0, 0.0, 0.0), generateGrayScale(lenna, 0.0, 1.0, 0.0), generateGrayScale(lenna, 0.0, 0.0, 1.0)) ``` ![png](media/cv1/output_21_0.png) Based on this output, the red layer is the brightest which makes sense because the majority of the image is in a pinkish/red tone. # Pixel Operations Pixel operations are simply things that you do to every pixel in the image. ## Negative To take the negative of an image, you simply invert the image. Ie: if the pixel was 0, it would now be 255, if the pixel was 0 it would now be 255. Since all the images are unsigned ints of length 8, right once, a pixel hits a boundary, it would automatically wrap over which is convenient for us. With NumPy, if you subtract a number from a matrix, it would do that for every element in that matrix -- neat. Therefore if we wanted to invert an image we could just take 255 and subtract it from the image. ```python invert_lenna = 255 - lenna printI(invert_lenna) ``` ![png](media/cv1/output_25_0.png) ## Darken And Lighten To brighten and darken an image you can add constants to the image because that would push the image closer twords 0 and 255 which is black and white. ```python bright_bad_lenna = lenna + 25 printI(bright_bad_lenna) ``` ![png](media/cv1/output_28_0.png) Notice that the image got brighter but in some parts the image got inverted. This is because when we add two images, and we don't want to wrap, we have to set a clipping threshold to be the 0 and 255. IE: when we add a constant to the image at pixel 240, we don't want it to wrap back to 0, we just want it to retain a value of 255. Open CV has built-in functions for this. ```python def brightenImg(img, num): a = np.zeros(img.shape, dtype=np.uint8) a[:] = num return cv2.add(img, a) def darkenImg(img, num): a = np.zeros(img.shape, dtype=np.uint8) a[:] = num return cv2.subtract(img, a) brighten_lenna = brightenImg(lenna, 50) darken_lenna = darkenImg(lenna, 50) printI2(brighten_lenna, darken_lenna) ``` ![png](media/cv1/output_30_0.png) ## Contrast Adjusting the contrast of an image is a matter of multiplying the image by a constant. Multiplying by a number greater than 1 would increase the contrast and multiplying by a number lower than 1 would decrease the contrast. ```python def adjustContrast(img, amount): """ changes the data type to float32 so we can adjust the contrast by more than integers, then we need to clip the values and convert data types at the end. """ a = np.zeros(img.shape, dtype=np.float32) a[:] = amount b = img.astype(float) c = np.multiply(a, b) np.clip(c, 0, 255, out=c) # clips between 0 and 255 return c.astype(np.uint8) ``` ```python printI2(adjustContrast(lenna, 0.8) ,adjustContrast(lenna, 1.3)) ``` ![png](media/cv1/output_33_0.png) # Noise I most cases you don't want to add random noise to your image, however, in some algorithms, it becomes necessary to do for testing. Noise is anything that makes the image imperfect. In the "real world" this is usually in the form of dead pixels on your camera lens or other things distorting your view. ## Salt and Pepper Salt and pepper noise is adding random black and white pixels to your image. ```python import random def uniformNoise(image, num): img = image.copy() h, w, c = img.shape x = np.random.uniform(0,w,num) y = np.random.uniform(0,h,num) for i in range(0, num): r = 0 if random.randrange(0,2) == 0 else 255 img[int(x[i])][int(y[i])] = np.asarray([r, r, r]) return img printI2(uniformNoise(lenna, 1000), uniformNoise(lenna, 7000)) ``` ![png](media/cv1/output_36_0.png) # Image Denoising It is possible to remove the salt and pepper noise from an image to clean it up. Unlike how my professor worded it, this is not "enhancing" the image, this is merely using filters that remove the noise from the image by blurring it. ## Moving Average The moving average technique sets each pixel equal to the average of its neighborhood. The bigger your neighborhood the more the image is blurred. ```python bad_lenna = uniformNoise(lenna, 6000) blur_lenna = cv2.blur(bad_lenna,(3,3)) printI2(bad_lenna, blur_lenna) ``` ![png](media/cv1/output_39_0.png) As you can see, most of the noise was removed from the image but, imperfections were left. To see the effects of the filter size, you can play around with it. ```python blur_lenna_3 = cv2.blur(bad_lenna,(3,3)) blur_lenna_8 = cv2.blur(bad_lenna,(8,8)) printI2(blur_lenna_3, blur_lenna_8) ``` ![png](media/cv1/output_41_0.png) ## Median Filtering Median filters transform every pixel by taking the median value of its neighborhood. This is a lot better than average filters for noise reduction because it has less of a blurring effect and it is extremely well at removing outliers like salt and pepper noise. ```python median_lenna = cv2.medianBlur(bad_lenna,3) printI2(bad_lenna, median_lenna) ``` ![png](media/cv1/output_43_0.png) # Remarks Open CV is a vastly powerful framework for image manipulation. This post only covered some of the more basic applications of Open CV. Future posts might explore some of the more advanced techniques in computer vision like filters, Canny edge detection, template matching, and Harris Corner detection.