Shared Avior Studio image scripts. Keep image automation here instead of copying Python scripts into individual game repositories.
These scripts require Python 3.12 and Pillow.
python -m pip install -e .Run project-specific scripts from the target game repository root, or pass --project-root explicitly.
python ../img-scripts/scripts/cut_sheet.py sheet.png --out-dir out
python ../img-scripts/scripts/image_resize.py image.png
# Prizm
python ../img-scripts/scripts/cut_sheet.py --project-root .
# Fixed-grid sheets
python ../img-scripts/scripts/cut_sheet.py sheet.png --grid 4x3 --out-dir out
# Inferred-layout sheets. This finds transparent horizontal/vertical corridors,
# recursively slices on those logical lines, then tightly crops each region.
python ../img-scripts/scripts/cut_sheet.py sheet.png --out-dir out
# If faint alpha debris blocks otherwise-clear corridors, tune the line mask.
python ../img-scripts/scripts/cut_sheet.py sheet.png --out-dir out --line-min-gap 12 --line-max-pixels 8
# Revik
python ../img-scripts/scripts/generate_terrain_tiles.py --project-root . grass forestcut_sheet.py records two per-asset counts in the manifest. They cover different
regions and each one is blind in a different mode, so they are only useful read
together with component_source and the flags the manifest records alongside them.
| Field | Region | Count |
|---|---|---|
padding_ring_foreground |
inside padded_box, outside box |
mask_area(padded_box) - mask_area(box) |
content_box_unclaimed_foreground |
inside box |
mask_area(box) - alpha_area |
The run prints a warning naming every crop with a non-zero count on either.
Both are foreground counts over a region and are never negative. Neither computes ownership, and zero is necessary but NOT sufficient. The ring count is "foreground in the ring, whoever owns it". The in-box count is "foreground in the content box that is not this component's own blob" -- another sprite overlapping the box, or this asset's own detached sparks. Read them with these limits in mind:
--padding 0makespadding_ring_foregroundidentically 0, by construction: there is no ring.--paddingdefaults to 0, so unless you pass a padding, the ring count measures nothing and--fail-on-ring-foregroundcan never fire. This is the common case, not the exception.content_box_unclaimed_foregroundis identically 0 unlesscomponent_sourceisconnected_componentsand--group-radiusis 0. Ongridandcorridors, at any--group-radiusabove 0, and for any asset--merge-distancemerged (see below),alpha_areais measured over the whole content box, so the subtraction is 0 by construction.--group-radiusdefaults to 0, but--layout-mode auto(the default) prefers corridors and only falls back to connected components when corridor slicing under-splits -- so which count is live depends on the sheet. The manifest'scomponent_sourcerecords which branch ran. Do not raise--group-radiusto clear a non-zero count: that silences the measurement without changing the crop.- Neither sees a neighbour swallowed whole. If a corridor cuts through the next
sprite and the box takes that part outright,
tight_mask_boxcounts those pixels as the asset's ownalpha_area, so both counts read clean. - A merged crop reports 0 on both. Two whole sprites in one box have nothing in
the ring and nothing unclaimed. Raising
--line-min-gapuntil sprites merge drives both counts to 0 while making the cut strictly worse. - They flag the asset's own art too. A box that clips a sprite leaves that
sprite's own pixels in its ring; an asset with detached sparks reports them as
unclaimed at
--group-radius 0. Non-zero means "something is there", not "a neighbour is there". --merge-distanceabove 0 zeroes the in-box count for the assets it merges. A merged asset's box is the union of its pieces, and itsalpha_areais measured over that union rather than summed from the pieces, so the subtraction is 0 by construction for it -- the same degeneracy--group-radiushas, for the same reason. Assets that merged with nothing still report the count. Summing instead would be wrong in both directions: it double-counts where two pieces' boxes overlap (reachable at--group-radiusabove 0) and misses foreground in the union box that belonged to no piece. Note that a merged asset'salpha_areatherefore counts every foreground pixel in its union box, including debris below--min-areathat belongs to no piece -- andalpha_areais not only reported. It also feeds--sort areaordering (so--namescan map to different sprites) and--alpha-threshold autoselection (which can change the saved PNG bytes). Both need--merge-distanceabove 0 together with that flag; neither is reachable otherwise.
Non-zero means a sprite ships with a fragment of something baked into it. It happens
when crop boxes sit closer together than --padding, or when a slice corridor runs
through art rather than between it. Fix it by lowering --line-min-gap, lowering
--padding, or spacing the assets further apart on the source sheet -- not by
editing the cut PNGs.
Do not raise --line-min-gap. That value is the minimum width of a clear
corridor for a split to be accepted, so raising it rejects narrow corridors and
merges neighbouring sprites into a single crop. Measured on a 16-asset sheet:
--line-min-gap 8 yields 16 assets, 24 yields 15 (two sprites in one crop), 40
yields 8 (one crop holding four sprites) -- and every one of those merged runs reports
0 on both counts, re-measured under the current formulas.
Because zero is not sufficient, both checks stay warnings by default. Pass
--fail-on-ring-foreground or --fail-on-unclaimed-foreground to exit non-zero
instead, so a consumer repo's CI can gate on them; a clean gate still needs a look at
the cut PNGs. Gate on the count that is actually live for your flags -- gating on a
count that is 0 by construction is a green light that measured nothing.
--fail-on-padding-ring is accepted as the old spelling of
--fail-on-ring-foreground.
# The warnings name the offenders and how many pixels each one carries.
python ../img-scripts/scripts/cut_sheet.py sheet.png --out-dir out --padding 6
# Same, but non-zero exit if any ring is dirty. Needs --padding above 0 to mean anything.
python ../img-scripts/scripts/cut_sheet.py sheet.png --out-dir out --padding 6 --fail-on-ring-foreground
# Gate the in-box count instead. Needs --group-radius 0 and a connected_components cut.
python ../img-scripts/scripts/cut_sheet.py sheet.png --out-dir out \
--layout-mode components --group-radius 0 --fail-on-unclaimed-foregroundThe manifest carries a padding_ring_foreground_note and a
content_box_unclaimed_foreground_note restating these limits, because the manifest
is the artifact that crosses a repo boundary and this README does not travel with it.
| Script | Origin | Notes |
|---|---|---|
scripts/cut_sheet.py |
Shared | Sprite sheet cutter with alpha/background detection, inferred transparent-corridor slicing, connected-component fallback, optional grid mode, auto threshold, padding, crop contamination counts, and manifest output. |
scripts/generate_terrain_tiles.py |
Revik | Revik-specific terrain hex tile generator. |
scripts/image_resize.py |
Revik | Pillow force-resize to exactly 512x512 while preserving the source extension. |