mirror of
https://github.com/curioustorvald/Terrarum-sans-bitmap.git
synced 2026-03-07 11:51:50 +09:00
122 lines
5.0 KiB
Markdown
122 lines
5.0 KiB
Markdown
# Autokem
|
|
|
|
CNN-based tool that predicts kerning tag bits for font sprite sheets.
|
|
Trains on manually-tagged `*_variable.tga` sheets (~2650 samples across 24 sheets), then applies learned predictions to new or untagged sheets.
|
|
|
|
## Building
|
|
|
|
```bash
|
|
cd Autokem
|
|
make # optimised build (-Ofast)
|
|
make debug # ASan + UBSan, no optimisation
|
|
make clean
|
|
```
|
|
|
|
## Usage
|
|
|
|
```bash
|
|
./autokem train # train on ../src/assets/*_variable.tga
|
|
./autokem apply ../src/assets/foo_variable.tga # apply model to a sheet
|
|
./autokem stats # print model tensor shapes + metadata
|
|
./autokem help
|
|
```
|
|
|
|
- `train` scans `../src/assets/` for `*_variable.tga` (skips `*extrawide*`), collects labelled samples, trains with 80/20 split + early stopping, saves `autokem.safetensors`
|
|
- `apply` creates `.bak` backup, runs inference per cell, writes Y+5 (lowheight) and Y+6 (kern data) pixels. Skips cells with width=0, writeOnTop, or compiler directives
|
|
- Model file `autokem.safetensors` must be in the working directory
|
|
|
|
## Architecture
|
|
|
|
### Neural network
|
|
|
|
```
|
|
Input: 15x20x1 binary (300 values, alpha >= 0x80 → 1.0)
|
|
Conv2D(1→12, 3x3, same) → LeakyReLU(0.01)
|
|
Conv2D(12→16, 3x3, same) → LeakyReLU(0.01)
|
|
Flatten → 4800
|
|
Dense(4800→24) → LeakyReLU(0.01)
|
|
├── Dense(24→10) → sigmoid (shape bits A-H, J, K)
|
|
├── Dense(24→1) → sigmoid (Y-type)
|
|
└── Dense(24→1) → sigmoid (lowheight)
|
|
Total: ~117,388 params (~460 KB float32)
|
|
```
|
|
|
|
Training: Adam (lr=0.001, beta1=0.9, beta2=0.999), BCE loss, batch size 32, early stopping patience 10.
|
|
|
|
### File layout
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `main.c` | CLI dispatch |
|
|
| `tga.h/tga.c` | TGA reader/writer — BGRA↔RGBA8888, row-order handling, per-pixel write-in-place |
|
|
| `nn.h/nn.c` | Tensor, Conv2D (same padding), Dense, LeakyReLU, sigmoid, Adam, He init |
|
|
| `safetensor.h/safetensor.c` | `.safetensors` serialisation — 12 named tensors + JSON metadata |
|
|
| `train.h/train.c` | Data collection from sheets, training loop, validation, label distribution |
|
|
| `apply.h/apply.c` | Backup, eligibility checks, inference, pixel composition |
|
|
|
|
## Pixel format
|
|
|
|
All pixels are RGBA8888: `(R<<24) | (G<<16) | (B<<8) | A`. TGA files store bytes as BGRA — the reader/writer swaps B↔R.
|
|
|
|
### Tag column (rightmost pixel column of each 16x20 cell)
|
|
|
|
| Row | Field | Encoding |
|
|
|-----|-------|----------|
|
|
| Y+0..Y+4 | Width | 5-bit binary, alpha != 0 → bit set |
|
|
| Y+5 | lowheight | alpha=0xFF → lowheight, alpha=0 → not |
|
|
| Y+6 | Kern data | See below |
|
|
| Y+9 | Compiler directive | opcode in R byte; skip cell if != 0 |
|
|
| Y+17 | writeOnTop | alpha != 0 → skip cell |
|
|
|
|
### Y+6 kern data pixel
|
|
|
|
```
|
|
R byte: Y0000000 (Y-type flag in MSB, bit 31)
|
|
G byte: JK000000 (J = bit 23, K = bit 22)
|
|
B byte: ABCDEFGH (A = bit 15, ..., H = bit 8)
|
|
A byte: 0xFF (hasKernData flag — must be 0xFF, not 0x01)
|
|
```
|
|
|
|
`tagify(pixel)`: returns 0 if alpha == 0, else full pixel value.
|
|
`kerningMask = (pixel >> 8) & 0xFFFFFF` then extract individual bits.
|
|
|
|
### Shape bit layout
|
|
|
|
```
|
|
A-B top (unset for lowheight minuscules like e)
|
|
|-|
|
|
C-D middle hole for majuscules (like C)
|
|
E-F middle hole for minuscules (like c)
|
|
G-H
|
|
--- baseline
|
|
|-|
|
|
J-K descender
|
|
```
|
|
|
|
## Key pitfalls
|
|
|
|
- **Alpha must be 0xFF, not 0x01.** All manually-tagged sheets use alpha=255 for kern/lowheight pixels. Writing alpha=1 is functionally accepted by the font engine (`& 0xFF != 0`) but produces visually transparent pixels that look like nothing was written.
|
|
- **TGA byte order**: file stores BGRA, memory is RGBA8888. Must swap B↔R on both read and write.
|
|
- **Row order**: check TGA descriptor bit 5 (`top_to_bottom`) for both read and write paths.
|
|
- **XY-swap**: `*_xyswap_variable.tga` sheets use column-major cell enumeration. Both train and apply detect `xyswap` in the filename.
|
|
- **Overfitting**: 117K params vs ~2650 samples — early stopping is essential. The model will memorise training data almost perfectly.
|
|
- **Sigmoid stability**: two-branch form (`x >= 0` vs `x < 0`) to avoid `exp()` overflow.
|
|
|
|
## Reference files
|
|
|
|
| File | What to check |
|
|
|------|---------------|
|
|
| `TerrarumSansBitmap.kt:917-930` | Tag parsing (Y+5, Y+6, tagify) |
|
|
| `TerrarumSansBitmap.kt:3082-3134` | Keming rules, kemingBitMask, rule matching |
|
|
| `OTFbuild/tga_reader.py` | TGA BGRA→RGBA conversion (reference impl) |
|
|
| `OTFbuild/glyph_parser.py:107-194` | Sheet parsing, eligibility, xyswap |
|
|
| `keming_machine.txt` | Bit encoding spec, shape examples, rule definitions |
|
|
|
|
## Verification
|
|
|
|
1. `make && ./autokem train` — should find ~2650 samples, label distribution should show A~55%, C~92%, etc.
|
|
2. `./autokem stats` — prints tensor shapes, training metadata
|
|
3. `./autokem apply ../src/assets/currencies_variable.tga` — creates `.bak`, writes kern bits
|
|
4. Check applied pixels with Python: `from tga_reader import read_tga; img.get_pixel(tag_x, tag_y+6)` — alpha should be 0xFF, not 0x00
|
|
5. `java -jar FontDemoGDX.jar` with modified sheet to visually verify kerning
|