OpenCV Computer Vision Course
4 / 13
Lesson 4 of 13

Lesson 04: Basic Image Processing

4 min readViet-Anh NguyenViet-Anh Nguyen

Image processing is the process of manipulating and analyzing digital images to improve their quality or extract useful information. In this lesson we cover the fundamental operations: reading, writing, displaying, and transforming images.

0. Download the Test Image

All examples in this lesson use the same image — a street scene with a bus, pedestrians, and cars from Ultralytics' public image library. Run this once at the start:

import urllib.request
import cv2

urllib.request.urlretrieve(
    "https://ultralytics.com/images/bus.jpg", "bus.jpg"
)
img = cv2.imread("bus.jpg")
print(f"Loaded: {img.shape}")  # (1080, 810, 3)  →  1080×810 px, BGR

Street scene used throughout this lesson

1. Reading and Writing Images

Read and display

import cv2

img = cv2.imread("bus.jpg")

cv2.imshow("Bus Scene", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: a 810×1080 window opens showing the bus and pedestrians

cv2.imread() returns a NumPy array with shape (height, width, channels). If the file is not found it returns None — always check:

img = cv2.imread("bus.jpg")
if img is None:
    raise FileNotFoundError("Image not found — check the path")

Write to file

import cv2

img = cv2.imread("bus.jpg")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

cv2.imwrite("bus_gray.jpg", gray)
print("Saved bus_gray.jpg")
# Expected: a grayscale JPEG written to disk, ~120 KB

2. Displaying Images

import cv2
import numpy as np

img = cv2.imread("bus.jpg")

# Side-by-side: original and grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray_bgr = cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)  # match channels for hconcat

side_by_side = cv2.hconcat([img, gray_bgr])
cv2.imshow("Original | Grayscale", side_by_side)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: left half is the color scene, right half is the same scene in grayscale

3. Manipulating Images

Resize

import cv2

img = cv2.imread("bus.jpg")
h, w = img.shape[:2]
print(f"Original: {w}×{h}")  # 810×1080

# Resize to fixed dimensions
small = cv2.resize(img, (400, 300))
print(f"Resized: {small.shape}")  # (300, 400, 3)

# Resize keeping aspect ratio — scale by 50%
half = cv2.resize(img, (0, 0), fx=0.5, fy=0.5)
print(f"Half size: {half.shape}")  # (540, 405, 3)

cv2.imshow("Original", img)
cv2.imshow("400×300", small)
cv2.imshow("50% scale", half)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: three windows — original large, squished small, and correctly proportioned half

Tip: Use cv2.INTER_AREA when shrinking for best quality; use cv2.INTER_LINEAR or cv2.INTER_CUBIC when enlarging:

small = cv2.resize(img, (400, 300), interpolation=cv2.INTER_AREA)

Rotate

import cv2

img = cv2.imread("bus.jpg")

rot_90  = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE)
rot_180 = cv2.rotate(img, cv2.ROTATE_180)
rot_ccw = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE)

cv2.imshow("Original",   img)
cv2.imshow("90° CW",     rot_90)
cv2.imshow("180°",       rot_180)
cv2.imshow("90° CCW",    rot_ccw)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: bus appears sideways (90°), upside-down (180°), or mirrored sideways (CCW)

For arbitrary rotation angles, use cv2.getRotationMatrix2D():

import cv2

img = cv2.imread("bus.jpg")
h, w = img.shape[:2]
center = (w // 2, h // 2)

M = cv2.getRotationMatrix2D(center, angle=45, scale=0.8)
rotated = cv2.warpAffine(img, M, (w, h))

cv2.imshow("45° rotation", rotated)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: bus tilted 45° clockwise, scaled to 80%, with black corners

Crop

Cropping is just NumPy array slicing — img[y1:y2, x1:x2]:

import cv2

img = cv2.imread("bus.jpg")

# Crop out roughly the bus in the upper-right area
bus_crop = img[50:600, 0:500]

cv2.imshow("Full Scene", img)
cv2.imshow("Bus Crop",   bus_crop)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: second window shows just the front of the bus

Flip

import cv2
import numpy as np

img = cv2.imread("bus.jpg")

flip_h  = cv2.flip(img,  1)   # horizontal (mirror)
flip_v  = cv2.flip(img,  0)   # vertical (upside down)
flip_hv = cv2.flip(img, -1)   # both

row1 = cv2.hconcat([img,    flip_h])
row2 = cv2.hconcat([flip_v, flip_hv])
grid = cv2.vconcat([row1, row2])

# Scale down for display
display = cv2.resize(grid, (0, 0), fx=0.4, fy=0.4)
cv2.imshow("Flip Grid (original | H | V | HV)", display)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: 2×2 grid — top-left original, top-right mirrored, bottom-left upside-down, bottom-right both

4. Pixel-Level Access

import cv2
import numpy as np

img = cv2.imread("bus.jpg")

# Read a single pixel (row=100, col=200)
pixel = img[100, 200]
print(f"Pixel BGR at (100, 200): {pixel}")  # e.g. [143 171 185]

# Draw a red rectangle around the bus area
annotated = img.copy()
cv2.rectangle(annotated, (0, 50), (500, 600), color=(0, 0, 255), thickness=3)
cv2.imshow("Annotated", annotated)
cv2.waitKey(0)
cv2.destroyAllWindows()
# Expected: a red box drawn over the bus area

Basic transforms at a glance: original, resized, rotated 90°, center crop, horizontal flip

5. Conclusion

You now know how to load any image from disk (or a URL), inspect it, display it, write it back, and apply the core geometric transforms. These operations are the building blocks for every CV pipeline. Next up: color spaces — how to work with color beyond simple RGB.