Turn one character illustration into a rigged, animated Spine 4.2 skeleton.
Drop in a PNG and OCS decomposes it into semantic layers, throws away the dummy layers, cuts the left and right arms and legs apart, lets you drag the bones where they belong, then builds a weighted mesh rig, packs an atlas, and hands back a self-contained HTML preview.
image ──► layer decomposition ──► layer cleanup ──► bone placement ──► limb split ──► rig ──► Spine + preview
(see-through, GPU) you review you drag automatic mesh+weights
Built on two upstream projects:
| see-through | layer decomposition, run as a git submodule (Apache-2.0) |
| spine-animation-ai | prior art for the rigging stage — read, not reused (PolyForm NC) |
See NOTICE.md for why the second one is referenced rather than vendored, and for the Spine runtime's licensing.
Reading see-through's source turned up three gaps that a straight pipeline would have shipped as bugs. Each one is a component here.
1. see-through never filters empty layers. Neither further_extr nor
dump_parts_psd has an area or alpha threshold anywhere in the path, so every tag
the model emitted lands in the PSD — including fully transparent ones for the
tail, wings and eyewear a character does not have. ocs/cleanup.py
scores each layer and splits the verdict in two: things that cannot be anything
but garbage are dropped automatically, and judgement calls are flagged with a
reason for you to resolve. Both directions are reversible in the UI.
Thresholds are calibrated against real output, not guessed. see-through's own
sample produces a 20 px nose and a 37 px mouth — genuinely small,
genuinely real. Those get flagged, never deleted.
2. The taxonomy has no skin tag, and two tag names are actively misleading.
The 23 tags are clothing, hair and face. Measured on the sample, handwear-l is a
116×463 vertical strip and legwear is 310×547 spanning both legs: these are the
whole arm and leg, bare skin included, not gloves and socks. And
see-through only left/right-splits handwear, ears and the four eye tags —
legwear and footwear arrive as one layer covering both sides. Where it does
split, part_lr_split keeps only the two largest connected components and
silently discards the rest.
So ocs/limbs.py derives the separation from geometry it controls:
every pixel goes to the bone segment it is nearest, the way a Blender bone envelope
works. That produces left and right limbs even for a single-blob silhouette, a
symmetric pose, or arms crossing the torso — and it keeps every component.
Separation is per side, not per joint. Cutting at the elbow and knee as well
seemed reasonable and was wrong: each piece then follows one bone, so the moment
anything moves the cut lines tear and ghost. On one character the skirt became
seven pieces overlapping themselves across 31185 px, and the seams were plainly
visible in motion. A weighted mesh does not need cutting — the weights blend the
bend — so limbs and garments stay whole and are weighted across the joints they
span. RigSettings.slice_limb_spanning still enables the old behaviour.
3. Region attachments cannot bend. ocs/rig.py builds
triangulated mesh attachments with bone weights for every deformable part, so
the elbow bends the pixels instead of pivoting a rigid quad. Small rigid features
(irides, nose, mouth) stay as regions, where meshing would only add vertices.
Two findings that only showed up by running it:
- Nearest-segment assignment alone is wrong. With the arms hanging at the sides,
the forearm bone is genuinely closer to the upper thigh than the thigh's own
bone is, so
legwearwas being cut intoarm_r_lowerand the shorts were weighted to the hands.taxonomy.TAG_ALLOWED_REGIONSconstrains which regions each tag may occupy, and the same table constrains the weighting. - "Left and right are mirrored, so flip the sign" is false. A bone's frame is its own aim direction, and on a standing figure both legs point down rather than outward, so they share a frame. Flipping on top of an alternating walk phase cancelled it exactly and both legs swung forward together.
- Windows (paths and helper scripts are PowerShell; the Python is portable)
- An NVIDIA GPU. 16 GB is comfortable; 8 GB works with the low-VRAM paths.
- uv, Python 3.12, git
- ~20 GB free for model weights
Verified on: Windows 11, RTX 5070 Ti (16 GB, sm_120), CUDA 12.8, Python 3.12.13, torch 2.8.0+cu128. Blackwell cards need cu128 wheels — earlier builds have no sm_120 kernels.
git clone --recurse-submodules https://github.com/sleeeppy/OCS.git
cd OCS
./scripts/setup_env.ps1One virtualenv holds both OCS and see-through. see-through's requirements.txt
already pins the heavy half of what OCS needs (numpy, opencv, pillow, scipy,
scikit-image, psd-tools), so a second environment would only invite version
conflicts. GPU work runs as a subprocess of the same interpreter.
Optional, for previews that work with no network:
./scripts/fetch_spine_player.ps1The Spine Web Player is not committed — its licence restricts redistribution and requires your own Spine licence. Without this step previews load the runtime from unpkg instead.
./scripts/run_ocs.ps1Then open http://127.0.0.1:8765/. Drop in an illustration and work through the four steps.
Use a transparent-background PNG. OCS falls back to estimating the outline from the background colour for flat RGB input, and warns you when it does. The silhouette is what the limb split is cut from, so a real cutout is the single biggest quality lever.
The first run downloads ~12 GB of model weights. Later runs take a couple of minutes per image.
- Upload — resolution, steps and seed are under 고급 설정.
group offloadis on by default: the plain path peaks at 12–16 GB at 1280, right at the edge of a 16 GB card, while offload brings it to ~10 GB. - Layer cleanup — thumbnails with the reason each layer was dropped or flagged. Untick to exclude; auto-dropped layers can be brought back.
- Bone placement — drag the joints.
Shift+drag moves the subtree,Xmirrors left↔right,Ssnaps symmetric pairs,0fits the view. Toggle the partition overlay to see exactly which pixels each bone will claim. - Rig & preview — mesh generation, atlas packing,
skeleton.json, and a preview you can download and open anywhere.
workspace/projects/<id>/export/
skeleton.json Spine 4.2 — weighted meshes, 5 preset animations
skeleton.atlas libgdx atlas
skeleton.png packed texture page
preview.html standalone, everything inlined as data URIs
Animation: idle — a 3.6 s resting breath, and the only preset exported by
default. It is the one animation that suits any pose; the others in
spine_export.PRESETS (walk, wave, jump, turn_head) assume a standing
figure, and a seated character swinging a leg through a floor-length skirt looks
broken however well the mesh deforms. Pass animations=[...] to export_skeleton
to get them.
The idle avoids the three things that make an idle read as mechanical: everything
keys on the same frames, the loop is short, and the whole body inflates together.
So the chest leads and the head follows, the neck and head counter-rotate to keep
the head level, hair trails wider and later — and torso is never moved at all,
because it is the root of the body and translating it lifts a skirt off the floor
with it (measured: 3 px of chest rise moved the bottom edge of a seated figure by
3 px).
root
└─ torso
├─ neck ── head ── eyes
├─ rightArm ── rightElbow ── (rightHand)
├─ leftArm ── leftElbow ── (leftHand)
├─ rightLeg ── rightKnee ── (rightFoot)
└─ leftLeg ── leftKnee ── (leftFoot)
{side}Arm is the shoulder and {side}Leg the hip. Bracketed bones are optional,
as are hairBack, tail and wings — added only when a matching layer exists, or
by hand in the editor.
right means the character's right, i.e. the viewer's left. This matches
see-through's own convention in label_lr_split, where the lower-centroid-x
component is tagged -r. Getting it backwards mirrors every animation.
Already have see-through output, or want to iterate on cleanup, partitioning or rigging without paying for inference each time:
.venv/Scripts/python.exe scripts/import_psd.py workspace/seethrough/foo.psd
.venv/Scripts/python.exe scripts/render_debug.pyrender_debug.py writes a sheet showing the source, the silhouette, the bones and
the bone partition — handy for judging a rig at a glance or filing a bug that
shows what OCS actually decided.
ocs/
taxonomy.py see-through's 23 tags, the bone template, tag→region/bone rules
seethrough.py subprocess driver for the submodule
psd_io.py PSD + sidecars → Part objects
silhouette.py character outline (harder than it sounds — see the module docstring)
cleanup.py requirement 2: dummy-layer detection, two tiers
limbs.py requirement 2-2: bone-driven partition, L/R guaranteed
skeleton.py requirement 1: initial bone guess, geodesic not proportional
rig.py requirement 3: meshes, weights, bone frames
spine_export.py Spine 4.2 writer + animation presets + validator
atlas.py shelf packer + .atlas writer
player.py requirement 4: standalone preview
pipeline.py project state machine
server.py FastAPI app, SSE progress
web/ editor UI — plain HTML/CSS/JS, no build step
tests/ pytest; fixtures modelled on real see-through output
.venv/Scripts/python.exe -m pytest tests -qThe interesting cases are the hard ones: a single connected silhouette in a single
layer (where only the skeleton can tell left from right), a layer covering both
legs, weights that must not leak between an arm and a leg, and that no NaN ever
reaches the JSON — bare NaN is not valid JSON and silently kills the browser's
state feed.