SebRincon commited on
Commit
1313f05
·
verified ·
1 Parent(s): 608808e

Upload AnyCalib WASM demo (ONNX Runtime Web)

Browse files
Files changed (4) hide show
  1. README.md +69 -5
  2. index.html +214 -18
  3. index.js +118 -0
  4. package.json +13 -0
README.md CHANGED
@@ -1,10 +1,74 @@
1
  ---
2
- title: Anycalib Wasm
3
- emoji: 🌖
4
- colorFrom: indigo
5
- colorTo: red
6
  sdk: static
7
  pinned: false
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: AnyCalib WASM Demo
3
+ emoji: 📷
4
+ colorFrom: blue
5
+ colorTo: green
6
  sdk: static
7
  pinned: false
8
+ license: apache-2.0
9
+ tags:
10
+ - wasm
11
+ - onnxruntime-web
12
+ - camera-calibration
13
+ - anycalib
14
  ---
15
 
16
+ # AnyCalib WASM Demo
17
+
18
+ Run camera calibration **entirely in the browser** using WebAssembly.
19
+
20
+ ## How it works
21
+
22
+ 1. The INT8 quantized ONNX model (~300 MB) is loaded via ONNX Runtime Web
23
+ 2. Images are preprocessed to 518x518 RGB float32 tensors
24
+ 3. The model predicts per-pixel ray directions
25
+ 4. A distortion heatmap is rendered from the ray predictions
26
+
27
+ ## Files
28
+
29
+ | File | Description |
30
+ |------|-------------|
31
+ | `index.html` | Interactive demo page |
32
+ | `index.js` | ES module with `AnyCalibrator` class |
33
+ | `package.json` | npm dependencies |
34
+
35
+ ## Quick start
36
+
37
+ ```bash
38
+ # Install ONNX Runtime Web
39
+ npm install
40
+
41
+ # Start local server
42
+ npx http-server . -p 8080 -c-1
43
+
44
+ # Open http://localhost:8080
45
+ ```
46
+
47
+ ## Using as an ES module
48
+
49
+ ```javascript
50
+ import { AnyCalibrator } from './index.js';
51
+
52
+ const calibrator = new AnyCalibrator({
53
+ // Options:
54
+ // modelUrl: 'custom-model-url.onnx',
55
+ // inputSize: 518,
56
+ // executionProvider: 'wasm' | 'webgpu',
57
+ });
58
+
59
+ await calibrator.init();
60
+
61
+ const img = document.getElementById('myImage');
62
+ const { rays, tangentCoords, elapsed } = await calibrator.predict(img);
63
+
64
+ console.log(`Inference took ${elapsed.toFixed(0)}ms`);
65
+
66
+ // Get distortion heatmap
67
+ const heatmap = calibrator.computeDistortionMap(rays);
68
+ ```
69
+
70
+ ## Model source
71
+
72
+ - ONNX models: [huggingface.co/SebRincon/anycalib-onnx](https://huggingface.co/SebRincon/anycalib-onnx)
73
+ - Raw PyTorch: [huggingface.co/SebRincon/anycalib](https://huggingface.co/SebRincon/anycalib)
74
+ - Original: [github.com/javrtg/AnyCalib](https://github.com/javrtg/AnyCalib)
index.html CHANGED
@@ -1,19 +1,215 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AnyCalib WASM Demo</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
10
+ h1 { margin-bottom: 1rem; font-size: 1.5rem; }
11
+ .container { max-width: 900px; margin: 0 auto; }
12
+ .status { padding: 0.75rem 1rem; border-radius: 0.5rem; margin-bottom: 1rem; font-size: 0.9rem; }
13
+ .status.loading { background: #1e3a5f; border: 1px solid #3b82f6; }
14
+ .status.ready { background: #14532d; border: 1px solid #22c55e; }
15
+ .status.error { background: #7f1d1d; border: 1px solid #ef4444; }
16
+ .upload-area {
17
+ border: 2px dashed #475569; border-radius: 0.75rem; padding: 3rem;
18
+ text-align: center; cursor: pointer; transition: border-color 0.2s;
19
+ margin-bottom: 1.5rem;
20
+ }
21
+ .upload-area:hover { border-color: #3b82f6; }
22
+ .upload-area.active { border-color: #22c55e; background: #0f2a1d; }
23
+ .results { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
24
+ .results canvas, .results img { width: 100%; border-radius: 0.5rem; background: #1e293b; }
25
+ .results .label { font-size: 0.85rem; color: #94a3b8; margin-top: 0.25rem; text-align: center; }
26
+ .timing { font-size: 0.85rem; color: #94a3b8; margin-top: 0.5rem; }
27
+ input[type="file"] { display: none; }
28
+ #backendSelect { background: #1e293b; color: #e2e8f0; border: 1px solid #475569;
29
+ padding: 0.5rem; border-radius: 0.375rem; margin-bottom: 1rem; }
30
+ </style>
31
+ </head>
32
+ <body>
33
+ <div class="container">
34
+ <h1>AnyCalib WASM Demo</h1>
35
+ <p style="margin-bottom: 1rem; color: #94a3b8;">
36
+ Camera calibration running entirely in your browser via WebAssembly.
37
+ Model: <a href="https://huggingface.co/SebRincon/anycalib-onnx" style="color:#60a5fa">
38
+ SebRincon/anycalib-onnx</a> (INT8, ~300 MB)
39
+ </p>
40
+
41
+ <label for="backendSelect">Backend: </label>
42
+ <select id="backendSelect">
43
+ <option value="wasm">WASM (CPU)</option>
44
+ <option value="webgpu">WebGPU (if available)</option>
45
+ </select>
46
+
47
+ <div id="status" class="status loading">Loading ONNX Runtime Web...</div>
48
+
49
+ <div id="uploadArea" class="upload-area">
50
+ <p>Drop an image here or click to upload</p>
51
+ <p style="font-size: 0.8rem; color: #64748b; margin-top: 0.5rem;">
52
+ Supports JPG, PNG, WebP
53
+ </p>
54
+ <input type="file" id="fileInput" accept="image/*">
55
+ </div>
56
+
57
+ <div class="results" id="results" style="display: none;">
58
+ <div>
59
+ <img id="inputImage" alt="Input">
60
+ <div class="label">Input Image</div>
61
+ </div>
62
+ <div>
63
+ <canvas id="heatmapCanvas"></canvas>
64
+ <div class="label">Distortion Heatmap</div>
65
+ </div>
66
+ </div>
67
+ <div id="timing" class="timing"></div>
68
+ </div>
69
+
70
+ <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/ort.min.js"></script>
71
+ <script>
72
+ const INPUT_SIZE = 518;
73
+ const MODEL_URL = 'https://huggingface.co/SebRincon/anycalib-onnx/resolve/main/model_int8.onnx';
74
+
75
+ let session = null;
76
+ const statusEl = document.getElementById('status');
77
+ const uploadArea = document.getElementById('uploadArea');
78
+ const fileInput = document.getElementById('fileInput');
79
+ const resultsEl = document.getElementById('results');
80
+ const timingEl = document.getElementById('timing');
81
+
82
+ async function loadModel() {
83
+ try {
84
+ const backend = document.getElementById('backendSelect').value;
85
+ statusEl.textContent = `Loading model (${backend})... This may take a minute on first load.`;
86
+ statusEl.className = 'status loading';
87
+
88
+ ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/';
89
+
90
+ const t0 = performance.now();
91
+ session = await ort.InferenceSession.create(MODEL_URL, {
92
+ executionProviders: [backend],
93
+ graphOptimizationLevel: 'all',
94
+ });
95
+ const elapsed = ((performance.now() - t0) / 1000).toFixed(1);
96
+
97
+ statusEl.textContent = `Model loaded in ${elapsed}s. Ready for inference.`;
98
+ statusEl.className = 'status ready';
99
+ } catch (err) {
100
+ statusEl.textContent = `Error loading model: ${err.message}`;
101
+ statusEl.className = 'status error';
102
+ console.error(err);
103
+ }
104
+ }
105
+
106
+ function preprocessImage(img) {
107
+ const canvas = document.createElement('canvas');
108
+ canvas.width = INPUT_SIZE;
109
+ canvas.height = INPUT_SIZE;
110
+ const ctx = canvas.getContext('2d');
111
+ ctx.drawImage(img, 0, 0, INPUT_SIZE, INPUT_SIZE);
112
+ const imageData = ctx.getImageData(0, 0, INPUT_SIZE, INPUT_SIZE);
113
+ const { data, width, height } = imageData;
114
+
115
+ const float32Data = new Float32Array(3 * width * height);
116
+ for (let i = 0; i < width * height; i++) {
117
+ float32Data[i] = data[i * 4] / 255.0;
118
+ float32Data[width * height + i] = data[i * 4 + 1] / 255.0;
119
+ float32Data[2 * width * height + i] = data[i * 4 + 2] / 255.0;
120
+ }
121
+ return new ort.Tensor('float32', float32Data, [1, 3, height, width]);
122
+ }
123
+
124
+ function renderHeatmap(rays) {
125
+ const [batch, channels, height, width] = rays.dims;
126
+ const data = rays.data;
127
+ const canvas = document.getElementById('heatmapCanvas');
128
+ canvas.width = width;
129
+ canvas.height = height;
130
+ const ctx = canvas.getContext('2d');
131
+ const imageData = ctx.createImageData(width, height);
132
+
133
+ for (let y = 0; y < height; y++) {
134
+ for (let x = 0; x < width; x++) {
135
+ const idx = y * width + x;
136
+ const rx = data[idx];
137
+ const ry = data[height * width + idx];
138
+ const rz = data[2 * height * width + idx];
139
+ const deviation = Math.sqrt(rx * rx + ry * ry) / Math.max(Math.abs(rz), 1e-6);
140
+ const v = Math.min(255, Math.floor(deviation * 128));
141
+
142
+ // Viridis-ish colormap
143
+ const pixIdx = idx * 4;
144
+ imageData.data[pixIdx] = Math.min(255, v * 2); // R
145
+ imageData.data[pixIdx + 1] = Math.min(255, 50 + v); // G
146
+ imageData.data[pixIdx + 2] = Math.max(0, 200 - v); // B
147
+ imageData.data[pixIdx + 3] = 255; // A
148
+ }
149
+ }
150
+ ctx.putImageData(imageData, 0, 0);
151
+ }
152
+
153
+ async function runInference(img) {
154
+ if (!session) {
155
+ statusEl.textContent = 'Model not loaded yet. Please wait...';
156
+ return;
157
+ }
158
+
159
+ statusEl.textContent = 'Running inference...';
160
+ statusEl.className = 'status loading';
161
+
162
+ // Show input
163
+ document.getElementById('inputImage').src = img.src;
164
+ resultsEl.style.display = 'grid';
165
+
166
+ try {
167
+ const inputTensor = preprocessImage(img);
168
+ const t0 = performance.now();
169
+ const results = await session.run({ image: inputTensor });
170
+ const elapsed = (performance.now() - t0).toFixed(0);
171
+
172
+ renderHeatmap(results.rays);
173
+
174
+ statusEl.textContent = `Inference complete.`;
175
+ statusEl.className = 'status ready';
176
+ timingEl.textContent = `Inference time: ${elapsed}ms | Input: ${INPUT_SIZE}x${INPUT_SIZE} | Rays shape: ${results.rays.dims.join('x')}`;
177
+ } catch (err) {
178
+ statusEl.textContent = `Inference error: ${err.message}`;
179
+ statusEl.className = 'status error';
180
+ console.error(err);
181
+ }
182
+ }
183
+
184
+ // File handling
185
+ uploadArea.addEventListener('click', () => fileInput.click());
186
+ uploadArea.addEventListener('dragover', (e) => {
187
+ e.preventDefault();
188
+ uploadArea.classList.add('active');
189
+ });
190
+ uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('active'));
191
+ uploadArea.addEventListener('drop', (e) => {
192
+ e.preventDefault();
193
+ uploadArea.classList.remove('active');
194
+ if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
195
+ });
196
+ fileInput.addEventListener('change', () => {
197
+ if (fileInput.files.length) handleFile(fileInput.files[0]);
198
+ });
199
+
200
+ function handleFile(file) {
201
+ const img = new Image();
202
+ img.onload = () => runInference(img);
203
+ img.src = URL.createObjectURL(file);
204
+ }
205
+
206
+ document.getElementById('backendSelect').addEventListener('change', () => {
207
+ session = null;
208
+ loadModel();
209
+ });
210
+
211
+ // Auto-load model on page load
212
+ loadModel();
213
+ </script>
214
+ </body>
215
  </html>
index.js ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * AnyCalib WASM — Camera calibration inference via ONNX Runtime Web.
3
+ *
4
+ * This module loads the AnyCalib ONNX model and runs inference in the browser
5
+ * using WebAssembly (WASM) or WebGPU backends.
6
+ *
7
+ * Usage:
8
+ * import { AnyCalibrayor } from './index.js';
9
+ * const calibrator = new AnyCalibrator();
10
+ * await calibrator.init();
11
+ * const result = await calibrator.predict(imageElement);
12
+ */
13
+
14
+ import * as ort from 'onnxruntime-web';
15
+
16
+ // Configure WASM paths
17
+ ort.env.wasm.wasmPaths = 'https://cdn.jsdelivr.net/npm/onnxruntime-web/dist/';
18
+
19
+ const MODEL_URL = 'https://huggingface.co/SebRincon/anycalib-onnx/resolve/main/model_int8.onnx';
20
+ const INPUT_SIZE = 518;
21
+
22
+ export class AnyCalibrator {
23
+ constructor(options = {}) {
24
+ this.modelUrl = options.modelUrl || MODEL_URL;
25
+ this.inputSize = options.inputSize || INPUT_SIZE;
26
+ this.session = null;
27
+ this.executionProvider = options.executionProvider || 'wasm';
28
+ }
29
+
30
+ async init() {
31
+ console.log(`[AnyCalib] Loading model from ${this.modelUrl}...`);
32
+ console.log(`[AnyCalib] Using ${this.executionProvider} backend`);
33
+
34
+ const startTime = performance.now();
35
+ this.session = await ort.InferenceSession.create(this.modelUrl, {
36
+ executionProviders: [this.executionProvider],
37
+ graphOptimizationLevel: 'all',
38
+ });
39
+ const elapsed = ((performance.now() - startTime) / 1000).toFixed(1);
40
+ console.log(`[AnyCalib] Model loaded in ${elapsed}s`);
41
+ return this;
42
+ }
43
+
44
+ /**
45
+ * Preprocess an image element or canvas to a float32 tensor.
46
+ * Resizes to inputSize x inputSize and normalizes to [0, 1].
47
+ */
48
+ preprocessImage(imageSource) {
49
+ const canvas = document.createElement('canvas');
50
+ canvas.width = this.inputSize;
51
+ canvas.height = this.inputSize;
52
+ const ctx = canvas.getContext('2d');
53
+ ctx.drawImage(imageSource, 0, 0, this.inputSize, this.inputSize);
54
+
55
+ const imageData = ctx.getImageData(0, 0, this.inputSize, this.inputSize);
56
+ const { data, width, height } = imageData;
57
+
58
+ // Convert RGBA HWC → RGB CHW float32 [0,1]
59
+ const float32Data = new Float32Array(3 * width * height);
60
+ for (let i = 0; i < width * height; i++) {
61
+ float32Data[i] = data[i * 4] / 255.0; // R
62
+ float32Data[width * height + i] = data[i * 4 + 1] / 255.0; // G
63
+ float32Data[2 * width * height + i] = data[i * 4 + 2] / 255.0; // B
64
+ }
65
+
66
+ return new ort.Tensor('float32', float32Data, [1, 3, height, width]);
67
+ }
68
+
69
+ /**
70
+ * Run inference on an image element, canvas, or video frame.
71
+ * Returns { rays, tangentCoords, elapsed }.
72
+ */
73
+ async predict(imageSource) {
74
+ if (!this.session) {
75
+ throw new Error('Model not initialized. Call init() first.');
76
+ }
77
+
78
+ const inputTensor = this.preprocessImage(imageSource);
79
+ const startTime = performance.now();
80
+ const results = await this.session.run({ image: inputTensor });
81
+ const elapsed = performance.now() - startTime;
82
+
83
+ return {
84
+ rays: results.rays,
85
+ tangentCoords: results.tangent_coords,
86
+ elapsed,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Compute a simple distortion heatmap from ray predictions.
92
+ * Returns a Uint8ClampedArray (H*W) with distortion magnitude per pixel.
93
+ */
94
+ computeDistortionMap(rays) {
95
+ const [batch, channels, height, width] = rays.dims;
96
+ const data = rays.data;
97
+ const heatmap = new Uint8ClampedArray(height * width);
98
+
99
+ for (let y = 0; y < height; y++) {
100
+ for (let x = 0; x < width; x++) {
101
+ const idx = y * width + x;
102
+ const rx = data[idx]; // channel 0
103
+ const ry = data[height * width + idx]; // channel 1
104
+ const rz = data[2 * height * width + idx]; // channel 2
105
+
106
+ // Deviation from pinhole (rz=1 for undistorted center rays)
107
+ const deviation = Math.sqrt(rx * rx + ry * ry) / Math.max(Math.abs(rz), 1e-6);
108
+ heatmap[idx] = Math.min(255, Math.floor(deviation * 128));
109
+ }
110
+ }
111
+ return { data: heatmap, width, height };
112
+ }
113
+ }
114
+
115
+ // For script tag usage (non-module)
116
+ if (typeof window !== 'undefined') {
117
+ window.AnyCalibrator = AnyCalibrator;
118
+ }
package.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "anycalib-wasm",
3
+ "version": "1.0.0",
4
+ "description": "AnyCalib camera calibration running in the browser via ONNX Runtime Web (WASM)",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "start": "npx http-server . -p 8080 -c-1",
8
+ "build": "echo 'No build step needed - pure JS'"
9
+ },
10
+ "dependencies": {
11
+ "onnxruntime-web": "^1.17.0"
12
+ }
13
+ }