Computer vision img

Pixel Operations

Add, blend, mask and threshold: changing each pixel on its own, and the overflow traps along the way.

What are pixel operations?

Pixel operations modify pixel values directly.

Point operations

Each output pixel depends only on the corresponding input pixel. This deck.

Local operations

The output depends on a neighbourhood. Covered in filtering.

Arithmetic

add, subtract, multiply, divide

Logical

AND, OR, XOR, NOT

Thresholding

binary, adaptive

Normalization

rescaling pixel values

Image arithmetic

Addition

result = img1 + img2

Brightens the image. Careful of overflow!

Subtraction

result = img1 - img2

Finds differences: motion detection.

Multiplication

result = img * scalar

Contrast adjustment, masking.

Important: use cv2.add() instead of + to prevent overflow.

NumPy vs OpenCV arithmetic

NumPy: modulo

0(a + b) % 256

255 + 10 = 9

Wraps around. Fast, but beware overflow.

OpenCV: saturated

0min(a + b, 255)

cv2.add(255, 10) = 255

Clips to the max. Safe: it saturates.

Image addition


        import cv2
        import numpy as np
        # numpy addition (overflow)
        img = np.array([[250]], dtype=np.uint8)
        result = img + np.array([[10]], dtype=np.uint8)
        # ans: (250 + 10) % 256 = 4 (overflow!)
        # opencv addition (saturation)
        result = cv2.add(img, np.array([[10]], dtype=np.uint8))
        # ans: min(250 + 10, 255) = 255 (clips to max)
      

Brightening and darkening


        # brighten image (add constant)
        bright = cv2.add(img, np.array([50]))  # ans:
        # ans: adds 50 to all pixels (saturates at 255)
        # darken image (subtract constant)
        dark = cv2.subtract(img, np.array([50]))  # ans:
        # ans: subtracts 50 (saturates at 0)
        # alternative using numpy (watch overflow!)
        bright = np.clip(img.astype(int) + 50, 0, 255).astype(np.uint8)
        # ans: clip prevents overflow (do the sum in a wider type first)
      

A scalar passed as np.array([50]) is added to the first channel only on a colour image; use (50, 50, 50, 0) to shift all three.

Saturate or wrap?

cv2.add / cv2.subtract: saturates
img + value as uint8: wraps around

The bright sky wraps past 255 into dark garbage; the saturated version just turns white.

Image blending


        # weighted sum of two images
        img1 = cv2.imread('image1.jpg')
        img2 = cv2.imread('image2.jpg')
        # alpha blending: result = α*img1 + β*img2 + γ
        blended = cv2.addWeighted(img1, 0.7, img2, 0.3, 0)  # ans:
        # ans: 70% img1 + 30% img2
        # must satisfy: α + β = 1 for proper blending
        # ans: ensures output stays in the valid range
      

addWeighted, live

img1
img2

Bitwise operations


          # bitwise AND (intersection)
          result = cv2.bitwise_and(img1, img2)
          # ans: keeps pixels where both are non-zero
          # bitwise OR (union)
          result = cv2.bitwise_or(img1, img2)
          # ans: combines regions from both images
          # bitwise NOT (invert)
          result = cv2.bitwise_not(img)
          # ans: inverts all bits (negative image)
          # bitwise XOR (difference)
          result = cv2.bitwise_xor(img1, img2)
          # ans: highlights differences
        
img1
img2

Binary thresholding


        # simple thresholding
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
        # ans: pixels > 127 become 255, others become 0
        # inverse threshold
        ret, inv = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY_INV)
        # ans: pixels > 127 become 0, others become 255
        # ret is the threshold value used
        # ans: useful for automatic thresholding methods (Otsu)
      

Threshold, live

gray
histogram of gray, pink line = thresh

Image masking


        # create circular mask
        mask = np.zeros(img.shape[:2], dtype=np.uint8)
        cv2.circle(mask, (center_x, center_y), radius, 255, -1)
        # ans: the circular region is 255, the rest is 0
        # apply mask to image
        result = cv2.bitwise_and(img, img, mask=mask)
        # ans: keeps only the circular region, the rest is black
        # inverse masking
        inv_mask = cv2.bitwise_not(mask)
        background = cv2.bitwise_and(img, img, mask=inv_mask)
        # ans: keeps everything except the circle
      

Masking, live

img: drag the circle
mask
bitwise_and(img, img, mask=mask)

Normalization


        # normalize to 0-255 range
        normalized = cv2.normalize(img, None, 0, 255, cv2.NORM_MINMAX)
        # ans: scales the min value to 0, the max to 255
        # why normalize?
        # ans: improves contrast, standardizes the range
        # ans: useful before visualization or further processing
        # manual normalization
        norm = ((img - img.min()) / (img.max() - img.min()) * 255).astype(np.uint8)  # ans:
        # ans: same effect as cv2.normalize
      

Stretching the histogram

Exercises

Answer first, then press → to check.

Exercises 1 · concepts


        # what's the difference?
        result1 = img + 10
        result2 = cv2.add(img, 10)
        # ans: result1 wraps (modulo), result2 saturates
        # when does overflow occur?
        img = np.array([[250]], dtype=np.uint8)
        result = img + np.array([[20]], dtype=np.uint8)
        # ans: (250+20) % 256 = 14, overflow!
        # what is the alpha blending formula?
        result = cv2.addWeighted(img1, 0.7, img2, 0.3, 0)
        # ans: result = 0.7*img1 + 0.3*img2 + 0
        # what should α + β equal?
        # ans: 1.0 for proper blending (preserves intensity)
      

Exercises 2 · concepts


        # what does bitwise_and do with masks?
        result = cv2.bitwise_and(img, img, mask=mask)
        # ans: keeps pixels where mask=255, others become 0
        # what does thresholding do?
        ret, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
        # ans: converts to binary (only 0 or 255)
        # why normalize images?
        # ans: improves contrast, standardizes the range
        # what is image inversion?
        inv = cv2.bitwise_not(img)
        # ans: dark becomes light, light becomes dark
      

Exercises 3 · coding


        # brighten image by 30
        bright = cv2.add(img, np.array([30]))  # ans:
        # ans: adds 30, saturates at 255
        # darken image by 50
        dark = cv2.subtract(img, np.array([50]))  # ans:
        # ans: subtracts 50, saturates at 0
        # blend two images 50-50
        blended = cv2.addWeighted(img1, 0.5, img2, 0.5, 0)  # ans:
        # ans: equal mix of both images
      

Exercises 4 · coding


        # create negative image
        negative = cv2.bitwise_not(img)  # ans:
        # ans: 255 - pixel_value for all pixels
        # threshold at 100
        ret, binary = cv2.threshold(gray, 100, 255, cv2.THRESH_BINARY)  # ans:
        # ans: > 100 becomes 255, ≤ 100 becomes 0
        # create rectangular mask
        mask = np.zeros((480, 640), dtype=np.uint8)  # ans:
        mask[100:300, 200:400] = 255  # ans:
        # ans: rows 100-300, columns 200-400 are white
      

Exercises 5 · coding


        # what is the output?
        img = np.array([[200]], dtype=np.uint8)
        result = cv2.add(img, np.array([[100]], dtype=np.uint8))
        # ans: 255 (saturates at max)
        # XOR of image with itself?
        result = cv2.bitwise_xor(img, img)
        # ans: all zeros (anything XOR itself = 0)
        # normalize image to 0-1 range
        norm = img.astype(float) / 255.0  # ans:
        # ans: scales to the [0.0, 1.0] range
        # apply mask to extract ROI
        roi = cv2.bitwise_and(img, img, mask=mask)  # ans:
        # ans: region of interest extracted, the rest is black
      

Exercises 6 · mixed


        # what happens here?
        img = np.array([[100, 200]], dtype=np.uint8)
        result = img * 2
        # ans: [200, 400 % 256] = [200, 144] (overflow on the 2nd)
        # using cv2.multiply instead?
        result = cv2.multiply(img, np.array([[2, 2]], dtype=np.uint8))
        # ans: [200, 255] (saturates at 255)
        # blend with a gamma
        blended = cv2.addWeighted(img1, 0.6, img2, 0.4, 10)
        # ans: 0.6*img1 + 0.4*img2 + 10 (adds 10 to all pixels)
      

Exercises 7 · mixed


        # invert only specific channel
        img = cv2.imread('img.jpg')
        img[:, :, 0] = cv2.bitwise_not(img[:, :, 0])
        # ans: inverts the blue channel only (OpenCV stores BGR)
        # threshold with different max value
        ret, binary = cv2.threshold(gray, 127, 200, cv2.THRESH_BINARY)
        # ans: pixels > 127 become 200 (not 255)
        # combine two masks
        mask3 = cv2.bitwise_or(mask1, mask2)
        # ans: union of both masks