splathash 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/dist/src/SplatHash.d.ts +35 -0
- package/dist/src/SplatHash.js +536 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +17 -0
- package/dist/src/splathash.d.ts +31 -0
- package/dist/src/splathash.js +556 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# SplatHash — TypeScript / JavaScript
|
|
2
|
+
|
|
3
|
+
TypeScript implementation of [SplatHash](../../README.md): compress any image to 16 bytes and reconstruct it.
|
|
4
|
+
|
|
5
|
+
Works in **Node.js** and **browsers** — the library itself has zero runtime dependencies.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install splathash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Node.js
|
|
16
|
+
|
|
17
|
+
The library works on raw RGBA bytes. Use any image-loading library (e.g. `sharp`) to get them:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import sharp from "sharp";
|
|
21
|
+
import { encode, decode } from "splathash";
|
|
22
|
+
|
|
23
|
+
const { data, info } = await sharp("photo.jpg")
|
|
24
|
+
.ensureAlpha()
|
|
25
|
+
.raw()
|
|
26
|
+
.toBuffer({ resolveWithObject: true });
|
|
27
|
+
|
|
28
|
+
// Encode: raw RGBA → 16-byte hash
|
|
29
|
+
const hash = encode(new Uint8ClampedArray(data), info.width, info.height);
|
|
30
|
+
console.log(Buffer.from(hash).toString("hex")); // e.g. a3f1bc...
|
|
31
|
+
|
|
32
|
+
// Decode: 16-byte hash → 32×32 RGBA
|
|
33
|
+
const result = decode(hash);
|
|
34
|
+
// result.width = 32
|
|
35
|
+
// result.height = 32
|
|
36
|
+
// result.rgba = Uint8ClampedArray (32 * 32 * 4 bytes)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Browser
|
|
40
|
+
|
|
41
|
+
Same package, same API. Get raw RGBA from a `<canvas>`:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { encode, decode } from "splathash";
|
|
45
|
+
|
|
46
|
+
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
|
|
47
|
+
const ctx = canvas.getContext("2d")!;
|
|
48
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
49
|
+
|
|
50
|
+
const hash = encode(data, canvas.width, canvas.height);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or from an `<img>` element:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
function hashImage(img: HTMLImageElement): Uint8Array {
|
|
57
|
+
const canvas = document.createElement("canvas");
|
|
58
|
+
canvas.width = img.naturalWidth;
|
|
59
|
+
canvas.height = img.naturalHeight;
|
|
60
|
+
const ctx = canvas.getContext("2d")!;
|
|
61
|
+
ctx.drawImage(img, 0, 0);
|
|
62
|
+
const { data } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
63
|
+
return encode(data, canvas.width, canvas.height);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
See [`examples/simple.ts`](examples/simple.ts) for a full Node.js example.
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
### `encode(rgba, width, height): Uint8Array`
|
|
72
|
+
|
|
73
|
+
| Parameter | Type | Description |
|
|
74
|
+
| :-------- | :--- | :---------- |
|
|
75
|
+
| `rgba` | `Uint8ClampedArray \| Uint8Array` | Raw RGBA pixel data (4 bytes per pixel) |
|
|
76
|
+
| `width` | `number` | Image width in pixels |
|
|
77
|
+
| `height` | `number` | Image height in pixels |
|
|
78
|
+
|
|
79
|
+
Returns a `Uint8Array` of exactly 16 bytes.
|
|
80
|
+
|
|
81
|
+
### `decode(hash): DecodedImage`
|
|
82
|
+
|
|
83
|
+
| Parameter | Type | Description |
|
|
84
|
+
| :-------- | :--- | :---------- |
|
|
85
|
+
| `hash` | `Uint8Array` | 16-byte SplatHash |
|
|
86
|
+
|
|
87
|
+
Returns a `DecodedImage`:
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
interface DecodedImage {
|
|
91
|
+
width: number; // always 32
|
|
92
|
+
height: number; // always 32
|
|
93
|
+
rgba: Uint8ClampedArray; // 32 * 32 * 4 bytes
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Throws if the hash is not exactly 16 bytes.
|
|
98
|
+
|
|
99
|
+
## Building from Source
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm install
|
|
103
|
+
npm run build # emits to dist/
|
|
104
|
+
npm test # build + run tests
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or via mise from the repo root:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
mise run test:ts
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## How It Works
|
|
114
|
+
|
|
115
|
+
See [ALGORITHM.md](../../ALGORITHM.md) for the full technical specification.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const TARGET_SIZE = 32;
|
|
2
|
+
export declare const RIDGE_LAMBDA = 0.001;
|
|
3
|
+
export declare const SIGMA_TABLE: number[];
|
|
4
|
+
export interface Splat {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
sigma: number;
|
|
8
|
+
l: number;
|
|
9
|
+
a: number;
|
|
10
|
+
b: number;
|
|
11
|
+
isLepton: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface DecodedImage {
|
|
14
|
+
meanL: number;
|
|
15
|
+
meanA: number;
|
|
16
|
+
meanB: number;
|
|
17
|
+
splats: Splat[];
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
rgba: Uint8ClampedArray;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Encodes an RGBA image buffer into a 16-byte SplatHash.
|
|
24
|
+
* @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
|
|
25
|
+
* @param width Image width
|
|
26
|
+
* @param height Image height
|
|
27
|
+
* @returns 16-byte Uint8Array hash
|
|
28
|
+
*/
|
|
29
|
+
export declare function encode(rgba: Uint8ClampedArray | Uint8Array, width: number, height: number): Uint8Array;
|
|
30
|
+
/**
|
|
31
|
+
* Decodes a 16-byte SplatHash back into RGBA pixel data.
|
|
32
|
+
* @param hash 16-byte Uint8Array
|
|
33
|
+
* @returns DecodedImage object containing splats and reconstructed buffer (32x32)
|
|
34
|
+
*/
|
|
35
|
+
export declare function decode(hash: Uint8Array): DecodedImage;
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SplatHash V4: The Universal Quantum Brain (TypeScript Edition)
|
|
3
|
+
//
|
|
4
|
+
// SplatHash describes an image using a "Quantum Entanglement" model.
|
|
5
|
+
// It decomposes the image into a background field (Mean) and 6 particles (Splats).
|
|
6
|
+
// The optimal configuration of these particles is found via a global Ridge Regression solver,
|
|
7
|
+
// allowing for extremely high data density (16 bytes).
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.SIGMA_TABLE = exports.RIDGE_LAMBDA = exports.TARGET_SIZE = void 0;
|
|
10
|
+
exports.encode = encode;
|
|
11
|
+
exports.decode = decode;
|
|
12
|
+
exports.TARGET_SIZE = 32;
|
|
13
|
+
exports.RIDGE_LAMBDA = 0.001;
|
|
14
|
+
exports.SIGMA_TABLE = [0.025, 0.1, 0.2, 0.35];
|
|
15
|
+
/**
|
|
16
|
+
* Encodes an RGBA image buffer into a 16-byte SplatHash.
|
|
17
|
+
* @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
|
|
18
|
+
* @param width Image width
|
|
19
|
+
* @param height Image height
|
|
20
|
+
* @returns 16-byte Uint8Array hash
|
|
21
|
+
*/
|
|
22
|
+
function encode(rgba, width, height) {
|
|
23
|
+
// 1. Preprocess to Oklab Grid
|
|
24
|
+
const grid = imageToOklabGrid(rgba, width, height, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
25
|
+
// 2. Compute Mean
|
|
26
|
+
let meanL = 0, meanA = 0, meanB = 0;
|
|
27
|
+
const n = Math.floor(grid.length / 3);
|
|
28
|
+
for (let i = 0; i < n; i++) {
|
|
29
|
+
meanL += grid[i * 3];
|
|
30
|
+
meanA += grid[i * 3 + 1];
|
|
31
|
+
meanB += grid[i * 3 + 2];
|
|
32
|
+
}
|
|
33
|
+
meanL /= n;
|
|
34
|
+
meanA /= n;
|
|
35
|
+
meanB /= n;
|
|
36
|
+
// QUANTUM STEP: Optimize against quantized mean
|
|
37
|
+
const pMean = packMean(meanL, meanA, meanB);
|
|
38
|
+
const uMean = unpackMean(pMean);
|
|
39
|
+
meanL = uMean.l;
|
|
40
|
+
meanA = uMean.a;
|
|
41
|
+
meanB = uMean.b;
|
|
42
|
+
// Residuals
|
|
43
|
+
const targetL = new Float64Array(n);
|
|
44
|
+
const targetA = new Float64Array(n);
|
|
45
|
+
const targetB = new Float64Array(n);
|
|
46
|
+
for (let i = 0; i < n; i++) {
|
|
47
|
+
targetL[i] = grid[i * 3] - meanL;
|
|
48
|
+
targetA[i] = grid[i * 3 + 1] - meanA;
|
|
49
|
+
targetB[i] = grid[i * 3 + 2] - meanB;
|
|
50
|
+
}
|
|
51
|
+
// 3. Basis Search (Greedy)
|
|
52
|
+
let basis = [];
|
|
53
|
+
const currentRecon = new Float64Array(exports.TARGET_SIZE * exports.TARGET_SIZE * 3);
|
|
54
|
+
for (let i = 0; i < 6; i++) {
|
|
55
|
+
const candidate = findBestSplat(grid, currentRecon, meanL, meanA, meanB, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
56
|
+
if (candidate.score < 0.00001)
|
|
57
|
+
break;
|
|
58
|
+
const s = candidate.splat;
|
|
59
|
+
s.isLepton = (i >= 3); // First 3 are Baryons (Full Color), Next 3 are Leptons (Luma Only)
|
|
60
|
+
basis.push(s);
|
|
61
|
+
addSplatToGrid(currentRecon, s, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
62
|
+
}
|
|
63
|
+
// 4. Global Linear Projection
|
|
64
|
+
if (basis.length > 0) {
|
|
65
|
+
basis = solveV4Weights(basis, targetL, targetA, targetB, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
66
|
+
}
|
|
67
|
+
// 5. Pack
|
|
68
|
+
return packV4(pMean, basis);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Decodes a 16-byte SplatHash back into RGBA pixel data.
|
|
72
|
+
* @param hash 16-byte Uint8Array
|
|
73
|
+
* @returns DecodedImage object containing splats and reconstructed buffer (32x32)
|
|
74
|
+
*/
|
|
75
|
+
function decode(hash) {
|
|
76
|
+
if (hash.length !== 16)
|
|
77
|
+
throw new Error("Invalid SplatHash: Must be 16 bytes.");
|
|
78
|
+
const { meanL, meanA, meanB, splats } = unpackV4(hash);
|
|
79
|
+
const w = 32;
|
|
80
|
+
const h = 32;
|
|
81
|
+
const grid = new Float64Array(w * h * 3);
|
|
82
|
+
// Fill background
|
|
83
|
+
for (let i = 0; i < grid.length; i += 3) {
|
|
84
|
+
grid[i] = meanL;
|
|
85
|
+
grid[i + 1] = meanA;
|
|
86
|
+
grid[i + 2] = meanB;
|
|
87
|
+
}
|
|
88
|
+
// Add splats
|
|
89
|
+
for (const s of splats) {
|
|
90
|
+
addSplatToGrid(grid, s, w, h);
|
|
91
|
+
}
|
|
92
|
+
// Convert to RGBA
|
|
93
|
+
const rgba = new Uint8ClampedArray(w * h * 4);
|
|
94
|
+
for (let y = 0; y < h; y++) {
|
|
95
|
+
for (let x = 0; x < w; x++) {
|
|
96
|
+
const idx = (y * w + x) * 3;
|
|
97
|
+
const l = grid[idx], a = grid[idx + 1], b = grid[idx + 2];
|
|
98
|
+
const { r, g, b: bl } = oklabToSrgb(l, a, b);
|
|
99
|
+
const pIdx = (y * w + x) * 4;
|
|
100
|
+
rgba[pIdx] = clampi(Math.round(r * 255), 0, 255);
|
|
101
|
+
rgba[pIdx + 1] = clampi(Math.round(g * 255), 0, 255);
|
|
102
|
+
rgba[pIdx + 2] = clampi(Math.round(bl * 255), 0, 255);
|
|
103
|
+
rgba[pIdx + 3] = 255;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { meanL, meanA, meanB, splats, width: w, height: h, rgba };
|
|
107
|
+
}
|
|
108
|
+
// --- Implementation Details ---
|
|
109
|
+
function solveV4Weights(basis, tL, tA, tB, w, h) {
|
|
110
|
+
const nTotal = basis.length;
|
|
111
|
+
let nBaryons = 0;
|
|
112
|
+
for (const s of basis)
|
|
113
|
+
if (!s.isLepton)
|
|
114
|
+
nBaryons++;
|
|
115
|
+
// Precompute Activations
|
|
116
|
+
const activations = basis.map(s => computeBasisMap(s, w, h));
|
|
117
|
+
// Linear Solve
|
|
118
|
+
const xL = solveChannel(activations, tL, nTotal, exports.RIDGE_LAMBDA);
|
|
119
|
+
const xA = solveChannel(activations.slice(0, nBaryons), tA, nBaryons, exports.RIDGE_LAMBDA);
|
|
120
|
+
const xB = solveChannel(activations.slice(0, nBaryons), tB, nBaryons, exports.RIDGE_LAMBDA);
|
|
121
|
+
// Update Basis
|
|
122
|
+
for (let i = 0; i < nTotal; i++) {
|
|
123
|
+
basis[i].l = xL[i];
|
|
124
|
+
if (i < nBaryons) {
|
|
125
|
+
basis[i].a = xA[i];
|
|
126
|
+
basis[i].b = xB[i];
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
basis[i].a = 0;
|
|
130
|
+
basis[i].b = 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return basis;
|
|
134
|
+
}
|
|
135
|
+
function solveChannel(activations, target, n, lambda) {
|
|
136
|
+
if (n === 0)
|
|
137
|
+
return new Float64Array(0);
|
|
138
|
+
const m = target.length;
|
|
139
|
+
const ata = new Float64Array(n * n);
|
|
140
|
+
const atb = new Float64Array(n);
|
|
141
|
+
// Build Normal Equations (ATA * x = ATb)
|
|
142
|
+
for (let i = 0; i < n; i++) {
|
|
143
|
+
for (let j = i; j < n; j++) {
|
|
144
|
+
let sum = 0.0;
|
|
145
|
+
const actI = activations[i];
|
|
146
|
+
const actJ = activations[j];
|
|
147
|
+
for (let p = 0; p < m; p++)
|
|
148
|
+
sum += actI[p] * actJ[p];
|
|
149
|
+
ata[i * n + j] = sum;
|
|
150
|
+
ata[j * n + i] = sum;
|
|
151
|
+
}
|
|
152
|
+
let sumB = 0.0;
|
|
153
|
+
const actI = activations[i];
|
|
154
|
+
for (let p = 0; p < m; p++)
|
|
155
|
+
sumB += actI[p] * target[p];
|
|
156
|
+
atb[i] = sumB;
|
|
157
|
+
}
|
|
158
|
+
// Ridge Regularization
|
|
159
|
+
for (let i = 0; i < n; i++)
|
|
160
|
+
ata[i * n + i] += lambda;
|
|
161
|
+
return solveLinearSystem(ata, atb, n);
|
|
162
|
+
}
|
|
163
|
+
function solveLinearSystem(mat, vec, n) {
|
|
164
|
+
// Gaussian Elimination
|
|
165
|
+
const a = new Float64Array(mat); // Copy
|
|
166
|
+
const b = new Float64Array(vec); // Copy
|
|
167
|
+
for (let k = 0; k < n - 1; k++) {
|
|
168
|
+
for (let i = k + 1; i < n; i++) {
|
|
169
|
+
const factor = a[i * n + k] / a[k * n + k];
|
|
170
|
+
for (let j = k; j < n; j++)
|
|
171
|
+
a[i * n + j] -= factor * a[k * n + j];
|
|
172
|
+
b[i] -= factor * b[k];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const x = new Float64Array(n);
|
|
176
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
177
|
+
let sum = 0.0;
|
|
178
|
+
for (let j = i + 1; j < n; j++)
|
|
179
|
+
sum += a[i * n + j] * x[j];
|
|
180
|
+
x[i] = (b[i] - sum) / a[i * n + i];
|
|
181
|
+
}
|
|
182
|
+
return x;
|
|
183
|
+
}
|
|
184
|
+
function findBestSplat(grid, recon, mL, mA, mB, w, h) {
|
|
185
|
+
let maxScore = -1.0;
|
|
186
|
+
let bestSplat = { x: 0, y: 0, sigma: 0.1, l: 0, a: 0, b: 0, isLepton: false };
|
|
187
|
+
// Compute Residuals
|
|
188
|
+
const n = Math.floor(grid.length / 3);
|
|
189
|
+
const resL = new Float64Array(n);
|
|
190
|
+
const resA = new Float64Array(n);
|
|
191
|
+
const resB = new Float64Array(n);
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
resL[i] = grid[i * 3] - mL - recon[i * 3];
|
|
194
|
+
resA[i] = grid[i * 3 + 1] - mA - recon[i * 3 + 1];
|
|
195
|
+
resB[i] = grid[i * 3 + 2] - mB - recon[i * 3 + 2];
|
|
196
|
+
}
|
|
197
|
+
const step = 2; // Optimization: stride
|
|
198
|
+
const sigmas = exports.SIGMA_TABLE;
|
|
199
|
+
for (let y = 0; y < h; y += step) {
|
|
200
|
+
for (let x = 0; x < w; x += step) {
|
|
201
|
+
const xf = x / w;
|
|
202
|
+
const yf = y / h;
|
|
203
|
+
for (const sigma of sigmas) {
|
|
204
|
+
let dotL = 0, dotA = 0, dotB = 0, dotG = 0;
|
|
205
|
+
const rad = Math.floor(sigma * w * 3.5);
|
|
206
|
+
const y0 = clampi(y - rad, 0, h - 1);
|
|
207
|
+
const y1 = clampi(y + rad, 0, h - 1);
|
|
208
|
+
const x0 = clampi(x - rad, 0, w - 1);
|
|
209
|
+
const x1 = clampi(x + rad, 0, w - 1);
|
|
210
|
+
for (let sy = y0; sy <= y1; sy++) {
|
|
211
|
+
const dy = (sy / h) - yf;
|
|
212
|
+
const rowBase = sy * w;
|
|
213
|
+
for (let sx = x0; sx <= x1; sx++) {
|
|
214
|
+
const dx = (sx / w) - xf;
|
|
215
|
+
const distSq = dx * dx + dy * dy;
|
|
216
|
+
const weight = Math.exp(-distSq / (2 * sigma * sigma));
|
|
217
|
+
const idx = rowBase + sx;
|
|
218
|
+
dotL += weight * resL[idx];
|
|
219
|
+
dotA += weight * resA[idx];
|
|
220
|
+
dotB += weight * resB[idx];
|
|
221
|
+
dotG += weight * weight;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (dotG < 1e-9)
|
|
225
|
+
continue;
|
|
226
|
+
// Score based on energy
|
|
227
|
+
const score = (dotL * dotL + dotA * dotA + dotB * dotB) / dotG;
|
|
228
|
+
if (score > maxScore) {
|
|
229
|
+
maxScore = score;
|
|
230
|
+
bestSplat = {
|
|
231
|
+
x: xf, y: yf, sigma,
|
|
232
|
+
l: dotL / dotG, a: dotA / dotG, b: dotB / dotG,
|
|
233
|
+
isLepton: false
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { splat: bestSplat, score: maxScore };
|
|
240
|
+
}
|
|
241
|
+
function computeBasisMap(s, w, h) {
|
|
242
|
+
const out = new Float64Array(w * h);
|
|
243
|
+
const rad = Math.floor(s.sigma * w * 3.5);
|
|
244
|
+
const cx = Math.floor(s.x * w);
|
|
245
|
+
const cy = Math.floor(s.y * h);
|
|
246
|
+
const y0 = clampi(cy - rad, 0, h - 1);
|
|
247
|
+
const y1 = clampi(cy + rad, 0, h - 1);
|
|
248
|
+
const x0 = clampi(cx - rad, 0, w - 1);
|
|
249
|
+
const x1 = clampi(cx + rad, 0, w - 1);
|
|
250
|
+
for (let y = y0; y <= y1; y++) {
|
|
251
|
+
const dy = (y / h) - s.y;
|
|
252
|
+
for (let x = x0; x <= x1; x++) {
|
|
253
|
+
const dx = (x / w) - s.x;
|
|
254
|
+
out[y * w + x] = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
function addSplatToGrid(grid, s, w, h) {
|
|
260
|
+
const rad = Math.floor(s.sigma * w * 3.5);
|
|
261
|
+
const cx = Math.floor(s.x * w);
|
|
262
|
+
const cy = Math.floor(s.y * h);
|
|
263
|
+
const y0 = clampi(cy - rad, 0, h - 1);
|
|
264
|
+
const y1 = clampi(cy + rad, 0, h - 1);
|
|
265
|
+
const x0 = clampi(cx - rad, 0, w - 1);
|
|
266
|
+
const x1 = clampi(cx + rad, 0, w - 1);
|
|
267
|
+
for (let y = y0; y <= y1; y++) {
|
|
268
|
+
const dy = (y / h) - s.y;
|
|
269
|
+
for (let x = x0; x <= x1; x++) {
|
|
270
|
+
const dx = (x / w) - s.x;
|
|
271
|
+
const weight = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
|
|
272
|
+
const idx = (y * w + x) * 3;
|
|
273
|
+
grid[idx] += s.l * weight;
|
|
274
|
+
if (!s.isLepton) {
|
|
275
|
+
grid[idx + 1] += s.a * weight;
|
|
276
|
+
grid[idx + 2] += s.b * weight;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// --- Image Helpers ---
|
|
282
|
+
function imageToOklabGrid(rgba, srcW, srcH, w, h) {
|
|
283
|
+
const out = new Float64Array(w * h * 3);
|
|
284
|
+
for (let y = 0; y < h; y++) {
|
|
285
|
+
const y0 = Math.floor((y * srcH) / h);
|
|
286
|
+
const y1 = Math.floor(((y + 1) * srcH) / h);
|
|
287
|
+
for (let x = 0; x < w; x++) {
|
|
288
|
+
const x0 = Math.floor((x * srcW) / w);
|
|
289
|
+
const x1 = Math.floor(((x + 1) * srcW) / w);
|
|
290
|
+
let rSum = 0, gSum = 0, bSum = 0, count = 0;
|
|
291
|
+
for (let iy = y0; iy < y1; iy++) {
|
|
292
|
+
if (iy >= srcH)
|
|
293
|
+
break;
|
|
294
|
+
for (let ix = x0; ix < x1; ix++) {
|
|
295
|
+
if (ix >= srcW)
|
|
296
|
+
break;
|
|
297
|
+
const idx = (iy * srcW + ix) * 4;
|
|
298
|
+
rSum += rgba[idx];
|
|
299
|
+
gSum += rgba[idx + 1];
|
|
300
|
+
bSum += rgba[idx + 2];
|
|
301
|
+
count++;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (count === 0)
|
|
305
|
+
continue;
|
|
306
|
+
const r = (rSum / count) / 255.0;
|
|
307
|
+
const g = (gSum / count) / 255.0;
|
|
308
|
+
const b = (bSum / count) / 255.0;
|
|
309
|
+
const lab = srgbToOklab(r, g, b);
|
|
310
|
+
const idx = (y * w + x) * 3;
|
|
311
|
+
out[idx] = lab.l;
|
|
312
|
+
out[idx + 1] = lab.a;
|
|
313
|
+
out[idx + 2] = lab.b;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
function srgbToOklab(r, g, b) {
|
|
319
|
+
const lin = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
320
|
+
r = lin(r);
|
|
321
|
+
g = lin(g);
|
|
322
|
+
b = lin(b);
|
|
323
|
+
const l1 = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
324
|
+
const m1 = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
325
|
+
const s1 = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
326
|
+
const l_ = Math.cbrt(l1), m_ = Math.cbrt(m1), s_ = Math.cbrt(s1);
|
|
327
|
+
return {
|
|
328
|
+
l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
|
329
|
+
a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
|
330
|
+
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function oklabToSrgb(l, a, b) {
|
|
334
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
335
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
336
|
+
const s_ = l - 0.0894841775 * a - 1.2914855480 * b;
|
|
337
|
+
const l3 = l_ * l_ * l_;
|
|
338
|
+
const m3 = m_ * m_ * m_;
|
|
339
|
+
const s3 = s_ * s_ * s_;
|
|
340
|
+
let r = +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
|
|
341
|
+
let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
|
|
342
|
+
let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.7076147010 * s3;
|
|
343
|
+
const srt = (c) => (c <= 0.0031308 ? 12.92 * c : (c < 0 ? 0 : 1.055 * Math.pow(c, 1.0 / 2.4) - 0.055));
|
|
344
|
+
return { r: srt(r), g: srt(g), b: srt(bl) };
|
|
345
|
+
}
|
|
346
|
+
// --- Bit Packing ---
|
|
347
|
+
class BitStream {
|
|
348
|
+
constructor() {
|
|
349
|
+
this.buf = [];
|
|
350
|
+
this.acc = 0n;
|
|
351
|
+
this.n = 0;
|
|
352
|
+
}
|
|
353
|
+
write(val, bits) {
|
|
354
|
+
this.acc = (this.acc << BigInt(bits)) | BigInt(val);
|
|
355
|
+
this.n += bits;
|
|
356
|
+
while (this.n >= 8) {
|
|
357
|
+
const shift = BigInt(this.n - 8);
|
|
358
|
+
const byteVal = Number((this.acc >> shift) & 0xffn);
|
|
359
|
+
this.buf.push(byteVal);
|
|
360
|
+
this.n -= 8;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
getBytes() {
|
|
364
|
+
if (this.n > 0) {
|
|
365
|
+
this.buf.push(Number((this.acc << BigInt(8 - this.n)) & 0xffn));
|
|
366
|
+
}
|
|
367
|
+
return new Uint8Array(this.buf);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
class BitReader {
|
|
371
|
+
constructor(data) {
|
|
372
|
+
this.pos = 0;
|
|
373
|
+
this.rem = 0;
|
|
374
|
+
this.curr = 0;
|
|
375
|
+
this.data = data;
|
|
376
|
+
}
|
|
377
|
+
read(bits) {
|
|
378
|
+
let val = 0;
|
|
379
|
+
let bitsRemaining = bits;
|
|
380
|
+
while (bitsRemaining > 0) {
|
|
381
|
+
if (this.rem === 0) {
|
|
382
|
+
if (this.pos >= this.data.length)
|
|
383
|
+
return val << bitsRemaining; // EOF
|
|
384
|
+
this.curr = this.data[this.pos++];
|
|
385
|
+
this.rem = 8;
|
|
386
|
+
}
|
|
387
|
+
// Optimization for partial read
|
|
388
|
+
const take = Math.min(this.rem, bitsRemaining);
|
|
389
|
+
const shift = this.rem - take;
|
|
390
|
+
const mask = (1 << take) - 1;
|
|
391
|
+
const chunk = (this.curr >> shift) & mask;
|
|
392
|
+
val = (val << take) | chunk;
|
|
393
|
+
this.rem -= take;
|
|
394
|
+
bitsRemaining -= take;
|
|
395
|
+
}
|
|
396
|
+
return val;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function packV4(mean, splats) {
|
|
400
|
+
const bw = new BitStream();
|
|
401
|
+
// Header (16 bits)
|
|
402
|
+
bw.write(mean, 16);
|
|
403
|
+
// Baryons (3x)
|
|
404
|
+
let count = 0;
|
|
405
|
+
for (const s of splats) {
|
|
406
|
+
if (s.isLepton)
|
|
407
|
+
continue;
|
|
408
|
+
if (count >= 3)
|
|
409
|
+
break;
|
|
410
|
+
const xi = clampi(Math.round(s.x * 15.0), 0, 15);
|
|
411
|
+
const yi = clampi(Math.round(s.y * 15.0), 0, 15);
|
|
412
|
+
const sigI = getSigmaIdx(s.sigma);
|
|
413
|
+
const lQ = quant(s.l, -0.8, 0.8, 4);
|
|
414
|
+
const aQ = quant(s.a, -0.4, 0.4, 4);
|
|
415
|
+
const bQ = quant(s.b, -0.4, 0.4, 4);
|
|
416
|
+
bw.write(xi, 4);
|
|
417
|
+
bw.write(yi, 4);
|
|
418
|
+
bw.write(sigI, 2);
|
|
419
|
+
bw.write(lQ, 4);
|
|
420
|
+
bw.write(aQ, 4);
|
|
421
|
+
bw.write(bQ, 4);
|
|
422
|
+
count++;
|
|
423
|
+
}
|
|
424
|
+
while (count < 3) {
|
|
425
|
+
bw.write(0, 22);
|
|
426
|
+
count++;
|
|
427
|
+
}
|
|
428
|
+
// Leptons (3x)
|
|
429
|
+
count = 0;
|
|
430
|
+
for (const s of splats) {
|
|
431
|
+
if (!s.isLepton)
|
|
432
|
+
continue;
|
|
433
|
+
if (count >= 3)
|
|
434
|
+
break;
|
|
435
|
+
const xi = clampi(Math.round(s.x * 15.0), 0, 15);
|
|
436
|
+
const yi = clampi(Math.round(s.y * 15.0), 0, 15);
|
|
437
|
+
const sigI = getSigmaIdx(s.sigma);
|
|
438
|
+
const lQ = quant(s.l, -0.8, 0.8, 5);
|
|
439
|
+
bw.write(xi, 4);
|
|
440
|
+
bw.write(yi, 4);
|
|
441
|
+
bw.write(sigI, 2);
|
|
442
|
+
bw.write(lQ, 5);
|
|
443
|
+
count++;
|
|
444
|
+
}
|
|
445
|
+
while (count < 3) {
|
|
446
|
+
bw.write(0, 15);
|
|
447
|
+
count++;
|
|
448
|
+
}
|
|
449
|
+
bw.write(0, 1); // Pad
|
|
450
|
+
return bw.getBytes();
|
|
451
|
+
}
|
|
452
|
+
function unpackV4(hash) {
|
|
453
|
+
const br = new BitReader(hash);
|
|
454
|
+
const meanMap = br.read(16);
|
|
455
|
+
const { l, a, b } = unpackMean(meanMap);
|
|
456
|
+
const splats = [];
|
|
457
|
+
// Baryons
|
|
458
|
+
for (let i = 0; i < 3; i++) {
|
|
459
|
+
const xi = br.read(4);
|
|
460
|
+
const yi = br.read(4);
|
|
461
|
+
const sigI = br.read(2);
|
|
462
|
+
const lQ = br.read(4);
|
|
463
|
+
const aQ = br.read(4);
|
|
464
|
+
const bQ = br.read(4);
|
|
465
|
+
if (xi === 0 && yi === 0 && lQ === 0 && aQ === 0 && bQ === 0)
|
|
466
|
+
continue;
|
|
467
|
+
splats.push({
|
|
468
|
+
x: xi / 15.0,
|
|
469
|
+
y: yi / 15.0,
|
|
470
|
+
sigma: exports.SIGMA_TABLE[sigI],
|
|
471
|
+
l: unquant(lQ, -0.8, 0.8, 4),
|
|
472
|
+
a: unquant(aQ, -0.4, 0.4, 4),
|
|
473
|
+
b: unquant(bQ, -0.4, 0.4, 4),
|
|
474
|
+
isLepton: false
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
// Leptons
|
|
478
|
+
for (let i = 0; i < 3; i++) {
|
|
479
|
+
const xi = br.read(4);
|
|
480
|
+
const yi = br.read(4);
|
|
481
|
+
const sigI = br.read(2);
|
|
482
|
+
const lQ = br.read(5);
|
|
483
|
+
if (xi === 0 && yi === 0 && lQ === 0)
|
|
484
|
+
continue;
|
|
485
|
+
splats.push({
|
|
486
|
+
x: xi / 15.0,
|
|
487
|
+
y: yi / 15.0,
|
|
488
|
+
sigma: exports.SIGMA_TABLE[sigI],
|
|
489
|
+
l: unquant(lQ, -0.8, 0.8, 5),
|
|
490
|
+
a: 0, b: 0,
|
|
491
|
+
isLepton: true
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
return { meanL: l, meanA: a, meanB: b, splats };
|
|
495
|
+
}
|
|
496
|
+
function getSigmaIdx(s) {
|
|
497
|
+
let minD = 100.0;
|
|
498
|
+
let idx = 0;
|
|
499
|
+
for (let i = 0; i < exports.SIGMA_TABLE.length; i++) {
|
|
500
|
+
const d = Math.abs(exports.SIGMA_TABLE[i] - s);
|
|
501
|
+
if (d < minD) {
|
|
502
|
+
minD = d;
|
|
503
|
+
idx = i;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return idx;
|
|
507
|
+
}
|
|
508
|
+
function packMean(l, a, b) {
|
|
509
|
+
const li = clampi(Math.floor(l * 63.5), 0, 63);
|
|
510
|
+
const ai = clampi(Math.floor(((a + 0.2) / 0.4) * 31.5), 0, 31);
|
|
511
|
+
const bi = clampi(Math.floor(((b + 0.2) / 0.4) * 31.5), 0, 31);
|
|
512
|
+
return (li << 10) | (ai << 5) | bi;
|
|
513
|
+
}
|
|
514
|
+
function unpackMean(p) {
|
|
515
|
+
const li = (p >> 10) & 0x3F;
|
|
516
|
+
const ai = (p >> 5) & 0x1F;
|
|
517
|
+
const bi = p & 0x1F;
|
|
518
|
+
return {
|
|
519
|
+
l: li / 63.0,
|
|
520
|
+
a: (ai / 31.0 * 0.4) - 0.2,
|
|
521
|
+
b: (bi / 31.0 * 0.4) - 0.2
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
function quant(v, min, max, bits) {
|
|
525
|
+
const steps = (1 << bits) - 1;
|
|
526
|
+
const norm = (v - min) / (max - min);
|
|
527
|
+
return clampi(Math.round(norm * steps), 0, steps);
|
|
528
|
+
}
|
|
529
|
+
function unquant(v, min, max, bits) {
|
|
530
|
+
const steps = (1 << bits) - 1;
|
|
531
|
+
const norm = v / steps;
|
|
532
|
+
return norm * (max - min) + min;
|
|
533
|
+
}
|
|
534
|
+
function clampi(v, min, max) {
|
|
535
|
+
return v < min ? min : (v > max ? max : v);
|
|
536
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./splathash";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./splathash"), exports);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export declare const TARGET_SIZE = 32;
|
|
2
|
+
export declare const RIDGE_LAMBDA = 0.001;
|
|
3
|
+
export declare const SIGMA_TABLE: number[];
|
|
4
|
+
export interface Splat {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
sigma: number;
|
|
8
|
+
l: number;
|
|
9
|
+
a: number;
|
|
10
|
+
b: number;
|
|
11
|
+
isLepton: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface DecodedImage {
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
rgba: Uint8ClampedArray;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Encodes an RGBA image buffer into a 16-byte SplatHash.
|
|
20
|
+
* @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
|
|
21
|
+
* @param width Image width
|
|
22
|
+
* @param height Image height
|
|
23
|
+
* @returns 16-byte Uint8Array hash
|
|
24
|
+
*/
|
|
25
|
+
export declare function encode(rgba: Uint8ClampedArray | Uint8Array, width: number, height: number): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* Decodes a 16-byte SplatHash back into RGBA pixel data.
|
|
28
|
+
* @param hash 16-byte Uint8Array
|
|
29
|
+
* @returns DecodedImage object containing splats and reconstructed buffer (32x32)
|
|
30
|
+
*/
|
|
31
|
+
export declare function decode(hash: Uint8Array): DecodedImage;
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SplatHash — TypeScript implementation
|
|
3
|
+
//
|
|
4
|
+
// Encodes any image into 16 bytes and reconstructs a 32x32 preview.
|
|
5
|
+
// An image is decomposed into a background color (Mean) and six Gaussian blobs (Splats):
|
|
6
|
+
// - 3 Baryons: full-color Splats for dominant features
|
|
7
|
+
// - 3 Leptons: luma-only Splats for texture and detail
|
|
8
|
+
//
|
|
9
|
+
// Splat positions are found greedily; Ridge Regression then refines all weights together.
|
|
10
|
+
// All computation is done in Oklab. The hash fits into exactly 128 bits.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.SIGMA_TABLE = exports.RIDGE_LAMBDA = exports.TARGET_SIZE = void 0;
|
|
13
|
+
exports.encode = encode;
|
|
14
|
+
exports.decode = decode;
|
|
15
|
+
exports.TARGET_SIZE = 32;
|
|
16
|
+
exports.RIDGE_LAMBDA = 0.001;
|
|
17
|
+
exports.SIGMA_TABLE = [0.025, 0.1, 0.2, 0.35];
|
|
18
|
+
/**
|
|
19
|
+
* Encodes an RGBA image buffer into a 16-byte SplatHash.
|
|
20
|
+
* @param rgba Raw RGBA data (Uint8ClampedArray or Uint8Array)
|
|
21
|
+
* @param width Image width
|
|
22
|
+
* @param height Image height
|
|
23
|
+
* @returns 16-byte Uint8Array hash
|
|
24
|
+
*/
|
|
25
|
+
function encode(rgba, width, height) {
|
|
26
|
+
// 1. Preprocess to Oklab Grid
|
|
27
|
+
const grid = imageToOklabGrid(rgba, width, height, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
28
|
+
// 2. Compute Mean
|
|
29
|
+
let meanL = 0, meanA = 0, meanB = 0;
|
|
30
|
+
const n = Math.floor(grid.length / 3);
|
|
31
|
+
for (let i = 0; i < n; i++) {
|
|
32
|
+
meanL += grid[i * 3];
|
|
33
|
+
meanA += grid[i * 3 + 1];
|
|
34
|
+
meanB += grid[i * 3 + 2];
|
|
35
|
+
}
|
|
36
|
+
meanL /= n;
|
|
37
|
+
meanA /= n;
|
|
38
|
+
meanB /= n;
|
|
39
|
+
// 3. Quantize Mean immediately so the solver optimizes against the reconstructed mean.
|
|
40
|
+
const pMean = packMean(meanL, meanA, meanB);
|
|
41
|
+
const uMean = unpackMean(pMean);
|
|
42
|
+
meanL = uMean.l;
|
|
43
|
+
meanA = uMean.a;
|
|
44
|
+
meanB = uMean.b;
|
|
45
|
+
// Residuals
|
|
46
|
+
const targetL = new Float64Array(n);
|
|
47
|
+
const targetA = new Float64Array(n);
|
|
48
|
+
const targetB = new Float64Array(n);
|
|
49
|
+
for (let i = 0; i < n; i++) {
|
|
50
|
+
targetL[i] = grid[i * 3] - meanL;
|
|
51
|
+
targetA[i] = grid[i * 3 + 1] - meanA;
|
|
52
|
+
targetB[i] = grid[i * 3 + 2] - meanB;
|
|
53
|
+
}
|
|
54
|
+
// 3. Basis Search (Greedy)
|
|
55
|
+
let basis = [];
|
|
56
|
+
const currentRecon = new Float64Array(exports.TARGET_SIZE * exports.TARGET_SIZE * 3);
|
|
57
|
+
for (let i = 0; i < 6; i++) {
|
|
58
|
+
const candidate = findBestSplat(grid, currentRecon, meanL, meanA, meanB, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
59
|
+
if (candidate.score < 0.00001)
|
|
60
|
+
break;
|
|
61
|
+
const s = candidate.splat;
|
|
62
|
+
s.isLepton = i >= 3; // First 3 are Baryons (Full Color), Next 3 are Leptons (Luma Only)
|
|
63
|
+
basis.push(s);
|
|
64
|
+
addSplatToGrid(currentRecon, s, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
65
|
+
}
|
|
66
|
+
// 4. Global Linear Projection
|
|
67
|
+
if (basis.length > 0) {
|
|
68
|
+
basis = solveV4Weights(basis, targetL, targetA, targetB, exports.TARGET_SIZE, exports.TARGET_SIZE);
|
|
69
|
+
}
|
|
70
|
+
// 5. Pack
|
|
71
|
+
return packV4(pMean, basis);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decodes a 16-byte SplatHash back into RGBA pixel data.
|
|
75
|
+
* @param hash 16-byte Uint8Array
|
|
76
|
+
* @returns DecodedImage object containing splats and reconstructed buffer (32x32)
|
|
77
|
+
*/
|
|
78
|
+
function decode(hash) {
|
|
79
|
+
if (hash.length !== 16)
|
|
80
|
+
throw new Error("Invalid SplatHash: Must be 16 bytes.");
|
|
81
|
+
const { meanL, meanA, meanB, splats } = unpackV4(hash);
|
|
82
|
+
const w = 32;
|
|
83
|
+
const h = 32;
|
|
84
|
+
const grid = new Float64Array(w * h * 3);
|
|
85
|
+
// Fill background
|
|
86
|
+
for (let i = 0; i < grid.length; i += 3) {
|
|
87
|
+
grid[i] = meanL;
|
|
88
|
+
grid[i + 1] = meanA;
|
|
89
|
+
grid[i + 2] = meanB;
|
|
90
|
+
}
|
|
91
|
+
// Add splats
|
|
92
|
+
for (const s of splats) {
|
|
93
|
+
addSplatToGrid(grid, s, w, h);
|
|
94
|
+
}
|
|
95
|
+
// Convert to RGBA
|
|
96
|
+
const rgba = new Uint8ClampedArray(w * h * 4);
|
|
97
|
+
for (let y = 0; y < h; y++) {
|
|
98
|
+
for (let x = 0; x < w; x++) {
|
|
99
|
+
const idx = (y * w + x) * 3;
|
|
100
|
+
const l = grid[idx], a = grid[idx + 1], b = grid[idx + 2];
|
|
101
|
+
const { r, g, b: bl } = oklabToSrgb(l, a, b);
|
|
102
|
+
const pIdx = (y * w + x) * 4;
|
|
103
|
+
rgba[pIdx] = clampi(Math.round(r * 255), 0, 255);
|
|
104
|
+
rgba[pIdx + 1] = clampi(Math.round(g * 255), 0, 255);
|
|
105
|
+
rgba[pIdx + 2] = clampi(Math.round(bl * 255), 0, 255);
|
|
106
|
+
rgba[pIdx + 3] = 255;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { width: w, height: h, rgba };
|
|
110
|
+
}
|
|
111
|
+
// --- Implementation Details ---
|
|
112
|
+
function solveV4Weights(basis, tL, tA, tB, w, h) {
|
|
113
|
+
const nTotal = basis.length;
|
|
114
|
+
let nBaryons = 0;
|
|
115
|
+
for (const s of basis)
|
|
116
|
+
if (!s.isLepton)
|
|
117
|
+
nBaryons++;
|
|
118
|
+
// Precompute Activations
|
|
119
|
+
const activations = basis.map((s) => computeBasisMap(s, w, h));
|
|
120
|
+
// Linear Solve
|
|
121
|
+
const xL = solveChannel(activations, tL, nTotal, exports.RIDGE_LAMBDA);
|
|
122
|
+
const xA = solveChannel(activations.slice(0, nBaryons), tA, nBaryons, exports.RIDGE_LAMBDA);
|
|
123
|
+
const xB = solveChannel(activations.slice(0, nBaryons), tB, nBaryons, exports.RIDGE_LAMBDA);
|
|
124
|
+
// Update Basis
|
|
125
|
+
for (let i = 0; i < nTotal; i++) {
|
|
126
|
+
basis[i].l = xL[i];
|
|
127
|
+
if (i < nBaryons) {
|
|
128
|
+
basis[i].a = xA[i];
|
|
129
|
+
basis[i].b = xB[i];
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
basis[i].a = 0;
|
|
133
|
+
basis[i].b = 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return basis;
|
|
137
|
+
}
|
|
138
|
+
function solveChannel(activations, target, n, lambda) {
|
|
139
|
+
if (n === 0)
|
|
140
|
+
return new Float64Array(0);
|
|
141
|
+
const m = target.length;
|
|
142
|
+
const ata = new Float64Array(n * n);
|
|
143
|
+
const atb = new Float64Array(n);
|
|
144
|
+
// Build Normal Equations (ATA * x = ATb)
|
|
145
|
+
for (let i = 0; i < n; i++) {
|
|
146
|
+
for (let j = i; j < n; j++) {
|
|
147
|
+
let sum = 0.0;
|
|
148
|
+
const actI = activations[i];
|
|
149
|
+
const actJ = activations[j];
|
|
150
|
+
for (let p = 0; p < m; p++)
|
|
151
|
+
sum += actI[p] * actJ[p];
|
|
152
|
+
ata[i * n + j] = sum;
|
|
153
|
+
ata[j * n + i] = sum;
|
|
154
|
+
}
|
|
155
|
+
let sumB = 0.0;
|
|
156
|
+
const actI = activations[i];
|
|
157
|
+
for (let p = 0; p < m; p++)
|
|
158
|
+
sumB += actI[p] * target[p];
|
|
159
|
+
atb[i] = sumB;
|
|
160
|
+
}
|
|
161
|
+
// Ridge Regularization
|
|
162
|
+
for (let i = 0; i < n; i++)
|
|
163
|
+
ata[i * n + i] += lambda;
|
|
164
|
+
return solveLinearSystem(ata, atb, n);
|
|
165
|
+
}
|
|
166
|
+
function solveLinearSystem(mat, vec, n) {
|
|
167
|
+
// Gaussian Elimination
|
|
168
|
+
const a = new Float64Array(mat); // Copy
|
|
169
|
+
const b = new Float64Array(vec); // Copy
|
|
170
|
+
for (let k = 0; k < n - 1; k++) {
|
|
171
|
+
for (let i = k + 1; i < n; i++) {
|
|
172
|
+
const factor = a[i * n + k] / a[k * n + k];
|
|
173
|
+
for (let j = k; j < n; j++)
|
|
174
|
+
a[i * n + j] -= factor * a[k * n + j];
|
|
175
|
+
b[i] -= factor * b[k];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const x = new Float64Array(n);
|
|
179
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
180
|
+
let sum = 0.0;
|
|
181
|
+
for (let j = i + 1; j < n; j++)
|
|
182
|
+
sum += a[i * n + j] * x[j];
|
|
183
|
+
x[i] = (b[i] - sum) / a[i * n + i];
|
|
184
|
+
}
|
|
185
|
+
return x;
|
|
186
|
+
}
|
|
187
|
+
function findBestSplat(grid, recon, mL, mA, mB, w, h) {
|
|
188
|
+
let maxScore = -1.0;
|
|
189
|
+
let bestSplat = {
|
|
190
|
+
x: 0,
|
|
191
|
+
y: 0,
|
|
192
|
+
sigma: 0.1,
|
|
193
|
+
l: 0,
|
|
194
|
+
a: 0,
|
|
195
|
+
b: 0,
|
|
196
|
+
isLepton: false,
|
|
197
|
+
};
|
|
198
|
+
// Compute Residuals
|
|
199
|
+
const n = Math.floor(grid.length / 3);
|
|
200
|
+
const resL = new Float64Array(n);
|
|
201
|
+
const resA = new Float64Array(n);
|
|
202
|
+
const resB = new Float64Array(n);
|
|
203
|
+
for (let i = 0; i < n; i++) {
|
|
204
|
+
resL[i] = grid[i * 3] - mL - recon[i * 3];
|
|
205
|
+
resA[i] = grid[i * 3 + 1] - mA - recon[i * 3 + 1];
|
|
206
|
+
resB[i] = grid[i * 3 + 2] - mB - recon[i * 3 + 2];
|
|
207
|
+
}
|
|
208
|
+
const step = 2; // Optimization: stride
|
|
209
|
+
const sigmas = exports.SIGMA_TABLE;
|
|
210
|
+
for (let y = 0; y < h; y += step) {
|
|
211
|
+
for (let x = 0; x < w; x += step) {
|
|
212
|
+
const xf = x / w;
|
|
213
|
+
const yf = y / h;
|
|
214
|
+
for (const sigma of sigmas) {
|
|
215
|
+
let dotL = 0, dotA = 0, dotB = 0, dotG = 0;
|
|
216
|
+
const rad = Math.floor(sigma * w * 3.5);
|
|
217
|
+
const y0 = clampi(y - rad, 0, h - 1);
|
|
218
|
+
const y1 = clampi(y + rad, 0, h - 1);
|
|
219
|
+
const x0 = clampi(x - rad, 0, w - 1);
|
|
220
|
+
const x1 = clampi(x + rad, 0, w - 1);
|
|
221
|
+
for (let sy = y0; sy <= y1; sy++) {
|
|
222
|
+
const dy = sy / h - yf;
|
|
223
|
+
const rowBase = sy * w;
|
|
224
|
+
for (let sx = x0; sx <= x1; sx++) {
|
|
225
|
+
const dx = sx / w - xf;
|
|
226
|
+
const distSq = dx * dx + dy * dy;
|
|
227
|
+
const weight = Math.exp(-distSq / (2 * sigma * sigma));
|
|
228
|
+
const idx = rowBase + sx;
|
|
229
|
+
dotL += weight * resL[idx];
|
|
230
|
+
dotA += weight * resA[idx];
|
|
231
|
+
dotB += weight * resB[idx];
|
|
232
|
+
dotG += weight * weight;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (dotG < 1e-9)
|
|
236
|
+
continue;
|
|
237
|
+
// Score based on energy
|
|
238
|
+
const score = (dotL * dotL + dotA * dotA + dotB * dotB) / dotG;
|
|
239
|
+
if (score > maxScore) {
|
|
240
|
+
maxScore = score;
|
|
241
|
+
bestSplat = {
|
|
242
|
+
x: xf,
|
|
243
|
+
y: yf,
|
|
244
|
+
sigma,
|
|
245
|
+
l: dotL / dotG,
|
|
246
|
+
a: dotA / dotG,
|
|
247
|
+
b: dotB / dotG,
|
|
248
|
+
isLepton: false,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { splat: bestSplat, score: maxScore };
|
|
255
|
+
}
|
|
256
|
+
function computeBasisMap(s, w, h) {
|
|
257
|
+
const out = new Float64Array(w * h);
|
|
258
|
+
const rad = Math.floor(s.sigma * w * 3.5);
|
|
259
|
+
const cx = Math.floor(s.x * w);
|
|
260
|
+
const cy = Math.floor(s.y * h);
|
|
261
|
+
const y0 = clampi(cy - rad, 0, h - 1);
|
|
262
|
+
const y1 = clampi(cy + rad, 0, h - 1);
|
|
263
|
+
const x0 = clampi(cx - rad, 0, w - 1);
|
|
264
|
+
const x1 = clampi(cx + rad, 0, w - 1);
|
|
265
|
+
for (let y = y0; y <= y1; y++) {
|
|
266
|
+
const dy = y / h - s.y;
|
|
267
|
+
for (let x = x0; x <= x1; x++) {
|
|
268
|
+
const dx = x / w - s.x;
|
|
269
|
+
out[y * w + x] = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
function addSplatToGrid(grid, s, w, h) {
|
|
275
|
+
const rad = Math.floor(s.sigma * w * 3.5);
|
|
276
|
+
const cx = Math.floor(s.x * w);
|
|
277
|
+
const cy = Math.floor(s.y * h);
|
|
278
|
+
const y0 = clampi(cy - rad, 0, h - 1);
|
|
279
|
+
const y1 = clampi(cy + rad, 0, h - 1);
|
|
280
|
+
const x0 = clampi(cx - rad, 0, w - 1);
|
|
281
|
+
const x1 = clampi(cx + rad, 0, w - 1);
|
|
282
|
+
for (let y = y0; y <= y1; y++) {
|
|
283
|
+
const dy = y / h - s.y;
|
|
284
|
+
for (let x = x0; x <= x1; x++) {
|
|
285
|
+
const dx = x / w - s.x;
|
|
286
|
+
const weight = Math.exp(-(dx * dx + dy * dy) / (2 * s.sigma * s.sigma));
|
|
287
|
+
const idx = (y * w + x) * 3;
|
|
288
|
+
grid[idx] += s.l * weight;
|
|
289
|
+
if (!s.isLepton) {
|
|
290
|
+
grid[idx + 1] += s.a * weight;
|
|
291
|
+
grid[idx + 2] += s.b * weight;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// --- Image Helpers ---
|
|
297
|
+
function imageToOklabGrid(rgba, srcW, srcH, w, h) {
|
|
298
|
+
const out = new Float64Array(w * h * 3);
|
|
299
|
+
for (let y = 0; y < h; y++) {
|
|
300
|
+
const y0 = Math.floor((y * srcH) / h);
|
|
301
|
+
const y1 = Math.ceil(((y + 1) * srcH) / h);
|
|
302
|
+
for (let x = 0; x < w; x++) {
|
|
303
|
+
const x0 = Math.floor((x * srcW) / w);
|
|
304
|
+
const x1 = Math.ceil(((x + 1) * srcW) / w);
|
|
305
|
+
let rSum = 0, gSum = 0, bSum = 0, count = 0;
|
|
306
|
+
for (let iy = y0; iy < y1; iy++) {
|
|
307
|
+
if (iy >= srcH)
|
|
308
|
+
break;
|
|
309
|
+
for (let ix = x0; ix < x1; ix++) {
|
|
310
|
+
if (ix >= srcW)
|
|
311
|
+
break;
|
|
312
|
+
const idx = (iy * srcW + ix) * 4;
|
|
313
|
+
rSum += rgba[idx];
|
|
314
|
+
gSum += rgba[idx + 1];
|
|
315
|
+
bSum += rgba[idx + 2];
|
|
316
|
+
count++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (count === 0)
|
|
320
|
+
continue;
|
|
321
|
+
const r = rSum / count / 255.0;
|
|
322
|
+
const g = gSum / count / 255.0;
|
|
323
|
+
const b = bSum / count / 255.0;
|
|
324
|
+
const lab = srgbToOklab(r, g, b);
|
|
325
|
+
const idx = (y * w + x) * 3;
|
|
326
|
+
out[idx] = lab.l;
|
|
327
|
+
out[idx + 1] = lab.a;
|
|
328
|
+
out[idx + 2] = lab.b;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
function srgbToOklab(r, g, b) {
|
|
334
|
+
const lin = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
335
|
+
r = lin(r);
|
|
336
|
+
g = lin(g);
|
|
337
|
+
b = lin(b);
|
|
338
|
+
const l1 = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
339
|
+
const m1 = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
340
|
+
const s1 = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
341
|
+
const l_ = Math.cbrt(l1), m_ = Math.cbrt(m1), s_ = Math.cbrt(s1);
|
|
342
|
+
return {
|
|
343
|
+
l: 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_,
|
|
344
|
+
a: 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_,
|
|
345
|
+
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function oklabToSrgb(l, a, b) {
|
|
349
|
+
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
|
|
350
|
+
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
|
|
351
|
+
const s_ = l - 0.0894841775 * a - 1.291485548 * b;
|
|
352
|
+
const l3 = l_ * l_ * l_;
|
|
353
|
+
const m3 = m_ * m_ * m_;
|
|
354
|
+
const s3 = s_ * s_ * s_;
|
|
355
|
+
let r = +4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
|
|
356
|
+
let g = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
|
|
357
|
+
let bl = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3;
|
|
358
|
+
const srt = (c) => c <= 0.0031308
|
|
359
|
+
? 12.92 * c
|
|
360
|
+
: c < 0
|
|
361
|
+
? 0
|
|
362
|
+
: 1.055 * Math.pow(c, 1.0 / 2.4) - 0.055;
|
|
363
|
+
return { r: srt(r), g: srt(g), b: srt(bl) };
|
|
364
|
+
}
|
|
365
|
+
// --- Bit Packing ---
|
|
366
|
+
class BitStream {
|
|
367
|
+
constructor() {
|
|
368
|
+
this.buf = [];
|
|
369
|
+
this.acc = 0n;
|
|
370
|
+
this.n = 0;
|
|
371
|
+
}
|
|
372
|
+
write(val, bits) {
|
|
373
|
+
this.acc = (this.acc << BigInt(bits)) | BigInt(val);
|
|
374
|
+
this.n += bits;
|
|
375
|
+
while (this.n >= 8) {
|
|
376
|
+
const shift = BigInt(this.n - 8);
|
|
377
|
+
const byteVal = Number((this.acc >> shift) & 0xffn);
|
|
378
|
+
this.buf.push(byteVal);
|
|
379
|
+
this.n -= 8;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
getBytes() {
|
|
383
|
+
if (this.n > 0) {
|
|
384
|
+
this.buf.push(Number((this.acc << BigInt(8 - this.n)) & 0xffn));
|
|
385
|
+
}
|
|
386
|
+
return new Uint8Array(this.buf);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
class BitReader {
|
|
390
|
+
constructor(data) {
|
|
391
|
+
this.pos = 0;
|
|
392
|
+
this.rem = 0;
|
|
393
|
+
this.curr = 0;
|
|
394
|
+
this.data = data;
|
|
395
|
+
}
|
|
396
|
+
read(bits) {
|
|
397
|
+
let val = 0;
|
|
398
|
+
let bitsRemaining = bits;
|
|
399
|
+
while (bitsRemaining > 0) {
|
|
400
|
+
if (this.rem === 0) {
|
|
401
|
+
if (this.pos >= this.data.length)
|
|
402
|
+
return val << bitsRemaining; // EOF
|
|
403
|
+
this.curr = this.data[this.pos++];
|
|
404
|
+
this.rem = 8;
|
|
405
|
+
}
|
|
406
|
+
// Optimization for partial read
|
|
407
|
+
const take = Math.min(this.rem, bitsRemaining);
|
|
408
|
+
const shift = this.rem - take;
|
|
409
|
+
const mask = (1 << take) - 1;
|
|
410
|
+
const chunk = (this.curr >> shift) & mask;
|
|
411
|
+
val = (val << take) | chunk;
|
|
412
|
+
this.rem -= take;
|
|
413
|
+
bitsRemaining -= take;
|
|
414
|
+
}
|
|
415
|
+
return val;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function packV4(mean, splats) {
|
|
419
|
+
const bw = new BitStream();
|
|
420
|
+
// Header (16 bits)
|
|
421
|
+
bw.write(mean, 16);
|
|
422
|
+
// Baryons (3x)
|
|
423
|
+
let count = 0;
|
|
424
|
+
for (const s of splats) {
|
|
425
|
+
if (s.isLepton)
|
|
426
|
+
continue;
|
|
427
|
+
if (count >= 3)
|
|
428
|
+
break;
|
|
429
|
+
const xi = clampi(Math.round(s.x * 15.0), 0, 15);
|
|
430
|
+
const yi = clampi(Math.round(s.y * 15.0), 0, 15);
|
|
431
|
+
const sigI = getSigmaIdx(s.sigma);
|
|
432
|
+
const lQ = quant(s.l, -0.8, 0.8, 4);
|
|
433
|
+
const aQ = quant(s.a, -0.4, 0.4, 4);
|
|
434
|
+
const bQ = quant(s.b, -0.4, 0.4, 4);
|
|
435
|
+
bw.write(xi, 4);
|
|
436
|
+
bw.write(yi, 4);
|
|
437
|
+
bw.write(sigI, 2);
|
|
438
|
+
bw.write(lQ, 4);
|
|
439
|
+
bw.write(aQ, 4);
|
|
440
|
+
bw.write(bQ, 4);
|
|
441
|
+
count++;
|
|
442
|
+
}
|
|
443
|
+
while (count < 3) {
|
|
444
|
+
bw.write(0, 22);
|
|
445
|
+
count++;
|
|
446
|
+
}
|
|
447
|
+
// Leptons (3x)
|
|
448
|
+
count = 0;
|
|
449
|
+
for (const s of splats) {
|
|
450
|
+
if (!s.isLepton)
|
|
451
|
+
continue;
|
|
452
|
+
if (count >= 3)
|
|
453
|
+
break;
|
|
454
|
+
const xi = clampi(Math.round(s.x * 15.0), 0, 15);
|
|
455
|
+
const yi = clampi(Math.round(s.y * 15.0), 0, 15);
|
|
456
|
+
const sigI = getSigmaIdx(s.sigma);
|
|
457
|
+
const lQ = quant(s.l, -0.8, 0.8, 5);
|
|
458
|
+
bw.write(xi, 4);
|
|
459
|
+
bw.write(yi, 4);
|
|
460
|
+
bw.write(sigI, 2);
|
|
461
|
+
bw.write(lQ, 5);
|
|
462
|
+
count++;
|
|
463
|
+
}
|
|
464
|
+
while (count < 3) {
|
|
465
|
+
bw.write(0, 15);
|
|
466
|
+
count++;
|
|
467
|
+
}
|
|
468
|
+
bw.write(0, 1); // Pad
|
|
469
|
+
return bw.getBytes();
|
|
470
|
+
}
|
|
471
|
+
function unpackV4(hash) {
|
|
472
|
+
const br = new BitReader(hash);
|
|
473
|
+
const meanMap = br.read(16);
|
|
474
|
+
const { l, a, b } = unpackMean(meanMap);
|
|
475
|
+
const splats = [];
|
|
476
|
+
// Baryons
|
|
477
|
+
for (let i = 0; i < 3; i++) {
|
|
478
|
+
const xi = br.read(4);
|
|
479
|
+
const yi = br.read(4);
|
|
480
|
+
const sigI = br.read(2);
|
|
481
|
+
const lQ = br.read(4);
|
|
482
|
+
const aQ = br.read(4);
|
|
483
|
+
const bQ = br.read(4);
|
|
484
|
+
if (xi === 0 && yi === 0 && lQ === 0 && aQ === 0 && bQ === 0)
|
|
485
|
+
continue;
|
|
486
|
+
splats.push({
|
|
487
|
+
x: xi / 15.0,
|
|
488
|
+
y: yi / 15.0,
|
|
489
|
+
sigma: exports.SIGMA_TABLE[sigI],
|
|
490
|
+
l: unquant(lQ, -0.8, 0.8, 4),
|
|
491
|
+
a: unquant(aQ, -0.4, 0.4, 4),
|
|
492
|
+
b: unquant(bQ, -0.4, 0.4, 4),
|
|
493
|
+
isLepton: false,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// Leptons
|
|
497
|
+
for (let i = 0; i < 3; i++) {
|
|
498
|
+
const xi = br.read(4);
|
|
499
|
+
const yi = br.read(4);
|
|
500
|
+
const sigI = br.read(2);
|
|
501
|
+
const lQ = br.read(5);
|
|
502
|
+
if (xi === 0 && yi === 0 && lQ === 0)
|
|
503
|
+
continue;
|
|
504
|
+
splats.push({
|
|
505
|
+
x: xi / 15.0,
|
|
506
|
+
y: yi / 15.0,
|
|
507
|
+
sigma: exports.SIGMA_TABLE[sigI],
|
|
508
|
+
l: unquant(lQ, -0.8, 0.8, 5),
|
|
509
|
+
a: 0,
|
|
510
|
+
b: 0,
|
|
511
|
+
isLepton: true,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
return { meanL: l, meanA: a, meanB: b, splats };
|
|
515
|
+
}
|
|
516
|
+
function getSigmaIdx(s) {
|
|
517
|
+
let minD = 100.0;
|
|
518
|
+
let idx = 0;
|
|
519
|
+
for (let i = 0; i < exports.SIGMA_TABLE.length; i++) {
|
|
520
|
+
const d = Math.abs(exports.SIGMA_TABLE[i] - s);
|
|
521
|
+
if (d < minD) {
|
|
522
|
+
minD = d;
|
|
523
|
+
idx = i;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return idx;
|
|
527
|
+
}
|
|
528
|
+
function packMean(l, a, b) {
|
|
529
|
+
const li = clampi(Math.floor(l * 63.5), 0, 63);
|
|
530
|
+
const ai = clampi(Math.floor(((a + 0.2) / 0.4) * 31.5), 0, 31);
|
|
531
|
+
const bi = clampi(Math.floor(((b + 0.2) / 0.4) * 31.5), 0, 31);
|
|
532
|
+
return (li << 10) | (ai << 5) | bi;
|
|
533
|
+
}
|
|
534
|
+
function unpackMean(p) {
|
|
535
|
+
const li = (p >> 10) & 0x3f;
|
|
536
|
+
const ai = (p >> 5) & 0x1f;
|
|
537
|
+
const bi = p & 0x1f;
|
|
538
|
+
return {
|
|
539
|
+
l: li / 63.0,
|
|
540
|
+
a: (ai / 31.0) * 0.4 - 0.2,
|
|
541
|
+
b: (bi / 31.0) * 0.4 - 0.2,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function quant(v, min, max, bits) {
|
|
545
|
+
const steps = (1 << bits) - 1;
|
|
546
|
+
const norm = (v - min) / (max - min);
|
|
547
|
+
return clampi(Math.round(norm * steps), 0, steps);
|
|
548
|
+
}
|
|
549
|
+
function unquant(v, min, max, bits) {
|
|
550
|
+
const steps = (1 << bits) - 1;
|
|
551
|
+
const norm = v / steps;
|
|
552
|
+
return norm * (max - min) + min;
|
|
553
|
+
}
|
|
554
|
+
function clampi(v, min, max) {
|
|
555
|
+
return v < min ? min : v > max ? max : v;
|
|
556
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "splathash",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "SplatHash: compress any image to 16 bytes and reconstruct it",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"require": "./dist/src/index.js",
|
|
10
|
+
"types": "./dist/src/index.d.ts"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist/src",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"image",
|
|
19
|
+
"hash",
|
|
20
|
+
"perceptual",
|
|
21
|
+
"thumbnail",
|
|
22
|
+
"blurhash"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/junevm/splathash"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "tsc && node --test dist/test/test.js",
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"example": "ts-node examples/simple.ts"
|
|
32
|
+
},
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^20.0.0",
|
|
37
|
+
"@types/sharp": "^0.31.1",
|
|
38
|
+
"sharp": "^0.34.5",
|
|
39
|
+
"ts-node": "^10.9.2",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|