Skip to content

Commit e9c2ab0

Browse files
committed
Add spherical viewer
1 parent 8cb8e67 commit e9c2ab0

9 files changed

Lines changed: 678 additions & 1 deletion

File tree

astro.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import astroExpressiveCode from "astro-expressive-code";
1515
export default defineConfig({
1616
vite: {
1717
plugins: [tailwindcss()],
18+
server: {
19+
watch: {
20+
usePolling: true,
21+
interval: 1000,
22+
},
23+
},
1824
},
1925
markdown: {
2026
remarkPlugins: [remarkMath],

public/spherical-image/fisheye.bin

7.91 MB
Binary file not shown.
12 MB
Binary file not shown.

public/spherical-image/pinhole.bin

7.03 MB
Binary file not shown.
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Decode USF spherical `.bin` bytes (same layout as `usf.utils.spherical_image._decode_batch_spherical_bin`).
3+
*
4+
* Header: 64 bytes — magic `USFBSIMG`, version, `N` (u64), `B` and `C` (u32), four dtype codes (u8).
5+
* Payloads: `batch_value` (B×N×C), `vector` (N×3), `polar` (N×2), `mask` (N), all little-endian.
6+
*/
7+
8+
const HEADER_SIZE = 64;
9+
const MAGIC_STR = "USFBSIMG";
10+
11+
/** @type {Record<number, number>} dtype code -> bytes per element (matches `BINARY_DTYPE_CODE_TO_NUMPY`) */
12+
const DTYPE_BYTES = {
13+
1: 4, // float32
14+
2: 8, // float64
15+
3: 1, // uint8
16+
4: 2, // uint16
17+
5: 4, // int32
18+
6: 8, // int64
19+
7: 1, // bool (numpy bool_ single byte)
20+
};
21+
22+
/**
23+
* @param {number} code
24+
* @returns {number}
25+
*/
26+
function dtypeBytes(code) {
27+
const b = DTYPE_BYTES[code];
28+
if (b === undefined) throw new Error(`Unknown dtype code ${code}`);
29+
return b;
30+
}
31+
32+
/**
33+
* @param {ArrayBuffer} buffer
34+
* @param {number} byteOffset
35+
* @param {number} count Elements (not bytes)
36+
* @param {number} code
37+
* @returns {Float32Array}
38+
*/
39+
function toFloat32Array(buffer, byteOffset, count, code) {
40+
const out = new Float32Array(count);
41+
const dv = new DataView(buffer, byteOffset);
42+
let o = 0;
43+
let off = 0;
44+
switch (code) {
45+
case 1: {
46+
const u8 = new Uint8Array(buffer, byteOffset, count * 4);
47+
const copy = new ArrayBuffer(count * 4);
48+
new Uint8Array(copy).set(u8);
49+
out.set(new Float32Array(copy));
50+
break;
51+
}
52+
case 2: {
53+
for (let i = 0; i < count; i++, off += 8) {
54+
out[o++] = dv.getFloat64(off, true);
55+
}
56+
break;
57+
}
58+
case 3: {
59+
for (let i = 0; i < count; i++, off += 1) {
60+
out[o++] = dv.getUint8(off);
61+
}
62+
break;
63+
}
64+
case 4: {
65+
for (let i = 0; i < count; i++, off += 2) {
66+
out[o++] = dv.getUint16(off, true);
67+
}
68+
break;
69+
}
70+
case 5: {
71+
for (let i = 0; i < count; i++, off += 4) {
72+
out[o++] = dv.getInt32(off, true);
73+
}
74+
break;
75+
}
76+
case 6: {
77+
for (let i = 0; i < count; i++, off += 8) {
78+
out[o++] = Number(dv.getBigInt64(off, true));
79+
}
80+
break;
81+
}
82+
case 7: {
83+
for (let i = 0; i < count; i++, off += 1) {
84+
out[o++] = dv.getUint8(off) !== 0 ? 1 : 0;
85+
}
86+
break;
87+
}
88+
default:
89+
throw new Error(`Unknown dtype code ${code}`);
90+
}
91+
return out;
92+
}
93+
94+
/**
95+
* @param {ArrayBuffer} buffer
96+
* @returns {{
97+
* n: number,
98+
* b: number,
99+
* c: number,
100+
* batchValue: Float32Array,
101+
* vector: Float32Array,
102+
* polar: Float32Array,
103+
* mask: Uint8Array,
104+
* }}
105+
*/
106+
export function decodeSphericalBin(buffer) {
107+
if (buffer.byteLength < HEADER_SIZE) {
108+
throw new Error("File too small for spherical .bin header.");
109+
}
110+
const u8 = new Uint8Array(buffer, 0, 8);
111+
let magic = "";
112+
for (let i = 0; i < 8; i++) magic += String.fromCharCode(u8[i]);
113+
if (magic !== MAGIC_STR) {
114+
throw new Error(`Bad magic: expected ${MAGIC_STR}, got ${magic}.`);
115+
}
116+
const dv = new DataView(buffer);
117+
const version = dv.getUint32(8, true);
118+
if (version !== 1) {
119+
throw new Error(
120+
`Unsupported spherical .bin version ${version} (expected 1).`,
121+
);
122+
}
123+
const n = Number(dv.getBigUint64(16, true));
124+
const b = dv.getUint32(24, true);
125+
const c = dv.getUint32(28, true);
126+
const dBv = dv.getUint8(32);
127+
const dV = dv.getUint8(33);
128+
const dP = dv.getUint8(34);
129+
const dM = dv.getUint8(35);
130+
131+
for (const code of [dBv, dV, dP, dM]) {
132+
if (DTYPE_BYTES[code] === undefined) {
133+
throw new Error(`Unknown dtype code ${code} in spherical .bin.`);
134+
}
135+
}
136+
137+
const esBv = dtypeBytes(dBv);
138+
const esV = dtypeBytes(dV);
139+
const esP = dtypeBytes(dP);
140+
const esM = dtypeBytes(dM);
141+
const pay = b * n * c * esBv + n * 3 * esV + n * 2 * esP + n * esM;
142+
if (buffer.byteLength !== HEADER_SIZE + pay) {
143+
throw new Error(
144+
`File size mismatch: expected ${HEADER_SIZE + pay} bytes, got ${buffer.byteLength}.`,
145+
);
146+
}
147+
148+
let off = HEADER_SIZE;
149+
const countBv = b * n * c;
150+
let batchValue = toFloat32Array(buffer, off, countBv, dBv);
151+
off += countBv * esBv;
152+
153+
const countV = n * 3;
154+
const vector = toFloat32Array(buffer, off, countV, dV);
155+
off += countV * esV;
156+
157+
const countP = n * 2;
158+
const polar = toFloat32Array(buffer, off, countP, dP);
159+
off += countP * esP;
160+
161+
const maskFloat = toFloat32Array(buffer, off, n, dM);
162+
off += n * esM;
163+
const mask = new Uint8Array(n);
164+
for (let i = 0; i < n; i++) {
165+
mask[i] = maskFloat[i] !== 0 ? 1 : 0;
166+
}
167+
168+
let maxVal = 0;
169+
for (let i = 0; i < batchValue.length; i++) {
170+
maxVal = Math.max(maxVal, batchValue[i]);
171+
}
172+
if (maxVal > 1.0) {
173+
for (let i = 0; i < batchValue.length; i++) {
174+
batchValue[i] /= 255.0;
175+
}
176+
}
177+
178+
return {
179+
n,
180+
b,
181+
c,
182+
batchValue,
183+
vector,
184+
polar,
185+
mask,
186+
};
187+
}

public/spherical-viewer/index.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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" />
6+
<title>USF spherical .bin viewer</title>
7+
<style>
8+
html,
9+
body {
10+
margin: 0;
11+
height: 100%;
12+
overflow: hidden;
13+
font-family: system-ui, sans-serif;
14+
}
15+
#usf-viewer-root {
16+
width: 100%;
17+
height: 100%;
18+
min-height: 280px;
19+
position: relative;
20+
display: flex;
21+
align-items: center;
22+
justify-content: center;
23+
}
24+
.usf-viewer-error {
25+
box-sizing: border-box;
26+
padding: 1rem 1.25rem;
27+
max-width: 42rem;
28+
margin: 2rem auto;
29+
line-height: 1.5;
30+
color: #1d1d1f;
31+
background: #fff;
32+
border-radius: 8px;
33+
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
34+
}
35+
.usf-viewer-spinner {
36+
display: flex;
37+
align-items: center;
38+
justify-content: center;
39+
}
40+
</style>
41+
<script type="importmap">
42+
{
43+
"imports": {
44+
"three": "https://cdn.jsdelivr.net/npm/three@0.183.2/build/three.module.js",
45+
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.183.2/examples/jsm/"
46+
}
47+
}
48+
</script>
49+
</head>
50+
<body>
51+
<div id="usf-viewer-root"></div>
52+
<script type="module" src="./viewer.js"></script>
53+
</body>
54+
</html>

0 commit comments

Comments
 (0)