# PerfectPixel: five directions come from the model, three come from a mirror

> A Wails desktop app that turns a one-line character description into 8-direction sprite sets, letting an image model render and then enforcing frame counts, alpha cleanliness and anchor stability with deterministic post-processing. The engineering write-up is specific and numeric, and the export side ships six artifact types per state. The README's own sample table, meanwhile, has eight empty cells.

**gykim80/perfectpixel-studio** — AI-powered animation sprite studio — generate character sprite sheets with 8 directions and 100+ actions from a single text prompt (Wails + Go + React)

- Repository: https://github.com/gykim80/perfectpixel-studio
- Stars: 576 · Forks: 93
- Language: Go
- License: MIT
- Published: 2026-09-17 · Updated: 2026-09-17 · Language: en
- Canonical page: https://hysenlabs.com/projects/gykim80-perfectpixel-studio

## Eight directions, five generations, 37.5 percent saved

The cost claim comes from the direction scheme rather than from a cheaper model. An 8-direction set has five directions generated by the image model and three derived by horizontal mirroring, which is stated as 37.5 percent lower generation cost. Five of eight is 62.5 percent of the directions, so the arithmetic is the saving itself, with nothing else folded in.

Input is a description, a style and a motion preset. Styles named include pixel art, chibi and cartoon, and the base character is generated with the background already removed. The motion catalog is described as 100 or more presets under keyword categories, with walk, run, jump, attack, magic and emotes as the examples, so a state is selected rather than drawn.

Mirroring is also the honest limitation of the whole approach. A character whose costume or weapon is asymmetric reads differently in a flipped direction, and the documentation presents the mirror as a cost decision without discussing that case.

## Scoring is frames found times 100, errors times 10

The self-correcting loop is where the design becomes concrete. One state runs a pipeline: build the prompt, generate a horizontal filmstrip, detect and matte the background, extract frames by cell segmentation, then inspect for frame count, identity drift and motion presence. A pass goes to pixel quantization and out. A failure produces a corrective retry hint and regenerates, up to three times.

Attempts are scored rather than accepted or rejected. Each one earns the frames found multiplied by one hundred, minus the errors multiplied by ten, and the best candidate is kept. A perfect result returns immediately. If three passes are still not perfect, the best result so far is returned rather than an empty hand. API errors and cancellations bail out early instead of consuming the remaining passes.

The weights encode the project's own priority: one extra frame found is worth ten errors, so a strip that is nearly right beats a strip that is clean and short. That is the right trade for animation, where a missing frame breaks playback outright.

## The chroma key is a mode, not a mean

Background removal does not threshold RGB. Colors are converted to YCbCr and only the chrominance components, Cb and Cr, are used, with luma discarded. That choice treats a shaded magenta and a bright magenta as the same color, and it is unaffected by JPEG 4:2:0 subsampling, which preserves luma while crushing chrominance.

The key color is estimated as the mode of a CbCr histogram rather than its mean, sampled from the four corners where a character rarely intrudes, so a gradient or noise across the backdrop does not drag the estimate. Edge feathering uses a Hermite smoothstep, despill projects out only the key-direction spill so the character's own colors survive, and a 4-connectivity flood fill clears residual background while preserving isolated interior pixels, which is what stops holes appearing inside the sprite. A self-diagnostic fallback re-mattes against pure `#FF00FF` when opacity or magenta-residue metrics spike.

The page puts numbers on it: naive RGB thresholding leaves 2,739 pixels of magenta residue and an 8,164 pixel pink halo, while the full path leaves 2 pixels of residue and a 3,447 pixel halo with roughly 456,000 opaque pixels of character body preserved.

## Dynamic programming finds the cuts a greedy split would miss

Asking a model for a six-frame filmstrip rarely yields six evenly spaced poses. Gaps come out uneven and arms touch the neighbouring pose. The technique borrowed here is the one OCR uses for character segmentation: a projection profile followed by an optimal cut.

A vertical alpha projection, written as P of x equals the sum of alpha over y for each column, makes the gutters between poses appear as valleys. After smoothing, the runs of content are counted as the natural pose count. When two poses fuse and the valley disappears, dynamic programming finds the globally optimal expected minus one cuts, minimising the sum of the projection value at each cut plus a penalty on how far each slice's width sits from the ideal width.

The stated difference from greedy or connected-component methods is that those fuse two touching poses into one blob. The measured case is a fire-mage kick strip of nine frames: an equal split puts all eight cut lines straight through the characters, while projection plus dynamic programming crosses zero and separates all nine poses intact.

## The centroid holds the torso still, not the bounding box

Centering a pose in its cell is where animation jitter comes from. Using the bounding-box center means a pose with an outstretched arm or a long weapon pushes the torso to the opposite side of the cell, and the character visibly shifts left and right during playback.

The fix is the alpha-weighted centroid, the center of mass, cx equals the sum of x times alpha divided by the sum of alpha. A large torso dominates that average, so however the limbs extend, the torso stays where it was put. A shared scale then unifies character size across a set, and the documentation notes it downscales only, with CatmullRom named as the resampling method before the section ends.

The pixel look is handled separately rather than being left to the model: quantization against a shared palette, plus snapping to a pixel grid, is applied after the frames pass inspection, so the dot-art texture is a deterministic step and not something the model has to be asked for twice.

## Four providers in the feature list, five keys in the template

The feature list names four backends: Gemini, OpenRouter, fal.ai and BytePlus. The environment template lists five keys:

```
GEMINI_API_KEY=
OPENAI_API_KEY=
OPENROUTER_API_KEY=
FAL_KEY=
BYTEPLUS_API_KEY=
```

OpenAI, labelled GPT Image 2, is configured but not offered as a choice in the feature list, which is the kind of gap that leaves a reader unsure whether it works. Gemini is marked the default provider. Three of the keys accept an alternative name as a fallback: GOOGLE_API_KEY for Gemini, FAL_API_KEY for fal.ai and ARK_API_KEY for BytePlus.

The template's comments are in Korean while the main page is in English and a Korean README sits beside it, and one comment is worth knowing before you debug a key that appears to be ignored: the settings screen inside the app takes priority over the environment variables, and `.env` and `.env.local` are excluded by `.gitignore`.

## The sample output table names eight states and shows nothing

The Sample Output section presents two rows of four states each: Walk, Idle, Cheer, Power Up, then Dance, Victory, Block and Low HP. In the published file every one of those eight cells is empty, so the section promises the output and shows none of it.

The repository does carry the artifacts. `samples/` holds nineteen named animations including walk, walk1, cheer, cheer1, crouch, dance1, defeat, hurt-heavy, low-hp, power-up, idle, idle_ranger and death. Four of them carry a direction suffix, death-north, victory-north, shield-up-south and idle-combat-south-east, and those four are not consistent in granularity, since three name a single compass point and one names a diagonal. Four more media directories sit at the repository root beside the source: `home_sample`, `ppsamples`, `report-images/` and `videos/`.

So the imagery exists and is in the tree, and the table that was supposed to show it does not reference it.

## Go 1.25 floor, an APNG dependency pinned to a commit

The module is named `perfectpixel` and requires Go 1.25.0, with three direct dependencies: Wails v2 at 2.12.0, golang.org/x/image at 0.42.0, and `github.com/kettek/apng` at a pseudo-version rather than a release tag. A pseudo-version in a require block means the APNG encoder is pinned to one specific upstream commit with no tag behind it, so APNG output follows that commit until someone bumps it deliberately.

Wails is what brings the rest of the stack in transitively: echo for the local server, go-webview2 for Windows, go-toast for notifications, gorilla/websocket and the desktop plumbing for windowing. The React side lives in `frontend/`, the Go entry points sit at the root as `main.go`, `app.go` and `gallery.go` with their tests beside them, and `wails.json` and `dev.sh` hold the build configuration.

There are no GitHub releases, the license is MIT, and the last push to `main` is 2026-06-22. The tree also carries `.claude-plugin/` and a `skill/` directory, so the project ships agent-facing configuration alongside the desktop app.

## Conclusion

PerfectPixel is one of the few AI image tools whose documentation is mostly about how it refuses to trust the model, and the numbers it gives for that refusal are the reason to believe the rest. It suits a game developer who needs a walk cycle to be exactly six frames with a stable anchor, and it does not suit someone who wants a one-click illustration, since three of the eight directions are mirrored copies and a strip that fails inspection is returned as its best attempt rather than rejected. Before generating anything, decide which provider key you are supplying, check the cost of the mirrored directions against your art direction, and read the export list, because the manifest and the Aseprite-compatible JSON are what your engine will actually read.

## FAQ

### How does PerfectPixel decide where to cut a sprite strip?

It builds a vertical alpha projection so the gutters between poses appear as valleys, counts the content runs as the natural pose count, and when poses fuse and the valley is gone, uses dynamic programming to find the globally optimal expected minus one cuts, minimising the projection value at each cut plus a width penalty. On a nine-frame kick strip, an equal split crosses all eight characters and this path crosses none.

### Which image providers does PerfectPixel support?

The feature list names Gemini, OpenRouter, fal.ai and BytePlus, and Gemini is the default. The environment template also ships OPENAI_API_KEY for GPT Image 2, with GOOGLE_API_KEY, FAL_API_KEY and ARK_API_KEY accepted as fallback names, and settings entered in the app take priority over environment variables.

### What does PerfectPixel export for a generated state?

A sprite sheet, a manifest.json, Aseprite-compatible JSON, a per-state GIF or APNG, and the individual frame PNGs, produced in one pass. Directions work as five generated by the model and three derived by horizontal mirroring, which is stated as 37.5 percent lower generation cost.

### What happens if the AI returns the wrong number of frames?

Inspection measures frame count, identity drift and motion presence, and a failure produces a corrective hint in English that is injected into the next prompt. Attempts are scored by frames found times one hundred minus errors times ten, the best candidate is kept, up to three passes run, and an imperfect result is still returned rather than nothing.

## Sources

- [gykim80/perfectpixel-studio on GitHub](https://github.com/gykim80/perfectpixel-studio)
- [Issues](https://github.com/gykim80/perfectpixel-studio/issues)
- [License: MIT](https://github.com/gykim80/perfectpixel-studio/blob/main/LICENSE)
- [README](https://github.com/gykim80/perfectpixel-studio/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/gykim80-perfectpixel-studio
