@driftengine/capture 4.0.0
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/LICENSE +202 -0
- package/NOTICE +29 -0
- package/README.md +68 -0
- package/dist/browserFrames.d.ts +31 -0
- package/dist/browserFrames.js +105 -0
- package/dist/collide.d.ts +59 -0
- package/dist/collide.js +92 -0
- package/dist/decimate.d.ts +42 -0
- package/dist/decimate.js +439 -0
- package/dist/delight.d.ts +42 -0
- package/dist/delight.js +351 -0
- package/dist/delightScene.d.ts +37 -0
- package/dist/delightScene.js +183 -0
- package/dist/depth.d.ts +44 -0
- package/dist/depth.js +53 -0
- package/dist/features.d.ts +33 -0
- package/dist/features.js +260 -0
- package/dist/frames.d.ts +47 -0
- package/dist/frames.js +87 -0
- package/dist/fusion.d.ts +77 -0
- package/dist/fusion.js +152 -0
- package/dist/gaussians/adam.d.ts +57 -0
- package/dist/gaussians/adam.js +88 -0
- package/dist/gaussians/depth.d.ts +8 -0
- package/dist/gaussians/depth.js +73 -0
- package/dist/gaussians/gradients.d.ts +36 -0
- package/dist/gaussians/gradients.js +228 -0
- package/dist/gaussians/harmonics.d.ts +46 -0
- package/dist/gaussians/harmonics.js +99 -0
- package/dist/gaussians/optimise.d.ts +50 -0
- package/dist/gaussians/optimise.js +125 -0
- package/dist/gaussians/parameters.d.ts +35 -0
- package/dist/gaussians/parameters.js +165 -0
- package/dist/gaussians/project.d.ts +72 -0
- package/dist/gaussians/project.js +137 -0
- package/dist/gaussians/projectGradient.d.ts +15 -0
- package/dist/gaussians/projectGradient.js +213 -0
- package/dist/gaussians/rasterise.d.ts +13 -0
- package/dist/gaussians/rasterise.js +65 -0
- package/dist/gaussians/refine.d.ts +36 -0
- package/dist/gaussians/refine.js +136 -0
- package/dist/gaussians/similarity.d.ts +25 -0
- package/dist/gaussians/similarity.js +80 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +96 -0
- package/dist/marching.d.ts +38 -0
- package/dist/marching.js +258 -0
- package/dist/masks.d.ts +59 -0
- package/dist/masks.js +165 -0
- package/dist/math/dense.d.ts +68 -0
- package/dist/math/dense.js +294 -0
- package/dist/math/schur.d.ts +23 -0
- package/dist/math/schur.js +140 -0
- package/dist/models/batchNorm.d.ts +17 -0
- package/dist/models/batchNorm.js +24 -0
- package/dist/models/clipTokenizer.d.ts +40 -0
- package/dist/models/clipTokenizer.js +122 -0
- package/dist/models/clipTower.d.ts +27 -0
- package/dist/models/clipTower.js +45 -0
- package/dist/models/depthAnything2.d.ts +38 -0
- package/dist/models/depthAnything2.js +163 -0
- package/dist/models/depthAnything3.d.ts +26 -0
- package/dist/models/depthAnything3.js +171 -0
- package/dist/models/dualDpt.d.ts +19 -0
- package/dist/models/dualDpt.js +138 -0
- package/dist/models/hiera.d.ts +49 -0
- package/dist/models/hiera.js +120 -0
- package/dist/models/hieraAttention.d.ts +16 -0
- package/dist/models/hieraAttention.js +68 -0
- package/dist/models/miniature.d.ts +36 -0
- package/dist/models/miniature.js +216 -0
- package/dist/models/mobileSam.d.ts +41 -0
- package/dist/models/mobileSam.js +88 -0
- package/dist/models/owlv2.d.ts +49 -0
- package/dist/models/owlv2.js +131 -0
- package/dist/models/owlv2Detect.d.ts +25 -0
- package/dist/models/owlv2Detect.js +80 -0
- package/dist/models/owlv2Miniature.d.ts +27 -0
- package/dist/models/owlv2Miniature.js +87 -0
- package/dist/models/rope.d.ts +23 -0
- package/dist/models/rope.js +84 -0
- package/dist/models/sam21.d.ts +45 -0
- package/dist/models/sam21.js +126 -0
- package/dist/models/sam21Miniature.d.ts +37 -0
- package/dist/models/sam21Miniature.js +194 -0
- package/dist/models/sam21Video.d.ts +30 -0
- package/dist/models/sam21Video.js +101 -0
- package/dist/models/sam2Choice.d.ts +19 -0
- package/dist/models/sam2Choice.js +55 -0
- package/dist/models/sam2Decoder.d.ts +36 -0
- package/dist/models/sam2Decoder.js +108 -0
- package/dist/models/sam2Memories.d.ts +54 -0
- package/dist/models/sam2Memories.js +93 -0
- package/dist/models/sam2Memory.d.ts +30 -0
- package/dist/models/sam2Memory.js +136 -0
- package/dist/models/sam2Positions.d.ts +24 -0
- package/dist/models/sam2Positions.js +94 -0
- package/dist/models/sam2Tracker.d.ts +69 -0
- package/dist/models/sam2Tracker.js +172 -0
- package/dist/models/samDecoder.d.ts +45 -0
- package/dist/models/samDecoder.js +96 -0
- package/dist/models/samMiniature.d.ts +43 -0
- package/dist/models/samMiniature.js +161 -0
- package/dist/models/samPrompt.d.ts +57 -0
- package/dist/models/samPrompt.js +160 -0
- package/dist/models/seeded.d.ts +16 -0
- package/dist/models/seeded.js +48 -0
- package/dist/models/tinyVit.d.ts +38 -0
- package/dist/models/tinyVit.js +82 -0
- package/dist/models/twoWay.d.ts +46 -0
- package/dist/models/twoWay.js +67 -0
- package/dist/models/vit.d.ts +45 -0
- package/dist/models/vit.js +157 -0
- package/dist/models/windowAttention.d.ts +25 -0
- package/dist/models/windowAttention.js +104 -0
- package/dist/poses.d.ts +31 -0
- package/dist/poses.js +750 -0
- package/dist/prepare.d.ts +30 -0
- package/dist/prepare.js +238 -0
- package/dist/propose.d.ts +36 -0
- package/dist/propose.js +63 -0
- package/dist/resample.d.ts +39 -0
- package/dist/resample.js +165 -0
- package/dist/run.d.ts +3 -0
- package/dist/run.js +1 -0
- package/dist/scene.d.ts +104 -0
- package/dist/scene.js +109 -0
- package/dist/segment.d.ts +40 -0
- package/dist/segment.js +184 -0
- package/dist/testScene.d.ts +55 -0
- package/dist/testScene.js +214 -0
- package/dist/twoView.d.ts +22 -0
- package/dist/twoView.js +435 -0
- package/package.json +63 -0
- package/src/browserFrames.ts +145 -0
- package/src/collide.ts +128 -0
- package/src/decimate.ts +510 -0
- package/src/delight.ts +427 -0
- package/src/delightScene.ts +249 -0
- package/src/depth.ts +100 -0
- package/src/features.ts +302 -0
- package/src/frames.ts +111 -0
- package/src/fusion.ts +229 -0
- package/src/gaussians/adam.ts +131 -0
- package/src/gaussians/depth.ts +87 -0
- package/src/gaussians/gradients.ts +285 -0
- package/src/gaussians/harmonics.ts +122 -0
- package/src/gaussians/optimise.ts +191 -0
- package/src/gaussians/parameters.ts +186 -0
- package/src/gaussians/project.ts +207 -0
- package/src/gaussians/projectGradient.ts +234 -0
- package/src/gaussians/rasterise.ts +81 -0
- package/src/gaussians/refine.ts +176 -0
- package/src/gaussians/similarity.ts +87 -0
- package/src/index.ts +225 -0
- package/src/marching.ts +358 -0
- package/src/masks.ts +201 -0
- package/src/math/dense.ts +317 -0
- package/src/math/schur.ts +167 -0
- package/src/models/batchNorm.ts +57 -0
- package/src/models/clipTokenizer.ts +129 -0
- package/src/models/clipTower.ts +87 -0
- package/src/models/depthAnything2.ts +276 -0
- package/src/models/depthAnything3.ts +229 -0
- package/src/models/dualDpt.ts +233 -0
- package/src/models/hiera.ts +231 -0
- package/src/models/hieraAttention.ts +121 -0
- package/src/models/miniature.ts +241 -0
- package/src/models/mobileSam.ts +135 -0
- package/src/models/owlv2.ts +190 -0
- package/src/models/owlv2Detect.ts +116 -0
- package/src/models/owlv2Miniature.ts +108 -0
- package/src/models/rope.ts +111 -0
- package/src/models/sam21.ts +182 -0
- package/src/models/sam21Miniature.ts +225 -0
- package/src/models/sam21Video.ts +154 -0
- package/src/models/sam2Choice.ts +62 -0
- package/src/models/sam2Decoder.ts +218 -0
- package/src/models/sam2Memories.ts +165 -0
- package/src/models/sam2Memory.ts +247 -0
- package/src/models/sam2Positions.ts +106 -0
- package/src/models/sam2Tracker.ts +300 -0
- package/src/models/samDecoder.ts +225 -0
- package/src/models/samMiniature.ts +190 -0
- package/src/models/samPrompt.ts +227 -0
- package/src/models/seeded.ts +50 -0
- package/src/models/tinyVit.ts +178 -0
- package/src/models/twoWay.ts +122 -0
- package/src/models/vit.ts +265 -0
- package/src/models/windowAttention.ts +155 -0
- package/src/poses.ts +894 -0
- package/src/prepare.ts +306 -0
- package/src/propose.ts +96 -0
- package/src/resample.ts +197 -0
- package/src/run.ts +7 -0
- package/src/scene.ts +141 -0
- package/src/segment.ts +220 -0
- package/src/testScene.ts +283 -0
- package/src/twoView.ts +536 -0
package/src/fusion.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Many views of a scene fused into one volume: a truncated signed distance, weighted by how
|
|
3
|
+
* squarely each view saw the surface.
|
|
4
|
+
*
|
|
5
|
+
* **Why a volume rather than the depth maps themselves.** Every view has a surface in it and no two
|
|
6
|
+
* agree exactly — noise, a fit's own error, and the silhouettes where a splat cloud thins out. A
|
|
7
|
+
* volume is where they are averaged: each view writes its opinion of the distance to the nearest
|
|
8
|
+
* surface into the samples along its own rays, and what comes out is the surface every view agrees
|
|
9
|
+
* on. `marchVolume` then reads the crossing.
|
|
10
|
+
*
|
|
11
|
+
* **Negative inside**, which is `ObjectSdf`'s convention in `@driftengine/assets` and the one the
|
|
12
|
+
* rest of this engine reads a distance field by. A sample nearer the camera than the surface is
|
|
13
|
+
* outside it and positive.
|
|
14
|
+
*
|
|
15
|
+
* **Truncated, because a depth map knows nothing about the far side of what it sees.** Past a few
|
|
16
|
+
* samples behind the surface a view has no information at all — the space there might be solid,
|
|
17
|
+
* might be another room — so writing a large negative distance would be inventing one. Everything
|
|
18
|
+
* beyond the truncation on the far side is left alone; everything beyond it on the near side is
|
|
19
|
+
* free space and is written as such, which is what carves the empty air out.
|
|
20
|
+
*
|
|
21
|
+
* **Weighted by the angle, which is what stops a grazing view winning.** A surface seen almost
|
|
22
|
+
* edge-on is a few pixels wide and every one of them is uncertain in depth by the whole width of
|
|
23
|
+
* the surface; seen square on, the same surface is measured well. The weight is the cosine between
|
|
24
|
+
* the view's own ray and the surface's normal, taken from the depth map's own gradient, so a
|
|
25
|
+
* grazing view contributes what it is worth rather than as much as any other.
|
|
26
|
+
*
|
|
27
|
+
* **A surface one view saw is kept, at the weight one view is worth.** Dropping it would leave a
|
|
28
|
+
* hole where the capture only ever looked once, which is most of a real clip's edges.
|
|
29
|
+
*/
|
|
30
|
+
import type { RasterCamera } from './gaussians/project.ts';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* One view's opinion: its depth, how well covered each pixel was, and where it stood.
|
|
34
|
+
*
|
|
35
|
+
* **Not `DepthView`, which is the depth *model's* shape** — that one carries a confidence and its
|
|
36
|
+
* own 3 × 3 intrinsics and does not know the size of its own frame. The two say much the same thing
|
|
37
|
+
* in two forms, and the pipeline that joins them is the place to convert, not here.
|
|
38
|
+
*/
|
|
39
|
+
export interface SurfaceView {
|
|
40
|
+
/** Along the camera's own axis, in metres; zero where nothing was seen. */
|
|
41
|
+
readonly depth: Float32Array;
|
|
42
|
+
/** Nought to one, as `renderDepth` answers it. */
|
|
43
|
+
readonly coverage: Float32Array;
|
|
44
|
+
readonly camera: RasterCamera;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A field of samples over a box of the world, `x` fastest, then `y`, then `z`. */
|
|
48
|
+
export interface Volume {
|
|
49
|
+
readonly dims: readonly [number, number, number];
|
|
50
|
+
/** Where the first sample sits, in the capture's world. */
|
|
51
|
+
readonly origin: readonly [number, number, number];
|
|
52
|
+
/** Metres between samples, the same on every axis. */
|
|
53
|
+
readonly spacing: number;
|
|
54
|
+
/** Truncated signed distance in metres, **negative inside**. */
|
|
55
|
+
readonly distance: Float32Array;
|
|
56
|
+
/** How much evidence each sample has; zero where no view ever saw it. */
|
|
57
|
+
readonly weight: Float32Array;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface FuseOptions {
|
|
61
|
+
/**
|
|
62
|
+
* How far either side of a surface a view is believed, in metres.
|
|
63
|
+
*
|
|
64
|
+
* Three samples by default: enough that the crossing is bracketed by samples with real evidence
|
|
65
|
+
* on both sides, which is what marching needs, and short enough that one view's surface does not
|
|
66
|
+
* reach through a wall into the next room.
|
|
67
|
+
*/
|
|
68
|
+
readonly truncation?: number;
|
|
69
|
+
/** Below this coverage a pixel's depth is an average of too little light to believe. */
|
|
70
|
+
readonly minimumCoverage?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const DEFAULT_COVERAGE = 0.5;
|
|
74
|
+
|
|
75
|
+
export function createVolume(
|
|
76
|
+
dims: readonly [number, number, number],
|
|
77
|
+
origin: readonly [number, number, number],
|
|
78
|
+
spacing: number,
|
|
79
|
+
): Volume {
|
|
80
|
+
const [nx, ny, nz] = dims;
|
|
81
|
+
return {
|
|
82
|
+
dims,
|
|
83
|
+
origin,
|
|
84
|
+
spacing,
|
|
85
|
+
distance: new Float32Array(nx * ny * nz),
|
|
86
|
+
weight: new Float32Array(nx * ny * nz),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Every view's surface written into `volume`, which accumulates rather than replaces.
|
|
92
|
+
*
|
|
93
|
+
* Calling this twice with two halves of the views is the same as calling it once with all of them,
|
|
94
|
+
* because a weighted mean is what each sample holds — which is what lets a clip be fused as it
|
|
95
|
+
* arrives rather than all at once.
|
|
96
|
+
*/
|
|
97
|
+
export function fuseDepth(
|
|
98
|
+
views: readonly SurfaceView[],
|
|
99
|
+
volume: Volume,
|
|
100
|
+
options: FuseOptions = {},
|
|
101
|
+
): void {
|
|
102
|
+
const [nx, ny, nz] = volume.dims;
|
|
103
|
+
const truncation = options.truncation ?? volume.spacing * 3;
|
|
104
|
+
const minimumCoverage = options.minimumCoverage ?? DEFAULT_COVERAGE;
|
|
105
|
+
if (!(truncation > 0)) throw new RangeError('capture: a fusion needs a truncation above zero');
|
|
106
|
+
|
|
107
|
+
for (const view of views) {
|
|
108
|
+
const { width, height, intrinsics } = view.camera;
|
|
109
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
110
|
+
const m = view.camera.worldToCamera;
|
|
111
|
+
const normals = surfaceNormals(view);
|
|
112
|
+
|
|
113
|
+
for (let k = 0; k < nz; k += 1) {
|
|
114
|
+
for (let j = 0; j < ny; j += 1) {
|
|
115
|
+
for (let i = 0; i < nx; i += 1) {
|
|
116
|
+
const world = [
|
|
117
|
+
(volume.origin[0] as number) + i * volume.spacing,
|
|
118
|
+
(volume.origin[1] as number) + j * volume.spacing,
|
|
119
|
+
(volume.origin[2] as number) + k * volume.spacing,
|
|
120
|
+
];
|
|
121
|
+
let viewX = 0;
|
|
122
|
+
let viewY = 0;
|
|
123
|
+
let viewZ = 0;
|
|
124
|
+
for (let r = 0; r < 3; r += 1) {
|
|
125
|
+
const value =
|
|
126
|
+
(m[r * 4] as number) * (world[0] as number) +
|
|
127
|
+
(m[r * 4 + 1] as number) * (world[1] as number) +
|
|
128
|
+
(m[r * 4 + 2] as number) * (world[2] as number) +
|
|
129
|
+
(m[r * 4 + 3] as number);
|
|
130
|
+
if (r === 0) viewX = value;
|
|
131
|
+
else if (r === 1) viewY = value;
|
|
132
|
+
else viewZ = value;
|
|
133
|
+
}
|
|
134
|
+
if (!(viewZ > 0)) continue;
|
|
135
|
+
const px = Math.floor((fx * viewX) / viewZ + cx);
|
|
136
|
+
const py = Math.floor((fy * viewY) / viewZ + cy);
|
|
137
|
+
if (px < 0 || px >= width || py < 0 || py >= height) continue;
|
|
138
|
+
const pixel = py * width + px;
|
|
139
|
+
if ((view.coverage[pixel] as number) < minimumCoverage) continue;
|
|
140
|
+
const measured = view.depth[pixel] as number;
|
|
141
|
+
if (!(measured > 0)) continue;
|
|
142
|
+
|
|
143
|
+
/*
|
|
144
|
+
* How squarely this view saw that surface: the ray through the pixel against the
|
|
145
|
+
* surface's own normal, both in the camera's frame. A view looking straight at it is
|
|
146
|
+
* worth one and a grazing view almost nothing.
|
|
147
|
+
*/
|
|
148
|
+
const rayX = (px + 0.5 - cx) / fx;
|
|
149
|
+
const rayY = (py + 0.5 - cy) / fy;
|
|
150
|
+
const rayLength = Math.sqrt(rayX * rayX + rayY * rayY + 1);
|
|
151
|
+
const facing = -(
|
|
152
|
+
(rayX * (normals[pixel * 3] as number) +
|
|
153
|
+
rayY * (normals[pixel * 3 + 1] as number) +
|
|
154
|
+
(normals[pixel * 3 + 2] as number)) /
|
|
155
|
+
rayLength
|
|
156
|
+
);
|
|
157
|
+
const angle = facing > 0 ? facing : 0;
|
|
158
|
+
const weight = angle * (view.coverage[pixel] as number);
|
|
159
|
+
if (!(weight > 0)) continue;
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* **In front of the surface is outside it, and the distance is measured along the
|
|
163
|
+
* surface's normal rather than along the camera's axis.** The difference of two depths is
|
|
164
|
+
* a distance along the view's own ray, which is the perpendicular distance only where the
|
|
165
|
+
* surface faces the camera squarely; everywhere else it overstates it by the obliquity.
|
|
166
|
+
* For a plane the correction is exact, and for a curved surface it is the first-order
|
|
167
|
+
* term — measured on a sphere fused from eight views, it took the median error in the
|
|
168
|
+
* band from 25 mm to 11 and the ninetieth percentile from 55 mm to 43. **The worst sample
|
|
169
|
+
* does not move**, because it is one at the very edge of the band whose distance
|
|
170
|
+
* saturates at the truncation either way.
|
|
171
|
+
*/
|
|
172
|
+
const signed = (measured - viewZ) * rayLength * angle;
|
|
173
|
+
if (signed < -truncation) continue;
|
|
174
|
+
const clamped = signed > truncation ? truncation : signed;
|
|
175
|
+
|
|
176
|
+
const at = (k * ny + j) * nx + i;
|
|
177
|
+
const held = volume.weight[at] as number;
|
|
178
|
+
const total = held + weight;
|
|
179
|
+
volume.distance[at] = ((volume.distance[at] as number) * held + clamped * weight) / total;
|
|
180
|
+
volume.weight[at] = total;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A normal per pixel, in the camera's frame, from the depth map's own gradient.
|
|
189
|
+
*
|
|
190
|
+
* **Central differences, and a pixel whose neighbours are not both surfaces gets the view's own
|
|
191
|
+
* axis instead.** At a silhouette the difference is across the gap between a near surface and a far
|
|
192
|
+
* one, which is not a gradient of anything; answering the axis there says "seen square on", which
|
|
193
|
+
* is generous rather than wrong, and the coverage is what actually discounts those pixels.
|
|
194
|
+
*/
|
|
195
|
+
function surfaceNormals(view: SurfaceView): Float64Array {
|
|
196
|
+
const { width, height, intrinsics } = view.camera;
|
|
197
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
198
|
+
const out = new Float64Array(width * height * 3);
|
|
199
|
+
const at = (x: number, y: number): readonly [number, number, number] | null => {
|
|
200
|
+
if (x < 0 || x >= width || y < 0 || y >= height) return null;
|
|
201
|
+
const z = view.depth[y * width + x] as number;
|
|
202
|
+
if (!(z > 0)) return null;
|
|
203
|
+
return [((x + 0.5 - cx) * z) / fx, ((y + 0.5 - cy) * z) / fy, z];
|
|
204
|
+
};
|
|
205
|
+
for (let y = 0; y < height; y += 1) {
|
|
206
|
+
for (let x = 0; x < width; x += 1) {
|
|
207
|
+
const pixel = y * width + x;
|
|
208
|
+
out[pixel * 3 + 2] = -1;
|
|
209
|
+
const left = at(x - 1, y);
|
|
210
|
+
const right = at(x + 1, y);
|
|
211
|
+
const up = at(x, y - 1);
|
|
212
|
+
const down = at(x, y + 1);
|
|
213
|
+
if (left === null || right === null || up === null || down === null) continue;
|
|
214
|
+
const ax = [right[0] - left[0], right[1] - left[1], right[2] - left[2]];
|
|
215
|
+
const ay = [down[0] - up[0], down[1] - up[1], down[2] - up[2]];
|
|
216
|
+
const nx = (ay[1] as number) * (ax[2] as number) - (ay[2] as number) * (ax[1] as number);
|
|
217
|
+
const ny = (ay[2] as number) * (ax[0] as number) - (ay[0] as number) * (ax[2] as number);
|
|
218
|
+
const nz = (ay[0] as number) * (ax[1] as number) - (ay[1] as number) * (ax[0] as number);
|
|
219
|
+
const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
|
|
220
|
+
if (!(length > 0)) continue;
|
|
221
|
+
/* Towards the camera, which is the negative z half of its own frame. */
|
|
222
|
+
const sign = nz > 0 ? -1 : 1;
|
|
223
|
+
out[pixel * 3] = (sign * nx) / length;
|
|
224
|
+
out[pixel * 3 + 1] = (sign * ny) / length;
|
|
225
|
+
out[pixel * 3 + 2] = (sign * nz) / length;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adam over a cloud's parameters, one moment pair per number.
|
|
3
|
+
*
|
|
4
|
+
* **Why Adam and not a plain descent.** The six families being fitted are in wildly different
|
|
5
|
+
* units — metres, logarithms of metres, a quaternion, a colour on nought to one — and a single
|
|
6
|
+
* step size that suits one of them moves another by nothing or by everything. Adam divides each
|
|
7
|
+
* number's step by the size of its own recent gradients, so a rate here is a *fraction of the
|
|
8
|
+
* parameter's own scale* and the families become comparable. It is also what makes the loss's
|
|
9
|
+
* absolute size irrelevant: a frame of a million pixels and one of a thousand descend alike.
|
|
10
|
+
*
|
|
11
|
+
* **The bias correction is a running product rather than a power.** `β₁ⁿ` would want `**`, which
|
|
12
|
+
* `packages/capture` may not use — the determinism gate refuses it, because an exponent is one of
|
|
13
|
+
* the operations a platform may round its own way. Multiplying by β each step is the same number
|
|
14
|
+
* by the same arithmetic the rest of the fit uses.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const BETA1 = 0.9;
|
|
18
|
+
const BETA2 = 0.999;
|
|
19
|
+
const EPSILON = 1e-8;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One optimised family: the values, their gradient, and Adam's two moments.
|
|
23
|
+
*
|
|
24
|
+
* Every array is allocated at the cloud's budget and used to `count * stride`, because the budget
|
|
25
|
+
* is the one thing the cloud may not exceed — so densification never reallocates and a plan can
|
|
26
|
+
* gather into the same arrays it reads.
|
|
27
|
+
*/
|
|
28
|
+
export interface Family {
|
|
29
|
+
/** Numbers per Gaussian. */
|
|
30
|
+
readonly stride: number;
|
|
31
|
+
/** The step size, as a fraction of what this family's own gradients have been. */
|
|
32
|
+
readonly rate: number;
|
|
33
|
+
readonly values: Float64Array;
|
|
34
|
+
readonly gradient: Float64Array;
|
|
35
|
+
readonly moment: Float64Array;
|
|
36
|
+
readonly second: Float64Array;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createFamily(stride: number, rate: number, budget: number): Family {
|
|
40
|
+
return {
|
|
41
|
+
stride,
|
|
42
|
+
rate,
|
|
43
|
+
values: new Float64Array(budget * stride),
|
|
44
|
+
gradient: new Float64Array(budget * stride),
|
|
45
|
+
moment: new Float64Array(budget * stride),
|
|
46
|
+
second: new Float64Array(budget * stride),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** How far into the decay the fit is: β₁ⁿ and β₂ⁿ, carried rather than raised. */
|
|
51
|
+
export interface AdamState {
|
|
52
|
+
unbias1: number;
|
|
53
|
+
unbias2: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createAdamState(): AdamState {
|
|
57
|
+
return { unbias1: 1, unbias2: 1 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One step over every family, on the gradients standing in them.
|
|
62
|
+
*
|
|
63
|
+
* **The gradient is read and not owned**: the caller writes every number of it before every step,
|
|
64
|
+
* so there is nothing here to clear. A caller that accumulated into it instead would need that
|
|
65
|
+
* clear back, and would be the one to add it.
|
|
66
|
+
*/
|
|
67
|
+
export function adamStep(families: readonly Family[], count: number, state: AdamState): void {
|
|
68
|
+
state.unbias1 *= BETA1;
|
|
69
|
+
state.unbias2 *= BETA2;
|
|
70
|
+
const correction1 = 1 - state.unbias1;
|
|
71
|
+
const correction2 = 1 - state.unbias2;
|
|
72
|
+
for (const family of families) {
|
|
73
|
+
const { values, gradient, moment, second, rate, stride } = family;
|
|
74
|
+
const end = count * stride;
|
|
75
|
+
for (let at = 0; at < end; at += 1) {
|
|
76
|
+
const g = gradient[at] as number;
|
|
77
|
+
const m = BETA1 * (moment[at] as number) + (1 - BETA1) * g;
|
|
78
|
+
const v = BETA2 * (second[at] as number) + (1 - BETA2) * g * g;
|
|
79
|
+
moment[at] = m;
|
|
80
|
+
second[at] = v;
|
|
81
|
+
values[at] =
|
|
82
|
+
(values[at] as number) -
|
|
83
|
+
(rate * (m / correction1)) / (Math.sqrt(v / correction2) + EPSILON);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Rewrite every family by a plan: slot `i` takes its values from `sources[i]`.
|
|
90
|
+
*
|
|
91
|
+
* **A slot marked fresh starts Adam over.** A Gaussian that was just split off its parent has none
|
|
92
|
+
* of that parent's history — its gradients are about to be different ones — and carrying the
|
|
93
|
+
* moments across would take a step sized for a shape that no longer exists. The reference
|
|
94
|
+
* implementation zeroes them for the same reason, and it is also what separates two halves of a
|
|
95
|
+
* split: identical values with identical moments receive identical steps forever.
|
|
96
|
+
*/
|
|
97
|
+
export function gather(
|
|
98
|
+
families: readonly Family[],
|
|
99
|
+
sources: Int32Array,
|
|
100
|
+
fresh: Uint8Array,
|
|
101
|
+
count: number,
|
|
102
|
+
scratch: Float64Array,
|
|
103
|
+
): void {
|
|
104
|
+
for (const family of families) {
|
|
105
|
+
gatherArray(family.values, family.stride, sources, fresh, count, scratch, false);
|
|
106
|
+
gatherArray(family.moment, family.stride, sources, fresh, count, scratch, true);
|
|
107
|
+
gatherArray(family.second, family.stride, sources, fresh, count, scratch, true);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One array gathered through `scratch`, because a slot may take from a slot below itself. */
|
|
112
|
+
function gatherArray(
|
|
113
|
+
array: Float64Array,
|
|
114
|
+
stride: number,
|
|
115
|
+
sources: Int32Array,
|
|
116
|
+
fresh: Uint8Array,
|
|
117
|
+
count: number,
|
|
118
|
+
scratch: Float64Array,
|
|
119
|
+
blankWhenFresh: boolean,
|
|
120
|
+
): void {
|
|
121
|
+
for (let slot = 0; slot < count; slot += 1) {
|
|
122
|
+
const from = (sources[slot] as number) * stride;
|
|
123
|
+
for (let c = 0; c < stride; c += 1) scratch[slot * stride + c] = array[from + c] as number;
|
|
124
|
+
}
|
|
125
|
+
for (let slot = 0; slot < count; slot += 1) {
|
|
126
|
+
const blank = blankWhenFresh && (fresh[slot] as number) !== 0;
|
|
127
|
+
for (let c = 0; c < stride; c += 1) {
|
|
128
|
+
array[slot * stride + c] = blank ? 0 : (scratch[slot * stride + c] as number);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How far away a cloud of Gaussians is, a pixel at a time.
|
|
3
|
+
*
|
|
4
|
+
* **A splat cloud has no surface**, so there is no depth to read off it the way a triangle
|
|
5
|
+
* rasteriser reads one. What it has is light arriving from a sequence of overlapping blobs, and the
|
|
6
|
+
* honest answer is where that light came from: each splat's own depth, weighted by how much of the
|
|
7
|
+
* pixel it actually contributed. A nearly opaque splat in front takes almost all of the weight, so
|
|
8
|
+
* the answer is its depth; a thin haze in front of a wall averages towards the wall.
|
|
9
|
+
*
|
|
10
|
+
* **The coverage is answered beside it, and callers need both.** At a silhouette a cloud thins out
|
|
11
|
+
* and a pixel may be a tenth covered — a depth averaged out of a tenth of a pixel's light is not a
|
|
12
|
+
* measurement of anything, and a fusion that treated it as one would carve the silhouette into the
|
|
13
|
+
* volume. So this answers how much light arrived as well, and the fusion weights by it rather than
|
|
14
|
+
* choosing a cutoff here on the caller's behalf.
|
|
15
|
+
*
|
|
16
|
+
* **Depth along the camera's own axis, not along the ray.** That is what a projection's `z` is and
|
|
17
|
+
* what `fuseDepth` compares a voxel against; a radial distance would be a different quantity with
|
|
18
|
+
* the same name, which is the kind of mistake that reads as a scene slightly the wrong shape.
|
|
19
|
+
*/
|
|
20
|
+
import { exactExp } from '@driftengine/core';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
visibleGaussians,
|
|
24
|
+
type GaussianSet,
|
|
25
|
+
type Projected,
|
|
26
|
+
type RasterCamera,
|
|
27
|
+
} from './project.ts';
|
|
28
|
+
|
|
29
|
+
/** Below this a Gaussian contributes less than a quarter of a value of an eight-bit channel. */
|
|
30
|
+
const FAINT = 1 / 255 / 4;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The cloud's depth and coverage, one of each per pixel.
|
|
34
|
+
*
|
|
35
|
+
* A pixel no light reached is left at zero in both, which is a coverage of nothing rather than a
|
|
36
|
+
* surface at the camera — a caller reading the depth alone would find a wall in front of its face.
|
|
37
|
+
*/
|
|
38
|
+
export function renderDepth(
|
|
39
|
+
set: GaussianSet,
|
|
40
|
+
camera: RasterCamera,
|
|
41
|
+
depth: Float32Array,
|
|
42
|
+
coverage: Float32Array,
|
|
43
|
+
): void {
|
|
44
|
+
const { width, height } = camera;
|
|
45
|
+
depth.fill(0);
|
|
46
|
+
coverage.fill(0);
|
|
47
|
+
const pixels = width * height;
|
|
48
|
+
const transmittance = new Float64Array(pixels).fill(1);
|
|
49
|
+
const weighted = new Float64Array(pixels);
|
|
50
|
+
const { order, projected } = visibleGaussians(set, camera);
|
|
51
|
+
|
|
52
|
+
for (const at of order) {
|
|
53
|
+
const one = projected[at] as Projected;
|
|
54
|
+
const determinant = one.a * one.d - one.b * one.b;
|
|
55
|
+
const inverse = [one.d / determinant, -one.b / determinant, one.a / determinant];
|
|
56
|
+
const left = Math.max(0, Math.floor(one.x - one.radius));
|
|
57
|
+
const right = Math.min(width - 1, Math.ceil(one.x + one.radius));
|
|
58
|
+
const top = Math.max(0, Math.floor(one.y - one.radius));
|
|
59
|
+
const bottom = Math.min(height - 1, Math.ceil(one.y + one.radius));
|
|
60
|
+
const opacity = set.opacities[at] as number;
|
|
61
|
+
for (let y = top; y <= bottom; y += 1) {
|
|
62
|
+
for (let x = left; x <= right; x += 1) {
|
|
63
|
+
const dx = x + 0.5 - one.x;
|
|
64
|
+
const dy = y + 0.5 - one.y;
|
|
65
|
+
const power =
|
|
66
|
+
-0.5 *
|
|
67
|
+
((inverse[0] as number) * dx * dx +
|
|
68
|
+
2 * (inverse[1] as number) * dx * dy +
|
|
69
|
+
(inverse[2] as number) * dy * dy);
|
|
70
|
+
const alpha = opacity * exactExp(power);
|
|
71
|
+
if (alpha < FAINT) continue;
|
|
72
|
+
const pixel = y * width + x;
|
|
73
|
+
const share = alpha * (transmittance[pixel] as number);
|
|
74
|
+
weighted[pixel] = (weighted[pixel] as number) + share * one.depth;
|
|
75
|
+
coverage[pixel] = (coverage[pixel] as number) + share;
|
|
76
|
+
transmittance[pixel] = (transmittance[pixel] as number) * (1 - alpha);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let pixel = 0; pixel < pixels; pixel += 1) {
|
|
82
|
+
const covered = coverage[pixel] as number;
|
|
83
|
+
/* Divided by the light that arrived rather than by one: a half-covered pixel is still at the
|
|
84
|
+
depth of what covered it, and the caller is told how much that was. */
|
|
85
|
+
if (covered > 0) depth[pixel] = (weighted[pixel] as number) / covered;
|
|
86
|
+
}
|
|
87
|
+
}
|