Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pipeline

Every file goes through the same stages, top to bottom:

decode ─▶ grayscale ─▶ resize to long edge ─▶ (optional pre-blur)
       ─▶ Laplacian ─▶ per-tile variance ─▶ scores ─▶ verdict

The pipeline follows OpenCV’s exact numeric semantics — the OpenCV names below are conventions being matched, not libraries being called. See Score Stability for why that matters.

1. Decode and grayscale

Sharpness is measured on the luminance channel only.

  • JPEG sources (CR3 embedded previews, standalone JPEG files) decode straight to their luminance plane: for a YCbCr JPEG, the Y component is the BT.601 luminance the encoder computed from the original pixels.

  • Every other source decodes to RGB and is converted with the BT.601 weights, matching OpenCV’s cvtColor:

    Y = round(0.299 R + 0.587 G + 0.114 B)
    

Either way, the rest of the pipeline works on an 8-bit grayscale image. Formats details which file takes which decode path.

2. Normalization

The variance of the Laplacian grows with image resolution: the same photo scores differently at 6000 px and at 1600 px. To make scores comparable across cameras and crops, every image whose longest edge exceeds --long-edge (default 1600) is downscaled to it.

The downscale replicates OpenCV’s INTER_AREA: each destination pixel is the average of the source pixels it covers, with fractional coverage weights on the window edges. The target dimensions are truncated, not rounded. Images already at or below the target size are left untouched — never upscaled.

3. Pre-blur

With --pre-blur SIGMA (> 0), a separable Gaussian filter mirroring OpenCV’s GaussianBlur on 8-bit images runs before the measurement: kernel size round(6σ + 1) forced odd (OpenCV’s automatic size), BORDER_REFLECT_101 borders, full-precision intermediate rows, result rounded back to 8 bits.

Its purpose is noise, not aesthetics: high-ISO sensor noise is high-frequency and inflates the variance of genuinely blurry photos. See Calibration.

4. Laplacian

The classic focus measure: the 3×3 Laplacian

0  1  0
1 -4  1
0  1  0

is applied at every pixel with BORDER_REFLECT_101 borders, matching OpenCV’s Laplacian with its default kernel size. A sharp edge produces a strong positive/negative swing in the response; a blurred edge barely registers.

5. Tile variance

The Laplacian response is measured per tile on an N×N grid (--grid-size, default 6×6 = 36 tiles). Tile boundaries use integer arithmetic (start = index * size / N), so every pixel belongs to exactly one tile. The statistic is the population variance (dividing by the pixel count, like NumPy’s default var()), computed per tile. When the image is smaller than the grid, the empty tiles are simply skipped.

6. Scores

Two numbers come out of the grid:

  • sharpness score — the tile variances are sorted descending and the best --top-fraction of them (max(1, round(tile_count × fraction)) tiles; the default keeps the best 4 of 36) are averaged. This is the score the verdict uses.
  • global score — the variance over the whole frame, reported for information. Comparing it to the sharpness score tells you whether a photo is sharp everywhere or only somewhere.

Scoring only the best tiles is deliberate: a razor-sharp subject on a melted bokeh background would fail a whole-frame variance test, yet the photo is a keeper. One sharp region is enough evidence that the focus landed.

7. Verdict

A photo is sharp when its sharpness score reaches --threshold, BLURRY below. Both scores are rounded to one decimal in the reports.

Files that fail anywhere in the pipeline become error rows instead — see Reports.