Appendix F: file formats
A subproject, work/<name>/
input/origin.png (the picture), parts.py (this picture’s parts table), overrides.json (line overrides) and README.md (written by a person); then one phaseN-<name>/ per milestone (phase1-layers, phase2-draft, phase3-paint), each with a README.md a person writes saying what it produces and its verdict; and inside those, one stepNN-<stage>/ per stage, numbered globally: main.py (this step’s wiring, with the upstream and the knobs at the top), README.md (the numbers main.py wrote plus the ## Verdict a person adds), job.json (for the three steps that paint: the Krita job’s arguments), and the artifacts. A failed or experimental step is stepNN-<stage>-legacy-<wrong|experiment>-<k>/ and is not a step.
What painting produces, in stepNN-*/krita/
The names are fixed:
| file | for whom | contents |
|---|---|---|
final.kra | the artist | the whole layer tree; clip, blend, opacity, label and vector layers all survive the round trip |
final.png | previewing and the criteria | the composite |
strokes.jsonl | replay and audit | the painting journal: every stroke (below) |
stages/NN-*.png, stages/*.kra | the process | one image per stage; the finished version saves a checkpoint after each group |
review/ | a person | the review images (below; what to look for in each is in ch. 7) |
The review images
The main text says which image to look at and what to look for; these are their names. The ones a painting step writes land in stepNN-*/krita/review/, the rest beside the step’s other artifacts.
| name | written by | what it shows |
|---|---|---|
regions.png, regions.json | segment | the map of numbered raw regions, which a person reads the numbers off |
review-parts.png, review-parts.md | segment | three columns – the hand-written table, the state after the majority vote, and the pixels the vote moved – plus the table of what was reassigned |
lines-review.png, lines-review.json | lineart | the kept lines over a faded source (blue = a kept AI line, brown = a kept contour, red dashes = dropped), and every candidate’s scores |
review-amodal.png | complete | one row of four panels per group: the occluded area, the completed image, this layer alone, its outline |
review-restack.png | complete | source, restacked, and the difference – red for a mismatch, yellow for what no group claimed (which must be empty) |
lines-<G>-review.png, .json | lines | one per layer, that layer alone on a ground, in the same three colours |
review-lines.png | lines | all the kept lines over the source, coloured by layer; the pale ones fall in the invented area |
composite-1x.png | draft, paint, trace | the whole picture at 1x – also where black rims around the silhouette show up |
crop-<tag>-1to1.png | draft, paint, trace | the 1:1 close-ups, one per entry in the parts table’s CROPS |
layer-<G>.png, layer-<G>-<kind>.png | draft, paint, trace | each layer alone on grey: its coverage, its holes, and what one method’s own layers look like |
group-<G>.png | draft, paint, trace | each group rendered alone with nothing over it – “is the kettle whole”, and where the hidden lines are |
cmp/ | gauge | the matching-position crops against another subproject’s finished picture: source, it, this one |
The painting journal, strokes.jsonl
Session(journal=True) wraps every painting and pixel-writing method and writes one line of JSON per call. It is the only evidence that this picture was really painted, and the input for replaying it. Every line has i (a sequence number), t (seconds) and operation. Coordinates are always canvas pixels (4096x6144 for a 4x job), with y downward.
Painting operations, which replay exactly: stroke (paintLine segment by segment, with points and pressure), path (one paintPath at constant pressure), line, dab, ellipse and polygon. Setting operations: set_preset, set_color and brush (the fast path, recording only the fields actually passed). Pixel-writing operations record a fingerprint rather than the pixels: set_pixels (box, shape, mean, sha1), ground, erase and filter.
{"i": 4, "t": 221.043, "operation": "brush", "preset": "b) Airbrush Soft"}
{"i": 5, "t": 221.044, "operation": "brush", "rgb": [0, 0, 0], "size": 584.95}
{"i": 6, "t": 221.045, "operation": "stroke", "layer": "BG_Glow", "points": [[0.0, 0.0], [292.32, 0.0]], "pressure": [0.55, 1.0]}
A stroke’s colour and brush size come from the nearest preceding brush entry; size is the value passed to setBrushSize and not the width painted (the brush factor, ch. 12).
A pixel-writing entry carries a why: writing pixels is allowed, but every write has to say why (ch. 7). One without a why is a bare write, of which the threshold is zero. ground calls set_pixels internally, so one fill records two entries, both carrying a why.
bake is another kind of entry, one per node as it is laid down: {"operation":"bake","node":"04_Color/Hair_Front/...","kind":"strokes","numbers":4097,"strokes":427}. numbers is computed from the layer tree’s node, not from pixels – a gradient covering 8K is nineteen numbers even though it wrote 25 million pixels.
gauge.report() tallies it, and Session.replay(jsonl, session) repaints from the journal (a pixel-writing entry records only a fingerprint, so the original data has to be present to replay it).
The layer tree, tree.json
This is the intermediate form handed to Krita, and the final form painting takes inside the library. What used to pass between the planner and Krita was a list of strokes, a model that forced everything to become strokes; now a tree passes, and the painting job (jobs/bake.py) is only its executor. It can be read, diffed, edited and counted without opening Krita.
Coordinates are always 1x source pixels (measured from the top-left corner, y downward); only ops/bake knows about scale.
{ "ir": 1, "unit": "px", "w": 1024, "h": 1536, "scale": 4, "dpi": 300.0,
"color": {"model": "RGBA", "depth": "U8", "profile": "sRGB-elle-V2-srgbtrc.icc"},
"source": {"path": "…/input.png"},
"mode": "layered", // layered | trace -- the criteria gate one and not the other (ch. 8)
"crops": {"eye": [4160, 3440]}, // 8x coordinates, straight from parts.py's CROPS
"root": [ <node>, … ], // **bottom to top**, the same direction Krita's layer docker reads
"made_by": "stages.tree.from_plan" }
Every node’s common fields: id (the layer’s path in Krita, unique in the tree), kind, name, blend, opacity (0 to 1, not 0 to 255), clip (which is setInheritAlpha), visible, label, why (which action of the vocabulary produced it) and cite (where in this book it comes from). Anything omitted takes its default, so the JSON holds only what differs.
| kind | its own fields | what it becomes |
|---|---|---|
group | children[] and passthrough | createGroupLayer |
fill | paths: [{shape, rgb}] | createVectorLayer plus addShapesFromSvg; a shape of null means a full-canvas solid colour and becomes a paint layer filled flat (three numbers) |
gradient | type (linear / radial / conical), p0, p1, stops[], repeat and shape | compute the pixels yourself and setPixelData – not one parameter of Krita’s gradient generator layer takes effect (appendix C) |
strokes | brush: {preset, width_factor} and strokes[] | paintPath, or paintLine segment by segment |
mask | of, shape and feather | createTransparencyMask plus setSelection; attached to the group it belongs to |
filter | as, filter: {name, params}, shape and feather | createFilterMask; attached to the parent group likewise, and at most one per layer (stacking more is O(n^2), appendix C) |
raster | file, box, size and a required reason | setPixelData; its description count follows size |
A shape: {"rings": [{"bezier": [[p0,c1,c2,p3], …], "closed": true}], "rule": "evenodd", "from": "read.complete/Hair_Front@amodal"} – the same ruler the lineart and the bands use (geometry/rings.py).
A stroke: {"points": [[x,y], …] | "bezier": […], "radius": 14.0, "rgb": [r,g,b], "opacity": 0.35, "taper": true, "brush": {…}}. A brush is written only when this stroke’s brush differs from its layer’s – a layer holding several kinds is the normal case (<G>_Shade takes both the soft airbrush’s shade and the hard round’s sharp).
The plans, in each step’s directory
Every coordinate is 1x, and the painting job multiplies.
segment (stepNN-segment/): plan.json (each sub-part’s Flat ring in parts[], the silhouette, assign_names), plan.npz (parts after the vote, parts_assign from the hand-written table, subject with its alpha holes filled, subject_assign, rgba), ai/mask_<word>.png, regions.png and regions.json, and review-parts.png and .md. trace (one-shot, stepNN-trace/): plan.json (w h scale expand smooth eps, the silhouette rings, and parts[] with id name px bbox flat tiers[], each band carrying role rgb rings[], where rings[].points are Catmull-Rom control points and band 0 bottom-up is always Flat), plan.npz (parts subject rgba shade light lines), strokes.jsonl (the plan, one stroke per row: kind points radius rgb opacity taper part tier – the same name as the painting journal and a different thing), tree.json, preview.png (the 1x simulation), preview_cel.png (the bands alone), parts.png and cells.png; plus krita/review/ with the close-ups and each part group and each band rendered alone.
lineart: ai/lineart_{manga,anime,std}.png, strokes_lines.jsonl (the arbitrated lines), lines.json (the line layers), and lines-review.png and .json. complete: seethrough/<source name>/ (what See-through produced: <layer>.png and _depth.png per layer, src_img.png, info.json; absent when another step’s is reused), seethrough.json (which directory was used, each layer’s pixel count, and the group-to-layer mapping), amodal.npz (amodal_<G> the whole mask and region_<G> the invented part), amodal.json (seed, and per group px_vis px_added px_region rule rings bez_rings plus the merge’s account px_cand px_island px_black shift px_overlap and restack), layer_<G>.png (each layer’s complete RGBA pixels: the source where visible, its own layer where invented), review-amodal.png and review-restack.png. lines: ai/solo_<G>.png (that layer alone on a ground), ai/<G>/lineart_*.png, strokes_lines.jsonl (each stroke also carrying group and made_up), lines-<G>-review.png and .json, review-lines.png, and lines.json (layers, per group contour ai kept made_up, source, agreement). draft: plan_draft.json (groups[] with name rgb rings bez_rings fills[]; lines[]; crops), tree.json, strokes_draft.jsonl, strokes_lines.jsonl (copied unchanged from lines), preview.png and krita/. paint: plan_draft.json (rewritten in place: the eye method replaces the Eye group’s Shape with the eye area the source defines), plan_paint.json (format 2: mode="parts", crops from parts.py’s CROPS, and groups[] with name method shade psnr_before psnr_after [strands] [blend], where method is a name in dtp.paint.METHODS; a banded group also carries shadow / deep / light, each {rings, bez_rings, rgb}, from which the Krita job builds the <G>_Shadow subgroups as a vector Shape plus a clipped Paint layer), strokes_paint.jsonl, strokes_lines.jsonl (weighted against each layer’s own image), tree.json (the layer tree, above), bg_field.png (the background colour field, 32x48), masks_paint.npz (each group’s visible mask at 1x, mutually exclusive, used by the rulers), preview_paint.png, krita.log (the Krita job’s full log, with [tree] N nodes), and krita/ (which also holds layers.json: path, type, blend, clip and bounds for every node in the tree, which has to agree with tree.json – that agreement is the evidence the tree was laid down correctly). The crops in each plan file are parts.py’s CROPS, which the job writes its close-ups from. An experimental step’s main.py copies the plan files from a baseline step and changes the plan for one group only. gauge: gauge.json (the five criteria plus groups, each with px dist interior src_interior edge src_edge psnr1x, plus tier_edges, nodes and lines_made_up) and cmp/.
overrides.json (shared by lineart and lines; its boxes are 1x full-image coordinates):
[{"box": [x0, y0, x1, y1], "use": "ai" | "contour" | "none"}, {"id": "…", "use": "…"}]
A stroke’s row in a plan
{"kind": "soft", "points": [[x, y], …], "radius": 14.0, "rgb": [r, g, b], "opacity": 0.35, "taper": true, "part": "Hair_Back", "tier": 0}
radius is the radius at 1x; converting it to a setBrushSize value is 2 x radius x multiplier / the brush factor, done while building the tree (stages/tree.py). taper is only a shape marker for the planner’s own simulation – tapering is decided by the stroke’s kind (ops/brushes.KIND) and no single stroke may override it. Letting a stroke decide was tried, and soft went from a light sweep to full pressure covering everything, blurring the whole picture. Both are now looked up once while building the tree and written into every stroke in tree.json; the painting job looks nothing up.