Skip to content

Pixel Perfect Checker — Detect Scale, Blur and Off-Grid Pixels

Is this sprite really 3× nearest, or 2.5× bilinear? The checker finds the columns and rows where colour changes and proves the block size from them — no sampling, no guess.

A pixel-perfect checker tells you what state a pixel-art image is in: already 1×, an exact integer upscale (with its factor and grid offset), a fractional or smooth resize, or blurred. Nerulio’s checker finds the rows and columns where the colour changes: if they all sit on multiples of 3 (after one common offset), the image is exactly 3× and the 1× source can be recovered pixel for pixel. Anything else is reported as a measurement with a “not confident” flag where needed — never repaired as if it were exact.

Drop your sprite frames (PNG) — several frames share one palette
Open the Pixel Lab
Runs in your browser. Files are not uploaded.
Pixel Lab: CC0 sprite frames locked to one extracted palette, with the palette and the share of the pixels in each colour
The real Pixel Lab with a CC0 asset (OpenGameArt).
Outputs
  • PNG framesMeasured· Pillow
  • PaletteMeasured· re-import
  • pixel-lab.jsonUNVERIFIED

How the checker decides, without sampling or guessing

Integer detection is arithmetic. In a clean k× upscale the colour only changes at block borders, so every changing column index leaves the same remainder when divided by k, and so does every changing row. The checker records those positions in one pass and tries k from 16 down to 2; the largest k that fits both axes is the scale, a grid starting at 0 is preferred, and a shared non-zero remainder is the offset of an off-grid crop.

Without an integer grid, runs of identical colour give an estimate. A 2.5× nearest resize alternates runs of 3 and 2 pixels, so the mean of the shortest runs (those within 1.5× of the minimum) reads 2.500×. Averaging every run over-estimates, because a flat border is one long run of many blocks: 2.885× on the same test sprite.

Blur shows as in-between colours: an edge pixel whose colour lies strictly between its two opposite neighbours in Oklab. Nearest-scaled art has none; the bilinear 2.5× version of the test sprite had 1243 such pixels (86.3 % of its edges) and 700 partially transparent ones. One noisy pixel inside a block is enough to break the exact proof — which is why JPEG images are never reported as exact integer upscales.

Scale
Screen pixels per art pixel: exact for a proven block grid, an estimate otherwise.
Offset
Pixels cut from the left and top of the grid; a 4× sprite cropped by 2 and 3 pixels reads offset 2, 3.
In-between pixel
An edge colour between its neighbours — evidence of smoothing.
Not confident
A one-colour image, or one that changes on only one axis, has no grid to measure.

Example: one 16 × 16 sprite, four versions

Reports for a 16 × 16 test sprite after different treatments:

input                              report
64 × 64   4× nearest               scale 4 (exact), offset 0, 0, logical size 16 × 16
62 × 61   same, cropped 2 and 3 px  scale 4 (exact), offset 2, 3, off-grid
40 × 40   2.5× nearest             no integer grid, estimate 2.500× (runs 3, 2, 3, 2 …)
40 × 40   2.5× bilinear            not integer; 1243 in-between edge pixels (86.3 %)

recover 1× of the crop: ceil((62 + 2) / 4) × ceil((61 + 3) / 4) = 16 × 16

The 2.5× versions have no exact 1× inside them: blocks are 2 and 3 pixels wide in turn, so the checker offers no recovery and points to the cleanup instead.

What it does

  • Exact integer scale and offset from where colour changes happen (finds off-grid crops too).
  • Non-integer scale estimated from the shortest runs only (a 2.5× nearest resize reads 2.500×).
  • Blur detection: counts edge pixels whose colour lies between two neighbours, plus soft-alpha pixels.
  • Recover the 1× source (only when an exact grid is proven), integer scaler 1×–8×, colour budget and silhouette check.

How to do it

  1. Drop the image.
  2. Read the report: scale, offset, blurred edges.
  3. If an exact grid was found, recover the 1× source; otherwise upscale by whole numbers.
  4. Export.

Check the result

  • For an exact result, the logical size is ceil((width + offset) ÷ scale) on each axis.
  • Upscale the recovered sprite by the same factor: for an uncropped input it matches the input pixel for pixel.
  • A “not confident” result on a flat or striped image is correct — there is nothing to measure.

Troubleshooting

SymptomLikely causeHow to checkFix
Reads 1× although the image is clearly enlargedSmoothing or JPEG removed the flat blocks, so no integer grid existsThe blur count shows many in-between edge pixelsUse the Cleanup panel (downscaler), which measures smooth and fractional resizes
Reads 2× for art you believe is 4×Some colour changes sit between the 4-pixel borders: the art was edited after upscaling, or it has 2-pixel detailsZoom into the edges that break the 4-pixel gridAccept 2×, or remove the later edits first
An estimate such as 2.500× but no Recover buttonA fractional scale: blocks have two widths, so no exact 1× existsThe report says “no integer grid”Rebuild 1× in the Cleanup panel, which cuts at the measured edges
“Not confident”One colour, or changes on one axis onlyLook at the image: a flat fill or stripesNothing to fix; check a frame with detail

Other ways to do it

OptionWhen it is the better choice
Cleanup panel in the Pixel workspaceThe image was resized by a fraction, smoothed, JPEG-compressed or generated: it measures those grids and rebuilds 1× as a new sprite.
A grid overlay in any pixel editorOne clean image: set the grid to the block size you see and check that every edge falls on it.

Outputs and how each was checked

"Measured" means the downloaded file was opened again outside the page (Pillow, numpy or an independent parser) and the property was measured. Loading these files in a game engine was not tested.

TargetFilesCheck
PNG framesanim_N.png · 1×–8× nearestMeasured PillowEvery exported colour re-read from the PNGs lies inside the locked palette.
Palette.gpl · .hex (Lospec) · .jsonMeasured re-importEach format was exported and imported back through the UI: 16 → 16 colours.
pixel-lab.jsongeneric frames + paletteUNVERIFIEDGeneric JSON; not loaded in any engine.

Evidence

8 anti-aliased frames → one 16-colour palette with 0 exported colours outside it; an unchanged region identical in all 8 frames with dithering off and with Bayer 4×4; the anti-alias remover took 271 colours to 16 without moving a single silhouette pixel; integer scale, offset and 2.5× non-integer scaling detected exactly.

Limits

  • Integer scales up to 16× are detected (and never more than the image’s shorter side).
  • The exact proof needs exact colours: one noisy pixel inside a block and the image is reported as not an integer upscale.
  • Frames larger than 4096 × 4096 are refused: every pass runs on the main thread, and there is no cancel button yet.
  • A sprite sheet is treated as one image; cut it into frames first (sprite sheet cutter).
  • With a dithering mode on, the cleanup counts include dither pixels; set dithering to None first.
  • The exported JSON is generic; importing it into Godot or Unity was not tested.

Versions and sources

Checked in the Pixel Lab tests (Chromium): 1×, 2×, 3×, 4× and 8× nearest detected exactly; a 4× sprite cropped by 2 and 3 px read offset 2, 3; 2.5× nearest read 2.500×; a 3× sprite recovered to 16 × 16 pixel-identical.

Questions

Why does it say "not confident"?

A one-colour image, or one that changes on only one axis, has no grid to measure. The checker says so instead of guessing.

Can it fix a blurry (bilinear) upscale?

It measures it — on the test sprite 1243 edge pixels (86.3 %) were in-between colours — but an exact recovery is only offered when a clean block grid exists.

Is it free, and do I need an account?

Everything on these pages is free and works without an account. There is no watermark and no export that is held back.

Are my sprites uploaded?

No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.

Does it need Aseprite or an engine plugin installed?

No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).