AI Hints
Home›Computer Vision Tutorial›Getting a Good Image
Computer Vision · Lesson 46 of 111

Sharp or Blurry?

A one-line focus check.

⏱ 2 min read▶ Video at 1:14:34🎓 Free
Watch this lesson in the Computer Vision courseWatch on YouTube ↗Full Computer Vision course

One Monday, someone bumps the camera, and the lens slips slightly out of focus. Nothing crashes. Every photo just gets a little softer. For two days, AppleEye quietly gets worse, and nobody knows why. A simple automatic check would have caught that in the first minute.

Sharp or Blurry?
From the video: Sharp or Blurry?
  • Someone bumps the camera
  • Every photo gets softer
  • Nobody knows why

Measuring sharpness

Blurry images have soft edges, and the Laplacian responds strongly to sharp edges. So if we take the Laplacian, and measure how much it varies, we've got a single number for sharpness. We test it by blurring an image more and more. Run it. The score falls steadily as the blur gets heavier. It's four point seven on the sharp photo, and under one on the heavy blurs. That's a whole focus monitor in one line of maths. Log that number for each frame, and alarm if it drops.

focus.pypython
import cv2
from belt import belt

img, _, _ = belt()

def sharpness(image):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    return cv2.Laplacian(gray, cv2.CV_64F).var()

for size in [1, 5, 15, 31]:
    blurred = cv2.GaussianBlur(img, (size, size), 0)
    print(f"Blur {size:>2}: sharpness {sharpness(blurred):7.1f}")
Output
Blur  1: sharpness     4.7
Blur  5: sharpness     2.1
Blur 15: sharpness     0.7
Blur 31: sharpness     0.5
Also uses belt.py
import numpy as np
import cv2

def belt(n=7, seed=1, size=(480, 640), bruises=2, light=1.0):
    """Draw a simulated conveyor belt of apples. Returns the image and the true boxes."""
    rng = np.random.default_rng(seed)
    h, w = size
    img = np.full((h, w, 3), (70, 75, 80), np.uint8)
    img = cv2.add(img, rng.normal(0, 6, img.shape).clip(0, 30).astype(np.uint8))
    boxes, spots, taken = [], [], []
    while len(boxes) < n:
        r = int(rng.integers(32, 48))
        x, y = int(rng.integers(r + 5, w - r - 5)), int(rng.integers(r + 5, h - r - 5))
        if any((x - a) ** 2 + (y - b) ** 2 < (r + c + 8) ** 2 for a, b, c in taken):
            continue
        taken.append((x, y, r))
        red = int(rng.integers(150, 225))
        cv2.circle(img, (x, y), r, (35, 40, red), -1, cv2.LINE_AA)
        cv2.circle(img, (x - r // 3, y - r // 3), r // 4, (90, 90, min(255, red + 30)), -1, cv2.LINE_AA)
        boxes.append((x - r, y - r, x + r, y + r))
    for x, y, r in taken[:bruises]:
        cv2.ellipse(img, (x + r // 3, y + r // 4), (r // 3, r // 4), 20, 0, 360, (25, 45, 70), -1, cv2.LINE_AA)
        spots.append((x, y, r))
    img = cv2.GaussianBlur(img, (3, 3), 0)
    return np.clip(img * light, 0, 255).astype(np.uint8), boxes, spots

Using a focus score

The right threshold depends on your scene, as a plain surface has few edges at all. So measure typical values during a good day, and alarm on a sudden drop. Reject blurred frames, rather than classifying them. And on a moving belt, blur often comes from speed, not focus. A shorter exposure and a brighter light will fix that far better than any algorithm.

Set from good daysdepends on the scene
Alarm on dropssudden change
Reject bad framesdon't classify them
Motion blur?shorter exposure

Summary

One number catches a bumped camera. Next, the mistake that fools nearly every colour based system.

Key takeaways
  • Laplacian variance measures focus
  • Log it for every frame
  • Motion blur needs short exposure
Next up: White Balance — when red isn't red