Pixel Art Cleanup — Remove Anti-Aliasing and Stray Pixels
Everything is reported as candidates first — highlighted on the image and counted — and changed only when you tick it. The anti-alias remover snaps each in-between pixel to one of the two palette colours it sits between.
Cleaning pixel art means removing what does not belong in a hard-edged, limited-palette sprite: anti-aliased in-between colours along edges, stray single pixels, tiny clusters, one-pixel holes and gaps in the outline. Nerulio’s Pixel Lab lists every candidate first — highlighted and counted — and changes only what you tick. Its anti-alias remover snaps each in-between pixel to one of the two palette colours it sits between, so the silhouette does not move. Input: 1× PNG frames; output: cleaned PNG frames and one shared palette.

- PNG framesMeasured· Pillow
- PaletteMeasured· re-import
- pixel-lab.jsonUNVERIFIED
What counts as dirt, and how each kind is fixed
Anti-aliasing helps an illustration and hurts a sprite: an edge pixel mixed from the outline and the fill is a new colour that is neither, and the test frames — meant for a 16-colour palette — carried 271 colours. Snapping such a pixel to the nearest palette colour is not enough: a colour half-way between dark outline and orange fill can be nearest to some unrelated brown. The Lab snaps it to the nearer end of the neighbour pair whose Oklab segment it lies on, and uses the globally nearest palette colour only when there is no such pair.
Strays, clusters and holes come from one scan of connected regions of the same palette index (4-connected). A stray is a region of one pixel, a tiny cluster is smaller than the size you set, a hole is a transparent region that never reaches the image edge and is ringed by opaque pixels. Each is filled with the colour around it; because one scan produces all three lists, the counts agree with each other.
Outline checks look only at straight edges, where exactly one of the four neighbours is outside the silhouette; corners are skipped because thickness there is ambiguous. The automatic fix closes gaps and never thins a doubled outline, since removing a pixel would move the silhouette.
- Anti-alias pixel
- An edge pixel whose colour lies between two palette colours that meet there.
- Stray (orphan) pixel
- A one-pixel region: none of its four neighbours has its colour.
- Oklab
- A perceptual colour space; distances in it follow what the eye sees more closely than RGB.
Example: eight anti-aliased 48 × 48 frames
A bobbing ball drawn with smoothing, then cleaned with one palette for all frames:
input 8 frames, 271 distinct colours (edges anti-aliased)
palette 16 colours, extracted from all 8 frames together
anti-alias remover, threshold 0.08 (Oklab distance)
colours 271 → 16, every exported colour inside the palette
alpha pixels 0 changed — the silhouette did not move
edge pixel half-way between outline and fill → the nearer of those two, never a third colourThe decisions are read from the original pixels, not from pixels already rewritten, so the result does not depend on the order in which the image is scanned.
What it does
- Anti-alias remover: a pixel between two palette colours snaps to the nearer end of that pair; decisions read the original pixels, so scan order cannot change the result.
- Stray pixels, tiny clusters (size adjustable) and single-pixel holes from one component scan.
- Outline checks: gaps, doubled runs and thickness on straight edges; the auto-fix only closes gaps.
- Alpha is untouched unless you set a cut-off, and then only rounded to 0 or 255.
How to do it
- Drop the frames.
- Read the candidate counts and look at the highlighted pixels.
- Tick the fixes you want (anti-alias, strays, clusters, holes, outline gaps).
- Export the cleaned frames.
Check the result
- Read the counts before ticking anything: stray, cluster, hole and anti-alias candidates are listed separately and highlighted on the image.
- After export, check the colours: every colour of the frames should be in the palette.
- Compare the silhouette before and after (Check stage): the shape should be identical.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| A dithered area is reported as hundreds of strays | Dithering is made of single pixels on purpose | Is a dither mode on in Convert? | Set dithering to None before cleaning, or leave strays unticked |
| An eye or a one-pixel highlight disappeared | It was a stray candidate and strays were ticked | Look at the highlighted candidates before applying | Untick strays, or undo (Ctrl+Z) and fix those pixels by hand |
| Edge pixels went to the wrong side | The threshold is wide enough to catch colours that belong to neither pair | Lower the anti-alias threshold (default 0.08) | Lower it, or touch up in the pixel editor |
| The outline is still doubled after the fix | The fix only closes gaps; thinning would move the silhouette | The outline report counts doubled runs | Thin it by hand where you want it |
| An upscaled or generated picture is far too noisy to clean | The image is not at 1× yet | The checker reports an integer scale or a resize | Bring it to 1× first with the downscaler, then clean |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| The Cleanup panel of the Studio’s Pixel workspace (for upscaled or generated art) | The picture is not at 1×: it re-grids and removes fringes in one pass and makes a new sprite you can keep editing. |
| Any pixel editor by hand | A few frames where you want to decide every pixel; there are no automatic candidates to review. |
| Palette extractor | You only need one shared palette for many frames, without changing any shapes. |
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.
| Target | Files | Check |
|---|---|---|
| PNG frames | anim_N.png · 1×–8× nearest | Measured PillowEvery exported colour re-read from the PNGs lies inside the locked palette. |
| Palette | .gpl · .hex (Lospec) · .json | Measured re-importEach format was exported and imported back through the UI: 16 → 16 colours. |
| pixel-lab.json | generic frames + palette | UNVERIFIEDGeneric 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
- Outline gaps are the only outline problem fixed automatically; doubled pixels are reported, not removed.
- Counts above 4 megapixels of total frame area cover the visible frame only.
- 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
Numbers from the Pixel Lab browser tests in Chromium (Firefox and WebKit not run), measured by re-opening the exported files outside the page.
Questions
Will it move my sprite's outline?
No. On the test frames the anti-alias remover changed 0 alpha pixels while taking 271 colours down to the 16 of the palette.
Does it work on dithered art?
Dither patterns are made of single pixels, so the counts include them; set dithering to None before cleaning.
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).