0% found this document useful (0 votes)
5 views106 pages

Image Processing Using Open CV

The document provides an overview of image processing using OpenCV, focusing on pixels, bit depth, and image formats. It details how to read and display images with OpenCV functions, as well as the internal workings of these functions, including file access, format detection, and pixel matrix creation. Additionally, it covers image resizing and the importance of interpolation methods in adjusting image dimensions.

Uploaded by

2024sl70006
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
5 views106 pages

Image Processing Using Open CV

The document provides an overview of image processing using OpenCV, focusing on pixels, bit depth, and image formats. It details how to read and display images with OpenCV functions, as well as the internal workings of these functions, including file access, format detection, and pixel matrix creation. Additionally, it covers image resizing and the importance of interpolation methods in adjusting image dimensions.

Uploaded by

2024sl70006
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

Image Processing using Open CV

Pixel : Basic unit of image

This is 9X9 Pixel box:

Each pixel has RedMore number


, Green , Blueof(RGB)
pixels =>
color strips. Changing Better Picture
the value Quality
of these
color strips gives different colors

Bit Depth
Black/White Image: Bi-Tonal / Binary Image/ 2-Bit Depth image-

it uses 2 colors: Black = 0 & White = 1


(Each pixel – 2 bits)

Greyscale Images-
8-bit depth- Uses 8 bits for each pixel–
2 colors, 8-bit depth for each pixel
 28 combinations/
Tones
Black = 0
Grey : 1-254
White = 256

Color Images-
Depth Range : 8 bits – 24 bits
Image Dimensions & Image Channels:

Image dim ( m, n, 3) => 3


channels => each pixel has 3
channels

Single Pixel
 m = height; n = width

Reading & Displaying images


 [Link](filename, flags) → Reads an image from a file
into memory.
 [Link](window_name, image) → Displays an image in
a window.
 [Link](delay) → Waits for a key press; used to keep the
window open until a key is pressed. 0 = forever
Usage / Example
import cv2 as cv

# 1. Read image (default: color)


img = [Link]("resources/[Link]")

# 2. Display the image in a window


[Link]("Pehli Image", img)

# 3. Wait until user presses any key


[Link](0)

# (Optional) Close windows after key press


[Link]()

Since Image is loaded as a Numpy array ,


Size of Image = [Link]
MATH BEHIND [Link]( )

When you call:


img = [Link]("resources/[Link]")
Here’s what happens inside, step by step:

1. File Access
 OpenCV first checks: “Hey, does this file exist on the given
path?”
 It uses C++ file I/O (like fopen or std::ifstream)
internally to read the binary contents of the file.

2. Image Format Detection


 Images are just bytes on disk.
 OpenCV doesn’t magically know if it’s JPG or PNG → it
inspects the magic number / header bytes of the file.
o Example:
 JPEG files always start with 0xFFD8 (called SOI
marker → Start Of Image).
 PNG files start with 0x89504E47 (‰PNG in
ASCII).
👉 So OpenCV peeks into the file header and decides which
codec/decoder to use.

3. Decoding the Image


 Each format (JPEG, PNG, BMP, etc.) has its own
compression rules.
 OpenCV uses its Image I/O module, which often wraps
around libjpeg, libpng, etc.
 The decoder decompresses the image and reconstructs
the raw pixel data.
Example:
 A JPEG is stored as a set of DCT (Discrete Cosine
Transform) coefficients that are quantized.
 When reading: OpenCV applies Inverse DCT (IDCT) to
reconstruct each 8×8 pixel block.
 A PNG is compressed using DEFLATE (lossless
compression), so it’s unzipped back to raw pixels.

4. Pixel Matrix Creation


 Once decoded, you now have raw pixel values.
 OpenCV stores them in a matrix (NumPy array in
Python):
o Shape: (height, width, channels)
o Channels: Usually 3 for color (B, G, R), 1 for
grayscale.
⚠️Note: OpenCV uses BGR order instead of RGB (weird, but
historical reason).
Example:
If the image is 640x480 (width × height) with 3 channels,
OpenCV allocates a NumPy array like: img[480][640][3]
Each pixel is usually stored as an 8-bit unsigned integer
(uint8), so values range 0–255.

Shape of the Array


For a color image of 640 (width) × 480 (height) with 3
channels (B, G, R):
[Link] # → (480, 640, 3)
 480 rows → each row corresponds to one horizontal line
of pixels (height).
 640 columns → each column in a row is one pixel (width).
 3 values per pixel → Blue, Green, Red intensities.

Visual Mental Model


You can imagine it like a 3D cube:
img[y][x][channel]
 y = row index (0 to 479) → top to bottom.
 x = column index (0 to 639) → left to right.
 channel = 0 (Blue), 1 (Green), 2 (Red).

🔢 Example Values
If we peek into it:
print(img[0][0]) # Pixel at top-left
# e.g. [123 56 78] → B=123, G=56, R=78

print(img[100][50]) # Pixel at row=100, col=50


# e.g. [255 0 0] → Pure blue pixel
So, each pixel = [B, G, R] array.
📊 Array Structure (Small Mock Example)
Let’s shrink it down to a 2×3 image with 3 channels so it’s
easier to see:
[Link] = (2, 3, 3)
It might look like this in NumPy:
array([
[ [123, 56, 78], [ 0, 255, 0], [ 0, 0, 255] ], # Row 0
[ [255, 255, 255], [128, 128, 128], [ 0, 0, 0] ] # Row 1
], dtype=uint8)
 First pixel (row 0, col 0): [123, 56, 78] → bluish-red
 Pixel (row 0, col 1): [0, 255, 0] → pure green
 Pixel (row 0, col 2): [0, 0, 255] → pure red
 And so on.

Visualization Example (Mapping Array → Screen)

## Accessing Individual Pixels


Let us see how to access a pixel in the image.

For accessing any pixel in a Numpy matrix, you have to use


matrix notation such as matrix[r,c], where the r is the row
number and c is the column number. Also note that the matrix
is 0-indexed.

For example, if you want to access the first pixel, you need to
specify matrix[0,0]. Let us see with some examples. We will
print one black pixel from top-left and one white pixel from top-
center.

# print the first pixel of the first black box


print(cb_img[0,0])

# print the first white pixel to the right of the first black
box
print(cb_img[0,6])
If your image is:
img = [ [ [255,0,0], [0,255,0] ],
[ [0,0,255], [255,255,0] ]
]
 Top-left pixel = blue (B,G,R)
 Top-right pixel = green
 Bottom-left pixel = red
 Bottom-right pixel = cyan

Flags Handling in [Link]()


[Link]() lets you control how the image is read using
flags:
img = [Link]("[Link]", cv.IMREAD_COLOR) # color only, no
transparency
img = [Link]("[Link]", cv.IMREAD_GRAYSCALE) # grayscale
img = [Link]("[Link]", cv.IMREAD_UNCHANGED) # keep alpha
channel
These flags determine:
1. Number of channels (how many values per pixel)
2. Whether transparency/alpha is preserved
3. Color to grayscale conversion if requested

1️⃣ cv.IMREAD_COLOR
 Forces image to 3 channels only: Blue, Green, Red → [B,
G, R]
 Alpha channel is dropped if it exists.
🔹 What’s alpha?
 Alpha channel = transparency information.
o Pixel = [B, G, R, A]
o A ranges 0–255: 0 = fully transparent, 255 = fully
opaque.
 Many PNGs have alpha (transparent background), but
JPEGs don’t support alpha.
When you do:
img = [Link]("[Link]", cv.IMREAD_COLOR)
 OpenCV internally:
o Checks number of channels.
o If 4 channels → drop the 4th channel (alpha)
o Result → 3-channel BGR image only.
2️⃣ cv.IMREAD_GRAYSCALE
 Converts any image into single channel grayscale
image.
🔹 How is grayscale calculated?
 Each pixel has [B, G, R] (or [R, G, B] in theory)
 Grayscale = perceived brightness
 Humans perceive green more strongly, red medium, blue

Gray = (0.299 x R) + (0.587 x G) + (0.114 x B) ~


weakly → so the formula:

converges to single value


This is called luminance conversion formula
🔹 How it’s applied in code:
 For each pixel:
B, G, R = img[y, x]
gray_pixel = 0.299*R + 0.587*G + 0.114*B (~ converges to
single value)
img_gray[y, x] = int(gray_pixel)
 Result: one value per pixel (0–255) instead of 3.
 Memory efficient, easier for edge detection,
thresholding etc.

3️⃣ cv.IMREAD_UNCHANGED
 Read image exactly as it is on disk, including alpha
channel if present.
 Pixel can be:
[B, G, R] → if no alpha
[B, G, R, A] → if alpha exists
 OpenCV will not drop any channel or convert to
grayscale.
 Useful if you want transparency for overlays,
blending, or compositing.

⚡ Example: What happens internally


Suppose a PNG pixel is [10, 20, 30, 128] (BGR + alpha 50%
transparent):
Flag Result Pixel Explanation
IMREAD_COLOR [10, 20, 30] Alpha dropped, keep BGR
22 ( reduced to Single 0.299*30 + 0.587*20 + 0.114*10
IMREAD_GRAYSCALE
channel) ≈ 22
IMREAD_UNCHANGE [10, 20, 30, 128] Everything preserved
Flag Result Pixel Explanation
D

6. Return to Python
 In Python, [Link]() returns a NumPy array, which is
just a wrapper around OpenCV’s internal cv::Mat (matrix
structure).
 Now you can index pixels like img[y, x].
MATH BEHIND [Link]( )
[Link]() expects the image as a NumPy array which is
created by [Link](). This Numpy array object/reference is
passed to [Link]()
This Numpy Array is basically a 2D or 3D matrix:
 Grayscale → 2D array: height × width
 Color → 3D array: height × width × 3 (for BGR channels)
Example pixel value for RGB:
img[10, 20] = [B, G, R] = [255, 128, 64]
Note: OpenCV uses BGR order, not RGB.

Step B: Window Creation

[Link]() expects the image as a NumPy array which is


created by [Link](). This Numpy array object/reference is
passed to [Link]().
img = [Link](file)
When you call:
[Link]("window", img)
1. OpenCV asks the GUI backend (Windows: Win32 API,
Linux: GTK/Qt, Mac: Cocoa) to create a window.
2. The backend allocates memory for the window and
prepares it to receive image pixels.
3. If the window already exists, it just reuses it.
💡 Internally, OpenCV stores this window in a window registry
(a map of window names to pixel buffers).

Step C: Image Conversion


OpenCV doesn’t always send your NumPy array “as-is”:
1. Data type conversion:
o OpenCV images are usually uint8 (0–255).
o If you pass float32 or other types, OpenCV converts
them to uint8 using:
Iuint8=clip(Ifloat×255,0,255)I_\text{uint8} = \text{clip}(I_\
text{float} \times 255, 0, 255)Iuint8=clip(Ifloat×255,0,255)
2. Color order:
o OpenCV uses BGR internally.
o GUI backends might expect RGB.
o So OpenCV swaps channels if necessary.
3. Contiguity check:
o The image array must be contiguous in memory.
o If not, OpenCV copies it to contiguous memory.

Step D: Rendering the Image


Now comes the core part:
1. OpenCV passes the pixel array to the GUI backend.
2. The backend creates a framebuffer, which is like a grid of
pixels that your monitor can display.
3. Each pixel in the NumPy array is mapped directly to a pixel
on the window:
o If the image is H × W × 3, each pixel’s [B, G, R] value
is drawn on the screen.
o If the window is larger than the image, the backend
may scale the image.
4. The refresh happens via the OS’s graphics pipeline
(OpenGL/DirectX/Quartz), not OpenCV.
💡 Simplified formula for a pixel’s color:
Framebuffer(x,y)=Image[y,x]\text{Framebuffer}(x, y) = \
text{Image}[y, x]Framebuffer(x,y)=Image[y,x]
where (x, y) is the window pixel location.

img = [Link]("checkerboard_18x18.png", 0)
 print(img) = “show me the raw numbers” → you’ll get the 2D
NumPy array of intensities.

[Link](img, cmap='gray') = “show me the picture” → you’ll


get the visual rendering of those numbers as shades of gray.
Image Resizing
– Changing the dimensions (width × height) of an image.

2️⃣ Syntax
resized_image= [Link](src_image, (width, height), fx=0, fy=0 interpolation=method)
Parameters:
 src_image → Original image.
 (width, height) → New dimensions.
 fx Scaling factor along x-axis (width)
 fy Scaling factor along y-axis (height)

 interpolation (optional) → Method to compute pixel values


(default: cv.INTER_LINEAR).
Common options:
o cv.INTER_NEAREST → Fastest, may be blocky.
o cv.INTER_LINEAR → Default, good for enlarging.
o cv.INTER_CUBIC → Slower, better quality.
o cv.INTER_LANCZOS4 → Best quality for upscaling.

3️⃣ Usage / Example


import cv2 as cv

# Read the image


img = [Link]("resources/[Link]")

# Resize to 800x600
img_resized = [Link](img, (800, 600))

# Display original and resized images


[Link]("Original Image", img)
[Link]("Resized Image", img_resized)

[Link](0)
[Link]()
✅ Output:
 Two windows appear: one with the original size, another
with 800×600 resized image.

MATH BEHIND [Link]( )


Step 1: Mapping coordinates
Imagine your original image is W x H (width × height) and you
want a new size W' x H'.
Each pixel in the new image needs to figure out where it
came from in the old image.
Formula for mapping:
For pixel (x', y') in the new image:

 x' and y' → coordinates in the resized image


 x and
original
image
💡 Notice: x and y are usually not integers, because the
new pixel might fall “between” old pixels. That’s why
we need INTERPOLATION.

Step 2: Interpolation (estimating pixel value)

Nearest Neighbor (INTER_NEAREST)


 Pick the closest original pixel:
I(x′,y′)=I(round(x),round(y))
 Super fast but can look blocky when enlarging.

Bilinear Interpolation (INTER_LINEAR)


 Take a weighted average of the 4 nearest pixels:
Suppose the four surrounding pixels in the original image are:
(x1, y1) --- (x2, y1)
| |
(x1, y2) --- (x2, y2)
 Distances to target point (x, y):
dx=x−x1,dy=y−y1
Compute weighted average:
I(x′,y′)=(1−dx)
(1−dy)I(x1,y1)+dx(1−dy)I(x2,y1)+(1−dx)dyI(x1,y2)+dxdyI(x2,y
2)

Smooth result, default in OpenCV.

Bicubic Interpolation (INTER_CUBIC)


 Uses 16 neighbors instead of 4.
 Uses cubic polynomials to calculate smooth pixel values.
 Formula is more complex, but conceptually it fits a
smooth curve to surrounding pixels to reduce jaggies.

Step 3: Creating the new image


 Loop through each pixel in the new image
 Map its coordinates to the old image
 Compute value using chosen interpolation
 Store in the output image matrix
So internally, OpenCV is doing a matrix operation (but
optimized with C++ for speed) to fill every pixel of the new
image.

Color Conversion( GrayScale Conversion) in


OpenCV

[Link]() changes an image from one color space to


another.
 Example:
import cv2 as cv

img = [Link]("[Link]") # BGR by default


gray = [Link](img, cv.COLOR_BGR2GRAY) # convert to
grayscale
hsv = [Link](img, cv.COLOR_BGR2HSV) # convert to HSV
Here:
 BGR = Blue, Green, Red (OpenCV default)
 GRAY = Black & White intensity
 HSV = Hue, Saturation, Value (more intuitive for color
manipulations)

Usage / Example
import cv2 as cv

# Read the image


img = [Link]("resources/[Link]")

# Resize to 800x600
img = [Link](img, (800, 600))
# Convert to grayscale
gray_img = [Link](img, cv.COLOR_BGR2GRAY)

# Display both images


[Link]("Original Image", img)
[Link]("Gray Image", gray_img)

# Wait until any key is pressed


[Link](0)

# Close all OpenCV windows


[Link]()
✅ Output: Two windows pop up — one showing the original image
(800x600), the other the grayscale version

MATH BEHIND [Link]( )


OpenCV stores images as matrices (NumPy arrays). Each pixel
has channels:
 BGR image → shape (height, width, 3)
 GRAY image → shape (height, width) (only one channel)
So [Link]() is basically a big per-pixel transformation:
For each pixel:
new_pixel = function(old_pixel)

Example 1: BGR → GRAY


Grayscale is a single brightness value(Single channel). The
human eye is more sensitive to green, less to blue.
The formula OpenCV uses:
Y=0.299*R + 0.587*G + 0.114*B => Reduces to single
value(single channel)
R, G, B are the values for a pixel (0–255).
 Y becomes the grayscale intensity for that pixel.
Engineering logic:
1. OpenCV loops through each pixel.
2. Applies this weighted sum.
3. Stores result in a single-channel matrix.

Convert Image to Black/White -


[Link]()
 First, the image is converted from color (BGR) to grayscale.
 Then, thresholding is applied to convert the grayscale image into
pure black and white.

2️⃣ Syntax
# Convert BGR image to grayscale
gray = [Link](src_image, cv.COLOR_BGR2GRAY)

# Apply thresholding to convert grayscale to binary


retval, binary = [Link](src_gray, threshold_value,
max_value, threshold_type)

# Display images
[Link](window_name, image)
[Link](delay)
[Link]()

Parameters for [Link]:


 src_gray: Grayscale input image.
 threshold_value: Pixel intensity threshold (0–255).
 max_value: Pixel value to assign if threshold condition is
met.
 threshold_type: Type of thresholding (e.g.,
cv.THRESH_BINARY).
Type Rule
THRESH_BINARY > thresh → maxVal, ≤ thresh → 0
THRESH_BINARY_IN
> thresh → 0, ≤ thresh → maxVal
V
> thresh → thresh, ≤ thresh →
THRESH_TRUNC
unchanged
> thresh → unchanged, ≤ thresh
THRESH_TOZERO
→0
THRESH_TOZERO_I > thresh → 0, ≤ thresh →
NV unchanged

3️⃣ Usage / Example


import cv2 as cv

# Read image
img = [Link]("resources/[Link]")

# Convert to grayscale
gray = [Link](img, cv.COLOR_BGR2GRAY)

# Convert to black and white using threshold


(thresh, binary) = [Link](gray, 127, 255, cv.THRESH_BINARY)

# Display
[Link]('Original', img)
[Link]('Grayscale', gray)
[Link]('Black and White', binary)

[Link](0)
[Link]()

What happens here:


 Pixels in gray > 127 → set to 255 (white)
 Pixels ≤ 127 → set to 0 (black)

MATH BEHIND [Link]()


First convert BGR image to grayscale
gray_img = [Link](src_image, cv.COLOR_BGR2GRAY)

 A grayscale image is just a 2D array (matrix) where each


pixel has a value between 0 and 255.
o 0 → black
o 255 → white
o 128 → medium gray
So gray_img is basically a big table of numbers representing
brightness.

2️⃣ What is thresholding?


Thresholding is the simplest way to convert grayscale → black-
and-
white:
 Pick
a

threshold value (here, 127).


 For each pixel:
o If pixel value > threshold → make it white (max
value, here 255)
o If pixel value ≤ threshold → make it black (min value,
here 0)
Mathematically:
 x, y are the pixel coordinates.

3️⃣ What [Link]() does internally


The [Link] function in OpenCV roughly does this:
1. Takes the input image (gray_img) as a matrix.
2. Loops through every pixel.
3. Compares it to the threshold (127).
4. Writes either 0 or maxval (here 255) into a new matrix
(binary).
💡 Pseudocode:
for i in range(height):
for j in range(width):
if gray_img[i, j] > 127:
binary[i, j] = 255
else:
binary[i, j] = 0

4️⃣ What about thresh?


 [Link] returns two things: (retval, dst)
 dst → the thresholded image (our binary)
 retval → the threshold value used (can be important in
adaptive or Otsu thresholding)
In your case:
thresh = 127 # just the threshold you set
binary = result image

5️⃣ Variations: cv.THRESH_BINARY


 OpenCV actually supports multiple threshold types:
Type Rule
THRESH_BINARY > thresh → maxVal, ≤ thresh → 0
THRESH_BINARY_IN
> thresh → 0, ≤ thresh → maxVal
V
> thresh → thresh, ≤ thresh →
THRESH_TRUNC
unchanged
> thresh → unchanged, ≤ thresh
THRESH_TOZERO
→0
THRESH_TOZERO_I > thresh → 0, ≤ thresh →
NV unchanged
You picked THRESH_BINARY, the simplest one.
Saving an Image: imwrite(path,
image_name)
Adaptive Thresholding-
[Link]()

Unlike normal thresholding (where you use one global value), here the threshold is
calculated for smaller regions of the image → this makes it powerful when lighting
conditions vary across the image.
It’s like giving each neighborhood of pixels its own judge for deciding "black or white,"
instead of one strict rule for the entire picture.

Syntax:
[Link](src, maxValue, adaptiveMethod, thresholdType,
blockSize, C)

 src → Input grayscale image.


 maxValue → Value assigned if condition is met (usually 255 for white).
 adaptiveMethod → How threshold is calculated:
o cv2.ADAPTIVE_THRESH_MEAN_C → Mean of neighborhood pixels.
o cv2.ADAPTIVE_THRESH_GAUSSIAN_C → Weighted sum of neighborhood
(Gaussian window).
 thresholdType → Always use cv2.THRESH_BINARY or cv2.THRESH_BINARY_INV.
 blockSize → Size of pixel neighborhood (must be odd, e.g., 11, 15).
 C → Constant subtracted from the calculated mean/gaussian value (fine-tuning
brightness).

Usage
import cv2
img = [Link]('text_image.png', 0)

# Adaptive Threshold - Mean


thresh_mean = [Link]( img, 255,
cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY,
11, 2)

# Adaptive Threshold - Gaussian


thresh_gauss = [Link]( img, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
11, 2)

👉 Example use case: Scanning old documents or OCR preprocessing when the paper has
uneven lighting/shadows.
MATH BEHIND [Link]()
Let’s say we are processing pixel (x, y):

(a) Neighborhood Window

 Define a window of size blockSize × blockSize centered on (x, y).


 Collect all pixel intensities inside this window → call them p1, p2, ..., pk.

(b) Compute Local Threshold T(x, y)

 Mean Method:

where N = total number of pixels in block.

 Gaussian Method:
Each neighbor pixel is weighted by a Gaussian kernel (closer pixels matter more).

where w_i = Gaussian weight.


(c) Binarization Rule
For pixel value I(x, y):

(d) Engineering Trick

 OpenCV optimizes this using integral images for mean calculation → making it O(1)
per pixel instead of recalculating the sum each time.
 For Gaussian, it uses convolution with Gaussian kernel (fast with separable filters).
Blurring Images
 Box Blur or Mean Filter
It replaces each pixel with the average of all pixels in
its neighborhood (within a kernel).
 All neighbors are treated equally (no fancy weights like
Gaussian).

Look at the kernel in the image:


x1 x1 x1
x1 x1 x1
x1 x1 x1
Every position in the kernel has the same weight = 1.
After applying, you divide by the total number of weights.
(Here, 3×3 = 9).
Output Pixel Value =
Sum of all pixel values
in window / Total number of
cells in kernel

The 3×3 neighborhood:

50 50 100
50 50 100
50 50 100
Sum = 600
Divide by 9 → Result = 66.66…
➡️This is why it’s called mean blur: just the average of
neighbors.
📌 Effect on image:
Smooths edges but doesn’t care about pixel distance.
Treats all neighbors equally, which can sometimes make
images look boxy or unnatural.

Box/Mean Blur in OpenCV

OpenCV gives two main ways:


1. [Link]() → Mean Filter (simple box blur)
import cv2 as cv

img = [Link]("resources/[Link]")
blur_img = [Link](img, (3, 3)) # 3x3 kernel
 (3, 3) is the kernel size.
 Larger kernel → stronger blur.

2. [Link]() → More control over box filtering


blur_img = [Link](img, -1, (5, 5), normalize=True)
 -1 → output image has same depth as input.
 (5,5) → kernel size.
 normalize=True → divide by kernel size (makes it
mean blur).
 If normalize=False, it just sums the pixel values
instead of averaging (rarely used directly).
Gaussian Blur
Gaussian Blur is used to smooth an image by reducing
noise and detail. Instead of a simple average of
surrounding pixels (like normal blur), it uses a Gaussian
distribution (bell curve) to give higher weight to the
center pixel and less to distant neighbors.

Gaussian Blur is just:


 Taking each pixel in your image,
 Looking at its neighbors,
 Averaging them — but not equally.
Instead of a plain average (like a box blur), it gives
more weight to the pixels closer to the center and
less weight to those further away.
This “weight distribution” comes from the Gaussian
function (that bell-shaped curve you’ve seen in
statistics).

🔹 The Math Behind It

1. The Gaussian Function


The Gaussian (normal distribution) in 2D looks like this:

 x,yx, yx,y → coordinates (distance from the center


pixel)
 σ\sigmaσ (sigma) → the “spread” of the bell curve
(controls how blurry it gets)
 The exponential ensures that values closer to the
center are bigger, and far away ones shrink toward
zero.
2. Creating the Kernel (Filter Matrix)
 OpenCV builds a small matrix of numbers (called a
kernel).
 Each entry in the kernel = the Gaussian function
value at that position.
Then the matrix is normalized (all values divided by
their sum), so that the total weight = 1.
This ensures brightness of the image stays the same,
not washed out.

3. The Convolution Process

Here’s what happens pixel-by-pixel:


1. Place the kernel over a pixel (with center aligned).
2. Multiply each neighbor pixel by the corresponding
kernel value.
(so closer neighbors count more, farther ones
count less).
3. Add them all up.
4. Replace the center pixel with this weighted
average.
Mathematically (discrete convolution):

 I = original image
 I′ = blurred image
 k = kernel “radius” (e.g., for 5×5 kernel,
k=2k=2k=2)
 G(i,j) = Gaussian kernel weight
Look at the
Gaussian kernel in
the image:

x1 x2 x1
x2 x4 x2
x1 x2 x1
Now the weights are not equal.
 The center pixel has the highest weight (4).
 Pixels nearby have medium weight (2).
 Corners have the lowest weight (1).

This comes from the Gaussian distribution (bell curve).


Closer pixels = more important.
Farther pixels = less important.

The math is:

The 3×3 neighborhood:


50 100 100
100 200 200
50 100 100
Weighted multiplication
(kernel applied):
(50×1) + (100×2) +
(100×1) +
(100×2) + (200×4) + (200×2) +
(50×1) + (100×2) + (100×1)
= 1000
Divide by sum of weights (16)
→ Result = 62.5
➡️This is Gaussian blur: a smarter average that
respects distance.
📌 Effect on image:
Produces smoother, more natural-looking blur.
Preserves edges better than box blur.
Looks closer to how things blur in real life (like out-of-
focus cameras).

Gaussian Blur in OpenCV

Syntax

[Link](src, ksize, sigmaX, dst=None,


sigmaY=None, borderType=None)

 src → Input image.


 ksize → Kernel size (must be odd, e.g., (3,3), (5,5),
(7,7)).
 sigmaX → Standard deviation in the X direction (if 0,
OpenCV calculates based on ksize).
 dst → output blurred image
 sigmaY → Same for Y (if left as 0, it equals sigmaX).
 borderType → How border pixels are handled (default:
cv.BORDER_DEFAULT).

IMPLEMENTATION:

Inside [Link]()
When you call:
blur_img = [Link](img, (7,7), 0)
Here’s the sequence of internal steps:
1. Kernel size → (7,7) means it creates a 7×7
Gaussian kernel.
o The kernel values come from the Gaussian
function.
o If sigma (σ) is set to 0, OpenCV auto-calculates
it based on kernel size.
2. Build the 1D Gaussian kernel for rows and
columns.
o Example: for 7 elements, values might look
like [0.004, 0.054, 0.242, 0.399, 0.242, 0.054,
0.004].
3. Apply convolution in X direction (horizontal
blur).
4. Apply convolution in Y direction (vertical blur).
5. Result → the final image is softened, edges are
smoother, noise is reduced.
Edge Detection using OpenCV

Edge detection is a case of finding the regions in an


image where we have a sharp change in intensity or
sharp change in color.

Think of an image as a giant spreadsheet of


numbers.
Each cell = pixel intensity (brightness).
 Brightness = low → dark pixel.
 Brightness = high → bright pixel.
👉 An edge is basically where the pixel brightness
changes suddenly.
Example: [ 10 10 10 200 200 200 ]
Left side = dark (10), right side = bright (200). The
“sudden jump” from 10 → 200 = an edge.

🔬 Math Behind Edge Detection


We detect edges by checking gradients.
A gradient is just: "How fast is the intensity changing?"
Formula:
If I(x, y) is the intensity of a pixel at coordinates (x, y):
 Horizontal Change (∂I/∂x):

 Vertical Change (∂I/∂y):


Together, Gradient Magnitude is:

Gradient direction (where edge is pointing):

So edges are basically big gradients in the pixel


matrix.

⚡ Engineering Trick — Derivatives in Images


We can’t really do calculus on pixels (they’re discrete,
not continuous).
So instead of ∂I/∂x, we use finite differences with
small kernels (filters).
Example:
 Approximate derivative in x-direction:

 Approximate derivative in y-direction:

That’s the essence: compare neighbors → see how


sharp the jump is.
 But, Plain difference is sensitive to noise (tiny
random variations look like edges).
🌀 Sobel
Operator —

Now, here comes


Sobel.
It’s basically a special filter (kernel) that approximates
derivatives but also reduces noise (smoother results
It uses 3×3 convolution kernels:

 For horizontal edges (Gx): For vertical


edges (Gy):

How Sobel Works Internally (Step by Step)

1. Take your image → grayscale.


2. Pick a pixel (center of a 3×3 region).
3. Multiply surrounding pixels with Sobel kernel
values → sum them up → that gives you an
approximation of derivative in x or y.
o This is just convolution.

4. Do this with both kernels → get Gx and Gy.

5. Compute gradient
magnitude:

6. Compare it with Threshold (decide: is this pixel part


of an edge or not?).

🔎 Why Sobel and not plain difference?


 Plain difference is sensitive to noise (tiny random
variations look like edges).
 Sobel gives more weight to the center pixels (the -
2, +2 middle row/col) → smoother + more stable.

Canny Edge Detection


1. Noise
Reduction (Gaussian Blur 🌀)
 Problem: images are noisy, and noise looks like
fake edges.
 Solution: smooth the image first using a Gaussian
filter.
👉 This ensures edges we detect later are not just
random pixel noise.

2. Intensity Gradient Calculation (Sobel inside!)


 After blurring, compute gradients (like we learned
earlier).
 Use Sobel operator to get:
o Gx → change in x-direction
o Gy → change in y-direction

Then:

👉 Now we know how strong the edge is (magnitude)


and which direction it points.

3. Non-Maximum Suppression (NMS ✂️)


 Problem: Gradients are thick → edges look like fat
blurry bands.
 Solution: Keep only the sharpest pixel along the
edge direction.
How it works:
When we compute the gradient magnitude (G = √(Gx²
+ Gy²)), we basically say:
“This pixel is part of an edge if the brightness changes
a lot here.”
But gradients often give us thick, fuzzy bands instead
of neat lines.
Example (a vertical black-white transition):
0 50 120 200 255
When Sobel looks at this, it might say:
Edge strength: 0 40 120 140 0
So instead of a thin line at the boundary, we get a
whole fat band (40, 120, 140).
We don’t want all of them → we only want the
sharpest one (the peak).
Non-Maximum Suppression (NMS)
NMS = “Thinning out the fat gradient to keep only the
strongest line.”
How? → Look in the direction of the gradient (θ) and
keep only the local maximum.

Step 1. Gradient Direction


Gradient angle θ tells us where the edge is pointing
(perpendicular to edge).
We round it to 4 main directions:
 0° → horizontal edge (look left-right)
 45° → diagonal edge (look top-right / bottom-left)
 90° → vertical edge (look up-down)
 135° → other diagonal (top-left / bottom-right)

Step 2. Compare Neighbors


For a pixel at (x, y) with gradient magnitude G(x,y):
 Look at the 2 neighboring pixels along the
gradient direction.
 If G(x,y) is NOT bigger than both neighbors → set it
to 0 (suppress it).
👉 This keeps only the peak of the edge.

🔢 Example (Mini Walkthrough)


Suppose we have gradient magnitudes like this:
Row of pixels: [ 20 80 150 120 30 ]
Gradient dir: all 90° (vertical edge)
Here the “fat band” is 80–150–120.
Now, apply NMS:
 Pixel = 20 → not larger than neighbors → 0
 Pixel = 80 → smaller than 150 → 0
 Pixel = 150 → larger than 80 and 120 → KEEP IT
 Pixel = 120 → smaller than 150 → 0
 Pixel = 30 → not edge → 0
Result after NMS:
[ 0 0 150 0 0 ]
✨ Now the thick band is reduced to a single thin line.

4. Double Thresholding ⚖️(Strong vs Weak edges)


 Now we decide: which pixels are real edges?
Use two thresholds: T_low and T_high.

 Strong edges:

 Weak edges:
 Non-edges: G<Tlow → discard.
👉 This prevents weak noise from being mistaken as
edges.

5. Edge Tracking by Hysteresis (Connecting the


dots 🪢)
 Weak edges can be legit IF they are connected to
strong edges.
 So:
o If a weak edge touches a strong edge → keep
it.
o If it floats alone → discard it.
👉 This final step produces clean, continuous edges.

In OpenCV

[Link](image, threshold1, threshold2, edges,


apertureSize, L2gradient)
 image → Input image (grayscale is expected; if colored,
OpenCV internally converts).
 threshold1 → Lower hysteresis threshold.
 threshold2 → Upper hysteresis threshold.
 apertureSize (optional) → Size of Sobel kernel (default
3). Used for gradient computation.
 L2gradient (optional) → If True, uses more accurate L2
norm
sqrt(Gx² + Gy²) instead of L1 norm |Gx| + |Gy|.

Image Dilation
Dilation is a morphological operation that makes objects
in a binary or edge-detected image thicker (i.e., grows white
regions).
Think of it like: "spreading out the white pixels" while
shrinking the black areas.
 Imagine your image as a grid of pixels (like a
chessboard).
 Each pixel is either black (0) or white (255) in a binary
image (like edges from Canny).
 Dilation is like making the white regions thicker by
spreading them out.
👉 In short: It grows the bright areas (foreground)
outward.

 Used for:
o Strengthening edges
o Closing small gaps/holes in contours
o Connecting broken parts of text or shapes

Syntax: [Link](src, kernel, iterations=1)


 src → Input image (usually binary or edges).
 kernel → Structuring element (matrix of 0/1 or
uint8).
 iterations → Number of times dilation is applied.

The Math Behind It

dilated_img = [Link](edge_img, (23,23),


iterations=1)
edge_img → the source image (binary edges)

(23,23) → this looks like a size but actually it should be


a structuring element (kernel).
Normally, you’d create it with:
mat_kernel = [Link] ( (3,3), np.uint8)
dilated_img_2 = [Link](edge_img, (mat_kernel),
iterations=1)

Here OpenCV is smart: if you just pass (23,23), it auto-


creates a rectangular kernel of that size.
iterations=1 → how many times you want to apply
dilation. (More iterations = thicker edges).

What happens inside?


Dilation uses a sliding window operation.
Take the 23×23 kernel (a matrix of ones):

🔹 Step 1: Structuring Element (Kernel)


 Kernel is a small matrix (3×3, 5×5, etc.) of 0s and
1s.
 It defines the neighborhood to check around each
pixel.
 Kernel size (23,23) → controls how much
expansion happens.
 Example (3×3 kernel of all 1s):
111
111
111

🔹 Step 2: Sliding the Kernel


 Kernel is centered on each pixel of the input image.
 For binary images:
o If any pixel under the kernel is white
(255) → The central pixel becomes white in
the output.
o Else → remains black.

🔹 Step 3: Iterations
 If iterations > 1, the output of dilation is fed back
into the next dilation step → making the object
thicker and thicker.

🔹 Mathematical Definition (Set Theory Form):


Let:
 Input image = I

 Dilation result = I ⊕ B
 Structuring element (kernel) = B
Then:

Meaning:
For every location (x,y), look at all pixels under the
kernel. Take the maximum.
 Since white = 255 and black = 0, "max" basically
means:
o If there’s at least one white pixel, make it
white.

🔹 Example Walkthrough
Imagine a binary image:
00000
01000
00000
Kernel (3×3 all ones):
111
111
111
 Center kernel on the white pixel (1).
 Since the kernel "covers" neighbors, all 8
surrounding pixels + center become white.
Output:
11100
11100
11100
So the white pixel expanded into a 3×3 block.

🔹 Engineering Insight
 OpenCV internally uses max filter (convolution)
when doing dilation:

Where K is the kernel neighborhood.


 So in practice → it’s just a sliding window that picks
the brightest pixel (white = 255) in the
neighborhood.
Image Erosion
[Link](dilated_img, mat_kernel,
iterations=1)
 dilated_img → your input image (binary or
grayscale, often edges/contours).
 mat_kernel → the structuring element (like a
small stencil, usually a square or circle).
 iterations=1 → how many times you repeat the
erosion.

Internally, erosion is a minimum filter operation.


1. Take the kernel (e.g., a 3×3 square).
[1 1 1
111
1 1 1]
o 1’s = positions where the stencil checks.
o Size and shape define how aggressively you
erode.
2. Slide the kernel over each pixel of the image.
o For each pixel (x,y) → place the kernel
centered on it.
o Look at the neighborhood of pixels under the
kernel.
3. Mathematical Rule (for binary images):
o If all pixels under the kernel are white
(255) → output pixel = white.
o Otherwise → output pixel = black (0).
o This removes “weak” whites around the edges.
4. For grayscale images:
o Erosion = take the minimum value of the
neighborhood.
o Formula:

 I(x,y)I(x,y) = input image intensity at (x,y)


 KK = set of offsets defined by the kernel
 Output pixel = smallest neighbor intensity.
👉 That’s why it’s called a min filter.

🔬 Engineering Insight (what OpenCV does)


 OpenCV implements erosion efficiently using
convolution-like sliding windows.
 It uses:
o Anchor point: the kernel center.
o Border handling: when kernel falls outside
the image, it pads with a border (default =
replicate edges).
o Iterations: repeating erosion multiple times =
applying the kernel again on the result.

🔎 Example (Binary Case)


Say your input (1=white, 0=black):
0110
1110
0100
Kernel = 3×3 of ones.
 At the center pixel, if any kernel element
overlaps with a 0 → output = 0.
 Only fully surrounded 1’s remain white.
Result after erosion:
0000
0100
0000
The shape shrinks.

🧠 Why is it useful?
 Removes small white noise (like tiny specks).
 Separates objects that are lightly touching.
 Thins shapes (useful before contour detection).
Image Cropping
Cropping is basically taking a rectangular portion of an
image and discarding the rest. In OpenCV (Python), images are
stored as NumPy arrays.
So cropping is just array slicing.

1. How Images are Stored in OpenCV


 When you load an image with [Link]("[Link]"),
OpenCV gives you a NumPy array.

2. Indexing / Slicing the Image


Now comes the cropping part.
If we say:
cropped = img[y1:y2, x1:x2]
 y1:y2 → rows → vertical slice (top to bottom)
 x1:x2 → columns → horizontal slice (left to right)
So OpenCV (via NumPy) just takes the block of pixels
between those ranges.

👉Mathematically-

where
 i=0…(y2−y1−1)
 j=0…(x2−x1−1)
 c=0,1,2 (for B, G, R channels)

3. What’s Happening Internally (Engineering View)


When you slice a NumPy array:
 It does not copy pixels immediately (to save memory).
 Instead, it creates a “view” — like saying: “Hey, just
look at this window inside the big image”.
 Only if you modify or explicitly copy it (.copy()), the pixels
get duplicated in memory.
This makes cropping in OpenCV super fast ⚡ because it’s just
adjusting array pointers, not re-drawing the image.

5. Example Walkthrough
Imagine a 5x5 grayscale image (for simplicity):

Now, if we crop img[1:4, 2:5] →


 Rows: 1 → 3 (Python excludes 4)
 Cols: 2 → 4
So result =

5. Why Cropping Works Like This (Memory Math 🧮)


An image array is stored in row-major order (like C arrays).
The memory offset for a pixel at (y, x, c) is computed as:
Address(y,x,c)=BaseAddress+(y×W×C+x×C+c)

 BaseAddress → start of the array


 WW = image width
 CC = channels
So slicing [y1:y2, x1:x2] is basically telling NumPy:
“Start reading from this base offset, and only step through
these rows and columns.”
No pixel-by-pixel copying → just pointer arithmetic.

Drawing White canvas / Black canvas:


Since, 0= black; 1= white:

img = [Link] ((600,600)) => Black canvas


img1 = [Link] ((600,600)) => White canvas

Create a blank colored image


colored_img = [Link]((600,600, 3), np.uint8)
 [Link]((600,600, 3), np.uint8) → Creates a 600x600 pixel image,
where each pixel has 3 values (B, G, R).
 uint8 means each channel goes from 0 to 255.
 Right now it’s pure black (all zeros).
Think of it as: a blank black canvas with RGB channels enabled.

Color the whole image


colored_img[:] = 255,0,255
 [:] means "apply this to the entire image".
 (255,0,255) = BGR format in OpenCV → that’s magenta.
 So now the whole canvas becomes bright pink 💖.

Color just a particular region


colored_img[150:230, 100:500] = 255,168,10
 This picks pixels from row 150 to 230 (height) and column 100 to 500
(width).
 (255,168,10) = orange-ish (BGR).
 Only that subregion turns orange.
So: big pink canvas + one orange strip.

Adding a line
[Link](colored_img, (100,100), (300,300), (255,255,50), 3)
 Draws a line from (100,100) to (300,300).
 Color = (255,255,50) = yellow-ish.
 Thickness = 3.
Another line:
[Link](colored_img, (0,0), (colored_img.shape[1], colored_img.shape[0]),
(255,0,0), 3)
 From (0,0) = top-left corner
 To (colored_img.shape[1], colored_img.shape[0]) = (width, height) =
bottom-right corner.
 So this is a diagonal blue line across the whole image.

Drawing rectangles
[Link](colored_img, (50, 100), (300, 400), (255,255,255), 3)
 Rectangle from (50,100) (top-left) to (300,400) (bottom-right).
 Color = white (255,255,255).
 Border thickness = 3.
Another one:
[Link](colored_img, (50, 100), (300, 400), (255,255,255), [Link])
 Same rectangle, but [Link] means completely filled white box.
Adding circles
[Link](colored_img, (400,300), 50, (255,100,0), 5)
 (400,300) → center of the circle (x=400, y=300).
 50 → radius.
 (255,100,0) → color in BGR (kind of teal/orange
mix).
 5 → thickness of the border.
This draws a hollow circle.

[Link](colored_img, (400,300), 50, (255,100,0),


[Link])
 Same circle, but [Link] → completely filled.
 So instead of just the outline, you get a solid
colored circle.

Adding text
[Link](
colored_img,
"python ka Chilla on Codanics Youtube Channel",
(200,500),
cv.FONT_HERSHEY_DUPLEX,
1,
(255,255,0),
1
)
 "python ka Chilla on Codanics Youtube Channel" →
the string you want to display.
 (200,500) → bottom-left corner of the text
placement.
 cv.FONT_HERSHEY_DUPLEX → the font style (there
are many like FONT_HERSHEY_SIMPLEX,
FONT_HERSHEY_COMPLEX, etc.).
 1 → font scale (controls size).
 (255,255,0) → text color (cyan-ish).
 1 → thickness of the letters.
This slaps a text label on your image.
Joining Images

 Horizontally join Images: hor_img = [Link]((img, img))

 Vertical join Images: ver_img = [Link]((img, img))

Perspective Transformation
Perspective Transformation is used to change the viewpoint of
an image.
It basically "warps" the image so that a quadrilateral region in
the source image becomes a rectangle in the output image (like
fixing the angle of a photo of a paper taken sideways).

👉 Think of it like when you take a photo of a card at an angle —


perspective transform flattens it so it looks like you scanned it.

[Link](src_points,
dst_points)
[Link](image, matrix, (width,
height))

 src_points: 4 points (corners of object in original image).


 dst_points: 4 points (where you want those corners to
map in the output image).
 matrix: 3×3 perspective transformation matrix.
 warpPerspective: applies the transformation matrix on
the image.
Usage / Example

import cv2 as cv
import numpy as np
img = [Link]('resources/[Link]')

# Define source points (corners in input image)


point1 = np.float32([[233,196], [82,471], [522,169],
[715,482]])
# Define target points (rectangle in output image)
width, height = 800, 900
point2 = np.float32([[0,0], [width,0], [0,height], [width,height]])

# Get transformation matrix


matrix = [Link](point1, point2)

# Apply perspective warp


out_img = [Link](img, matrix, (width,
height))

MATH BEHIND IT
💡 Perspective Transformation = Homography

We are finding a 3×3 Homography matrix (H) that maps


any point (x, y) in the input image to a new point (x’, y’)
in the output image.

The 3×3 Homography matrix:

The math works like this:

Then we normalize:

👉 This division by the third row (w) is what introduces


the "perspective effect".
It allows parallel lines to appear as if they converge at a
vanishing point.

How OpenCV builds the matrix:


 You give 4 source points + 4 destination points.
 That gives 8 equations (each point contributes 2
equations: one for x', one for y').

 Now the homography matrix has 8 unknowns (h₁₁...h₃₂, the


last element fixed to 1)

 OpenCV solves this system using linear algebra →


[Link](point1, point2) solves
this system of equations to find the homography matrix
M

 Actually Applying the Transform:


Now we have: out_img = [Link](img, matrix,
(width, height))
 For every pixel (x', y') in the output image (800×900),
OpenCV figures out which (x, y) in the original image
corresponds to it using that matrix M.
 It pulls the pixel color from the original image and paints it
in the new image.
This is why your tilted receipt suddenly looks like a flat scanned
document.
Corner Detection
Harris Corner Detection is a feature detection algorithm used to find corners
(keypoints) in an image.
 A corner is where image intensity changes significantly in both x and y
directions.

[Link](src, blockSize, ksize, k)


 src → Input image (grayscale, float32 type).
 blockSize → Neighborhood size (local window) considered for corner detection.
- Larger block → more averaging, smoother results (robust to noise,
but may miss fine corners).
- Smaller block → more sensitive, detects tiny corners (but also noisy
ones).
Typical values: 2–5
3x3 → fine details, but noisy.
5x5 or 7x7 → smoother, stable detection.
 ksize → Aperture parameter for the Sobel derivative (gradient operator).
o Larger ksize → smoother derivative, less sensitive to small
noise, but blurs fine detail.
o  Smaller ksize → sharp gradient response, but more sensitive to noise.
 k → The sensitivity factor in the Harris response function:

k adjusts the balance between "edge-like" and "corner-like".


Smaller k (~0.04) → favors detecting more corners (risk: false
positives).
Larger k (~0.06–0.1) → stricter, only strongest corners survive.

Usage In OpenCV
import cv2
import numpy as np

# 1. Load image → grayscale


img = [Link]("[Link]") # lots of corners!
gray = [Link](img, cv2.COLOR_BGR2GRAY)

# 2. Convert to float32 (Harris needs float32, not uint8)


gray = np.float32(gray)
# 3. Apply Harris Corner Detection
dst = [Link](src=gray, blockSize=2, ksize=3, k=0.04)

# 4. Dilate result to make corners more visible


dst = [Link](dst, None)

# 5. Threshold for an optimal value (mark corners in red)


img[dst > 0.01 * [Link]()] = [0, 0, 255] # red = corners

# 6. Show results
[Link]("Harris Corners", img)
[Link](0)
[Link]()

⚙️What’s happening behind the scenes here

gray = np.float32(gray) - Harris needs higher precision for gradient math.

dst is basically a heatmap (same size as the image), where each pixel has its Harris "corner
score" R.

o Large positive values → likely corners.


o Small or negative values → edges or flat areas.

Think of dst as a grayscale corner strength map.

[Link](gray, 2, 3, 0.04)
blockSize=2: looks at a 2×2 neighborhood around each pixel when
forming M.
ksize=3: Sobel kernel 3×3 to compute gradients Ix,Iy
k=0.04: Harris constant

Thresholding- img[dst > 0.01 * [Link]()] = [0, 0, 255]


1. [Link]() → find the maximum corner response in the image.
o That’s the strongest corner OpenCV found.

2. 0.01 * [Link]() → set a cutoff at 1% of that max value.


o This means:

 If a pixel’s corner strength ≥ 1% of the best corner, call it a corner.


 If it’s weaker than that → ignore it (noise, weak edge, etc).
3. dst > 0.01 * [Link]() → this creates a mask (a boolean array)
o True = corner
o False = not a corner

4. img[mask] = [0,0,255] → paint those True pixels red on the image.

MATH BEHIND IT
What is a "corner"?
 An edge is where intensity changes a lot in one direction (like going from
black to white).
 A flat region has almost no change in intensity anywhere (all pixels are
similar).
 A corner is where intensity changes a lot in both directions (like the
corner of a rectangle, or a sharp L-shaped turn).
So the problem is:
👉 How can we mathematically measure "change in both directions"?

Step 1: Gradient Calculation


We start with your grayscale image (gray).
We need to know how pixel intensity changes in X and Y direction.
This is done with image gradients. At each pixel we compute:

where I(x,y) is the image intensity.


In OpenCV, it uses Sobel filters (a kind of edge detector) to approximate these
partial derivatives.
Think of Ix as “how much brightness changes left–right”
and Iy as “how much brightness changes up–down.”

2. The Outer Product of Gradients


Now, imagine you just look at one pixel’s gradient.
We could form a little matrix:

 Top-left: "how strong is horizontal change" (Ix2)


 Bottom-right: "how strong is vertical change" (Iy2)
 Off-diagonal: "do X and Y changes happen together?" (IxIy)

So this tiny matrix encodes how the intensity is varying around one pixel.

3. Expanding to a Neighborhood (blockSize window)

But one pixel is noisy. What if it’s just a speck?


So we sum over a small window around the pixel.

Here:

 The summation is over all pixels in a neighborhood (size = blockSize).


 w(x,y)w(x,y)w(x,y) is just a weighting function (box or Gaussian), to smooth things.

This gives a more stable measurement of how gradients behave in that region.

4. Intuition Behind the Matrix

This matrix is like a "map" of how much the image changes inside the window.

 If gradients are mostly horizontal → top-left is large, bottom-right is small.


 If gradients are mostly vertical → bottom-right is large, top-left is small.
 If gradients are mixed in both directions → both diagonal terms are large.
 The off-diagonal terms tell us if changes in X and Y happen together (like a slanted
edge).

 Top-left = how strong changes are in X direction.


 Bottom-right = how strong changes are in Y direction.
 Off-diagonal = how much X and Y changes are correlated.

Step 3: Eigenvalue Analysis (The Magic 🎩✨)


We have our structure tensor (2×2 matrix):

This is a symmetric, positive semi-definite matrix.


Symmetric means it has nice mathematical properties:
👉 it will always have two real eigenvalues (λ1,λ2\lambda_1, \lambda_2λ1,λ2).

1. What are eigenvalues here?


An eigenvalue basically tells you how much stretching happens along a
special direction.
 The “special direction” = eigenvector.
 The “stretch factor” along that direction = eigenvalue.
So for our matrix MMM, the two eigenvalues represent the strength of
intensity variation in two perpendicular directions in the patch.

2. How do we find them?


For a 2×2 matrix:

The eigenvalues are solutions of:

That expands to:


Where:
3. Interpreting the Eigenvalues
Now, the magic:
 Flat region: both eigenvalues small.
→ No strong gradient in any direction.
 Edge: one eigenvalue ≫ the other.
→ Strong gradient only in one direction.
 Corner: both eigenvalues large.
→ Strong gradient in two perpendicular directions.

Step 4: Harris Response Function


Actually computing eigenvalues at every pixel is expensive. Harris gave us a
shortcut.
Instead of calculating λ1,λ2 , he defined a "cornerness score":

where:
 det(M)=λ1λ2
 trace(M)=λ1+λ2
 k is a small constant (like 0.04 to 0.06)
Interpretation:
 If R is large positive → corner.
 If R is negative → edge.
 If R is small → flat region.

Parameters in [Link]
[Link](src, blockSize, ksize, k)
 src → grayscale image (float32).
 blockSize → size of the local neighborhood window (how big the
summation region is for matrix M).
 ksize → aperture parameter for the Sobel operator (3 means it uses a 3×3
filter to compute gradients).
 k → Harris free parameter (usually 0.04–0.06).

What Actually Happens in Code (Simplified Flow)


1. Convert image → grayscale float32.
2. Compute gradients Ix,Iy using Sobel (size = ksize).
3. Build the matrix M for each pixel using a blockSize × blockSize
neighborhood.
4. Compute R = det(M) – k * (trace(M))².
5. Threshold R: large positive → mark as corner.
Shi-TomasiCorner Detection
Shi-Tomasi is an improvement over the Harris Corner
Detector.
Shi-Tomasi modifies Harris by using the minimum eigenvalue
of the structure tensor instead of Harris’s response function.
It gives more reliable results, selecting only the strongest
corners.

[Link]( image, maxCorners, qualityLevel,


minDistance, mask, blockSize, useHarrisDetector, k)

Parameters:
 image → Input grayscale image.
 maxCorners → Maximum number of corners to return (e.g.,
100).
Small number (like 50) → just strongest points (good for simple tracking).
Large number (1000+) → more features, but slower and noisier.

 qualityLevel → Float between 0–1; minimum accepted


corner quality. where R is the Shi-Tomasi
response score and Rmax is the best score in the image.
Choosing values:
 0.01 → Keep corners that are at least 1% as strong as the best one (good default).
 0.1 → Stricter, keeps only the top 10% strongest corners.

 minDistance → Minimum Euclidean distance between


detected corners. After detecting corners, OpenCV rejects
any corner that is closer than minDistance to a stronger
one.
Choosing values:
 Small images → 5–10 pixels.
 Large/high-res images → 20–50 pixels.
 If corners look too clustered → increase this.
 If you miss fine details → decrease this.

 mask → Optional mask (only detect corners in white


regions. White regions → detect corners; black regions →
ignore.
Usage: Focus only on ROI (like face, object, or region of
interest).
 blockSize → Neighborhood size (local window) considered for corner detection.
- Larger block → more averaging, smoother results (robust to noise,
but may miss fine corners).
- Smaller block → more sensitive, detects tiny corners (but also noisy
ones).
Typical values: 2–5
3x3 → fine details, but noisy.
5x5 or 7x7 → smoother, stable detection.
 useHarrisDetector → Boolean (default False → Shi-Tomasi,
True → Harris).
 k → Harris detector free parameter (if Harris is used).

Usage / Example
import cv2
import numpy as np

# Load and convert to grayscale


img = [Link]('[Link]')
gray = [Link](img, cv2.COLOR_BGR2GRAY)

# Shi-Tomasi corner detection


corners = [Link](gray, maxCorners=50,
qualityLevel=0.01, minDistance=10)

# Convert corners to int At each detected corner, we will


corners = np.int0(corners) draw a solid red dot with radius
4.
# Draw detected corners
for c in corners:
x, y = [Link]()
[Link](img, (x, y), 4, (0, 0, 255), -1)

[Link]('Corners', img)

 The function [Link] returns corner


coordinates as floating-point numbers (sub-pixel
precision).
Example: [[[123.45, 67.89]], [[200.12, 150.56]], ...]
 But when we draw shapes on an image (like [Link]),
OpenCV expects integer pixel coordinates (because an
image array is a grid of discrete pixel indices).
Convert corners to int- corners = np.int0(corners)
 After conversion: [[[123, 67]], [[200, 150]], ...]
⚡️ Without this conversion → [Link] may throw an error or
silently misplace the circle (rounding automatically).
Part 2: Draw detected corners
for c in corners:
x, y = [Link]()
[Link](img, (x, y), 4, (0, 0, 255), -1)
✅ Why .ravel()?
 Each corner from goodFeaturesToTrack has shape [[x, y]]
(a 2D array).
 Example: c = [[123, 67]].
 [Link]() flattens it → [123, 67].
 Now you can unpack directly: x, y = 123, 67.

✅ What does [Link] do here?


[Link](img, (x, y), 4, (0, 0, 255), -1)
 (x, y) → center of the circle (the corner location).
 4 → radius in pixels (small circle).
 (0, 0, 255) → BGR color (red).
 -1 → thickness = filled circle (negative means "fill the
inside").
So at each detected corner, it draws a solid red dot with
radius 4.

MATH BEHIND Shi-Tomasi Corner Detection


Step A: The Image Gradient
We first compute image gradients in x and y directions:

These measure how intensity changes in x and y.

Step B: Auto-correlation (Structure Tensor Matrix)


For every pixel, in a small window around it (say 3×3), compute:

This is called the structure tensor or second-moment matrix.


It encodes how much the intensity changes in both directions.

Step C: Eigenvalues of M
The magic:
The eigenvalues (λ1,λ2 ) of this matrix tell us how much change happens
along two orthogonal directions.
If both λ1,λ2 are large → lots of change in both directions → corner 🎯
If one eigenvalue is large, the other small → edge ; If both small → flat
region

Step D: Shi-Tomasi score


Unlike Harris corner detector, Shi-Tomasi uses:

So the corner strength = the smaller eigenvalue.


👉 Why? Because we only want corners where both directions have strong
variation.

Step E: Normalize scores → Find max Rmax


Apply threshold: keep only corners with R > qualityLevel × Rmax
Step E:
Sort by strength
Enforce minDistance: suppress nearby weaker corners.
Return the list of corner coordinates.
Contour Detection-
[Link]

 Contours are curves that join all continuous points along a boundary with the same
color or intensity.
 In computer vision, contour detection is used for object detection, shape analysis,
and image segmentation.
 Think of it like drawing outlines of all blobs/objects in an image.

contours, hierarchy = [Link](


image, # Binary image (usually thresholded or edged)
mode, # Contour retrieval mode
method # Approximation method
)

 Parameters:
o image → input binary image (must be 8-bit single channel, e.g., from
[Link] or [Link]).
o mode → retrieval mode (e.g., cv2.RETR_EXTERNAL, cv2.RETR_TREE).
o method → contour approximation (e.g., cv2.CHAIN_APPROX_SIMPLE,
cv2.CHAIN_APPROX_NONE).
 Returns:
o contours → list of detected contours (each contour = numpy array of (x,y)
points).
o hierarchy → describes the parent-child relationship between contours (useful
if objects are nested).

🟢 3. Usage / Example
import cv2

# Step 1: Read image and convert to grayscale


img = [Link]("[Link]")
gray = [Link](img, cv2.COLOR_BGR2GRAY)

# Step 2: Threshold (binary image required)


_, thresh = [Link](gray, 127, 255, cv2.THRESH_BINARY)

# Step 3: Find contours


contours, hierarchy = [Link](thresh, cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)

# Step 4: Draw contours


[Link](img, contours, -1, (0, 255, 0), 2)

[Link]("Contours", img)
[Link](0)
[Link]()
[Link]

It’s OpenCV’s way of finding the boundaries (outlines) of shapes/objects in a binary


image.
 Think of it like tracing with a pencil around every blob of white pixels on a
black background.

⚙️Internal Working Logic (Step by Step)


1. Input Requirement
 It expects a binary image (black & white), not a normal colored one.
 Example: [Link] or [Link] is usually used before.

👉 Why binary?
Because contours are just outlines. If you give grayscale or colored images, it doesn’t know
what’s “foreground” and “background”.

2. Contour Definition (Math)


A contour is a curve joining all continuous points (x, y) along a boundary of same
intensity.

Formally:

 Here I(x,y) is the pixel intensity.


 For binary images, k = 255 (white).

So contour = a set of connected points making the object boundary.

Understanding How OpenCV Traces Contours (Suzuki’s


Algorithm Simplified)

Imagine you’ve got a sheet of graph paper with a black


background (0) and some white blobs (255).
You give this to OpenCV’s findContours.
Now let’s see what the little OpenCV robot does:
(a) Scan Line by Line
 Think of your robot reading like text: row by row, left to
right.
 Every pixel it checks:
o If it’s black (0), it skips.
o If it’s white (255), it asks:
"Has this white pixel already been part of a contour I traced
earlier?"
 Yes → skip.
 No → 🎉 "New contour found! Let’s trace it."
So this is like spotting the first pixel of a new blob.

(b) Border Following


Now comes the fun part:
The robot needs to walk around the border of the blob like
drawing an outline with a pencil.
How?
1. Start at that white pixel.
Let’s call it P0=(x0,y0)
2. Look around in 8 directions (neighbors):
NW N NE
W P E
SW S SE
(P = current pixel).
3. The rule:
o Move to the next white neighbor (clockwise or
counter-clockwise depending on variant).
o Store that coordinate.
o Repeat the check from there.
4. Keep moving until you’re back at the starting pixel
P0P_0P0.
(That’s how you know the contour is closed).
👉 That’s why it’s called border following.
You never wander inside the blob—you just hug its outline.

Example -
We’ll use a tiny 5×5 binary image with a square blob and a
hole in the middle:
00000
01110
01010
01110
00000
Here:
 0 = background (black)
 1 = object (white)
 The center (2,2) = hole (black inside white)

Step 1: Scan Line by Line


Robot scans row by row.
At (1,1) → first white pixel found.
👉 Mark as start of contour.

00000
0S110
01010
01110
00000
(S = Start pixel)

Step 2: Border Following (walk clockwise)


Look around (1,1) in 8 directions until you find the next white
pixel on the border.
Path so far:
(1,1) → (1,2) → (1,3) → (2,3) → (3,3) → (3,2) → (3,1) → (2,1) →
back to (1,1)
Animation-ish view :
00000
0***0
0*0*0
0***0
00000
(* = border traced)

Step 3: Closing the Loop


Once robot comes back to (1,1),
👉 Outer contour finished! 🎉

Step 4: Detecting Hole (Child Contour)


While scanning, robot notices (2,2) is a black pixel fully
surrounded by white.
This means → it’s a hole inside the blob.
So it starts border-following that hole the same way:
00000
01110
01*10
01110
00000
Path for inner contour (hole):
(2,2) → (2,2) neighbors → forms inner boundary

(c) Hierarchy Handling


First, What Problem Hierarchy Solves?
Imagine you have a donut 🍩:
 Outer ring = boundary of donut.
 Hole in the middle = another contour.
Now, if OpenCV just gave you a list of contours with no
relations, you wouldn’t know:
 Which contour is “outer shape”
 Which contour is “a hole inside”
 Which contour is just another separate object
👉 Hierarchy is needed to describe relationships between
contours.

🧩 How OpenCV Decides Parent vs Child


This comes from Suzuki’s border-following algorithm
(1985) — the one OpenCV uses.
The core idea:
 When scanning, direction of the contour tells if it’s an
outer boundary (object) or an inner boundary (hole).
Rule of Thumb:
1. Outer boundaries (objects) → traced clockwise →
Parent.
2. Inner boundaries (holes) → traced counter-clockwise
→ Child.

🔬 Logic Behind It
 When the robot finds a new white pixel that isn’t part of
any contour,
it checks: Is this pixel’s neighbor background (black)
inside or outside?
 If the background lies outside → it’s starting a new outer
contour.
 If the background lies inside (inside an already found
contour) → it’s a child contour (a hole).
This is why the scanning order (top-to-bottom, left-to-right)
matters — it guarantees that by the time we hit a hole, its
parent (the surrounding outer blob) was already discovered.

🧮 Example with Donut 🍩


Binary image (simplified):
0000000
0111110 ← outer contour start
0100010 ← inner hole detected later
0111110
0000000
 Step 1: Robot hits (1,1) → new contour.
Traces it clockwise → marks as Outer Parent Contour.
 Step 2: While scanning inside, it finds (2,2) (black pixel
inside white ring).
This is a hole → new contour traced counter-clockwise →
marked as Child Contour of the parent.
So hierarchy says:
Contour[0] = outer ring (parent)
Contour[1] = hole (child of 0)

🏗 How OpenCV Stores This (Hierarchy Array)

For each contour, OpenCV gives a list of 4 integers:

[ Next, Previous, FirstChild, Parent ]

 Next → index of the next contour at the same hierarchy level.


 Previous → index of the previous contour at the same hierarchy level.
 FirstChild → index of the first child contour (if it has holes).
 Parent → index of the parent contour (if this contour is inside something).

If something doesn’t exist → it’s -1.

Simple Example – Just One Shape


Image:

00000
01110
01110
01110
00000
(One rectangle blob, no holes)
Contours:
 Contour[0] → the rectangle.

Hierarchy:
Hierarchy[0] = [ -1, -1, -1, -1 ]
👉 No siblings, no parent, no child.

Donut Example 🍩 (One Object with One Hole)


Image:

0000000
0111110
0100010
0111110
0000000
Contours:

 Contour[0] → outer rectangle (the donut shape).


 Contour[1] → hole inside (the empty space).

Hierarchy:
Hierarchy[0] = [ -1, -1, 1, -1 ]
Hierarchy[1] = [ -1, -1, -1, 0 ]
Explanation:

 Contour[0] has a child (1).


 Contour[1] has a parent (0).
 No “next” or “previous” because there’s only one object.

Multiple Objects Example 🟦🟩


Image has two separate blobs:
000000
01110 000
01110 0110
01110 000
000000
Contours:

 Contour[0] = first square.


 Contour[1] = second square.

Hierarchy:
Hierarchy[0] = [ 1, -1, -1, -1 ]
Hierarchy[1] = [ -1, 0, -1, -1 ]
Explanation:

 Contour[0] has Next = 1 (meaning another contour exists at same level).


 Contour[1] has Previous = 0.
 Neither has children or parents → both are top-level objects.
Nested Example 🎂 (Shape with Hole, and Hole has
Another Shape Inside)
Think of a cake:

 Big outer rectangle (contour 0).


 Hole inside it (contour 1).
 Small white blob inside the hole (contour 2).

Hierarchy:
Hierarchy[0] = [ -1, -1, 1, -1 ] # contour 0 has child (1)
Hierarchy[1] = [ -1, -1, 2, 0 ] # contour 1 is child of 0, and has its
own child (2)
Hierarchy[2] = [ -1, -1, -1, 1 ] # contour 2 is child of 1
This builds a tree:
Contour 0 (outer shape)
└── Contour 1 (hole)
└── Contour 2 (inner shape)

🔑 Rules to Remember
1. Next / Previous → move sideways (siblings).
2. FirstChild → move downward (holes inside).
3. Parent → move upward (who contains this contour).

It’s literally a linked tree structure, stored as a flat array.

✅ Why This Logic Works


Because the scanning order always discovers parent
contours before their children:
 Outer boundary must be found first (since scan hits it
before the inner hole).
 When an inner contour is found, the algorithm already
knows it lies inside another contour → so it sets that as its
parent.
The clockwise/counter-clockwise tracing direction ensures
OpenCV knows if the contour is outer (object) or inner (hole).

Awesome 🙌 you’re asking the golden question → “what does the hierarchy data structure
really look like in memory?”
Let’s peel it apart.

The Return of findContours: Contours Array &


Hierarchy Array
When you call:
contours, hierarchy = [Link](img, cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE)
You get back:

 contours → a Python list of numpy arrays.


o Each element = one contour.
o Each contour is an array of (x,y) points.
o Example: contours[0] = first contour, contours[1] = second, etc.
o Each contour = a numpy array of shape (N, 1, 2) where:
 N = number of points in that contour
 1 = OpenCV keeps it as column-style data
 2 = (x, y) coordinates of the points

Example:
contours[0] =
array([[[10, 20]],
[[11, 21]],
[[12, 22]],
... ])

 hierarchy → a numpy array of shape (1, num_contours, 4)


o Each [i] corresponds to contours[i].
o hierarchy[i] gives the [Next, Previous, FirstChild, Parent] info
about contours[i].
o Each entry has 4 integers:
o [Next, Previous, FirstChild, Parent]
Example:
hierarchy =
array([[[ 1, -1, -1, -1],
[-1, 0, -1, -1]]], dtype=int32)
This means:

 hierarchy[0][0] = [ 1, -1, -1, -1 ] → contour 0 has next = contour 1.


 hierarchy[0][1] = [-1, 0, -1, -1 ] → contour 1 has previous = contour 0.

Visual Representation in Memory


Imagine you have 3 contours. The data looks like this:
contours = [
[ (x1,y1), (x2,y2), (x3,y3), ... ], # contour 0 points
[ (x1,y1), (x2,y2), (x3,y3), ... ], # contour 1 points
[ (x1,y1), (x2,y2), (x3,y3), ... ] # contour 2 points
]

hierarchy = [
[ next0, prev0, child0, parent0 ], # info about contour 0
[ next1, prev1, child1, parent1 ], # info about contour 1
[ next2, prev2, child2, parent2 ] # info about contour 2
]
So:
 contours[i] = shape itself.
 hierarchy[i] = relationship info for that shape.

Mini Example (Donut 🍩 with hole)


contours = [
[ (10,10), (10,20), (20,20), (20,10) ], # outer rectangle
[ (13,13), (13,17), (17,17), (17,13) ] # inner hole
]

hierarchy = [
[ -1, -1, 1, -1 ], # contour 0 has child = 1
[ -1, -1, -1, 0 ] # contour 1 has parent = 0
]

4. Contour Approximation
When OpenCV finds a contour, it is basically “walking along” the boundary pixel by pixel,
collecting coordinates.
Now imagine a rectangle:

 If you walk along its boundary pixel by pixel, you’ll get hundreds of points (every
pixel along the edges).
 But do we really need all those points? The shape is just 4 straight lines!

That’s where contour approximation modes come in:

 CHAIN_APPROX_NONE

 Stores every single boundary pixel.


 Rectangle (100×100): ~400 points.
 Data: looks like [(0,0), (1,0), (2,0), ..., (99,0), (99,1), ..., (0,99)]

👉 Good if you really need pixel-level detail (like handwriting or jagged shapes).
But wastes memory for simple shapes.

 CHAIN_APPROX_SIMPLE

 Compresses points that lie on a straight line (horizontal, vertical, diagonal).


 Keeps only the endpoints of line segments.

So that 100×100 rectangle? Instead of 400 points, you just get 4 corner points:
[(0,0), (100,0), (100,100), (0,100)]
👉 This is almost always what we want, because the rectangle can be fully reconstructed from
those corners.
5. Hierarchy (RETR Modes)

For each contour, OpenCV stores 4 values:

[Next, Previous, First_Child, Parent]

👉 Example: hierarchy[0][i] = [next, prev, child, parent]

RETR modes:

1. RETR_EXTERNAL

 Finds only the outermost contours (ignores holes inside).


 Hierarchy = flat (no parent-child relationships).
 Use case: When you only care about "shapes" and not holes inside them.

Example: Letter O → only the outer circle, ignores the inner hole.

2. RETR_LIST

 Finds all contours, but doesn’t assign hierarchy.


 So every contour’s parent = -1, child = -1.
 Use case: When you just want all contours individually, no nesting.

Example: Letter O → you get both outer and inner circles, but OpenCV won’t tell you that
the inner one is a hole of the outer one.

3. RETR_TREE

 Finds all contours + builds the full hierarchy (parents, children, nesting).
 Perfect for when contours are inside each other multiple times.

Example: Think of rings inside rings:


Outer ring → has a child → inner ring → has its own child → another smaller
ring.
RETR_TREE tells you exactly which one is inside which.

Visualizing Example
Imagine an image with:

 A rectangle (outermost).
 A circle inside the rectangle (hole).
 A tiny rectangle inside that circle.
 With RETR_EXTERNAL → You only get the big rectangle.
 With RETR_LIST → You get all 3, but no info about who is inside whom.
 With RETR_TREE → You get all 3 + hierarchy mapping (rectangle → circle →
small rectangle).
Splitting & Merging Color Channels
 Splitting ([Link]) → Separates an image into its individual color
channels (e.g., Blue, Green, Red in OpenCV’s BGR format).

Syntax: b, g, r = [Link](img)

 Merging ([Link]) → Combines individual channels back into a


single multi-channel image.

Syntax: imgMerged = [Link]((b, g, r))

3. Usage / Example
import cv2
import [Link] as plt
# Read image in color (BGR order)
img = [Link]("[Link]", cv2.IMREAD_COLOR)

# Split into channels


b, g, r = [Link](img)

# Visualize the channels


[Link](figsize=[20,5])
[Link](141); [Link]( r, cmap='gray'); [Link]("Red
Channel")
[Link](142); [Link]( g, cmap='gray'); [Link]("Green
Channel")
[Link](143); [Link]( b, cmap='gray'); [Link]("Blue
Channel")

# Merge channels back into original


imgMerged = [Link]((b,g,r))

[Link](144); [Link](imgMerged[:,:,::-1]); [Link]("Merged


Output")
[Link]()

Image Representation

For BGR:
img[y, x, 0] = Blue intensity at pixel (x,y)
img[y, x, 1] = Green intensity at pixel (x,y)
img[y, x, 2] = Red intensity at pixel (x,y)
Splitting ([Link])
 Mathematically, splitting is just slicing along the 3rd axis of the tensor:

 Internally, OpenCV doesn’t loop pixel-by-pixel; it uses optimized array slicing in


C++ to extract the planes efficiently.

Merging ([Link])
 Merging is simply stacking 2D matrices along the 3rd axis:
I(x,y,:)=[b(x,y),g(x,y),r(x,y)]

Under the hood, OpenCV performs a channel-wise concatenation:

 [Link]((b,g,r))
 All channels must have the same height and width, otherwise merging fails.
MatplotLib Plotting :
[Link](figsize=[20,5])

 Creates a new figure (the canvas to draw on).


 figsize=[20,5] → sets the size of the figure in inches:
o width = 20 inches
o height = 5 inches
 So it’s making a long rectangle canvas → good for placing multiple plots side by
side.

🔎 [Link](141)
This one is a bit tricky at first glance. It’s shorthand for [Link](nrows, ncols,
index).

 141 → means 1 row, 4 columns, and this is the 1st subplot.


o The digits are read as:
 1 → number of rows.
 4 → number of columns.
 1 → position index (which subplot to activate).

So effectively:

 [Link](141) = make a grid of 1x4 plots and select the first slot.
 Then when you call [Link]() or [Link](), it applies to that slot.

Next calls:

 [Link](142) → 1 row, 4 columns, 2nd slot.


 [Link](143) → 3rd slot.
 [Link](144) → 4th slot.

So you end up with 4 images neatly arranged side by side.


Flipping Images- [Link]

Flips the array in one of three different ways (row and column indices are 0-based):

dst = [Link]( src_image, flipCode )

The function has 2 required arguments:

1. src: input image


2. flipCode: a flag to specify how to flip the array;
- 0 means flipping around the x-axis
- Positive value (for example, 1) means flipping around y-axis
- Negative value (for example, -1) means flipping around both axes.

img_horizontal_flip = [Link](img, 1)
img_vertical_flip = [Link](img, 0)
img_flipped_both = [Link](img, -1)

# Show the images


[Link](img_horizontal_flip);[Link]("Horizontal Flip");
[Link](img_vertical_flip);[Link]("Vertical Flip");
[Link](img_flipped_both);[Link]("Both Flipped");
[Link](img);[Link]("Original");
Brightness Adjustment
Brightness adjustment means shifting the intensity of all pixels by a constant value.
 Increase brightness → Add a constant value.
 Decrease brightness → Subtract a constant value.
 OpenCV handles clipping automatically (values stay in [0, 255] for uint8 images).

Syntax

matrix = [Link]([Link], dtype="uint8") * value


brighter = [Link](img, matrix)
darker = [Link](img, matrix)

 value → brightness adjustment level (e.g., 50).


 [Link]() and [Link]() handle overflow/underflow safely (unlike raw NumPy addition).

Usage
import cv2
import numpy as np

# Load image
img = [Link]("[Link]")

# Create adjustment matrix


matrix = [Link]([Link], dtype="uint8") * 50

# Apply brightness changes


brighter = [Link](img, matrix) # increase brightness
darker = [Link](img, matrix) # decrease brightness

# Show results
[Link]("Original", img)
[Link]("Brighter", brighter)
[Link]("Darker", darker)
[Link](0)
[Link]()

MATH BEHIND Brightness Adjustment


Let’s say a pixel intensity is p(x, y, c) where:

 x, y → pixel position
 c → channel (R, G, B)

Brightness adjustment is:

 p(x,y,c) → the original pixel value at location (x, y) and color channel c (Red, Green,
or Blue).
 p′(x,y,c) → the new pixel value after brightness adjustment.
 Δ → how much you want to brighten (+) or darken (–).
 clip(..., 0, 255) → makes sure pixel values don’t go below 0 or above 255.

🔦 Why this formula matters

Digital images are stored in 8-bit per channel → values range from 0 to 255:
 0 = black
 255 = pure white
So, brightness adjustment is basically:
 Add Δ → push values toward white → image looks brighter.
 Subtract Δ → push values toward black → image looks darker.
BUT… if you don’t clip, you’d break the rules:
 Example: 250 + 50 = 300 → not valid (since max is 255).
 Example: 10 – 50 = –40 → not valid (since min is 0).
So the clip function is like the safety guard.
Contrast Adjustment

Contrast in images refers to the difference in intensity (brightness levels) between pixels.

 Low contrast → the image looks washed out, with little difference between light and
dark regions.
 High contrast → sharp difference between light and dark areas, making details pop
out more.

In image processing, contrast adjustment is simply a linear scaling of pixel values.

Mathematically, for each pixel:

 I(x,y) → original pixel intensity (0–255).


 α → contrast control factor ( >1 increases contrast, <1 decreases contrast).
 β → brightness offset (optional, shifts values up or down).

Syntax:

matrix = [Link]([Link]) * alpha


contrast_img = np.uint8([Link](np.float64(img),
matrix))

Usage:
import cv2
import numpy as np

# Read an image
img_rgb = [Link]("[Link]")

# Reduce contrast
matrix1 = [Link](img_rgb.shape) * 0.8
img_rgb_darker = np.uint8([Link](np.float64(img_rgb), matrix1))

# Increase contrast
matrix2 = [Link](img_rgb.shape) * 1.2
img_rgb_brighter = np.uint8([Link](np.float64(img_rgb), matrix2))

 With 0.8, the image looks flatter, with less difference between light and dark.
 With 1.2, darks get darker and lights get lighter → higher contrast.
Let’s break what actually happens under the hood in OpenCV when you do this:

1. Conversion to Float
o Pixel values in images are usually uint8 (0–255).
o Multiplying directly on uint8 may cause rounding/truncation.
o So np.float64(img_rgb) converts the image to floating point to preserve
precision.

2. Element-Wise Multiplication
o The operation [Link](image, matrix) computes:
o

o Here, (i,j,k) refers to pixel location (i,j) and color channel k (R/G/B).
o Since matrix is just a constant (0.8 or 1.2 everywhere), it scales every pixel
intensity.

3. Clipping / Saturation
o After multiplication, values may exceed 255 (bright pixels amplified) or
drop below 0.
o When converting back to np.uint8, OpenCV clips values:
 If result < 0 → becomes 0.
 If result > 255 → becomes 255.
o This keeps valid image intensity levels.

4. Why Contrast Changes?


o Suppose you have two pixel values: 100 (medium gray) and 200 (bright).
o Scaling by 0.8 → (80, 160) → difference shrinks → less contrast.
o Scaling by 1.2 → (120, 240) → difference grows → more contrast.
o That difference growth/shrinkage is contrast adjustment.
OpenCV [Link]() for
Brightness/Contrast

[Link]() is a combined operation that adjusts both contrast and


brightness of an image.
It applies a linear transformation to each pixel:

 I(x,y)→ input pixel intensity (0–255)


 α → contrast factor ( >1 increases contrast, <1 decreases contrast)
 β → brightness offset (adds/subtracts light intensity uniformly)
 Output → scaled, shifted, clipped to 0–255, and converted to 8-bit (uint8).

Key feature → The "Abs" means absolute value is taken before converting to uint8. This
avoids negative pixel values messing up the image.

2. Syntax
dst = [Link](src, alpha=contrast,
beta=brightness)

 src → input image


 alpha (default = 1.0) → contrast control
 beta (default = 0) → brightness control
 dst → output image with applied transformation

3. Usage / Example
import cv2

# Load an image
img = [Link]("[Link]")

# Contrast and Brightness adjustment


alpha = 1.5 # contrast factor
beta = 40 # brightness offset

adjusted = [Link](img, alpha=alpha, beta=beta)

👉 In this example:

 alpha=1.5 → makes darks darker, lights lighter → higher contrast.


 beta=40 → adds +40 brightness to every pixel → brighter image overall.

Let’s peek inside the engine of [Link]() 🚗⚙️:


1. Linear Transformation
o Each pixel value is computed as:

temp=α⋅src(i,j,k)+β

Applied independently for every pixel (i,j) and every channel k (B, G, R).

2. Absolute Value
o If any calculation gives a negative number (e.g., after subtracting brightness
with a negative beta),

temp=∣temp∣

This ensures no weird negative values appear when converting to 8-bit.

3. Clipping
o Results might go out of the valid 8-bit range [0, 255].
o OpenCV clips automatically:
 If temp > 255 → set to 255
 If temp < 0 → set to 0 (after abs, rarely happens).
4. Conversion to 8-bit
o Final step: cast the values back to uint8 → so the image is displayable.
ORB Feature Detector in OpenCV
ORB (Oriented FAST and Rotated BRIEF) finds important
feature points (keypoints) and their unique descriptors in an
image.

What is a "keypoint"?
 A keypoint = a special pixel location that stands out.
Think of corners, blobs, or places in the image where intensity
changes sharply.
(Imagine a chessboard corner: it’s easy to spot and recognize even
if rotated.)
ORB tries to find up to nfeatures (500 here) of the "most interesting" ones.

Syntax:
orb = cv2.ORB_create(nfeatures=500)
keypoints, descriptors =
[Link](img_gray, None)

👉 This tells OpenCV:


“Hey, run ORB (Oriented FAST and Rotated BRIEF) on my grayscale
image, give me up to 500 important feature points (keypoints)
and their unique fingerprints (descriptors).”

What is a "descriptor/ fingerprint "?


 A descriptor = the "what" (a unique fingerprint that describes the
appearance around that keypoint). A descriptor is just a
compressed digital signature of the local patch around the
keypoint.
👉 Think of a keypoint as a house address (x,y position), and the descriptor
as the detailed description of the house (red roof, two windows, door on
the left).

That way, when comparing images, ORB doesn’t just say:


“I found a corner at (100,200)” (because another image might also have a
corner there).
It says: “I found a corner at (100,200) with this unique fingerprint, let’s
see if another image has the same fingerprint nearby.”

"fingerprint" lets you say:


“Hey, that corner in image A looks the same as that corner in image B.”
MATH BEHIND ORB

🧩 Step 1: Keypoint Detection → FAST algorithm


 ORB starts with FAST (Features from Accelerated Segment Test) corner detector.
 For each pixel p:
1. Take a circle of 16 pixels around p.
2. Compare each neighbor’s intensity I(n) to I(p).
3. If at least a contiguous arc of N pixels are
brighter or darker than I(p) ± threshold, mark p as a keypoint.

➡️Result: FAST gives a LOT of corners, but it doesn’t tell us which ones are good.

🧩 Step 2: Ranking with Harris Corner Measure


 To keep only strong, stable corners, ORB applies Harris corner score

Where M is the second-moment matrix of image gradients.

 High R = strong corner.


 Sort all corners by R.
 Keep top 500 (since you asked for nfeatures=500).

🧩 Step 3: Orientation Assignment (the “O” in ORB)


ORB is rotation invariant. It finds an angle for each keypoint so it doesn’t get confused if the
image is rotated.

Example - You found a nice corner (keypoint).


Now if I rotate the whole image 90° — that corner is still there, but “which way it’s facing”
changes.
If we don’t record the “facing direction,” our descriptors (the fingerprint) would mismatch
when comparing rotated images.

👉 Solution: ORB gives each keypoint its angle of orientation.


This way, the fingerprint can be rotated to match the keypoint’s actual direction.

Look around the corner in a small patch.

 Find where the “brightest average point” is (centroid).


 Measure the angle from center → centroid.

So each keypoint gets an angle = "compass direction."

Why Do This?

 Later, when ORB builds the keypoint’s “fingerprint” (descriptor), it rotates the
fingerprint pattern by this angle.
 So even if the whole image is rotated, the fingerprint is still the same.
 That’s why ORB is rotation invariant.

🧩 Step 4: Builidng the Descriptor → BRIEF (Binary Robust


Independent Elementary Features)

Building the "fingerprint" for each keypoint.

ORB uses BRIEF:

1. Take a patch of pixels( Sample pairs) around the keypoint (like a small 31×31 square
cutout).
2. Choose pairs of pixel positions inside the patch.
Example: compare pixel at (3,5) vs pixel at (7,9).
3. For each pair:
o If left pixel intensity < right pixel intensity → write 1
o Else → write 0
4. Do this for 256 pairs → you get a string of 256 bits (like 1010100110...).

That’s the descriptor (the fingerprint).

🧩 Why it works

 That 256-bit string is like a barcode for the local area around the keypoint.
 Two patches that look the same (even if rotated, thanks to step 4 with orientation) →
produce the same or very similar descriptors.
 Matching between images is then just comparing barcodes using Hamming distance
(counting different bits).
🧩 Step 6: Rotation Awareness → rBRIEF
We already learned that BRIEF makes a fingerprint by comparing pixel pairs inside a
patch.
⚠️Problem: plain BRIEF is not rotation invariant.

 Imagine a “T” shape in an image.


 If you rotate the whole image by 90°, the pixel pairs you compare inside the patch
also rotate.
 That means the binary string changes, even though it’s the same corner.
 So plain BRIEF fails on rotated images.

👉 ORB’s Fix: rBRIEF (Rotated BRIEF)

 Remember the orientation angle θθθ we calculated using the intensity centroid?
 ORB takes the sampling pattern (the chosen pixel pairs for comparisons) and rotates
that pattern by θ.

So, even if the whole image rotates, ORB rotates its "measurement stencil" the same way.
➡️Fingerprint stays consistent.
Analogy:

 Imagine you’re wearing glasses with little arrows drawn on them to measure
directions.
 If you tilt your head sideways (rotate the image), you also tilt the glasses → the
arrows still line up the same way.

🧩 Step 7: Matching Descriptors


Now that each keypoint has a binary fingerprint (descriptor), we need a way to compare
fingerprints between two images.
Since descriptors are just bit strings (0s and 1s), comparison is very fast.
👉 The tool: Hamming distance.

⚖️What is Hamming distance?


It’s simply the number of bits that are different between two binary strings.
Formula:

Where:

 a,b = two binary strings (descriptors).


 ai,bi = ith bit of each string.

⊕ = XOR (exclusive OR → 1 if bits are different, 0 if same).




🔎 Example
Say we have two toy descriptors (8 bits each):

 A = 10110010
 B = 10010110

Compare bit by bit:


A: 1 0 1 1 0 0 1 0
B: 1 0 0 1 0 1 1 0
^ ^ ^
Differences at positions 3, 5, 6 → 3 differences.
So: dH(A,B)=3

👉 The smaller the distance, the more similar the descriptors are.

Draw the feature points (keypoints) detected using ORB

[Link](
img,
keypoints,
outImage=[Link]([]),
color=(255, 0, 0),
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_
KEYPOINTS)
The Descriptor Matcher Tool-
[Link]
matcher=
cv2.DescriptorMatcher_create(cv2.DESCRIPTOR_MATCHER_BRUTEFORCE_HAMMI
NG)

 DescriptorMatcher = a tool that compares descriptors (fingerprints).


 "BRUTEFORCE" means compares every descriptor from image1 against every
descriptor from image2.
 HAMMING = the distance measure we use (counting differing bits).
o Perfect for ORB, because ORB descriptors are binary strings (0s/1s).

So → this matcher = “Go brute-force check all pairs using Hamming distance.”

🧩 Step 2: Use the Matcher tool in the Match Function


matches = [Link](descriptor1, descriptor2, None)

 des1: descriptors from image 1 (say 500×32 array → 500 keypoints, each 32-byte
descriptor).
 des2: descriptors from image 2

What happens internally:

1. For each descriptor in des1, compare it with every


descriptor in des2.
2. Compute Hamming distance for each pair.

(count differing bits).

3. Find the best match (the one with smallest distance).


4. Store the result in a [Link] object.

🧩 Step 3: The Output


matches = list of [Link] objects.
Each DMatch stores:

 queryIdx → index of descriptor in des1


 trainIdx → index of matching descriptor in des2
 distance → the actual Hamming distance
👉 So, if distance = 5, that means only 5 bits differ between the two binary strings — very
good match!

🔎 Mini Example
Let’s say:

 des1 has 2 descriptors: [10110010, 11001010]


 des2 has 2 descriptors: [10010110, 11001110]

For first descriptor (10110010):

 Compare with 10010110 → distance = 3


 Compare with 11001110 → distance = 2 → best match

For second descriptor (11001010):

 Compare with 10010110 → distance = 2


 Compare with 11001110 → distance = 1 → best match

So matches will contain 2 DMatch objects with distances 2 and 1.

✅ Super Simple Summary

 ORB gives you descriptors (binary fingerprints).


 BruteForce-Hamming matcher says:
o “Compare every fingerprint in image1 with every fingerprint in image2.”
o “Use Hamming distance (count different bits).”
o “Keep the closest match for each one.”
 Result = list of best matches, with distances you can sort/threshold.

Draw the matches


[Link]( image1, keypoints1, image2, keypoints2, matches, None )
🧩 Find Homography- Mapping Matrix b/w the
Matched Points

🔹 1. Extract location of good matches


points1 = [Link]((len(matches), 2), dtype=np.float32)
points2 = [Link]((len(matches), 2), dtype=np.float32)

for i, match in enumerate(matches):


points1[i, :] = keypoints1[[Link]].pt
points2[i, :] = keypoints2[[Link]].pt

 keypoints1 → keypoints detected in image 1.


 keypoints2 → keypoints detected in image 2.
 Each match tells you:
o queryIdx → index of keypoint in image 1.
o trainIdx → index of keypoint in image 2.

So we’re building two arrays:

 points1[i] = coordinates (x,y)(x, y)(x,y) of a good feature in image 1.


 points2[i] = coordinates (x,y)(x, y)(x,y) of the corresponding matched feature in
image 2.

Basically → we now have a set of matching point pairs:


(x1,y1)↔(x2,y2) for all good matches.

🔹 2. Find Homography

h, mask = [Link](points2, points1, [Link])

What does findHomography() do?


Imagine you have two images of the same scene, but taken from different viewpoints (like
two photos of the same wall, but from different angles).
You’ve detected matching points between them (say corners of posters, windows, etc.).
Now:
👉 The goal of findHomography() is to compute a transformation
matrix H(3×3) that maps points from one image (say points2) to
the other (points1).
This matrix H is called a homography.
🧮 The Math: Homography Basics
A homography relates 2D points between two planes (like two camera views of a flat wall).

The core equation is:

 (x, y) = point in image 2


 (x’, y’) = corresponding point in image 1
 H = 3×3 homography matrix
 ~ means "equal up to scale" (since homogeneous coordinates allow scaling)

So:

Where hij are entries of the matrix H.

⚙️Inside [Link](points2, points1, [Link])

1. Input:
o points2 (source points)
o points1 (destination points)
o method=[Link] (robust fitting method)
2. Step 1: Build equations
Each pair of matching points gives 2 equations based on the above formulas.
For N matches, you get 2N equations.
Since H has 8 unknowns (9 values but scale factor makes 1 redundant), you need at
least 4 point pairs.

Example equation form (for one correspondence (x, y) → (x’, y’)):


Put all these into a big linear system A⋅h=0A \cdot h = 0A⋅h=0,
where h is the flattened 9-vector of matrix H.
Solve using SVD (Singular Value Decomposition) to get the best-fit H.

Why RANSAC?
Real-world matching has outliers (bad matches).
If you solve with all points, even 1 bad match can ruin H.
That’s why we use RANSAC (RANdom SAmple Consensus):

 Randomly pick 4 point pairs.


 Compute candidate homography H.
 Project all source points → check how well they map to destination points.
 Count inliers (those that match within a tolerance, e.g. <3px).
 Repeat many times → keep the H with max inliers.
 Finally recompute H using all inliers (clean set).

👉 That’s why the function returns:

 h = the best homography matrix


 mask = 0/1 array marking which matches were inliers (good matches)

We’ll use this h in the


warpPerspective step to actually align the pixels.

Step 4: Warping

 Once H is found, apply it using:

 OpenCV’s warpPerspective() performs this


mapping for every pixel.

✅ Result: Image image2 is geometrically transformed to perfectly


overlap with image1.
Warped Image by matching
features(Keypoints) from Original
Original
FULL FLOW:

# Convert images to grayscale


im1_gray = [Link](im1, cv2.COLOR_BGR2GRAY)
im2_gray = [Link](im2, cv2.COLOR_BGR2GRAY)

# Detect ORB features and compute descriptors.


MAX_NUM_FEATURES = 500
orb = cv2.ORB_create(MAX_NUM_FEATURES)
keypoints1, descriptors1 = [Link](im1_gray, None)
keypoints2, descriptors2 = [Link](im2_gray, None)

# Display
im1_display = [Link](im1, keypoints1, outImage=[Link]([]),
color=(255, 0, 0),
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
im2_display = [Link](im2, keypoints2, outImage=[Link]([]),
color=(255, 0, 0),
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
## Step 3 : Match keypoints in the two image

# Match features.
matcher =
cv2.DescriptorMatcher_create(cv2.DESCRIPTOR_MATCHER_BRUTEFORCE_
HAMMING)
matches = [Link](descriptors1, descriptors2, None)

# Sort matches by score


[Link](key=lambda x: [Link], reverse=False)

# Remove not so good matches


numGoodMatches = int(len(matches) * 0.1)
matches = matches[:numGoodMatches]

# Draw top matches


im_matches = [Link](im1, keypoints1, im2, keypoints2,
matches, None)

## Step 4: Find Homography- Extract location of good


matches
points1 = [Link]((len(matches), 2), dtype=np.float32)
points2 = [Link]((len(matches), 2), dtype=np.float32)

for i, match in enumerate(matches):


points1[i, :] =

keypoints1[[Link]].pt
points2[i, :] = keypoints2[[Link]].pt
# Find homography
h, mask = [Link](points2, points1, [Link])

## Step 5: Warp image- Use homography to warp image


height, width, channels = [Link]
im2_reg = [Link](im2, h, (width, height))

[Link](122); [Link](im2_reg); [Link]('off'); [Link]("Scanned


Form");

You might also like