ChangeImageTo

How to Detect JPEG Compression Artifacts in Python

Blockiness detection using border energy analysis and FFT in Python

JPEG compression is lossy and creates two distinct types of visible artefacts: blockiness (8×8 pixel grid boundaries that become visible at low quality settings) and ringing (halo-like distortions around sharp edges). Both harm AI pipelines — blockiness creates false high-frequency signal that confuses CNNs, and ringing creates phantom edge artefacts that mislead segmentation models.

This article shows you exactly how to detect these programmatically in Python, what thresholds work in practice, and when to reject an image versus when to accept it despite compression.

Why JPEG artefacts cause problems downstream

JPEG splits images into 8×8 blocks and compresses each block independently. At low quality (high compression), the boundaries between blocks become visible as a grid pattern. This is a problem for:

Method 1: Border energy ratio (fast, reliable)

The most direct approach: measure the pixel-difference energy at 8×8 block boundaries and compare it to the energy of differences within blocks. Heavily compressed images have disproportionately high border energy.

import cv2
import numpy as np

def compression_score(image_path: str) -> float:
    """
    Returns 0–100 compression quality score.
    Below 50 indicates heavy JPEG artifacts.
    """
    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
    gf = img.astype(np.float64)
    h, w = gf.shape
    block = 8

    # Energy at vertical block borders
    vb = sum(
        float(np.sum(np.abs(gf[:, x - 1] - gf[:, x % w])))
        for x in range(block, w, block)
    )
    # Energy at horizontal block borders
    hb = sum(
        float(np.sum(np.abs(gf[y - 1, :] - gf[y % h, :])))
        for y in range(block, h, block)
    )
    border_energy = vb + hb

    # Intra-block energy (all pixel differences)
    intra = (
        float(np.sum(np.abs(gf[:, 1:] - gf[:, :-1]))) +
        float(np.sum(np.abs(gf[1:, :] - gf[:-1, :]))) +
        1e-6
    )

    blockiness = border_energy / intra
    # 0.12 is the empirical threshold for noticeable blockiness
    score = max(0.0, min(100.0, 100.0 - (blockiness / 0.12) * 100.0))
    return score
Edge case: Very flat images (solid colours, logos, simple graphics) have naturally low intra-block energy, which makes the ratio unusually high and gives false positives. Always check the image entropy first — if entropy is below 20/100, skip the compression check and assume quality is fine.

Method 2: FFT grid harmonic analysis (more sensitive)

A complementary approach uses the 2D FFT. A blocky image has periodic structure at 8-pixel intervals, which shows up as energy peaks at frequencies corresponding to those harmonics.

def fft_grid_ratio(gray: np.ndarray) -> float:
    """Returns 0–1; above ~0.12 suggests strong grid artefacts."""
    h, w = gray.shape
    max_side = 512
    scale = max(1.0, max(h, w) / max_side)
    small = cv2.resize(gray.astype(np.float64),
                       (int(w / scale), int(h / scale)),
                       interpolation=cv2.INTER_AREA) if scale > 1 else gray.astype(np.float64)

    mag = np.abs(np.fft.fftshift(np.fft.fft2(small)))
    sh, sw = small.shape
    cy, cx = sh // 2, sw // 2

    # Zero out DC and low-freq components
    yy, xx = np.ogrid[:sh, :sw]
    mag[((yy - cy)**2 + (xx - cx)**2) <= max(3, int(0.02 * min(sh, sw)))**2] = 0

    band_r = max(1, int(0.01 * min(sh, sw)))
    grid_e = 0.0
    for k in (1, 2, 3):
        for bx, by in [(int(sw/8*k), 0), (0, int(sh/8*k))]:
            if bx: grid_e += mag[:, max(0,cx-bx-band_r):cx-bx+band_r].sum() + mag[:, cx+bx-band_r:min(sw,cx+bx+band_r)].sum()
            if by: grid_e += mag[max(0,cy-by-band_r):cy-by+band_r, :].sum() + mag[cy+by-band_r:min(sh,cy+by+band_r), :].sum()

    return float(grid_e) / (float(mag.sum()) + 1e-6)

Interpreting the scores

Score (0–100)QualityAction
80–100No visible artefactsAccept
50–79Mild compressionAccept for most tasks; flag for OCR
25–49Noticeable blockinessReject for OCR/segmentation; flag for classifiers
0–24Severe artefactsReject for all AI tasks

Skip the implementation: use imageguard

Both methods above are already implemented and production-hardened in the imageguard library, with flat-image edge-case handling included. The compression_score and pixelation_score fields in the result cover both border-energy and FFT-based detection.

imageguard — compression detection + 5 other quality signals

Open-source on GitHub. Used in production at changeimageto.com.

View on GitHub →
from imageguard import validate

result = validate("product.jpg")
print(result.score)   # overall 0–1
print(result.issues)  # ['compressed'] if heavy artifacts found