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:
- OCR: Blocking artefacts create fake edges around characters, confusing character segmentation algorithms.
- Image classification: Convolutional models can learn to associate blocking artefacts with specific classes if a training class is over-represented by heavily compressed web images.
- Background removal: Mask prediction along subject edges is confused by blocking artefacts near the boundary.
- Image upscaling: Super-resolution models amplify blocking artefacts rather than reconstructing real detail.
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
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) | Quality | Action |
|---|---|---|
| 80–100 | No visible artefacts | Accept |
| 50–79 | Mild compression | Accept for most tasks; flag for OCR |
| 25–49 | Noticeable blockiness | Reject for OCR/segmentation; flag for classifiers |
| 0–24 | Severe artefacts | Reject 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