Chameleon learned layout generator
A 6,600-weight MLP that, given the analysed layout of a design and a target canvas size, predicts the box each content unit
should occupy on the target. The Chameleon resize engine turns the prediction into extra candidate layouts (learned,
learned:<rung>); the engine's text fitting, hard checks, rule score and critic then judge them like every other candidate. It
runs on numpy alone, offline, in well under a millisecond per task. Trained on layouts made by human designers (Crello / VistaCreate
size variants), geometry only.
Status: judged (round 6) and shipped behind an admission guard. It beats the uniform stretch clearly on held-out design families and moves the engine's picks closer to what designers made, but a strong vision judge preferred its changed picks only 66 to 44 (small text on narrow canvases, top-heavy stacks on tall ones). With the guard (a learned layout ships only if it keeps the smallest text within 1 px and the utilization score within 0.05 of the pick made without it) the kept picks were judged 54 to 17, and 71 to 27 out of sample on 90 fresh designs (round 7; the reverted picks 15 to 28). Evidence, numbers and caveats:
layout_report.mdยง4 to ยง6c; every changed pick:layout_picks.md.
Licence. Apache-2.0 (confirmed by the team on 2026-10-28). The weights derive from the geometry of Crello / VistaCreate templates (dataset licence CDLA-Permissive-2.0; no pixels, text, fonts or colours were used or are included).
Model details
- Type. Multi-layer perceptron, 33 inputs, two hidden layers of 64
tanhunits, 4 outputs. The output is a correction added to the source box (the uniform stretch):box = src_box + net(standardise(features)), in shares of the target canvas (cx, cy, w, h). - Developed by. Dev 3, Chameleon hackathon team (2026-10-27). Framework. numpy for training (a hand-written Adam loop) and inference.
- Size. 31.3 KB (
engine/models/layout.npz, float32); 6,596 weights. Limit 200 KB. - File.
layout.npz:kind("mlp"),activation("tanh"),n_layers(3),feature_names(33 strings, checked againstengine.resize.LAYOUT_FEATURESat load),muandsd(input standardisation),w0..w2andb0..b2,trained_on(a note). Arrays and strings only; loads withallow_pickle=False. - Loader.
engine.resize._layout_model()(lazy, cached per file state); a missing, corrupt or inconsistent file means the family is not generated and the engine's output is byte-identical to the output without it.CHAMELEON_LEARNED=0switches it off.
Intended use
Inside the engine only: _learned_candidates (end of section 2 of engine/resize.py). For every free content unit (not corner
furniture, header/footer band or rotated side label) it supplies where the unit's centre goes and how big its box is; text takes
the predicted width and the largest whole font size whose wrapped text fits the predicted box (never below what the proportional
family gives, never above the anchor's cap), images, groups and rotated elements are fitted into the box keeping their aspect.
Floors, margins, collisions, decorations and everything else are the engine's. It never moves, adds or removes an element by
itself and never touches content or fonts. The engine also picks without the family and lets a learned layout replace that pick only
when its smallest text is within TUNABLES["learned"]["guard_px"] (1 px) and its utilization score within guard_util (0.05) of it.
Out of scope. Aspect changes under 3% (not generated; the model also does not beat the stretch below an aspect change of 0.2), designs far from poster / social / banner layouts, scoring or ranking layouts, any claim about what a human or the organizers' judge prefers (see Limitations).
Inputs and output
Per content unit, 33 numbers from engine.resize._layout_features(analysis, target) (the same function builds the training set):
the unit's source box and ink box as shares of the source canvas (8), kind flags (text, image, shape, pinned corner furniture,
band, rotated, multi-element group), prominence, largest font size over the source short side, log text length, block index, block
count and relative position, flow axis, number of content units, source / target / log-ratio aspect (natural logs), source and target
short sides over 1000, rank by area and by font size, alignment (left / right) and a sibling flag. Geometry and text length only:
nothing reads ids, asset names, text, fonts or exact canvas sizes (a rename test covers it). Output: the correction to
(cx, cy, w, h) of the source box.
Training data
cyberagent/crello v5.1 (VistaCreate designs), geometry only (tools/crello/extract_layouts.py). 18,449 usable size-variant pairs
(same element count, type sequence and text lengths; pairs.py) in 1,316 families, each read in both directions: 36,898 directed
pairs, 240,227 content-unit rows, 5,563 source templates, no pair dropped after the pair filters. Source templates are converted
into Chameleon tasks (grey boxes for vector art, missing images, synthetic words of the real length) and analysed with the engine's
analyze_source; the target of a unit is the union of the designer's boxes of its member elements at the other size. Families are
split 70 / 30 (915 train, 401 test) by a hash of the family, so no template family is on both sides. The shipped weights were trained
on 85% of the train families (127,554 rows, both directions), early-stopped on the other 15% (33,443 rows); the 401 test families
were never trained on or selected with. Rows are weighted by 1 / sqrt(pairs in the family).
Loss: the engine's own designer distance on unit boxes, hypot(dcx, dcy) + 0.5 (|dw| + |dh|) (smoothed), 40 epochs of Adam
(learning rate 2e-3, cosine decay, batch 1,024, weight decay 1e-5), 5 seeds, the best on validation shipped.
Evaluation
On the 6,412 held-out pairs (39,620 unit rows, 401 families), distance to the designer layout (lower is better):
| unit distance | element distance (pairs pooled / family-balanced) | MAE cx / cy / w / h | |
|---|---|---|---|
| uniform stretch | 0.1886 | 0.1905 / 0.1260 | 0.068 / 0.066 / 0.103 / 0.059 |
| ridge regression | 0.1828 | 0.1822 / 0.1365 | 0.074 / 0.069 / 0.082 / 0.057 |
| sklearn MLP, squared loss | 0.1976 | 0.1993 / 0.1612 | 0.091 / 0.077 / 0.076 / 0.053 |
| this model | 0.1525 (5 seeds: 0.1524 ยฑ 0.0004) | 0.1545 / 0.1081 | 0.063 / 0.061 / 0.062 / 0.043 |
It beats the stretch in 67% of held-out pairs (35% when the aspect changes by under 0.2, 71% from 0.2 to 0.7, 84% above 0.7). In the
engine (3,000 pairs, 1,008 in test families; layout_eval.py): a learned candidate exists in 94% of test pairs, is valid in 96%
of those, is the valid candidate closest to the designer in 53%, and the selector ships it in 29%; pick distance 0.2522 โ 0.2352
(gain +0.0169, family-bootstrap 95% interval [+0.0132, +0.0209]; train families +0.0162). On our own sets (samples 46, synthetic 271,
holdout 845 pairs) hard checks, fallbacks and all-text-at-floor counts are unchanged and 136 picks move to learned (text smaller in
43, larger in 8). A strong vision judge, shown each of those changed picks against the engine's own (both letter orders), preferred the
learned pick 66 to 44 (110 decided). With the admission guard, as shipped: picked in 23.5% of test pairs, pick distance 0.2522 โ 0.2429
(+0.0093 [+0.0062, +0.0127]), the changed picks' smallest text larger in 392 and smaller in 36 of 699; on our three sets 97 picks change
(smaller text in 15, larger in 7), judged 54 to 17; on 90 fresh designs (1,900 pairs) 117 picks change, judged 71 to 27. Full tables,
the exploration (features, sizes, losses), both judge rounds and visual notes: layout_report.md.
Limitations and risks
- Designers are not the judge. Distance to the designer rewards what designers did: text kept near the area-preserving size, sparse canvases, content bleeding at the edges. The earlier judge rounds asked for larger text and filled canvases; the model inherits the designers' habits (top-heavy stacks on taller targets, small labels), and the rule score's fidelity term likes them. Visual review: tiny text at 160 x 600 and 728 x 90, a crowded 16:9 collage, a half-empty square stack among the changes.
- Different distribution. Crello is VistaCreate social posts, flyers and banners with grey placeholder art and synthetic text; the organizers' briefs are brand banners with real photos, logos and copy. Nothing here has been checked on a held-out brief.
- The guard's thresholds were chosen on round 6 and checked out of sample in round 7 (90 fresh designs, 168 comparisons): kept picks 71 to 27, reverted 15 to 28. The utilization axis replicated; the text axis alone did not (7 to 6).
- The critic never saw these layouts. 10 of the 136 unguarded changed picks are the judge-distilled critic choosing a learned finalist (judged 6 to 4); the 8 other picks its finalist set moved revert through the guard.
- Validity. 95% of generated candidates pass every hard check; the rest are last-resort layouts with text overlapping text (never shipped: the selector only takes valid ones). At large aspect changes (>= 0.7) 91%.
- Local measurements only (macOS CoreText text measurement); the Docker (FreeType) run was not repeated for this model.
- Trained on the train families only so that the held-out numbers are honest; retraining on all 1,316 families
(
train_layout.py) would add about 30% more data and cannot be evaluated on Crello afterwards.
Reproduce
.venv/bin/python tools/crello/layout_dataset.py # 5 s
OMP_NUM_THREADS=4 /opt/anaconda3/bin/python3 tools/crello/train_layout.py --skip-sklearn --export engine/models/layout.npz # ~2.5 min
.venv/bin/python tools/crello/designer_eval.py --out output/crello/eval_learned # ~1.5 min on 8 workers
.venv/bin/python tools/crello/layout_eval.py --base output/crello/eval_base/candidates.jsonl --new output/crello/eval_learned/candidates.jsonl
.venv/bin/python -m unittest tests.test_learned
Standalone forward pass (no engine): standardise the 33 features with mu and sd, apply tanh(x @ w0 + b0), tanh(. @ w1 + b1),
. @ w2 + b2, add the result to the first four features (the source box as cx, cy, w, h shares).