summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cnn_v3/docs/HOW_TO_CNN.md102
-rw-r--r--cnn_v3/tools/index.html1
-rw-r--r--cnn_v3/tools/tester.js45
-rw-r--r--cnn_v3/tools/weights.js7
-rw-r--r--cnn_v3/training/export_cnn_v3_weights.py28
5 files changed, 121 insertions, 62 deletions
diff --git a/cnn_v3/docs/HOW_TO_CNN.md b/cnn_v3/docs/HOW_TO_CNN.md
index 4966a61..624deaa 100644
--- a/cnn_v3/docs/HOW_TO_CNN.md
+++ b/cnn_v3/docs/HOW_TO_CNN.md
@@ -58,13 +58,13 @@ Input: 20-channel G-buffer feature textures (rgba32uint)
```
photos/Blender → pack → dataset/ → train_cnn_v3.py → checkpoint.pth
- export_cnn_v3_weights.py
- ┌─────────┴──────────┐
- cnn_v3_weights.bin cnn_v3_film_mlp.bin
- │
- CNNv3Effect::upload_weights()
- │
- demo / HTML tool
+ export_cnn_v3_weights.py [--html]
+ ┌──────────┴────────────┬──────────────┐
+ cnn_v3_weights.bin cnn_v3_film_mlp.bin weights.js
+ │ (HTML tool
+ CNNv3Effect::upload_weights() defaults)
+ │
+ demo
```
---
@@ -284,29 +284,36 @@ The U-Net conv weights and FiLM MLP train **jointly** in a single run. No separa
### Prerequisites
+`train_cnn_v3.py` and `export_cnn_v3_weights.py` carry inline `uv` dependency metadata
+(`# /// script`). Use `uv run` — no manual `pip install` needed:
+
```bash
-pip install torch torchvision pillow numpy opencv-python
cd cnn_v3/training
+uv run train_cnn_v3.py --input dataset/ --epochs 1 --patch-size 32 --detector random
```
-**With `uv` (no pip needed):** dependencies are declared inline in `train_cnn_v3.py`
-and installed automatically on first run:
+**Without `uv` (manual pip):**
```bash
+pip install torch torchvision pillow numpy opencv-python
cd cnn_v3/training
-uv run train_cnn_v3.py --input dataset/ --epochs 1 --patch-size 32 --detector random
+python3 train_cnn_v3.py ...
```
+The pack scripts (`pack_photo_sample.py`, `pack_blender_sample.py`) and
+`gen_test_vectors.py` do **not** have uv metadata — run them with `python3` directly
+(they only need `numpy`, `pillow`, and optionally `openexr`).
+
### Quick-start commands
**Smoke test — 1 epoch, validates end-to-end without GPU:**
```bash
-python3 train_cnn_v3.py --input dataset/ --epochs 1 \
+uv run train_cnn_v3.py --input dataset/ --epochs 1 \
--patch-size 32 --detector random
```
**Standard photo training (patch-based):**
```bash
-python3 train_cnn_v3.py \
+uv run train_cnn_v3.py \
--input dataset/ \
--input-mode simple \
--epochs 200
@@ -314,7 +321,7 @@ python3 train_cnn_v3.py \
**Blender G-buffer training:**
```bash
-python3 train_cnn_v3.py \
+uv run train_cnn_v3.py \
--input dataset/ \
--input-mode full \
--epochs 200
@@ -322,7 +329,7 @@ python3 train_cnn_v3.py \
**Full-image mode (better global coherence, slower):**
```bash
-python3 train_cnn_v3.py \
+uv run train_cnn_v3.py \
--input dataset/ \
--input-mode full \
--full-image --image-size 256 \
@@ -340,7 +347,8 @@ python3 train_cnn_v3.py \
| `--lr F` | 1e-3 | Reduce to 1e-4 if loss oscillates or NaN |
| `--patch-size N` | 64 | Smaller = faster epoch, less spatial context |
| `--patches-per-image N` | 256 | Reduce for small datasets |
-| `--detector` | `harris` | `random` for smoke tests; `shi-tomasi` as alternative |
+| `--detector` | `harris` | `random` for smoke tests; also `shi-tomasi`, `fast`, `gradient` |
+| `--patch-search-window N` | 0 | Search ±N px in target to find best alignment (grayscale MSE) per patch; 0=disabled. Use when source and target are not perfectly co-registered (e.g. photo + hand-painted target). Offsets cached at dataset init. |
| `--channel-dropout-p F` | 0.3 | Lower if all samples have geometry (Blender only) |
| `--full-image` | off | Resize full image instead of patch crops |
| `--image-size N` | 256 | Resize target; only used with `--full-image` |
@@ -454,14 +462,27 @@ The final checkpoint is always written even if `--checkpoint-every 0`.
## 3. Exporting Weights
-Converts a trained `.pth` checkpoint to two raw binary files for the C++ runtime.
+Converts a trained `.pth` checkpoint to two raw binary files for the C++ runtime,
+and optionally updates the HTML tool's embedded defaults.
+**Standard export (C++ runtime only):**
```bash
cd cnn_v3/training
-python3 export_cnn_v3_weights.py checkpoints/checkpoint_epoch_200.pth \
+uv run export_cnn_v3_weights.py checkpoints/checkpoint_epoch_200.pth \
--output ../../workspaces/main/weights/
```
+**Export + update HTML tool defaults (`cnn_v3/tools/weights.js`):**
+```bash
+uv run export_cnn_v3_weights.py checkpoints/checkpoint_epoch_200.pth \
+ --output ../../workspaces/main/weights/ \
+ --html
+```
+
+`--html` base64-encodes both `.bin` files and rewrites `cnn_v3/tools/weights.js`
+so the HTML tool loads the new weights as its embedded defaults at startup.
+Use `--html-output PATH` to write to a different `weights.js` location.
+
Output files are registered in `workspaces/main/assets.txt` as:
```
WEIGHTS_CNN_V3, BINARY, weights/cnn_v3_weights.bin, "CNN v3 conv weights (f16, 3928 bytes)"
@@ -636,7 +657,7 @@ Do not reference them from outside the effect unless debugging.
```bash
cmake -B build -DCMAKE_BUILD_TYPE=Release
-cmake --build build -j$(nproc)
+cmake --build build -j4
./build/demo
```
@@ -733,13 +754,14 @@ If results drift after shader edits, verify these invariants match the Python re
## 7. HTML WebGPU Tool
-**Location:** `cnn_v3/tools/` — three files, no build step.
+**Location:** `cnn_v3/tools/` — four files, no build step.
| File | Lines | Contents |
|------|-------|----------|
-| `index.html` | 147 | HTML + CSS |
-| `shaders.js` | 252 | WGSL shader constants, weight-offset constants |
-| `tester.js` | 540 | `CNNv3Tester` class, event wiring |
+| `index.html` | 168 | HTML + CSS |
+| `shaders.js` | 312 | WGSL shader constants, weight-offset constants |
+| `tester.js` | 913 | `CNNv3Tester` class, inference pipeline, layer viz |
+| `weights.js` | 7 | Embedded default weights (base64); auto-generated by `--html` |
### Usage
@@ -750,32 +772,27 @@ python3 -m http.server 8080
# Open: http://localhost:8080/cnn_v3/tools/
```
-Or on macOS with Chrome:
+Weights are **loaded automatically at startup** from `weights.js` (embedded base64).
+If the tool is served from the repo root, it also tries to fetch the latest
+`workspaces/main/weights/*.bin` over HTTP and uses those if available.
+Use the **↺ Reload** button to re-fetch after updating weights on disk.
+
+To update the embedded defaults after a training run, use `--html` (§3):
```bash
-open -a "Google Chrome" --args --allow-file-access-from-files
-open cnn_v3/tools/index.html
+uv run export_cnn_v3_weights.py checkpoints/checkpoint.pth \
+ --output ../../workspaces/main/weights/ --html
```
### Workflow
-1. **Drop `cnn_v3_weights.bin`** onto the left "weights" drop zone.
-2. **Drop a PNG or video** onto the centre canvas → CNN runs immediately.
-3. _(Optional)_ **Drop `cnn_v3_film_mlp.bin`** → FiLM sliders become active.
-4. Adjust **beat_phase / beat_norm / audio_int / style_p0 / style_p1** sliders → reruns on change.
-5. Click layer buttons (**Feat · Enc0 · Enc1 · BN · Dec1 · Output**) in the right panel to inspect activations.
-6. **Save PNG** to export the current output.
+1. **Drop a PNG or video** onto the canvas → CNN runs immediately (weights pre-loaded).
+2. Adjust **beat_phase / beat_norm / audio_int / style_p0 / style_p1** sliders.
+3. Click layer buttons (**Feat · Enc0 · Enc1 · BN · Dec1 · Output**) to inspect activations.
+4. **Save PNG** to export the current output.
+5. _(Optional)_ Drop updated `.bin` files onto the left panel to override embedded weights.
Keyboard: `[SPACE]` toggle original · `[D]` diff×10.
-### Input files
-
-| File | Format | Notes |
-|------|--------|-------|
-| `cnn_v3_weights.bin` | raw u32 (no header) | 982 u32 = 1964 f16 = ~3.9 KB |
-| `cnn_v3_film_mlp.bin` | raw f32 | 776 f32 = 3.1 KB; optional — identity FiLM used if absent |
-
-Both produced by `export_cnn_v3_weights.py` (§3).
-
### Texture chain
| Texture | Format | Size |
@@ -816,7 +833,7 @@ all geometric channels (normal, depth, depth_grad, mat_id, prev) = 0.
| `cnn_v3/training/pack_photo_sample.py` | Photo → zeroed-geometry sample directory |
| `cnn_v3/training/cnn_v3_utils.py` | Dataset class, feature assembly, channel dropout, salient-point detection |
| `cnn_v3/training/train_cnn_v3.py` | CNNv3 model definition, training loop, CLI |
-| `cnn_v3/training/export_cnn_v3_weights.py` | Checkpoint → `cnn_v3_weights.bin` + `cnn_v3_film_mlp.bin` |
+| `cnn_v3/training/export_cnn_v3_weights.py` | Checkpoint → `cnn_v3_weights.bin` + `cnn_v3_film_mlp.bin`; `--html` rewrites `weights.js` |
| `cnn_v3/training/gen_test_vectors.py` | NumPy reference forward pass + C header generator |
| `cnn_v3/test_vectors.h` | Compiled-in test vectors (auto-generated, do not edit) |
| `cnn_v3/src/cnn_v3_effect.h` | C++ class, Params structs, `CNNv3FiLMParams` API |
@@ -827,6 +844,7 @@ all geometric channels (normal, depth, depth_grad, mat_id, prev) = 0.
| `cnn_v3/tools/index.html` | HTML tool — UI shell + CSS |
| `cnn_v3/tools/shaders.js` | HTML tool — inline WGSL shaders + weight-offset constants |
| `cnn_v3/tools/tester.js` | HTML tool — CNNv3Tester class, inference pipeline, layer viz |
+| `cnn_v3/tools/weights.js` | HTML tool — embedded default weights (base64, auto-generated) |
| `cnn_v2/tools/cnn_v2_test/index.html` | HTML tool reference pattern (v2) |
---
diff --git a/cnn_v3/tools/index.html b/cnn_v3/tools/index.html
index 26fee9b..6c7b406 100644
--- a/cnn_v3/tools/index.html
+++ b/cnn_v3/tools/index.html
@@ -162,6 +162,7 @@ video{display:none}
</div>
<script src="shaders.js"></script>
+<script src="weights.js"></script>
<script src="tester.js"></script>
</body>
</html>
diff --git a/cnn_v3/tools/tester.js b/cnn_v3/tools/tester.js
index 0412cae..81c869d 100644
--- a/cnn_v3/tools/tester.js
+++ b/cnn_v3/tools/tester.js
@@ -52,29 +52,34 @@ class CNNv3Tester {
async preload() {
const base = '../../workspaces/main/weights/';
const files = [
- {url: base+'cnn_v3_weights.bin', isFilm: false},
- {url: base+'cnn_v3_film_mlp.bin', isFilm: true},
+ {url: base+'cnn_v3_weights.bin', isFilm: false, b64: CNN_V3_WEIGHTS_B64},
+ {url: base+'cnn_v3_film_mlp.bin', isFilm: true, b64: CNN_V3_FILM_MLP_B64},
];
- for (const {url, isFilm} of files) {
+ for (const {url, isFilm, b64} of files) {
+ let buf = null;
+ const name = url.split('/').pop();
try {
const r = await fetch(url);
- if (!r.ok) { this.log(`preload skip: ${url.split('/').pop()} (${r.status})`); continue; }
- const buf = await r.arrayBuffer();
- const name = url.split('/').pop();
- if (isFilm) {
- this.filmMlp = this.parseFilm(buf);
- const el = document.getElementById('fDrop');
- el.textContent = `✓ ${name}`; el.classList.add('ok');
- document.getElementById('fSt').textContent = 'FiLM MLP loaded';
- document.getElementById('fSt').style.color = '#28a745';
- } else {
- this.weightsU32 = this.parseWeights(buf); this.weightsBuffer = buf;
- if (this.weightsGPU) { this.weightsGPU.destroy(); this.weightsGPU = null; }
- const el = document.getElementById('wDrop');
- el.textContent = `✓ ${name}`; el.classList.add('ok');
- }
- this.log(`Preloaded: ${name}`);
- } catch(e) { this.log(`preload error (${url.split('/').pop()}): ${e.message}`, 'err'); }
+ if (r.ok) { buf = await r.arrayBuffer(); this.log(`Preloaded: ${name}`); }
+ } catch(_) {}
+ if (!buf) {
+ const s = atob(b64); const u = new Uint8Array(s.length);
+ for (let i = 0; i < s.length; i++) u[i] = s.charCodeAt(i);
+ buf = u.buffer;
+ this.log(`Loaded embedded: ${name}`);
+ }
+ if (isFilm) {
+ this.filmMlp = this.parseFilm(buf);
+ const el = document.getElementById('fDrop');
+ el.textContent = `✓ ${name}`; el.classList.add('ok');
+ document.getElementById('fSt').textContent = 'FiLM MLP loaded';
+ document.getElementById('fSt').style.color = '#28a745';
+ } else {
+ this.weightsU32 = this.parseWeights(buf); this.weightsBuffer = buf;
+ if (this.weightsGPU) { this.weightsGPU.destroy(); this.weightsGPU = null; }
+ const el = document.getElementById('wDrop');
+ el.textContent = `✓ ${name}`; el.classList.add('ok');
+ }
}
if (this.weightsU32) {
if (this.image || this.isVideo) this.run();
diff --git a/cnn_v3/tools/weights.js b/cnn_v3/tools/weights.js
new file mode 100644
index 0000000..a6c8080
--- /dev/null
+++ b/cnn_v3/tools/weights.js
@@ -0,0 +1,7 @@
+'use strict';
+// Auto-generated — do not edit by hand.
+// Regenerate: base64 -i workspaces/main/weights/cnn_v3_weights.bin > /tmp/w.b64
+// base64 -i workspaces/main/weights/cnn_v3_film_mlp.bin > /tmp/f.b64
+// then update the two constants below (strip newlines).
+const CNN_V3_WEIGHTS_B64='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';
+const CNN_V3_FILM_MLP_B64='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';
diff --git a/cnn_v3/training/export_cnn_v3_weights.py b/cnn_v3/training/export_cnn_v3_weights.py
index 99f3a81..edf76e2 100644
--- a/cnn_v3/training/export_cnn_v3_weights.py
+++ b/cnn_v3/training/export_cnn_v3_weights.py
@@ -31,6 +31,7 @@ Usage
"""
import argparse
+import base64
import struct
import sys
from pathlib import Path
@@ -158,13 +159,40 @@ def export_weights(checkpoint_path: str, output_dir: str) -> None:
print(f"\nDone → {out}/")
+_WEIGHTS_JS_DEFAULT = Path(__file__).parent.parent / 'tools' / 'weights.js'
+
+
+def update_weights_js(weights_bin: Path, film_mlp_bin: Path,
+ js_path: Path = _WEIGHTS_JS_DEFAULT) -> None:
+ """Encode both .bin files as base64 and write cnn_v3/tools/weights.js."""
+ w_b64 = base64.b64encode(weights_bin.read_bytes()).decode('ascii')
+ f_b64 = base64.b64encode(film_mlp_bin.read_bytes()).decode('ascii')
+ js_path.write_text(
+ "'use strict';\n"
+ "// Auto-generated by export_cnn_v3_weights.py --html — do not edit by hand.\n"
+ f"const CNN_V3_WEIGHTS_B64='{w_b64}';\n"
+ f"const CNN_V3_FILM_MLP_B64='{f_b64}';\n"
+ )
+ print(f"\nweights.js → {js_path}")
+ print(f" CNN_V3_WEIGHTS_B64 {len(w_b64)} chars ({weights_bin.stat().st_size} bytes)")
+ print(f" CNN_V3_FILM_MLP_B64 {len(f_b64)} chars ({film_mlp_bin.stat().st_size} bytes)")
+
+
def main() -> None:
p = argparse.ArgumentParser(description='Export CNN v3 trained weights to .bin')
p.add_argument('checkpoint', help='Path to .pth checkpoint file')
p.add_argument('--output', default='export',
help='Output directory (default: export/)')
+ p.add_argument('--html', action='store_true',
+ help=f'Also update {_WEIGHTS_JS_DEFAULT} with base64-encoded weights')
+ p.add_argument('--html-output', default=None, metavar='PATH',
+ help='Override default weights.js path (implies --html)')
args = p.parse_args()
export_weights(args.checkpoint, args.output)
+ if args.html or args.html_output:
+ out = Path(args.output)
+ js_path = Path(args.html_output) if args.html_output else _WEIGHTS_JS_DEFAULT
+ update_weights_js(out / 'cnn_v3_weights.bin', out / 'cnn_v3_film_mlp.bin', js_path)
if __name__ == '__main__':