blockgrid-nano

46,834 parameters. 187 KB. Was this image cropped off its JPEG block grid?

A JPEG is compressed in 8Γ—8 blocks. Crop an image by an offset that is not a multiple of 8, re-save it, and the new block grid no longer aligns with the old compression artefacts. This detects that misalignment β€” evidence the image was cropped and re-encoded rather than being an original save.

Measured

Input is the block-DCT of the patch, not the pixels. That choice is most of the model:

representation accuracy best in-sample scalar
pixels 0.865 0.526
block DCT 0.985 0.818

Third confirmation of a pattern in this family β€” motion vectors beat pixels 0.992 to 0.785 for camera motion, DCT beat pixels 0.998 to 0.971 for upscale factor. The signature here is periodic structure at the block boundary, which is precisely what the DCT basis expresses.

Out of distribution β€” 1,559 patches from a Logitech BRIO, a different sensor whose frames have already been through the camera's own MJPG encoder:

accuracy
COCO photographs (held out) 0.985
Logitech BRIO, real office 0.992

A transfer gap of +0.007. It does not degrade off-corpus.

Why a model and not a threshold

An entropy threshold on the same DCT input scores 0.954 on real frames β€” but only when fitted on those frames. A threshold you deploy has to be a number chosen in advance. Fitted on COCO and applied unchanged to real frames:

block-grid detection, real frames accuracy
entropy threshold, fitted in-sample 0.954
entropy threshold, fitted on COCO and transferred 0.565 (recall 0.125)
this model, trained on COCO and transferred 0.992

The threshold collapses; the model does not. A threshold is a single number tuned to one distribution's location and scale, and a sensor change leaves it in the wrong place with nothing to compensate.

Scope

For: image forensics triage, dataset hygiene, detecting that content has been re-processed rather than delivered as originally encoded.

Not for:

  • Not a manipulation or deepfake detector. It detects a processing history. Cropping is overwhelmingly innocent β€” every social platform re-encodes uploads.
  • Not proof of tampering, and not evidence. A misaligned grid means re-encoding happened, not that anything was concealed.
  • Not authorship or provenance.
  • Not a judgement about any person.

Known failure modes

  1. Needs texture. Flat regions β€” sky, blank wall β€” carry no block structure. Patches with standard deviation below ~6 were excluded from evaluation and should be skipped at inference.
  2. Assumes a JPEG history. On never-compressed source (PNG, RAW) there is no grid to align to and the output is meaningless.
  3. Very high quality settings weaken the signature. Training used quality 65–92; above that, block artefacts are faint.
  4. Single 64Γ—64 patch. Vote across several patches for a frame-level answer.
  5. Grayscale. Chroma subsampling artefacts, which carry additional grid information, are unused.

Usage

import cv2, numpy as np, onnxruntime as ort

def dct_blocks(p):                       # p: float32 64x64 grayscale patch
    out = np.zeros_like(p)
    for by in range(0, 64, 8):
        for bx in range(0, 64, 8):
            out[by:by+8, bx:bx+8] = cv2.dct(p[by:by+8, bx:bx+8] - 128.0)
    return np.sign(out) * np.log1p(np.abs(out))

so = ort.SessionOptions()
so.intra_op_num_threads = 1
so.add_session_config_entry("session.intra_op.allow_spinning", "0")
sess = ort.InferenceSession("blockgrid.onnx", sess_options=so,
                            providers=["CPUExecutionProvider"])

img = cv2.imread("frame.jpg", cv2.IMREAD_GRAYSCALE)
p = img[y:y+64, x:x+64].astype(np.float32)        # native crop; do NOT resize
if p.std() < 6:                                    # flat patch: no information
    raise SystemExit("skip")
v = dct_blocks(p)
v = (v - v.mean()) / (v.std() + 1e-8)
print(["aligned", "off-grid"][int(sess.run(None, {"dct": v[None, None]})[0][0].argmax())])

Take a native crop and do not resize first β€” resizing is itself a resampling operation and destroys the grid structure the model reads.

Deployment note: cap the ONNX Runtime thread pool

ONNX Runtime sizes its intra-op pool to the core count and those workers spin-wait between inferences. Running two 47K models this way burned about 1.9 cores to serve 0.28 ms inferences. With intra_op_num_threads=1 and allow_spinning=0: idle CPU 192% β†’ 16.5% of one core, throughput unchanged.

Training

1,950 train / 650 held-out COCO images, split by source image Β· block-DCT input, log-magnitude, per-patch standardised Β· 4 conv layers (16β†’32β†’48β†’64) Β· Adam 3e-3, 22 epochs, batch 64.

Same architecture as resoajoe/camera-motion-nano, resoajoe/camera-health-nano and resoajoe/alarm-nano, unchanged.

Verification

ONNX vs PyTorch, both CPU, 256 inputs: max relative logit difference 2.5e-07, 100% argmax agreement.

Prior art β€” this task is not new

JPEG block artifact grid (BAG) analysis is an established forensics field, and this model should be read as a size-and-transfer study of a known technique, not as a new capability. Detecting that a crop broke the 8Γ—8 grid has been published since at least 2009, including:

  • Li, Yuan & Yu, Passive detection of doctored JPEG image via block artifact grid extraction, Signal Processing (2009) β€” blind BAG extraction, with grid mismatch used as a tamper trail.
  • Iakovidou et al., Content-aware detection of JPEG grid inconsistencies for intuitive image forensics, JVCIR (2018).
  • Local JPEG grid detectors applied region-wise to localise forgeries.

What is different here. Classical BAG methods are unsupervised algorithms published as papers: they localise the grid analytically and are typically evaluated in-corpus. This is a supervised 47K-parameter CNN shipped as a 187 KB ONNX file that runs on a CPU thread, plus three measurements those papers do not report:

  • Cross-sensor transfer, measured rather than assumed: 0.985 on COCO β†’ 0.992 on a different camera's frames.
  • The DCT-versus-pixel comparison at fixed capacity β€” 0.985 vs 0.865, same architecture, same data β€” isolating how much of the performance is the representation rather than the model.
  • The transferred-threshold collapse: a single-statistic baseline reads 0.954 fitted in-sample and 0.565 fitted on the source and transferred, while the model holds at 0.992. That is the argument for a learned detector over a hand-set threshold on this task, and it is not in the classical literature because those methods are not framed as a threshold-versus-model choice.

Use the classical methods when you want localisation and interpretability. Use this when you want a fixed-cost binary answer on an edge device.

What "scalar baseline" means on this card

Every margin quoted here is against a stated baseline, because a margin without one is not a measurement. The baseline is the best single-threshold classifier over ten cheap statistics, fitted optimistically:

mean Β· std Β· lapvar Β· hf (high-frequency energy ratio) Β· grad (Sobel magnitude) Β· entropy Β· centre_edge Β· radial_slope Β· row_fft_peak Β· col_fft_peak

The last four are spatially aware, added after an earlier six-statistic baseline β€” all global aggregates β€” was found to systematically overstate model value on spatially structured tasks. A baseline that cannot see where anything is loses to a CNN by default. On one test task that flaw inflated an apparent margin from +0.060 to +0.261.

Two questions are asked with it, and they disagree:

  • in-sample β€” threshold fitted on the data it is scored on. Deliberately generous. Answers is there structure beyond a low-order statistic?
  • transferred β€” threshold fitted on the training corpus, applied unchanged to the target. Answers what should I ship? On one task the in-sample figure was 0.954 and the transferred figure 0.565.

Where this card quotes a single scalar figure without qualification, it is the in-sample one.

Provenance

COCO val2017 (public) plus frames from the author's own rig. No personal data.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support