@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/dist/delight.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A captured surface's material, separated from the light it was photographed under.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is, said plainly
|
|
5
|
+
*
|
|
6
|
+
* **Inverse rendering is ill-posed and this does not pretend otherwise.** A photograph of a grey
|
|
7
|
+
* wall in shadow and a photograph of a dark wall in light are the same photograph. Nothing recovers
|
|
8
|
+
* the difference from pixels alone; what recovers it is an *assumption*, and the whole of the
|
|
9
|
+
* honesty in this file is in saying which assumptions and where they fail.
|
|
10
|
+
*
|
|
11
|
+
* **Shadow baked into albedo is the field's named failure.** A capture delit badly looks correct
|
|
12
|
+
* until it is relit — and then the old light is still painted on, a shadow follows an object that
|
|
13
|
+
* has moved, and the scene reads as flat and wrong in a way nobody can point at. Every measurement
|
|
14
|
+
* below is aimed at that one failure.
|
|
15
|
+
*
|
|
16
|
+
* **No learned model ships here, and the reason is licensing rather than quality.** The models that
|
|
17
|
+
* do this well are trained on datasets whose terms do not permit redistribution in a product, and
|
|
18
|
+
* a weight file a consumer cannot lawfully ship is worse than no weight file at all. So this is
|
|
19
|
+
* multi-view inverse rendering with a classical separation, and `tools/capture-prior/` is where a
|
|
20
|
+
* prior the project owns would be trained if one is wanted — on scenes DriftEngine itself renders,
|
|
21
|
+
* which is data nobody else has terms on.
|
|
22
|
+
*
|
|
23
|
+
* ## The two assumptions, and what each is worth
|
|
24
|
+
*
|
|
25
|
+
* **A Lambertian point looks the same from everywhere; a highlight does not.** That is what
|
|
26
|
+
* *multi-view* buys, and it buys exactly one thing: the view-dependent part. A point seen from six
|
|
27
|
+
* cameras gives six observations whose *median* rejects a highlight in any minority of them, and
|
|
28
|
+
* whose spread above that median is the specular energy — which is where the roughness estimate
|
|
29
|
+
* comes from. **What it cannot do is remove a cast shadow**, and that is worth stating because it
|
|
30
|
+
* is the assumption people expect it to carry: a shadowed point is shadowed from every camera, so
|
|
31
|
+
* every observation agrees, and agreement is not evidence of albedo.
|
|
32
|
+
*
|
|
33
|
+
* **A shadow changes intensity and not colour; a material change usually changes colour.** That is
|
|
34
|
+
* Retinex, and it is what separates a cast shadow from a dark patch of paint. Over the mesh's own
|
|
35
|
+
* edges, an edge where the intensity jumps and the chromaticity holds is called shading and the
|
|
36
|
+
* jump is given to the light; an edge where the chromaticity moves is called reflectance and the
|
|
37
|
+
* jump is given to the albedo. **Where it fails is a grey object on a grey floor** — no chromaticity
|
|
38
|
+
* to tell them apart — and that is what the confidence is for.
|
|
39
|
+
*
|
|
40
|
+
* ## What it achieves, measured
|
|
41
|
+
*
|
|
42
|
+
* On the synthetic scene in `delight.test.ts`: a cast shadow that makes one half of a floor **4.8
|
|
43
|
+
* times darker than the other in the photographs** comes back **2.2 times darker in the albedo** —
|
|
44
|
+
* half of it removed, in the logarithm. A matte floor's albedo lands within about a tenth of the
|
|
45
|
+
* truth, and the whole scene relit under a light it never saw is within a tenth of a channel of the
|
|
46
|
+
* renderer's own answer.
|
|
47
|
+
*
|
|
48
|
+
* **Half a shadow is a real result and not a finished one**, and the number is written here rather
|
|
49
|
+
* than rounded up because it is the number a learned prior would have to beat to be worth its
|
|
50
|
+
* weights. What is left is the part a chromaticity cannot see: where a shadow's edge falls inside a
|
|
51
|
+
* single material, the only thing separating shading from paint is a *prior about scenes*, and this
|
|
52
|
+
* has none.
|
|
53
|
+
*
|
|
54
|
+
* ## The confidence is the point, not a decoration
|
|
55
|
+
*
|
|
56
|
+
* Every surface here gets a number saying how much of this to believe, and the test that makes it
|
|
57
|
+
* mean something **correlates it against the actual error**. A channel that does not track the error
|
|
58
|
+
* is worse than none, because a consumer trusts it. Low confidence is the honest answer for glass,
|
|
59
|
+
* for a surface one camera saw, and for a grey object on a grey floor — and a pipeline that knows
|
|
60
|
+
* which surfaces those are can leave them alone rather than delighting them wrongly.
|
|
61
|
+
*/
|
|
62
|
+
import { exactExp, exactLog } from '@driftengine/core';
|
|
63
|
+
import { cameraCentre, nearestHit } from './delightScene.js';
|
|
64
|
+
const DEFAULT_MINIMUM_VIEWS = 2;
|
|
65
|
+
const DEFAULT_CHROMATICITY_STEP = 0.05;
|
|
66
|
+
const DEFAULT_ITERATIONS = 120;
|
|
67
|
+
/** Where in the sorted shading the scene's "fully lit" is taken to be. */
|
|
68
|
+
const ANCHOR_PERCENTILE = 0.9;
|
|
69
|
+
/** Views past which another one adds little: four sample a hemisphere well enough to reject by. */
|
|
70
|
+
const ENOUGH_VIEWS = 4;
|
|
71
|
+
export function createDelightOut(vertices) {
|
|
72
|
+
return {
|
|
73
|
+
albedo: new Float32Array(vertices * 3),
|
|
74
|
+
roughness: new Float32Array(vertices),
|
|
75
|
+
normal: new Float32Array(vertices * 3),
|
|
76
|
+
confidence: new Float32Array(vertices),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** `mesh`'s material, as seen in `views`. */
|
|
80
|
+
export function delight(views, mesh, options, out) {
|
|
81
|
+
const vertices = mesh.positions.length / 3;
|
|
82
|
+
const minimumViews = options.minimumViews ?? DEFAULT_MINIMUM_VIEWS;
|
|
83
|
+
const step = options.chromaticityStep ?? DEFAULT_CHROMATICITY_STEP;
|
|
84
|
+
const iterations = options.iterations ?? DEFAULT_ITERATIONS;
|
|
85
|
+
out.normal.set(mesh.normals.subarray(0, vertices * 3));
|
|
86
|
+
/* What each view saw of each vertex, where it saw it at all. */
|
|
87
|
+
const observed = new Float64Array(vertices * 3);
|
|
88
|
+
const seen = new Int32Array(vertices);
|
|
89
|
+
const spread = new Float64Array(vertices);
|
|
90
|
+
gather(views, mesh, observed, seen, spread);
|
|
91
|
+
/*
|
|
92
|
+
* The intensity a point returned and the colour it returned it in, kept apart. Retinex works on
|
|
93
|
+
* the first and leaves the second alone, because a shadow is the one and a material is the other.
|
|
94
|
+
*/
|
|
95
|
+
const logObserved = new Float64Array(vertices * 3);
|
|
96
|
+
const chroma = new Float64Array(vertices * 3);
|
|
97
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
98
|
+
let total = 0;
|
|
99
|
+
for (let c = 0; c < 3; c += 1)
|
|
100
|
+
total += observed[vertex * 3 + c];
|
|
101
|
+
for (let c = 0; c < 3; c += 1) {
|
|
102
|
+
logObserved[vertex * 3 + c] = exactLog(Math.max(observed[vertex * 3 + c], 1e-4));
|
|
103
|
+
chroma[vertex * 3 + c] = observed[vertex * 3 + c] / Math.max(total, 1e-6);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/*
|
|
107
|
+
* **One solve per channel, sharing one classification.** An intensity-only separation cannot tell
|
|
108
|
+
* a warm key light from a neutral ambient: a surface in shadow is lit by one and out of it by the
|
|
109
|
+
* other, so its recovered *colour* carries the difference even when its brightness does not.
|
|
110
|
+
* Measured on the synthetic scene, that alone was most of what was left — the shadowed and lit
|
|
111
|
+
* halves of one floor came back 35% apart. The decision about which edges are shading is still
|
|
112
|
+
* made once, on chromaticity, because that is what a shadow does not change.
|
|
113
|
+
*/
|
|
114
|
+
const channelShading = [];
|
|
115
|
+
const channel = new Float64Array(vertices);
|
|
116
|
+
for (let c = 0; c < 3; c += 1) {
|
|
117
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
118
|
+
channel[vertex] = logObserved[vertex * 3 + c];
|
|
119
|
+
}
|
|
120
|
+
channelShading.push(solveShading(mesh, channel, chroma, step, iterations, seen, minimumViews));
|
|
121
|
+
}
|
|
122
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
123
|
+
/* What is left of each channel once its own light is taken out. */
|
|
124
|
+
for (let c = 0; c < 3; c += 1) {
|
|
125
|
+
const shading = channelShading[c][vertex];
|
|
126
|
+
const value = exactExp(logObserved[vertex * 3 + c] - shading);
|
|
127
|
+
out.albedo[vertex * 3 + c] = value > 1 ? 1 : value;
|
|
128
|
+
}
|
|
129
|
+
/*
|
|
130
|
+
* **The spread above the median is specular energy, and a wider lobe means a rougher surface.**
|
|
131
|
+
* A mirror puts all of it in one view and a chalk surface in none, so the roughness falls as
|
|
132
|
+
* the disagreement rises. Coarse by construction: six views sample a lobe badly, and a number
|
|
133
|
+
* that says "rough" or "polished" is what a capture can honestly carry.
|
|
134
|
+
*/
|
|
135
|
+
const excess = spread[vertex];
|
|
136
|
+
out.roughness[vertex] = Math.min(1, Math.max(0.05, 1 - Math.min(1, excess * 4)));
|
|
137
|
+
out.confidence[vertex] = confidenceAt(seen[vertex], minimumViews, excess, views.length);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Every view's opinion of every vertex: the median colour, how many saw it, and their disagreement.
|
|
142
|
+
*
|
|
143
|
+
* **Visibility is a ray back to the camera against the mesh itself.** A vertex behind a wall is one
|
|
144
|
+
* the camera never saw, and reading the pixel it projects to reads the wall's colour as the
|
|
145
|
+
* surface's — which is not a subtle error, it is another object's paint.
|
|
146
|
+
*/
|
|
147
|
+
function gather(views, mesh, observed, seen, spread) {
|
|
148
|
+
const vertices = mesh.positions.length / 3;
|
|
149
|
+
const samples = [];
|
|
150
|
+
for (let vertex = 0; vertex < vertices; vertex += 1)
|
|
151
|
+
samples.push([]);
|
|
152
|
+
for (const view of views) {
|
|
153
|
+
const { width, height, intrinsics } = view.camera;
|
|
154
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
155
|
+
const m = view.camera.worldToCamera;
|
|
156
|
+
const eye = cameraCentre(m);
|
|
157
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
158
|
+
const point = [
|
|
159
|
+
mesh.positions[vertex * 3],
|
|
160
|
+
mesh.positions[vertex * 3 + 1],
|
|
161
|
+
mesh.positions[vertex * 3 + 2],
|
|
162
|
+
];
|
|
163
|
+
/* Facing away from the camera is not seen, whatever the depth says. */
|
|
164
|
+
let facing = 0;
|
|
165
|
+
for (let k = 0; k < 3; k += 1) {
|
|
166
|
+
facing += mesh.normals[vertex * 3 + k] * (eye[k] - point[k]);
|
|
167
|
+
}
|
|
168
|
+
if (!(facing > 0))
|
|
169
|
+
continue;
|
|
170
|
+
let viewZ = m[11];
|
|
171
|
+
let viewX = m[3];
|
|
172
|
+
let viewY = m[7];
|
|
173
|
+
for (let k = 0; k < 3; k += 1) {
|
|
174
|
+
viewX += m[k] * point[k];
|
|
175
|
+
viewY += m[4 + k] * point[k];
|
|
176
|
+
viewZ += m[8 + k] * point[k];
|
|
177
|
+
}
|
|
178
|
+
if (!(viewZ > 0))
|
|
179
|
+
continue;
|
|
180
|
+
const px = Math.floor((fx * viewX) / viewZ + cx);
|
|
181
|
+
const py = Math.floor((fy * viewY) / viewZ + cy);
|
|
182
|
+
if (px < 0 || px >= width || py < 0 || py >= height)
|
|
183
|
+
continue;
|
|
184
|
+
/* Occluded by the surface itself: the ray to the camera meets something first. */
|
|
185
|
+
const toEye = [
|
|
186
|
+
eye[0] - point[0],
|
|
187
|
+
eye[1] - point[1],
|
|
188
|
+
eye[2] - point[2],
|
|
189
|
+
];
|
|
190
|
+
const distance = Math.sqrt(toEye[0] * toEye[0] + toEye[1] * toEye[1] + toEye[2] * toEye[2]);
|
|
191
|
+
const direction = [
|
|
192
|
+
toEye[0] / distance,
|
|
193
|
+
toEye[1] / distance,
|
|
194
|
+
toEye[2] / distance,
|
|
195
|
+
];
|
|
196
|
+
if (nearestHit(mesh, point, direction, 1e-3, distance - 1e-3) !== null)
|
|
197
|
+
continue;
|
|
198
|
+
const pixel = (py * width + px) * 4;
|
|
199
|
+
samples[vertex].push(view.frame[pixel], view.frame[pixel + 1], view.frame[pixel + 2]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
203
|
+
const held = samples[vertex];
|
|
204
|
+
const count = held.length / 3;
|
|
205
|
+
seen[vertex] = count;
|
|
206
|
+
if (count === 0)
|
|
207
|
+
continue;
|
|
208
|
+
/* The median a channel at a time: a highlight in a minority of views is rejected by it. */
|
|
209
|
+
for (let c = 0; c < 3; c += 1) {
|
|
210
|
+
const column = [];
|
|
211
|
+
for (let at = 0; at < count; at += 1)
|
|
212
|
+
column.push(held[at * 3 + c]);
|
|
213
|
+
column.sort((a, b) => a - b);
|
|
214
|
+
observed[vertex * 3 + c] = column[Math.floor((count - 1) / 2)];
|
|
215
|
+
}
|
|
216
|
+
/*
|
|
217
|
+
* **How far the views stand from their own median, in either direction.** A view brighter than
|
|
218
|
+
* the rest is a highlight and one darker is an occlusion or an inter-reflection, and both say
|
|
219
|
+
* the same thing about the answer: the median is standing in for observations that did not
|
|
220
|
+
* agree. The mean of those distances and the worst of them were both measured here and are
|
|
221
|
+
* interchangeable on these scenes, so it is the mean — the plainer statistic, and the one that
|
|
222
|
+
* does not turn on a single camera.
|
|
223
|
+
*/
|
|
224
|
+
let apart = 0;
|
|
225
|
+
for (let at = 0; at < count; at += 1) {
|
|
226
|
+
for (let c = 0; c < 3; c += 1) {
|
|
227
|
+
apart += Math.abs(held[at * 3 + c] - observed[vertex * 3 + c]);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
spread[vertex] = apart / (count * 3);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* The light, as a field over the mesh that explains every intensity jump a colour jump does not.
|
|
235
|
+
*
|
|
236
|
+
* A sweep at a time: each vertex takes the average of what its neighbours imply, where a shading
|
|
237
|
+
* edge implies the neighbour's shading plus the observed jump and a reflectance edge implies the
|
|
238
|
+
* neighbour's shading unchanged.
|
|
239
|
+
*
|
|
240
|
+
* **The constant is assumed rather than solved for, and which assumption matters.** Nothing in a
|
|
241
|
+
* photograph says whether the light was bright and the paint dark or the other way round, so one
|
|
242
|
+
* end has to be fixed. The textbook choice is *the brightest surface is white*, and it is wrong for
|
|
243
|
+
* a capture: a room of brown floor and blue wall has no white in it, and the recovery comes back
|
|
244
|
+
* about sixty per cent too bright — measured on the synthetic scene, which then failed its
|
|
245
|
+
* relighting gate by a third of a channel. What is assumed here instead is **the brightest
|
|
246
|
+
* illumination in the scene is full illumination**, which is a statement about the light rather
|
|
247
|
+
* than about the paint, and is true of any scene with a surface facing the light.
|
|
248
|
+
*/
|
|
249
|
+
function solveShading(mesh, logIntensity, chroma, step, iterations, seen, minimumViews) {
|
|
250
|
+
const vertices = logIntensity.length;
|
|
251
|
+
const neighbours = adjacency(mesh, vertices);
|
|
252
|
+
const shading = new Float64Array(vertices);
|
|
253
|
+
const next = new Float64Array(vertices);
|
|
254
|
+
for (let sweep = 0; sweep < iterations; sweep += 1) {
|
|
255
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
256
|
+
const around = neighbours[vertex];
|
|
257
|
+
if (around.length === 0) {
|
|
258
|
+
next[vertex] = shading[vertex];
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
let total = 0;
|
|
262
|
+
for (const other of around) {
|
|
263
|
+
let apart = 0;
|
|
264
|
+
for (let c = 0; c < 3; c += 1) {
|
|
265
|
+
apart += Math.abs(chroma[vertex * 3 + c] - chroma[other * 3 + c]);
|
|
266
|
+
}
|
|
267
|
+
const materialChange = apart > step;
|
|
268
|
+
const jump = materialChange
|
|
269
|
+
? 0
|
|
270
|
+
: logIntensity[vertex] - logIntensity[other];
|
|
271
|
+
total += shading[other] + jump;
|
|
272
|
+
}
|
|
273
|
+
next[vertex] = total / around.length;
|
|
274
|
+
}
|
|
275
|
+
shading.set(next);
|
|
276
|
+
}
|
|
277
|
+
/*
|
|
278
|
+
* The most-lit surface anybody saw properly is taken as fully lit, which fixes the constant.
|
|
279
|
+
*
|
|
280
|
+
* **At a high percentile rather than at the maximum**, and that choice is the load-bearing part:
|
|
281
|
+
* one vertex is enough to set a maximum, and the one that sets it is whichever caught a highlight
|
|
282
|
+
* the views could not average away — the whole scene is then scaled against that vertex.
|
|
283
|
+
* Measured on the synthetic scene, a block's four-vertex top did exactly that and left a matte
|
|
284
|
+
* floor half again too bright.
|
|
285
|
+
*
|
|
286
|
+
* **The shift itself is usually small**, because the solve starts from zero everywhere and its
|
|
287
|
+
* iteration has no absolute term, so it settles around a shading of nought without being told to.
|
|
288
|
+
* That is an accident of the method rather than a decision, which is why the constant is set
|
|
289
|
+
* here explicitly instead of being left to it.
|
|
290
|
+
*/
|
|
291
|
+
const levels = [];
|
|
292
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
293
|
+
if (seen[vertex] < minimumViews)
|
|
294
|
+
continue;
|
|
295
|
+
levels.push(shading[vertex]);
|
|
296
|
+
}
|
|
297
|
+
if (levels.length > 0) {
|
|
298
|
+
levels.sort((a, b) => a - b);
|
|
299
|
+
const brightest = levels[Math.floor((levels.length - 1) * ANCHOR_PERCENTILE)];
|
|
300
|
+
for (let vertex = 0; vertex < vertices; vertex += 1) {
|
|
301
|
+
shading[vertex] = shading[vertex] - brightest;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return shading;
|
|
305
|
+
}
|
|
306
|
+
/** Which vertices share an edge with which, by position so a split seam is still one surface. */
|
|
307
|
+
function adjacency(mesh, vertices) {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (let vertex = 0; vertex < vertices; vertex += 1)
|
|
310
|
+
out.push([]);
|
|
311
|
+
const triangles = mesh.indices.length / 3;
|
|
312
|
+
const seen = new Set();
|
|
313
|
+
for (let face = 0; face < triangles; face += 1) {
|
|
314
|
+
for (let slot = 0; slot < 3; slot += 1) {
|
|
315
|
+
const a = mesh.indices[face * 3 + slot];
|
|
316
|
+
const b = mesh.indices[face * 3 + ((slot + 1) % 3)];
|
|
317
|
+
if (a === b)
|
|
318
|
+
continue;
|
|
319
|
+
const key = a < b ? a * vertices + b : b * vertices + a;
|
|
320
|
+
if (seen.has(key))
|
|
321
|
+
continue;
|
|
322
|
+
seen.add(key);
|
|
323
|
+
out[a].push(b);
|
|
324
|
+
out[b].push(a);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return out;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* How much of this vertex to believe.
|
|
331
|
+
*
|
|
332
|
+
* Three things make an answer weak and they are multiplied rather than averaged, because any one of
|
|
333
|
+
* them alone is enough to make the answer wrong: too few views to reject anything, a surface whose
|
|
334
|
+
* views disagree wildly — glass, a mirror, water — and a surface nobody saw at all.
|
|
335
|
+
*/
|
|
336
|
+
function confidenceAt(seen, minimumViews, excess, views) {
|
|
337
|
+
if (seen === 0)
|
|
338
|
+
return 0;
|
|
339
|
+
if (seen < minimumViews)
|
|
340
|
+
return 0.1;
|
|
341
|
+
/*
|
|
342
|
+
* Every view that saw it is evidence, with diminishing returns past a handful — **counted
|
|
343
|
+
* absolutely, not against how many cameras there happened to be**. Dividing by the number
|
|
344
|
+
* supplied made two views of two look exactly as good as seven of seven, which is the opposite
|
|
345
|
+
* of what the number is for: a point two cameras saw is one where nothing could be rejected.
|
|
346
|
+
*/
|
|
347
|
+
const coverage = Math.min(1, seen / ENOUGH_VIEWS);
|
|
348
|
+
/* And disagreement across views is the tell for a surface that is not diffuse at all. */
|
|
349
|
+
const agreement = 1 / (1 + excess * 12);
|
|
350
|
+
return Math.max(0, Math.min(1, coverage * agreement));
|
|
351
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MeshData } from '@driftengine/drft';
|
|
2
|
+
import type { RasterCamera } from './gaussians/project.ts';
|
|
3
|
+
export interface LitScene {
|
|
4
|
+
readonly mesh: MeshData;
|
|
5
|
+
/** Three per vertex, linear: what the recovery is measured against. */
|
|
6
|
+
readonly albedo: Float32Array;
|
|
7
|
+
/** One per vertex: the same. */
|
|
8
|
+
readonly roughness: Float32Array;
|
|
9
|
+
}
|
|
10
|
+
export interface Light {
|
|
11
|
+
/** Towards the light, unit length. */
|
|
12
|
+
readonly direction: readonly [number, number, number];
|
|
13
|
+
readonly colour: readonly [number, number, number];
|
|
14
|
+
/** What arrives from everywhere else, so a shadowed point is dark rather than black. */
|
|
15
|
+
readonly ambient: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* `scene` seen from `camera`, into `out` as linear premultiplied RGBA.
|
|
19
|
+
*
|
|
20
|
+
* Each pixel is one ray: the nearest triangle, its interpolated normal and albedo, Lambert against
|
|
21
|
+
* the light, a shadow ray back to it, and a Blinn–Phong lobe whose width is the roughness. What a
|
|
22
|
+
* real capture would have instead of the shadow ray is a shadow in the photograph; what matters for
|
|
23
|
+
* the test is that the shadow is *there* and that the caller knows where.
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderLit(scene: LitScene, camera: RasterCamera, light: Light, out: Float32Array): void;
|
|
26
|
+
/** Where a camera stands, from its 3 × 4 world-to-camera. */
|
|
27
|
+
export declare function cameraCentre(worldToCamera: Float64Array): [number, number, number];
|
|
28
|
+
export interface MeshHit {
|
|
29
|
+
readonly triangle: number;
|
|
30
|
+
readonly distance: number;
|
|
31
|
+
/** Barycentric weights of the three corners, in order. */
|
|
32
|
+
readonly weights: readonly [number, number, number];
|
|
33
|
+
}
|
|
34
|
+
/** The nearest triangle a ray meets, by Möller–Trumbore over every one of them. */
|
|
35
|
+
export declare function nearestHit(mesh: MeshData, origin: readonly [number, number, number], direction: readonly [number, number, number], near: number, far: number): MeshHit | null;
|
|
36
|
+
/** A per-vertex attribute at a hit, by its barycentric weights. */
|
|
37
|
+
export declare function interpolate(mesh: MeshData, values: ArrayLike<number>, hit: MeshHit, stride: number): number[];
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lit scene with the answer known: what a delighting test needs and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* **A fixture, not a renderer.** `testScene.ts` ray casts analytic boxes and planes for the
|
|
5
|
+
* geometry stages; this renders a *triangle mesh* with a light in it, because delighting is asked
|
|
6
|
+
* about a mesh and about shading — a cast shadow, a highlight, and an albedo that the caller knows
|
|
7
|
+
* because it put it there. It is deliberately the simplest thing that can pose the question: one
|
|
8
|
+
* directional light, Lambert plus a Blinn–Phong lobe, and a shadow by testing the ray to the light
|
|
9
|
+
* against the same triangles.
|
|
10
|
+
*
|
|
11
|
+
* **It lives beside the code rather than in the test file** because three tests need it and because
|
|
12
|
+
* the *relighting* gate needs to render the same scene under a second light — which is the one
|
|
13
|
+
* measurement that says whether a recovered material is a material or a picture of one.
|
|
14
|
+
*/
|
|
15
|
+
import { exactExp, exactLog } from '@driftengine/core';
|
|
16
|
+
/**
|
|
17
|
+
* `scene` seen from `camera`, into `out` as linear premultiplied RGBA.
|
|
18
|
+
*
|
|
19
|
+
* Each pixel is one ray: the nearest triangle, its interpolated normal and albedo, Lambert against
|
|
20
|
+
* the light, a shadow ray back to it, and a Blinn–Phong lobe whose width is the roughness. What a
|
|
21
|
+
* real capture would have instead of the shadow ray is a shadow in the photograph; what matters for
|
|
22
|
+
* the test is that the shadow is *there* and that the caller knows where.
|
|
23
|
+
*/
|
|
24
|
+
export function renderLit(scene, camera, light, out) {
|
|
25
|
+
const { width, height, intrinsics } = camera;
|
|
26
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
27
|
+
const m = camera.worldToCamera;
|
|
28
|
+
const eye = cameraCentre(m);
|
|
29
|
+
out.fill(0);
|
|
30
|
+
for (let y = 0; y < height; y += 1) {
|
|
31
|
+
for (let x = 0; x < width; x += 1) {
|
|
32
|
+
const ahead = toWorld(m, [(x + 0.5 - cx) / fx, (y + 0.5 - cy) / fy, 1]);
|
|
33
|
+
const direction = normalise([
|
|
34
|
+
ahead[0] - eye[0],
|
|
35
|
+
ahead[1] - eye[1],
|
|
36
|
+
ahead[2] - eye[2],
|
|
37
|
+
]);
|
|
38
|
+
const hit = nearestHit(scene.mesh, eye, direction, 1e-4, Infinity);
|
|
39
|
+
const pixel = (y * width + x) * 4;
|
|
40
|
+
if (hit === null) {
|
|
41
|
+
out[pixel + 3] = 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const point = [0, 1, 2].map((k) => eye[k] + hit.distance * direction[k]);
|
|
45
|
+
const normal = interpolate(scene.mesh, scene.mesh.normals, hit, 3);
|
|
46
|
+
const albedo = interpolate(scene.mesh, scene.albedo, hit, 3);
|
|
47
|
+
const rough = interpolate(scene.mesh, scene.roughness, hit, 1)[0];
|
|
48
|
+
const facing = dot(normal, light.direction);
|
|
49
|
+
/* A ray back to the light, against the same triangles: a hard shadow with a hard edge. */
|
|
50
|
+
const lit = facing > 0 &&
|
|
51
|
+
nearestHit(scene.mesh, point, light.direction, 1e-3, 40) ===
|
|
52
|
+
null;
|
|
53
|
+
const toEye = normalise([
|
|
54
|
+
eye[0] - point[0],
|
|
55
|
+
eye[1] - point[1],
|
|
56
|
+
eye[2] - point[2],
|
|
57
|
+
]);
|
|
58
|
+
const half = normalise([
|
|
59
|
+
toEye[0] + light.direction[0],
|
|
60
|
+
toEye[1] + light.direction[1],
|
|
61
|
+
toEye[2] + light.direction[2],
|
|
62
|
+
]);
|
|
63
|
+
/*
|
|
64
|
+
* **A Blinn–Phong lobe that widens *and weakens* as the surface roughens.** The exponent
|
|
65
|
+
* alone is not enough: with a strength that does not fall, a rough surface comes out with an
|
|
66
|
+
* enormous broad highlight over the whole of it, which is not a surface anybody has seen. It
|
|
67
|
+
* also made this fixture's floor 1.77 times too bright through a delighting that was working
|
|
68
|
+
* correctly — the light really was mostly specular, so the recovery was right about a scene
|
|
69
|
+
* nobody would build.
|
|
70
|
+
*/
|
|
71
|
+
const square = rough * rough;
|
|
72
|
+
const shininess = 2 / Math.max(1e-4, square * square) - 2;
|
|
73
|
+
/*
|
|
74
|
+
* **Normalised, which is the difference between a rough surface and a bright one.** A lobe
|
|
75
|
+
* that widens without losing energy puts a broad sheen over the whole of a rough surface,
|
|
76
|
+
* and it is not a small effect: measured here, the dark channels of a matte blue floor came
|
|
77
|
+
* back nearly twice as bright through a delighting that was working correctly. The peak of a
|
|
78
|
+
* Blinn–Phong lobe scales as (n + 2) / 8π, and 0.04 is a dielectric's reflectance head-on.
|
|
79
|
+
*/
|
|
80
|
+
const strength = (0.04 * (shininess + 2)) / (8 * Math.PI);
|
|
81
|
+
/*
|
|
82
|
+
* The power by the engine's own reproducible pair rather than `Math.pow`, which the
|
|
83
|
+
* determinism gate refuses — two engines may differ by a unit in the last place, and a
|
|
84
|
+
* fixture that renders differently on two machines is a test nobody can compare.
|
|
85
|
+
*/
|
|
86
|
+
const facingHalf = Math.max(0, dot(normal, half));
|
|
87
|
+
const lobe = facingHalf > 0 ? exactExp(shininess * exactLog(facingHalf)) : 0;
|
|
88
|
+
const specular = lit ? lobe * strength : 0;
|
|
89
|
+
for (let c = 0; c < 3; c += 1) {
|
|
90
|
+
const irradiance = (lit ? facing : 0) * light.colour[c] + light.ambient;
|
|
91
|
+
out[pixel + c] =
|
|
92
|
+
albedo[c] * irradiance + specular * light.colour[c];
|
|
93
|
+
}
|
|
94
|
+
out[pixel + 3] = 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Where a camera stands, from its 3 × 4 world-to-camera. */
|
|
99
|
+
export function cameraCentre(worldToCamera) {
|
|
100
|
+
const out = [0, 0, 0];
|
|
101
|
+
for (let r = 0; r < 3; r += 1) {
|
|
102
|
+
for (let c = 0; c < 3; c += 1) {
|
|
103
|
+
out[c] -= worldToCamera[r * 4 + c] * worldToCamera[r * 4 + 3];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** A camera-space point in the world: the projection run backwards. */
|
|
109
|
+
function toWorld(m, view) {
|
|
110
|
+
const out = [0, 0, 0];
|
|
111
|
+
for (let r = 0; r < 3; r += 1) {
|
|
112
|
+
for (let c = 0; c < 3; c += 1) {
|
|
113
|
+
out[c] += m[r * 4 + c] * (view[r] - m[r * 4 + 3]);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
/** The nearest triangle a ray meets, by Möller–Trumbore over every one of them. */
|
|
119
|
+
export function nearestHit(mesh, origin, direction, near, far) {
|
|
120
|
+
let best = null;
|
|
121
|
+
const triangles = mesh.indices.length / 3;
|
|
122
|
+
for (let face = 0; face < triangles; face += 1) {
|
|
123
|
+
const a = mesh.indices[face * 3];
|
|
124
|
+
const b = mesh.indices[face * 3 + 1];
|
|
125
|
+
const c = mesh.indices[face * 3 + 2];
|
|
126
|
+
const e1 = corner(mesh, b, a);
|
|
127
|
+
const e2 = corner(mesh, c, a);
|
|
128
|
+
const p = cross(direction, e2);
|
|
129
|
+
const determinant = dot(e1, p);
|
|
130
|
+
if (Math.abs(determinant) < 1e-12)
|
|
131
|
+
continue;
|
|
132
|
+
const inverse = 1 / determinant;
|
|
133
|
+
const t = [0, 1, 2].map((k) => origin[k] - mesh.positions[a * 3 + k]);
|
|
134
|
+
const u = dot(t, p) * inverse;
|
|
135
|
+
if (u < 0 || u > 1)
|
|
136
|
+
continue;
|
|
137
|
+
const q = cross(t, e1);
|
|
138
|
+
const v = dot(direction, q) * inverse;
|
|
139
|
+
if (v < 0 || u + v > 1)
|
|
140
|
+
continue;
|
|
141
|
+
const distance = dot(e2, q) * inverse;
|
|
142
|
+
if (distance < near || distance > far)
|
|
143
|
+
continue;
|
|
144
|
+
if (best !== null && distance >= best.distance)
|
|
145
|
+
continue;
|
|
146
|
+
best = { triangle: face, distance, weights: [1 - u - v, u, v] };
|
|
147
|
+
}
|
|
148
|
+
return best;
|
|
149
|
+
}
|
|
150
|
+
/** A per-vertex attribute at a hit, by its barycentric weights. */
|
|
151
|
+
export function interpolate(mesh, values, hit, stride) {
|
|
152
|
+
const out = new Array(stride).fill(0);
|
|
153
|
+
for (let corner = 0; corner < 3; corner += 1) {
|
|
154
|
+
const vertex = mesh.indices[hit.triangle * 3 + corner];
|
|
155
|
+
const weight = hit.weights[corner];
|
|
156
|
+
for (let k = 0; k < stride; k += 1) {
|
|
157
|
+
out[k] = out[k] + weight * values[vertex * stride + k];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
function corner(mesh, to, from) {
|
|
163
|
+
return [0, 1, 2].map((k) => mesh.positions[to * 3 + k] - mesh.positions[from * 3 + k]);
|
|
164
|
+
}
|
|
165
|
+
function cross(a, b) {
|
|
166
|
+
return [
|
|
167
|
+
a[1] * b[2] - a[2] * b[1],
|
|
168
|
+
a[2] * b[0] - a[0] * b[2],
|
|
169
|
+
a[0] * b[1] - a[1] * b[0],
|
|
170
|
+
];
|
|
171
|
+
}
|
|
172
|
+
function dot(a, b) {
|
|
173
|
+
return (a[0] * b[0] +
|
|
174
|
+
a[1] * b[1] +
|
|
175
|
+
a[2] * b[2]);
|
|
176
|
+
}
|
|
177
|
+
function normalise(v) {
|
|
178
|
+
const length = Math.sqrt(v[0] * v[0] +
|
|
179
|
+
v[1] * v[1] +
|
|
180
|
+
v[2] * v[2]);
|
|
181
|
+
const scale = length > 0 ? 1 / length : 0;
|
|
182
|
+
return [v[0] * scale, v[1] * scale, v[2] * scale];
|
|
183
|
+
}
|
package/dist/depth.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depth, its confidence and a camera per view, from a clip's frames rather than from tensors.
|
|
3
|
+
*
|
|
4
|
+
* **The frames arrive as a host hands them over** — RGBA bytes at whatever size the clip is — and
|
|
5
|
+
* this prepares them as Depth Anything 3's own processor does, runs the views together so the
|
|
6
|
+
* model's across-view blocks see each other, and decodes what comes back. A graph bakes its shapes,
|
|
7
|
+
* so one is built per count of views and prepared size and kept: a clip's frames are all the same
|
|
8
|
+
* size, so the second estimate over the same clip builds nothing.
|
|
9
|
+
*
|
|
10
|
+
* **It answers with its own arrays rather than filling the caller's**, which the engine's hot paths
|
|
11
|
+
* never do: the prepared size is not known until the frames are prepared, so a caller cannot have
|
|
12
|
+
* sized anything, and this runs once a clip rather than once a frame.
|
|
13
|
+
*/
|
|
14
|
+
import { type WeightSource } from '@driftengine/texture';
|
|
15
|
+
import { type DepthAnything3Config } from './models/depthAnything3.ts';
|
|
16
|
+
import type { GraphRun } from './run.ts';
|
|
17
|
+
/** A frame as a host hands it over: RGBA bytes, and the size they are. */
|
|
18
|
+
export interface RawFrame {
|
|
19
|
+
readonly pixels: Uint8Array;
|
|
20
|
+
readonly width: number;
|
|
21
|
+
readonly height: number;
|
|
22
|
+
}
|
|
23
|
+
export interface DepthView {
|
|
24
|
+
/** `[height, width]` metres, and one confidence each. */
|
|
25
|
+
readonly depth: Float32Array;
|
|
26
|
+
readonly confidence: Float32Array;
|
|
27
|
+
/** 3 × 4 row-major, world to camera. */
|
|
28
|
+
readonly worldToCamera: Float32Array;
|
|
29
|
+
/** 3 × 3 row-major. */
|
|
30
|
+
readonly intrinsics: Float32Array;
|
|
31
|
+
}
|
|
32
|
+
export interface DepthEstimate {
|
|
33
|
+
/** The size the frames were prepared at, which the maps and the cameras are in. */
|
|
34
|
+
readonly width: number;
|
|
35
|
+
readonly height: number;
|
|
36
|
+
readonly views: readonly DepthView[];
|
|
37
|
+
}
|
|
38
|
+
export interface DepthEstimator {
|
|
39
|
+
estimate(frames: readonly RawFrame[]): Promise<DepthEstimate>;
|
|
40
|
+
}
|
|
41
|
+
/** An estimator over one model's weights, running its graphs through `run`. */
|
|
42
|
+
export declare function createDepthEstimator(config: DepthAnything3Config, weights: WeightSource, run: GraphRun, options?: {
|
|
43
|
+
readonly size?: number;
|
|
44
|
+
}): DepthEstimator;
|
package/dist/depth.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Depth, its confidence and a camera per view, from a clip's frames rather than from tensors.
|
|
3
|
+
*
|
|
4
|
+
* **The frames arrive as a host hands them over** — RGBA bytes at whatever size the clip is — and
|
|
5
|
+
* this prepares them as Depth Anything 3's own processor does, runs the views together so the
|
|
6
|
+
* model's across-view blocks see each other, and decodes what comes back. A graph bakes its shapes,
|
|
7
|
+
* so one is built per count of views and prepared size and kept: a clip's frames are all the same
|
|
8
|
+
* size, so the second estimate over the same clip builds nothing.
|
|
9
|
+
*
|
|
10
|
+
* **It answers with its own arrays rather than filling the caller's**, which the engine's hot paths
|
|
11
|
+
* never do: the prepared size is not known until the frames are prepared, so a caller cannot have
|
|
12
|
+
* sized anything, and this runs once a clip rather than once a frame.
|
|
13
|
+
*/
|
|
14
|
+
import { graphFromWeights } from '@driftengine/texture';
|
|
15
|
+
import { decodeCamera, decodeDepth, depthAnything3, } from './models/depthAnything3.js';
|
|
16
|
+
import { prepareDepthFrame } from './prepare.js';
|
|
17
|
+
/** An estimator over one model's weights, running its graphs through `run`. */
|
|
18
|
+
export function createDepthEstimator(config, weights, run, options = {}) {
|
|
19
|
+
const size = options.size ?? 504;
|
|
20
|
+
const graphs = new Map();
|
|
21
|
+
return {
|
|
22
|
+
async estimate(frames) {
|
|
23
|
+
if (frames.length === 0)
|
|
24
|
+
throw new RangeError('a depth estimate needs a frame');
|
|
25
|
+
const prepared = frames.map((frame) => prepareDepthFrame(frame.pixels, frame.width, frame.height, size));
|
|
26
|
+
const { width, height } = prepared[0];
|
|
27
|
+
for (const [at, one] of prepared.entries()) {
|
|
28
|
+
if (one.width !== width || one.height !== height) {
|
|
29
|
+
throw new RangeError(`frame ${at} prepares to ${one.width}×${one.height} and the first to ${width}×${height}` +
|
|
30
|
+
': one estimate is one clip, at one size');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const key = `${frames.length}.${height}x${width}`;
|
|
34
|
+
let graph = graphs.get(key);
|
|
35
|
+
if (graph === undefined) {
|
|
36
|
+
graph = graphFromWeights(weights, depthAnything3(config, frames.length, height, width));
|
|
37
|
+
graphs.set(key, graph);
|
|
38
|
+
}
|
|
39
|
+
const out = await run(graph, new Map(prepared.map((one, view) => [`image${view}`, one.pixels])));
|
|
40
|
+
const pose = out.get('pose');
|
|
41
|
+
const views = prepared.map((_, view) => {
|
|
42
|
+
const depth = new Float32Array(width * height);
|
|
43
|
+
const confidence = new Float32Array(width * height);
|
|
44
|
+
decodeDepth(out.get(`logits${view}`), depth, confidence);
|
|
45
|
+
const worldToCamera = new Float32Array(12);
|
|
46
|
+
const intrinsics = new Float32Array(9);
|
|
47
|
+
decodeCamera(pose, view, height, width, worldToCamera, intrinsics);
|
|
48
|
+
return { depth, confidence, worldToCamera, intrinsics };
|
|
49
|
+
});
|
|
50
|
+
return { width, height, views };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|