Downloads · 30 days
0
resoajoe/blockgrid-nano
blockgrid-nano is a image classification model from resoajoe. Use it when you need a label for an image. The card lists the license as mit.
46,834 parameters. 187 KB. Was this image cropped off its JPEG block grid?
Downloads · 30 days
0
Access
Public
Updated Sep 3, 2026
Repo size
390 KB
Likes
0
Public
Click a slice to open those files.
.pt199 KB · 49%
From the Hugging Face model README
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.
Domain measured / deployment domain tested: measured on COCO photographs with synthetic block-grid artefacts; deployment domain: no real compressed footage tested. (Fifth line of the card standard, added 2026-09-02: a number is only as good as the domain it was measured in.)
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.
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.
For: image forensics triage, dataset hygiene, detecting that content has been re-processed rather than delivered as originally encoded.
Not for:
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.
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.
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.
ONNX vs PyTorch, both CPU, 256 inputs: max relative logit difference 2.5e-07, 100% argmax agreement.
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:
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:
Use the classical methods when you want localisation and interpretability. Use this when you want a fixed-cost binary answer on an edge device.
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:
Where this card quotes a single scalar figure without qualification, it is the in-sample one.
COCO val2017 (public) plus frames from the author's own rig. No personal data.