20 OpenCV concepts with Before-and-After Examples
Table of contents
- 1. Installing OpenCV π¦
- 2. Loading and Displaying an Image πΌοΈ
- 3. Converting an Image to Grayscale π
- 4. Resizing an Image π
- 5. Drawing Shapes on an Image π¨
- 6. Saving an Image to Disk πΎ
- 7. Drawing Circles on an Image βͺ
- 8. Edge Detection using Canny Algorithm πΌοΈ
- 9. Blurring an Image π«οΈ
- 10. Rotating an Image π
- 11. Cropping an Image βοΈ
- 12. Thresholding an Image π
- 13. Erosion of an Image π§±
- 14. Dilating an Image π
- 15. Detecting Contours π
- 16. Histogram Equalization π
- 17. Sobel Edge Detection πΌοΈ
- 18. Laplacian Edge Detection π
- 19. Image Pyramids (Scaling Images) π
- 20. Image Translation (Shifting) β‘οΈ
1. Installing OpenCV π¦
Use Case: Install the OpenCV library to perform image processing and computer vision tasks.
Goal: Set up OpenCV to work with images and videos using Python. π―
Sample Code:
pip install opencv-python
Before Example: You use low-level libraries for image processing, which lack advanced functionalities for computer vision tasks.
# Manually processing images without a dedicated library.
After Example: With OpenCV installed, you can easily perform a wide range of image processing tasks.
Successfully installed opencv-python
# OpenCV installed and ready for use.
2. Loading and Displaying an Image πΌοΈ
Use Case: Load an image from disk and display it using OpenCV.
Goal: Use OpenCV to read and display images easily in Python. π―
Sample Code:
import cv2
# Load and display image
image = cv2.imread("image.jpg")
cv2.imshow("Image", image)
cv2.waitKey(0) # Wait for a key press to close the window
cv2.destroyAllWindows()
Before Example: You manually read image files using file handling methods that are not optimized for image processing.
# Manually opening image files:
with open("image.jpg", "rb") as f:
image_data = f.read()
After Example: With OpenCV, loading and displaying images is simplified and optimized for speed.
# Image displayed using OpenCV, enabling fast and efficient visualization.
3. Converting an Image to Grayscale π
Boilerplate Code:
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
Use Case: Convert a color image to grayscale for simplified processing or analysis.
Goal: Use OpenCV to convert color images into grayscale efficiently. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Convert the image to grayscale
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# Display the grayscale image
cv2.imshow("Grayscale Image", gray_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually handle image pixel manipulation to convert the image into grayscale, which can be slow.
# Manually converting to grayscale:
gray_image = [[sum(pixel) / 3 for pixel in row] for row in image]
After Example: With OpenCV, converting images to grayscale is efficient and requires only a single function call.
# Grayscale image displayed using OpenCV.
4. Resizing an Image π
Boilerplate Code:
resized_image = cv2.resize(image, (width, height))
Use Case: Resize an image to a specific width and height.
Goal: Use OpenCV to resize images efficiently for analysis, modeling, or display purposes. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Resize the image to 200x200 pixels
resized_image = cv2.resize(image, (200, 200))
# Display the resized image
cv2.imshow("Resized Image", resized_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually scale the image, which can be error-prone and slower compared to using a dedicated library.
# Manually resizing the image:
resized_image = [[pixel for pixel in row[:200]] for row in image[:200]]
After Example: With OpenCV, resizing images is handled efficiently and takes just one function call.
# Image resized and displayed using OpenCV.
5. Drawing Shapes on an Image π¨
Boilerplate Code:
cv2.rectangle(image, (x, y), (x+w, y+h), color, thickness)
Use Case: Draw shapes like rectangles, circles, or lines on an image for annotation or visualization.
Goal: Use OpenCV to annotate or highlight regions in an image by drawing shapes. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Draw a rectangle on the image
cv2.rectangle(image, (50, 50), (200, 200), (255, 0, 0), 3)
# Display the image with the rectangle
cv2.imshow("Image with Rectangle", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually modify pixel values to draw shapes, which is inefficient and slow for large images.
# Manually drawing a rectangle by modifying pixel values:
for i in range(50, 200):
for j in range(50, 200):
image[i][j] = [255, 0, 0]
After Example: With OpenCV, drawing shapes like rectangles, circles, and lines is straightforward and efficient.
# Rectangle drawn on the image and displayed with OpenCV.
6. Saving an Image to Disk πΎ
Boilerplate Code:
cv2.imwrite("output.jpg", image)
Use Case: Save the processed image back to the disk.
Goal: Use OpenCV to save images after processing, such as resizing or annotation. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Save the image to a new file
cv2.imwrite("output.jpg", image)
Before Example: You manually handle file saving with standard Python methods, which may not handle image formats efficiently.
# Manually saving an image:
with open("output.jpg", "wb") as f:
f.write(image_data)
After Example: With OpenCV, saving images is optimized and supports various image formats easily.
# Image saved as "output.jpg" using OpenCV.
7. Drawing Circles on an Image βͺ
Boilerplate Code:
cv2.circle(image, (center_x, center_y), radius, color, thickness)
Use Case: Draw circles on an image to highlight or mark certain areas.
Goal: Use OpenCV to efficiently draw circles on images for annotation or visualization. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Draw a circle on the image
cv2.circle(image, (100, 100), 50, (0, 255, 0), 3)
# Display the image with the circle
cv2.imshow("Image with Circle", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually alter pixel values to draw a circle, which is inefficient for large-scale image processing.
# Manually drawing a circle by changing pixel values:
for i in range(50):
for j in range(50):
image[center_x + i][center_y + j] = [0, 255, 0]
After Example: With OpenCV, drawing circles is fast and requires only a single function call.
# Circle drawn and displayed on the image using OpenCV.
8. Edge Detection using Canny Algorithm πΌοΈ
Boilerplate Code:
edges = cv2.Canny(image, threshold1, threshold2)
Use Case: Detect edges in an image using the Canny edge detection algorithm, which is useful for object detection and segmentation.
Goal: Use OpenCV to extract edges from an image for analysis or further processing. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Apply Canny edge detection
edges = cv2.Canny(image, 100, 200)
# Display the edge-detected image
cv2.imshow("Edges", edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually detect edges by writing custom algorithms, which may be inefficient and inaccurate.
# Manually applying a basic edge-detection filter:
edges = [[image[i][j] - image[i-1][j-1] for j in range(len(image[0]))] for i in range(len(image))]
After Example: With OpenCV, Canny edge detection is highly efficient and robust.
# Edges detected and displayed using OpenCV.
9. Blurring an Image π«οΈ
Boilerplate Code:
blurred_image = cv2.GaussianBlur(image, (kernel_size, kernel_size), 0)
Use Case: Apply Gaussian blur to an image to reduce noise or smooth the image.
Goal: Use OpenCV to blur an image for denoising or artistic effects. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Apply Gaussian blur
blurred_image = cv2.GaussianBlur(image, (15, 15), 0)
# Display the blurred image
cv2.imshow("Blurred Image", blurred_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You write custom code to average neighboring pixels for blurring, which can be inefficient.
# Manually applying a basic blur:
blurred_image = [[sum(pixel)/9 for pixel in row] for row in image]
After Example: With OpenCV, Gaussian blurring is fast, efficient, and works with just a single function call.
# Blurred image displayed using OpenCV.
10. Rotating an Image π
Boilerplate Code:
rotated_image = cv2.warpAffine(image, rotation_matrix, (width, height))
Use Case: Rotate an image by a specific angle.
Goal: Use OpenCV to rotate images for visualization, augmentation, or analysis. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Get the rotation matrix
center = (image.shape[1] // 2, image.shape[0] // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, 45, 1.0)
# Rotate the image
rotated_image = cv2.warpAffine(image, rotation_matrix, (image.shape[1], image.shape[0]))
# Display the rotated image
cv2.imshow("Rotated Image", rotated_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually rotate the image by calculating new pixel positions, which is inefficient and inaccurate.
# Manually rotating an image:
rotated_image = [[image[j][i] for j in range(len(image[0]))] for i in range(len(image))]
After Example: With OpenCV, rotating an image is accurate and efficient using the warpAffine
function.
# Image rotated and displayed using OpenCV.
11. Cropping an Image βοΈ
Boilerplate Code:
cropped_image = image[y1:y2, x1:x2]
Use Case: Crop a specific region from an image.
Goal: Use OpenCV to extract a specific portion of an image for further processing or analysis. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Crop the image (x1, y1, x2, y2)
cropped_image = image[50:200, 100:300]
# Display the cropped image
cv2.imshow("Cropped Image", cropped_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually handle pixel values to crop an image, which is time-consuming.
# Manually cropping an image using loops:
cropped_image = [row[100:300] for row in image[50:200]]
After Example: With OpenCV, cropping an image is efficient, handled with a simple slicing operation.
# Cropped image displayed using OpenCV.
12. Thresholding an Image π
Boilerplate Code:
_, thresholded_image = cv2.threshold(image, threshold_value, max_value, threshold_type)
Use Case: Apply a threshold to an image to convert it to binary (black and white).
Goal: Use OpenCV to threshold an image, creating a binary image based on pixel intensity. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Apply thresholding
_, thresholded_image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
# Display the thresholded image
cv2.imshow("Thresholded Image", thresholded_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually apply thresholding using nested loops, which is inefficient and slow.
# Manually applying thresholding:
thresholded_image = [[255 if pixel > 127 else 0 for pixel in row] for row in image]
After Example: With OpenCV, thresholding is handled efficiently with just one function call.
# Thresholded image displayed using OpenCV.
13. Erosion of an Image π§±
In image processing, erosion is a technique that shrinks or thins objects in a binary image. The idea is that it "erodes" away the boundaries of bright (white) regions.
In erosion:
A kernel (small matrix, like a 5x5 grid) moves over the image.
If even a single black pixel is found within the kernelβs window, the central pixel of the window is set to black. This process "shrinks" the white areas (foreground objects).
Effect:
Erosion thins objects or removes small white noise from the image.
Itβs often used for tasks like removing small white spots (noise) or separating connected objects in an image.
Boilerplate Code:
eroded_image = cv2.erode(image, kernel, iterations=1)
Use Case: Erode an image to reduce noise by removing small details, commonly used in morphological operations.
Goal: Use OpenCV to apply erosion to an image for noise reduction or other image processing tasks. π―
Sample Code:
import cv2
import numpy as np
# Load the image
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Define a kernel
kernel = np.ones((5, 5), np.uint8)
# Apply erosion
eroded_image = cv2.erode(image, kernel, iterations=1)
# Display the eroded image
cv2.imshow("Eroded Image", eroded_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually reduce noise by iterating over pixels and applying a custom erosion function, which can be inefficient.
# Manually applying erosion using loops:
for i in range(1, height - 1):
for j in range(1, width - 1):
if all_neighbours_equal(image, i, j):
eroded_image[i][j] = 0
After Example: With OpenCV, erosion is handled automatically, making it more efficient and easier to apply.
# Eroded image displayed using OpenCV.
14. Dilating an Image π
Dilate means to expand or enlarge. In image processing, dilation is the opposite of erosion. It expands or grows the white regions (objects) in a binary image.
In dilation:
A kernel (small matrix, like a 5x5 grid) moves over the image.
If any white pixel is found within the kernelβs window, the central pixel of the window is set to white. This makes the white areas (foreground objects) grow.
Effect:
Dilation thickens objects by expanding the boundaries of white regions.
It can also help fill small holes or gaps within objects and is useful for accentuating features or connecting broken parts of an objec
Boilerplate Code:
dilated_image = cv2.dilate(image, kernel, iterations=1)
Use Case: Dilate an image to increase the size of objects, often used to accentuate features.
Goal: Use OpenCV to apply dilation to an image for enhancing objects or features. π―
Sample Code:
import cv2
import numpy as np
# Load the image
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Define a kernel
kernel = np.ones((5, 5), np.uint8)
# Apply dilation
dilated_image = cv2.dilate(image, kernel, iterations=1)
# Display the dilated image
cv2.imshow("Dilated Image", dilated_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually implement dilation, modifying pixel values around objects, which can be slow.
# Manually applying dilation:
for i in range(1, height - 1):
for j in range(1, width - 1):
if any_neighbours_set(image, i, j):
dilated_image[i][j] = 255
After Example: With OpenCV, dilation is applied efficiently using built-in functions.
# Dilated image displayed using OpenCV.
15. Detecting Contours π
Detecting contours is like drawing a line around the object in an image, highlighting its boundary or shape. The main purpose of detecting contours is to simplify the image by focusing on the object outlines, which are often useful for further analysis.
What's the point of detecting contours?
Detecting contours helps in various tasks where the shape and structure of objects are important. Here are some key reasons why itβs useful:
Object Detection and Recognition:
By identifying the outline or shape of an object, you can recognize specific shapes (like circles, rectangles, etc.) or classify objects based on their contour features.Shape Analysis:
Contours help in measuring the geometry of objects, such as:Size of an object.
Area and perimeter of the shape.
Aspect ratio (width to height) for identifying shapes like squares or rectangles.
Image Segmentation:
Contours can help you segment an image by separating objects from the background, making it easier to isolate specific regions of interest.Feature Extraction:
You can extract meaningful features from contours (like the shape, corners, and angles) that can be used for more advanced tasks like machine learning and computer vision applications.Object Tracking:
In video processing, contours help track moving objects by detecting and drawing their outlines across frames, making it easier to follow their movement.
Boilerplate Code:
contours, _ = cv2.findContours(image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
The underscore (_
) means you're ignoring the second value returned by cv2.findContours()
.
cv2.findContours()
returns two things:Contours (the outlines you're interested in).
Hierarchy (information about contour relationships, which you're not using).
By using _
, you're telling Python to ignore the hierarchy since you don't need it. It's a way to make the code cleaner when you only need one of the returned values.
Use Case: Detect contours (outlines) in an image, which is useful for object detection and shape analysis.
Goal: Use OpenCV to detect and draw contours on an image. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Apply thresholding to create a binary image
_, thresholded_image = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
# Find contours
contours, _ = cv2.findContours(thresholded_image, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# Draw contours on the original image
image_with_contours = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image_with_contours, contours, -1, (0, 255, 0), 2)
# Display the image with contours
cv2.imshow("Contours", image_with_contours)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually trace shapes and objects by analyzing pixel intensity, which is tedious and error-prone.
# Manually tracing contours:
for i in range(height):
for j in range(width):
if is_edge_pixel(image, i, j):
contours.append((i, j))
After Example: With OpenCV, contour detection is fast and accurate, handling various image types.
# Contours detected and drawn on the image using OpenCV.
16. Histogram Equalization π
Boilerplate Code:
equalized_image = cv2.equalizeHist(image)
Use Case: Improve the contrast of an image by spreading out the pixel intensity values. Automatically adjusts the pixel values to enhance the image's contrast with just one function call.
Goal: Use OpenCV to apply histogram equalization and enhance the contrast of an image. π―
Sample Code:
import cv2
# Load the image in grayscale
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Apply histogram equalization
equalized_image = cv2.equalizeHist(image)
# Display the original and equalized images
cv2.imshow("Original Image", image)
cv2.imshow("Equalized Image", equalized_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually adjust pixel values to enhance contrast, which is time-consuming and not always effective.
# Manually adjusting pixel values:
adjusted_image = [[min(255, pixel * 2) for pixel in row] for row in image]
After Example: With OpenCV, histogram equalization is applied efficiently with one function call, improving contrast significantly.
# Contrast-enhanced image displayed using OpenCV.
17. Sobel Edge Detection πΌοΈ
Sobel Edge Detection is like using a highlighter to trace the edges or boundaries of objects in an image. It helps identify where things change quickly, such as the borders between light and dark areas.
Imagine running your fingers over a bumpy surface (the image). When your fingers feel a sharp bump (a sudden change in pixel intensity), that's where the edges are. The Sobel filter helps find and emphasize these "bumps" in the image, marking the boundaries of objects.
In practice:
Before: The image looks normal with no clear emphasis on the edges.
After: Sobel highlights the edges (sharp changes in brightness) so you can easily see the boundaries of objects.
This is useful for tasks like object detection or image analysis.
Boilerplate Code:
sobel_x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=5)
sobel_y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=5)
Use Case: Detect edges in an image using the Sobel operator, which highlights horizontal and vertical edges.
Goal: Use OpenCV to apply Sobel edge detection in both x and y directions. π―
Sample Code:
import cv2
# Load the image in grayscale
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Apply Sobel edge detection
sobel_x = cv2.Sobel(image, cv2.CV_64F, 1, 0, ksize=5) # X direction
sobel_y = cv2.Sobel(image, cv2.CV_64F, 0, 1, ksize=5) # Y direction
# Display the Sobel edge detection results
cv2.imshow("Sobel X", sobel_x)
cv2.imshow("Sobel Y", sobel_y)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually calculate gradients to detect edges, which is computationally expensive and prone to errors.
# Manually calculating gradients for edge detection:
gradient_x = [[image[i][j+1] - image[i][j] for j in range(width)] for i in range(height)]
After Example: With OpenCV, Sobel edge detection is applied with a simple function, efficiently detecting both horizontal and vertical edges.
# Sobel edges in x and y directions detected and displayed using OpenCV.
18. Laplacian Edge Detection π
Imagine you're tracing a mountain range on a map. The Laplacian detects the steepest cliffsβthe areas where the image transitions from light to dark (or vice versa) the fastest. It finds the edges by calculating how fast the brightness is changing.
Laplacian Edge Detection is a method that finds edges in an image by looking for places where the intensity (brightness) changes most sharply.
Boilerplate Code:
laplacian = cv2.Laplacian(image, cv2.CV_64F)
Use Case: The Laplacian method is useful for tasks like detecting edges in images where you want to emphasize the boundaries of objects.
Goal: Use OpenCV to apply Laplacian edge detection for highlighting edges in all directions. π―
Sample Code:
import cv2
# Load the image in grayscale
image = cv2.imread("image.jpg", cv2.IMREAD_GRAYSCALE)
# Apply Laplacian edge detection
laplacian = cv2.Laplacian(image, cv2.CV_64F)
# Display the Laplacian edge-detected image
cv2.imshow("Laplacian Edge Detection", laplacian)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually calculate second-order derivatives, which is computationally expensive and complex to implement.
A first-order derivative finds the rate of change (like how steep a hill is).
A second-order derivative finds the change in the rate of change (like noticing how quickly the hill gets steeper).
First-order: Think of detecting the top of a hill.
Second-order: Detecting where the hillβs steepness increases sharply, like the very sharp part of a peak.
# Manually calculating second-order derivatives for edge detection:
laplacian = [[image[i+1][j] + image[i-1][j] - 2 * image[i][j] for j in range(width)] for i in range(height)]
After Example: With OpenCV, Laplacian edge detection is applied efficiently, detecting edges in all directions.
# Laplacian edge detection applied and displayed using OpenCV.
19. Image Pyramids (Scaling Images) π
Think of it like zooming in or out on a picture:
Zooming in makes the image larger (pyrUp).
Zooming out makes the image smaller (pyrDown).
Image pyramids help you look at the same image at different sizes. This is useful for tasks like object detection, where you want to find objects no matter how big or small they are.
Boilerplate Code:
smaller = cv2.pyrDown(image)
larger = cv2.pyrUp(image)
Use Case: Create image pyramids to scale images up or down for various computer vision tasks like object detection.
Goal: Use OpenCV to scale images up and down using image pyramids. π―
Sample Code:
import cv2
# Load the image
image = cv2.imread("image.jpg")
# Scale down and scale up the image
smaller = cv2.pyrDown(image)
larger = cv2.pyrUp(image)
# Display the scaled images
cv2.imshow("Smaller Image", smaller)
cv2.imshow("Larger Image", larger)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually resize images using basic scaling methods, which can be inaccurate or lose important details.
# Manually scaling the image:
smaller_image = [[image[i//2][j//2] for j in range(width//2)] for i in range(height//2)]
After Example: With OpenCV, image pyramids provide efficient scaling methods that maintain image quality.
# Image scaled up and down using OpenCV's pyramid functions.
20. Image Translation (Shifting) β‘οΈ
Boilerplate Code:
translation_matrix = np.float32([[1, 0, tx], [0, 1, ty]])
shifted_image = cv2.warpAffine(image, translation_matrix, (width, height))
Use Case: Translate (shift) an image by a specific number of pixels in the x or y direction.
Goal: Use OpenCV to shift an image by moving its pixels in any direction. π―
Sample Code:
import cv2
import numpy as np
# Load the image
image = cv2.imread("image.jpg")
# Define the translation matrix (tx, ty represent the shift in x and y directions)
tx, ty = 100, 50
translation_matrix = np.float32([[1, 0, tx], [0, 1, ty]])
# Apply the translation
shifted_image = cv2.warpAffine(image, translation_matrix, (image.shape[1], image.shape[0]))
# Display the translated image
cv2.imshow("Shifted Image", shifted_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
Before Example: You manually adjust pixel values to shift the image, which is inefficient and hard to scale for larger images.
# Manually shifting the image:
shifted_image = [[image[i-ty][j-tx] for j in range(width)] for i in range(height)]
After Example: With OpenCV, translating an image is handled efficiently using the warpAffine
function.
# Image shifted horizontally and vertically, displayed using OpenCV.
Subscribe to my newsletter
Read articles from Anix Lynch directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by