Title: Convolutions with OpenCV and Python - PyImageSearch

URL Source: https://pyimagesearch.com/2016/07/25/convolutions-with-opencv-and-python/

Published Time: 2016-07-25T14:00:06+00:00

Markdown Content:
I’m going to start today’s blog post by asking a series of questions which will then be addressed later in the tutorial:

*   _What are image convolutions?_
*   _What_ do they do?
*   _Why_ do we use them?
*   _How do we apply them?_
*   **And what role do convolutions play in deep learning?**

![Image 1: Figure 12: Finding horizontal gradients in an image using the Sobel-y operator and convolutions.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20800%20315'%3E%3C/svg%3E)

The word “convolution” sounds like a fancy, complicated term — but it’s really not. In fact, if you’ve ever worked with computer vision, image processing, or OpenCV before, you’ve _already_ applied convolutions, _**whether you realize it or not!**_

Ever apply _blurring_ or _smoothing?_ Yep, that’s a convolution.

What about _edge detection?_ Yup, convolution.

Have you opened Photoshop or GIMP to _sharpen an image?_ You guessed it — convolution.

Convolutions are one of the most _critical, fundamental building-blocks_ in computer vision and image processing. But the term itself tends to scare people off — in fact, on the the surface, the word even appears to have a negative connotation.

_Trust me, convolutions are anything but scary_. They’re actually quite easy to understand.

In reality, an (image) _**convolution is simply an element-wise multiplication of two matrices followed by a sum.**_

Seriously. That’s it. _You just learned what convolution is:_

1.  Take two matrices (which both have the same dimensions).
2.  Multiply them, element-by-element (i.e., _not_ the dot-product, just a simple multiplication).
3.  Sum the elements together.

To understand more about convolutions, why we use them, how to apply them, and the _**overall role they play in deep learning + image classification**_, be sure to keep reading this post.

![Image 2](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20128%20128'%3E%3C/svg%3E)

#### Looking for the source code to this post?

[Jump Right To The Downloads Section](#download-the-code)

Think of it this way — an image is just a _multi-dimensional matrix_. Our image has a width (# of columns) and a height (# of rows), just like a matrix.

But unlike the traditional matrices you may have worked with back in grade school, images also have a _depth_ to them — the number of _channels_ in the image. For a standard RGB image, we have a depth of _3_ — one channel for _each_ of the Red, Green, and Blue channels, respectively.

Given this knowledge, we can think of an image as a _**big matrix**_ and _kernel_ or _convolutional matrix_ as a **_tiny matrix_** that is used for blurring, sharpening, edge detection, and other image processing functions.

Essentially, this _tiny_ kernel sits on top of the _big_ image and slides from left-to-right and top-to-bottom, applying a mathematical operation (i.e., a _convolution_) at each _(x, y)_\-coordinate of the original image.

It’s normal to hand-define kernels to obtain various image processing functions. In fact, you might already be familiar with blurring (average smoothing, Gaussian smoothing, median smoothing, etc.), edge detection (Laplacian, Sobel, Scharr, Prewitt, etc.), and sharpening — _all_ of these operations are forms of hand-defined kernels that are _specifically designed_ to perform a particular function.

So that raises the question, _is there a way to **automatically** learn these types of filters?_ And even use these filters for _image classification_ and _object detection?_

**You bet there is.**

But before we get there, we need to understand kernels and convolutions a bit more.

### Kernels

Again, let’s think of an image as a _**big matrix**_ and a kernel as **_tiny matrix_** (at least in respect to the original “big matrix” image):

![Image 3: Figure 1: A kernel is a small matrix that slides from left-to-right and top-to-bottom across a larger image. At each pixel in the input image, the neighborhood of the image is convolved with the kernel and the output stored. Source, PyImageSearch Gurus](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20706%20706'%3E%3C/svg%3E)

**Figure 1:** A kernel is a small matrix that slides from left-to-right and top-to-bottom across a larger image. At each pixel in the input image, the neighborhood of the image is convolved with the kernel and the output stored. [Source: PyImageSearch Gurus](https://pyimagesearch.com/pyimagesearch-gurus/?src=post-convolutions)

As the figure above demonstrates, we are sliding the kernel from left-to-right and top-to-bottom along the original image.

At each _(x, y)_\-coordinate of the original image, we stop and examine the neighborhood of pixels located at the _**center**_ of the image kernel. We then take this neighborhood of pixels, _convolve_ them with the kernel, and obtain a single output value. This output value is then stored in the output image at the same _(x, y)_\-coordinates as the center of the kernel.

If this sounds confusing, no worries, we’ll be reviewing an example in the _“Understanding Image Convolutions”_ section later in this blog post.

But before we dive into an example, let’s first take a look at what a kernel looks like:

Above we have defined a square _3 x 3_ kernel (any guesses on what this kernel is used for?)

Kernels can be an arbitrary size of _M x N_ pixels, provided that **both** _M_ and _N_ are _**odd integers.**_

_**Note:** Most kernels you’ll typically see are actually square N x N matrices._

We use an _odd_ kernel size to ensure there is a valid integer _(x, y)_\-coordinate at the center of the image:

![Image 4: Figure 3: A 3 x 3 kernel with a valid integer center (x, y)-coordinate (left). A 2 x 2 kernel without a valid integer (x, y)-center. Source, PyImageSearch Gurus](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20644%20364'%3E%3C/svg%3E)

**Figure 3:** A _3 x 3_ kernel with a valid integer center _(x, y)_\-coordinate _(left)_. A _2 x 2_ kernel without a valid integer _(x, y)_\-center _(right)_. [Source: PyImageSearch Gurus](https://pyimagesearch.com/pyimagesearch-gurus/?src=post-convolutions)

On the _left_, we have a _3 x 3_ matrix. The center of the matrix is obviously located at _x=1, y=1_ where the top-left corner of the matrix is used as the origin and our coordinates are zero-indexed.

But on the _right_, we have a _2 x 2_ matrix. The center of this matrix would be located at _x=0.5, y=0.5_. But as we know, without applying interpolation, there is no such thing as pixel location _(0.5, 0.5)_ — our pixel coordinates must be integers! This reasoning is exactly why we use _odd_ kernel sizes — to always ensure there is a valid _(x, y)_\-coordinate at the center of the kernel.

### Understanding Image Convolutions

Now that we have discussed the basics of kernels, let’s talk about a mathematical term called _convolution._

In image processing, a convolution requires three components:

1.  An input image.
2.  A kernel matrix that we are going to apply to the input image.
3.  An output image to store the output of the input image convolved with the kernel.

Convolution itself is actually very easy. All we need to do is:

1.  Select an _(x, y)_\-coordinate from the original image.
2.  Place the **center** of the kernel at this _(x, y)_\-coordinate.
3.  Take the element-wise multiplication of the input image region and the kernel, then sum up the values of these multiplication operations into a single value. The sum of these multiplications is called the **kernel output**.
4.  Use the same _(x, y)_\-coordinates from **Step #1**, but this time, store the kernel output in the same _(x, y)_\-location as the output image.

Below you can find an example of convolving (denoted mathematically as the _“\*”_ operator) a _3 x 3_ region of an image with a _3 x 3_ kernel used for blurring:

Therefore,

After applying this convolution, we would set the pixel located at the coordinate _(i, j)_ of the output image _O_ to _O\_i,j = 126._

That’s all there is to it!

Convolution is simply the sum of element-wise matrix multiplication between the kernel and neighborhood that the kernel covers of the input image.

### Implementing Convolutions with OpenCV and Python

That was fun discussing kernels and convolutions — but now let’s move on to looking at some actual code to ensure you _understand_ how kernels and convolutions are implemented. This source code will also help you understand how to _apply convolutions to images._

Open up a new file, name it `convolutions.py` , and let’s get to work:

\# import the necessary packages
from skimage.exposure import rescale\_intensity
import numpy as np
import argparse
import cv2

We start on **Lines 2-5** by importing our required Python packages. You should already have NumPy and OpenCV installed on your system, but you might not have [scikit-image](http://scikit-image.org/) installed. To install scikit-image, just use `pip` :

$ pip install -U scikit-image

Next, we can start defining our custom `convolve` method:

def convolve(image, kernel):
	# grab the spatial dimensions of the image, along with
	# the spatial dimensions of the kernel
	(iH, iW) = image.shape\[:2\]
	(kH, kW) = kernel.shape\[:2\]

	# allocate memory for the output image, taking care to
	# "pad" the borders of the input image so the spatial
	# size (i.e., width and height) are not reduced
	pad = (kW - 1) // 2
	image = cv2.copyMakeBorder(image, pad, pad, pad, pad,
		cv2.BORDER\_REPLICATE)
	output = np.zeros((iH, iW), dtype="float32")

The `convolve` function requires two parameters: the (grayscale) `image` that we want to convolve with the `kernel` .

Given both our `image` and `kernel` (which we presume to be NumPy arrays), we then determine the spatial dimensions (i.e., width and height) of each (**Lines 10 and 11**).

Before we continue, it’s important to understand that the process of “sliding” a convolutional matrix across an image, applying the convolution, and then storing the output will actually _decrease_ the spatial dimensions of our output image.

Why is this?

Recall that we “center” our computation around the center _(x, y)_\-coordinate of the input image that the kernel is currently positioned over. _This implies there is no such thing as “center” pixels for pixels that fall along the border of the image._ The decrease in spatial dimension is simply a side effect of applying convolutions to images. Sometimes this effect is desirable and other times its not, it simply depends on your application.

However, in most cases, we want our _output image_ to have the _same dimensions_ as our _input image_. To ensure this, we apply _**padding**_ (**Lines 16-19**). Here we are simply replicating the pixels along the border of the image, such that the output image will match the dimensions of the input image.

Other padding methods exist, including _zero padding_ (filling the borders with zeros — very common when building Convolutional Neural Networks) and _wrap around_ (where the border pixels are determined by examining the opposite end of the image). In most cases, you’ll see either replicate or zero padding.

We are now ready to apply the actual convolution to our image:

	# loop over the input image, "sliding" the kernel across
	# each (x, y)-coordinate from left-to-right and top to
	# bottom
	for y in np.arange(pad, iH + pad):
		for x in np.arange(pad, iW + pad):
			# extract the ROI of the image by extracting the
			# \*center\* region of the current (x, y)-coordinates
			# dimensions
			roi = image\[y - pad:y + pad + 1, x - pad:x + pad + 1\]

			# perform the actual convolution by taking the
			# element-wise multiplicate between the ROI and
			# the kernel, then summing the matrix
			k = (roi \* kernel).sum()

			# store the convolved value in the output (x,y)-
			# coordinate of the output image
			output\[y - pad, x - pad\] = k

**Lines 24 and 25** loop over our `image` , “sliding” the kernel from left-to-right and top-to-bottom 1 pixel at a time.

**Line 29** extracts the Region of Interest (ROI) from the `image` using NumPy array slicing. The `roi` will be centered around the current _(x, y)_\-coordinates of the `image` . The `roi` will also have the same size as our `kernel` , which is critical for the next step.

Convolution is performed on **Line 34** by taking the element-wise multiplication between the `roi` and `kernel` , followed by summing the entries in the matrix.

The output value `k` is then stored in the `output` array at the same _(x, y)_\-coordinates (relative to the input image).

We can now finish up our `convolve` method:

	# rescale the output image to be in the range \[0, 255\]
	output = rescale\_intensity(output, in\_range=(0, 255))
	output = (output \* 255).astype("uint8")

	# return the output image
	return output

When working with images, we typically deal with pixel values falling in the range _\[0, 255\]_. However, when applying convolutions, we can easily obtain values that fall _outside_ this range.

In order to bring our `output` image back into the range _\[0, 255\]_, we apply the `rescale_intensity` function of scikit-image (**Line 41**). We also convert our image back to an unsigned 8-bit integer data type on **Line 42** (previously, the `output` image was a floating point type in order to handle pixel values outside the range _\[0, 255\]_).

Finally, the `output` image is returned to the calling function on **Line 45**.

Now that we’ve defined our `convolve` function, let’s move on to the driver portion of the script. This section of our program will handle parsing command line arguments, defining a series of kernels we are going to apply to our image, and then displaying the output results:

\# construct the argument parse and parse the arguments
ap = argparse.ArgumentParser()
ap.add\_argument("-i", "--image", required=True,
	help="path to the input image")
args = vars(ap.parse\_args())

# construct average blurring kernels used to smooth an image
smallBlur = np.ones((7, 7), dtype="float") \* (1.0 / (7 \* 7))
largeBlur = np.ones((21, 21), dtype="float") \* (1.0 / (21 \* 21))

# construct a sharpening filter
sharpen = np.array((
	\[0, -1, 0\],
	\[-1, 5, -1\],
	\[0, -1, 0\]), dtype="int")

**Lines 48-51** handle parsing our command line arguments. We only need a single argument here, `--image` , which is the path to our input path.

We then move on to **Lines 54 and 55** which define a _7 x 7_ kernel and a _21 x 21_ kernel used to blur/smooth an image. The larger the kernel is, the more the image will be blurred. Examining this kernel, you can see that the output of applying the kernel to an ROI will simply be the _average_ of the input region.

We define a _sharpening kernel_ on **Lines 58-61**, used to enhance line structures and other details of an image. Explaining each of these kernels in detail is outside the scope of this tutorial, so if you’re interested in learning more about kernel construction, I would suggest [starting here](https://en.wikipedia.org/wiki/Kernel_\(image_processing\)) and then playing around with the [excellent kernel visualization tool on Setosa.io](http://setosa.io/ev/image-kernels/).

Let’s define a few more kernels:

\# construct the Laplacian kernel used to detect edge-like
# regions of an image
laplacian = np.array((
	\[0, 1, 0\],
	\[1, -4, 1\],
	\[0, 1, 0\]), dtype="int")

# construct the Sobel x-axis kernel
sobelX = np.array((
	\[-1, 0, 1\],
	\[-2, 0, 2\],
	\[-1, 0, 1\]), dtype="int")

# construct the Sobel y-axis kernel
sobelY = np.array((
	\[-1, -2, -1\],
	\[0, 0, 0\],
	\[1, 2, 1\]), dtype="int")

**Lines 65-68** define a [Laplacian operator](http://docs.opencv.org/2.4/doc/tutorials/imgproc/imgtrans/laplace_operator/laplace_operator.html) that can be used as a form of edge detection.

_**Note:** The Laplacian is also very useful for [detecting blur in images](https://pyimagesearch.com/2015/09/07/blur-detection-with-opencv/)._

Finally, we’ll define two [Sobel filters](https://en.wikipedia.org/wiki/Sobel_operator) on **Lines 71-80**. The first (**Lines 71-74**) is used to detect _vertical changes_ in the gradient of the image. Similarly, **Lines 77-80** constructs a filter used to detect _horizontal changes_ in the gradient.

Given all these kernels, we lump them together into a set of tuples called a “kernel bank”:

\# construct the kernel bank, a list of kernels we're going
# to apply using both our custom \`convole\` function and
# OpenCV's \`filter2D\` function
kernelBank = (
	("small\_blur", smallBlur),
	("large\_blur", largeBlur),
	("sharpen", sharpen),
	("laplacian", laplacian),
	("sobel\_x", sobelX),
	("sobel\_y", sobelY)
)

Finally, we are ready to apply our `kernelBank` to our `--input` image:

\# load the input image and convert it to grayscale
image = cv2.imread(args\["image"\])
gray = cv2.cvtColor(image, cv2.COLOR\_BGR2GRAY)

# loop over the kernels
for (kernelName, kernel) in kernelBank:
	# apply the kernel to the grayscale image using both
	# our custom \`convole\` function and OpenCV's \`filter2D\`
	# function
	print("\[INFO\] applying {} kernel".format(kernelName))
	convoleOutput = convolve(gray, kernel)
	opencvOutput = cv2.filter2D(gray, -1, kernel)

	# show the output images
	cv2.imshow("original", gray)
	cv2.imshow("{} - convole".format(kernelName), convoleOutput)
	cv2.imshow("{} - opencv".format(kernelName), opencvOutput)
	cv2.waitKey(0)
	cv2.destroyAllWindows()

**Lines 95 and 96** load our image from disk and convert it to grayscale. Convolution operators can certainly be applied to RGB (or other multi-channel images), but for the sake of simplicity in this blog post, we’ll only apply our filters to grayscale images).

We start looping over our set of kernels in the `kernelBank` on **Line 99** and then apply the current `kernel` to the `gray` image on **Line 104** by calling our custom `convolve` method which we defined earlier.

As a sanity check, we also call `cv2.filter2D` which also applies our `kernel` to the `gray` image. The `cv2.filter2D` function is a _much_ more optimized version of our `convolve` function. The main reason I included the implementation of `convolve` in this blog post is to give you a better understanding of how convolutions work under the hood.

Finally, **Lines 108-112** display the output images to our screen.

### Example Convolutions with OpenCV and Python

Today’s example image comes from a photo I took a few weeks ago at my favorite bar in South Norwalk, CT — [Cask Republic](http://caskrepublic.com/). In this image you’ll see a glass of my favorite beer (Smuttynose Findest Kind IPA) along with three 3D-printed Pokemon from the (unfortunately, now closed) Industrial Chimp shop:

![Image 5: Figure 6: The example image we are going to apply our convolutions to.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20450'%3E%3C/svg%3E)

**Figure 6:** The example image we are going to apply our convolutions to.

To run our script, just issue the following command:

$ python convolutions.py --image 3d\_pokemon.png

You’ll then see the results of applying our `smallBlur` kernel to the input image:

![Image 6: Figure 7: Applying a small blur convolution with our "convolve" function and then validating it against the results of OpenCV's "cv2.filter2D" function.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20160'%3E%3C/svg%3E)

**Figure 7:** Applying a small blur convolution with our _“convolve”_ function and then validating it against the results of OpenCV’s _“cv2.filter2D”_ function.

On the _left,_ we have our original image. Then in the _center_ we have the results from the `convolve` function. And on the _right_, the results from `cv2.filter2D` . As the results demonstrate, our output matches `cv2.filter2D` , indicating that our `convolve` function is working properly. Furthermore, our original image now appears “blurred” and “smoothed”, thanks to the smoothing kernel.

Next, let’s apply a larger blur:

![Image 7: Figure 8: As we convolve our image with a larger smoothing kernel, our image becomes more blurred.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20224'%3E%3C/svg%3E)

**Figure 8:** As we convolve our image with a larger smoothing kernel, our image becomes more blurred.

Comparing **Figure 7** and **Figure 8**, notice how as the size of the averaging kernel _increases_, the amount of blur in the output image _increases_ as well.

We can also sharpen our image:

![Image 8: Figure 9: Using a sharpening kernel enhances edge-like structures and other details in our image.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20225'%3E%3C/svg%3E)

**Figure 9:** Using a sharpening kernel enhances edge-like structures and other details in our image.

Let’s compute edges using the Laplacian operator:

![Image 9: Figure 10: Applying the Laplacian operator via convolution with OpenCV and Python.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20229'%3E%3C/svg%3E)

**Figure 10:** Applying the Laplacian operator via convolution with OpenCV and Python.

Find vertical edges with the Sobel operator:

![Image 10: Figure 11: Utilizing the Sobel-x kernel to find vertical images.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20230'%3E%3C/svg%3E)

**Figure 11:** Utilizing the Sobel-x kernel to find vertical images.

And find horizontal edges using Sobel as well:

![Image 11: Figure 12: Finding horizontal gradients in an image using the Sobel-y operator and convolutions.](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20600%20236'%3E%3C/svg%3E)

**Figure 12:** Finding horizontal gradients in an image using the Sobel-y operator and convolutions.

### The Role of Convolutions in Deep Learning

As you’ve gathered through this blog post, we must _manually hand-define_ each of our kernels for applying various operations such as smoothing, sharpening, and edge detection.

That’s all fine and good, **but what if there was a way to _learn_ these filters instead?** Is it possible to define a machine learning algorithm that can look at images and eventually learn these types of operators?

In fact, there is — these types of algorithms are a sub-type of _Neural Networks_ called _Convolutional Neural Networks_ (CNNs). By applying convolutional filters, nonlinear activation functions, pooling, and backpropagation, CNNs are able to learn filters that can detect edges and blob-like structures in lower-level layers of the network — and then use the edges and structures as building blocks, eventually detecting higher-level objects (i.e., faces, cats, dogs, cups, etc.) in the deeper layers of the network.

Exactly _how_ do CNNs do this?

I’ll show you — but it will have to wait for another few blog posts until we cover enough basics.

### What's next? We recommend [PyImageSearch University](https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend).

**Course information:**  
84 total classes • 114+ hours of on-demand code walkthrough videos • Last updated: February 2024  
★★★★★ 4.84 (128 Ratings) • 16,000+ Students Enrolled

**I strongly believe that if you had the right teacher you could _master_ computer vision and deep learning.**

Do you think learning computer vision and deep learning has to be time-consuming, overwhelming, and complicated? Or has to involve complex mathematics and equations? Or requires a degree in computer science?

That’s _not_ the case.

All you need to master computer vision and deep learning is for someone to explain things to you in _simple, intuitive_ terms. _And that’s exactly what I do_. My mission is to change education and how complex Artificial Intelligence topics are taught.

If you're serious about learning computer vision, your next stop should be PyImageSearch University, the most comprehensive computer vision, deep learning, and OpenCV course online today. Here you’ll learn how to _successfully_ and _confidently_ apply computer vision to your work, research, and projects. Join me in computer vision mastery.

**Inside PyImageSearch University you'll find:**

*   ✓ **84 courses** on essential computer vision, deep learning, and OpenCV topics
*   ✓ **84 Certificates** of Completion
*   ✓ **114+ hours** of on-demand video
*   ✓ **Brand new courses released _regularly_**, ensuring you can keep up with state-of-the-art techniques
*   ✓ **Pre-configured Jupyter Notebooks in Google Colab**
*   ✓ Run all code examples in your web browser — works on Windows, macOS, and Linux (no dev environment configuration required!)
*   ✓ Access to **centralized code repos for _all_ 536+ tutorials** on PyImageSearch
*   ✓ **Easy one-click downloads** for code, datasets, pre-trained models, etc.
*   ✓ **Access** on mobile, laptop, desktop, etc.

[Click here to join PyImageSearch University](https://pyimagesearch.com/pyimagesearch-university/?utm_source=blogPost&utm_medium=bottomBanner&utm_campaign=What%27s%20next%3F%20I%20recommend)

Summary
-------

In today’s blog post, we discussed _image kernels_ and _convolutions_. If we think of an image as a _big matrix_, then an image kernel is just a _tiny matrix_ that sits on top of the image.

This kernel then slides from left-to-right and top-to-bottom, computing the sum of element-wise multiplications between the input image and the kernel along the way — we call this value the _kernel output_. The kernel output is then stored in an output image at the same _(x, y)_\-coordinates as the input image (after accounting for any padding to ensure the output image has the same dimensions as the input).

Given our newfound knowledge of convolutions, we defined an OpenCV and Python function to apply a series of kernels to an image. These operators allowed us to blur an image, sharpen it, and detect edges.

Finally, we briefly discussed the roles kernels/convolutions play in deep learning, specifically _Convolutional Neural Networks_, and how these filters can be learned _automatically_ instead of needing to _manually_ define them first.

In next week’s blog post, **I’ll be showing you how to train your _first_ Convolutional Neural Network from scratch using Python — _be sure to signup for the PyImageSearch Newsletter using the form below to be notified when the blog post goes live!_**

![Image 12](data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%20410%20567'%3E%3C/svg%3E)

#### Download the Source Code and FREE 17-page Resource Guide

Enter your email address below to get a .zip of the code and a **FREE 17-page Resource Guide on Computer Vision, OpenCV, and Deep Learning.** Inside you'll find my hand-picked tutorials, books, courses, and libraries to help you master CV and DL!