@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/poses.ts
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The camera path of a clip: where each frame was taken from, and how sure the answer is.
|
|
3
|
+
*
|
|
4
|
+
* **Two starts, one refinement.** Where a depth model ran, its own cameras are the start — they are
|
|
5
|
+
* roughly right and cost nothing — and where it did not, the start is two-view geometry between the
|
|
6
|
+
* first frame and the first one far enough from it, with every later frame placed by its view of
|
|
7
|
+
* the points already triangulated. Either way the finish is a bundle adjustment: every camera and
|
|
8
|
+
* every point moved together until the pictures agree, solved through the Schur complement because
|
|
9
|
+
* the points outnumber the cameras by orders of magnitude.
|
|
10
|
+
*
|
|
11
|
+
* **A clip that only turns is reported rather than fitted.** If no pair in the clip shows parallax,
|
|
12
|
+
* there is no baseline to recover: the rotations are answered, every camera is left standing at the
|
|
13
|
+
* first one's place, and `parallax` is false. **Fitting it anyway is the failure that matters** —
|
|
14
|
+
* the translations would be noise, every triangulated point would take its scale from them, and a
|
|
15
|
+
* scene built on that is wrong in a way nothing downstream can see.
|
|
16
|
+
*
|
|
17
|
+
* **Scale is relative unless something in the scene has a length.** Two views fix a path up to one
|
|
18
|
+
* number; a known distance between two frames' places fixes that number, and only then is the
|
|
19
|
+
* answer in metres.
|
|
20
|
+
*
|
|
21
|
+
* **Poses are the engine's**: 3 × 4 row-major world-to-camera, y up and right handed, with the
|
|
22
|
+
* camera's own axes x right, y down and z forward, which is what an image has.
|
|
23
|
+
*
|
|
24
|
+
* **Which world, though, depends on what was given.** Started from a depth model's cameras, the
|
|
25
|
+
* answer stays in the world those cameras are in. Started from the pictures alone, there is no such
|
|
26
|
+
* world to stay in — two views fix a path up to a rigid motion and a scale — so the first frame is
|
|
27
|
+
* the origin and everything is measured from it.
|
|
28
|
+
*/
|
|
29
|
+
import { exactCos, exactSin } from '@driftengine/core';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
createFeatureSet,
|
|
33
|
+
describeFeatures,
|
|
34
|
+
detectFeatures,
|
|
35
|
+
matchFeatures,
|
|
36
|
+
type Descriptors,
|
|
37
|
+
type FeatureSet,
|
|
38
|
+
} from './features.ts';
|
|
39
|
+
import { levenbergMarquardt } from './math/dense.ts';
|
|
40
|
+
import { schurSolve } from './math/schur.ts';
|
|
41
|
+
import { relativePose, triangulate } from './twoView.ts';
|
|
42
|
+
|
|
43
|
+
export interface CaptureFrame {
|
|
44
|
+
readonly pixels: Uint8Array;
|
|
45
|
+
readonly width: number;
|
|
46
|
+
readonly height: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PoseOptions {
|
|
50
|
+
/** `fx`, `fy`, `cx`, `cy` of the frames as they are given. */
|
|
51
|
+
readonly intrinsics: readonly [number, number, number, number];
|
|
52
|
+
/** The seeded generator RANSAC draws from; a capture answers the same twice. */
|
|
53
|
+
readonly random?: () => number;
|
|
54
|
+
/** Corners a frame. */
|
|
55
|
+
readonly budget?: number;
|
|
56
|
+
/** A depth model's own cameras, 3 × 4 world-to-camera each, where it ran. */
|
|
57
|
+
readonly modelPoses?: readonly ArrayLike<number>[] | null;
|
|
58
|
+
/** A distance in metres between two frames' places, which makes the scale metric. */
|
|
59
|
+
readonly knownLength?: {
|
|
60
|
+
readonly metres: number;
|
|
61
|
+
readonly from: number;
|
|
62
|
+
readonly to: number;
|
|
63
|
+
} | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PoseResult {
|
|
67
|
+
readonly count: number;
|
|
68
|
+
/** Per frame, between zero and one: how much of it the answer rests on. */
|
|
69
|
+
readonly confidence: Float32Array;
|
|
70
|
+
/** False where the clip never moved: the rotations are answered and no baseline is claimed. */
|
|
71
|
+
readonly parallax: boolean;
|
|
72
|
+
readonly scale: 'metric' | 'relative';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A pair with less parallax than this, in degrees, carries no baseline. */
|
|
76
|
+
const TURNED = 1;
|
|
77
|
+
/** How many corners a frame, and how far a reprojection may sit from its observation. */
|
|
78
|
+
const BUDGET = 400;
|
|
79
|
+
const OUTLIER = 4;
|
|
80
|
+
/** A link that starts further than this from where it was seen is a wrong match, not a bad pose. */
|
|
81
|
+
const STRAY = 8;
|
|
82
|
+
const ITERATIONS = 12;
|
|
83
|
+
|
|
84
|
+
/** One observation of a track: which frame, and where in it. */
|
|
85
|
+
interface Observation {
|
|
86
|
+
readonly frame: number;
|
|
87
|
+
readonly x: number;
|
|
88
|
+
readonly y: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A rotation from a rotation vector, by Rodrigues, into `out` (3 × 3 row-major). */
|
|
92
|
+
function rotationOf(vector: ArrayLike<number>, out: Float64Array): void {
|
|
93
|
+
const x = vector[0] as number;
|
|
94
|
+
const y = vector[1] as number;
|
|
95
|
+
const z = vector[2] as number;
|
|
96
|
+
const angle = Math.sqrt(x * x + y * y + z * z);
|
|
97
|
+
if (angle < 1e-12) {
|
|
98
|
+
out.set([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const c = exactCos(angle);
|
|
102
|
+
const s = exactSin(angle);
|
|
103
|
+
const ax = x / angle;
|
|
104
|
+
const ay = y / angle;
|
|
105
|
+
const az = z / angle;
|
|
106
|
+
const rest = 1 - c;
|
|
107
|
+
out[0] = c + ax * ax * rest;
|
|
108
|
+
out[1] = ax * ay * rest - az * s;
|
|
109
|
+
out[2] = ax * az * rest + ay * s;
|
|
110
|
+
out[3] = ay * ax * rest + az * s;
|
|
111
|
+
out[4] = c + ay * ay * rest;
|
|
112
|
+
out[5] = ay * az * rest - ax * s;
|
|
113
|
+
out[6] = az * ax * rest - ay * s;
|
|
114
|
+
out[7] = az * ay * rest + ax * s;
|
|
115
|
+
out[8] = c + az * az * rest;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `a · b` for 3 × 3 row-major matrices, into `out`. */
|
|
119
|
+
function compose(a: Float64Array, b: Float64Array, out: Float64Array): void {
|
|
120
|
+
const held = new Float64Array(9);
|
|
121
|
+
for (let r = 0; r < 3; r += 1) {
|
|
122
|
+
for (let c = 0; c < 3; c += 1) {
|
|
123
|
+
let sum = 0;
|
|
124
|
+
for (let k = 0; k < 3; k += 1) sum += (a[r * 3 + k] as number) * (b[k * 3 + c] as number);
|
|
125
|
+
held[r * 3 + c] = sum;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
out.set(held);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A pose as the engine wants it, from a rotation and a translation. */
|
|
132
|
+
function writePose(
|
|
133
|
+
rotation: Float64Array,
|
|
134
|
+
translation: Float64Array,
|
|
135
|
+
out: Float32Array,
|
|
136
|
+
at: number,
|
|
137
|
+
): void {
|
|
138
|
+
for (let r = 0; r < 3; r += 1) {
|
|
139
|
+
for (let c = 0; c < 3; c += 1) out[at * 12 + r * 4 + c] = rotation[r * 3 + c] as number;
|
|
140
|
+
out[at * 12 + r * 4 + 3] = translation[r] as number;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every frame's corners and their descriptions. */
|
|
145
|
+
function look(
|
|
146
|
+
frames: readonly CaptureFrame[],
|
|
147
|
+
budget: number,
|
|
148
|
+
): { features: FeatureSet[]; descriptors: Descriptors[] } {
|
|
149
|
+
const features: FeatureSet[] = [];
|
|
150
|
+
const descriptors: Descriptors[] = [];
|
|
151
|
+
for (const frame of frames) {
|
|
152
|
+
const set = createFeatureSet(budget);
|
|
153
|
+
detectFeatures(frame.pixels, frame.width, frame.height, set, budget);
|
|
154
|
+
features.push(set);
|
|
155
|
+
descriptors.push(describeFeatures(frame.pixels, frame.width, frame.height, set));
|
|
156
|
+
}
|
|
157
|
+
return { features, descriptors };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Tracks across the clip: a feature followed from frame to frame, at most one observation a frame.
|
|
162
|
+
*
|
|
163
|
+
* **Every pair within a reach of each other is matched, not only the neighbours.** A track built from
|
|
164
|
+
* neighbours alone survives only as long as every step in the chain matches, and the pair that
|
|
165
|
+
* carries the parallax is rarely a neighbour — measured on the test orbit, the neighbours give
|
|
166
|
+
* matches in the eighties while the pair four apart, the one with seven degrees of parallax, is
|
|
167
|
+
* what the reconstruction has to start from. A feature seen in two frames of the window joins their
|
|
168
|
+
* tracks, which is what carries an observation across a step that failed.
|
|
169
|
+
*/
|
|
170
|
+
function follow(
|
|
171
|
+
features: readonly FeatureSet[],
|
|
172
|
+
descriptors: readonly Descriptors[],
|
|
173
|
+
reach: number,
|
|
174
|
+
): Observation[][] {
|
|
175
|
+
const counts = features.map((set) => set.count);
|
|
176
|
+
const firstOf: number[] = [];
|
|
177
|
+
let total = 0;
|
|
178
|
+
for (const count of counts) {
|
|
179
|
+
firstOf.push(total);
|
|
180
|
+
total += count;
|
|
181
|
+
}
|
|
182
|
+
/* Union-find over every (frame, feature) in the clip. */
|
|
183
|
+
const parent = new Int32Array(total);
|
|
184
|
+
for (let i = 0; i < total; i += 1) parent[i] = i;
|
|
185
|
+
const rootOf = (at: number): number => {
|
|
186
|
+
let root = at;
|
|
187
|
+
while ((parent[root] as number) !== root) root = parent[root] as number;
|
|
188
|
+
let walk = at;
|
|
189
|
+
while ((parent[walk] as number) !== walk) {
|
|
190
|
+
const next = parent[walk] as number;
|
|
191
|
+
parent[walk] = root;
|
|
192
|
+
walk = next;
|
|
193
|
+
}
|
|
194
|
+
return root;
|
|
195
|
+
};
|
|
196
|
+
const pairs = new Int32Array(2 * BUDGET);
|
|
197
|
+
for (let a = 0; a < features.length; a += 1) {
|
|
198
|
+
for (let b = a + 1; b < features.length && b <= a + reach; b += 1) {
|
|
199
|
+
const count = matchFeatures(
|
|
200
|
+
descriptors[a] as Descriptors,
|
|
201
|
+
descriptors[b] as Descriptors,
|
|
202
|
+
pairs,
|
|
203
|
+
{},
|
|
204
|
+
);
|
|
205
|
+
for (let m = 0; m < count; m += 1) {
|
|
206
|
+
const left = rootOf((firstOf[a] as number) + (pairs[m * 2] as number));
|
|
207
|
+
const right = rootOf((firstOf[b] as number) + (pairs[m * 2 + 1] as number));
|
|
208
|
+
if (left !== right) parent[left] = right;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const byRoot = new Map<number, Observation[]>();
|
|
214
|
+
const seenFrames = new Map<number, Set<number>>();
|
|
215
|
+
const dropped = new Set<number>();
|
|
216
|
+
for (let frame = 0; frame < features.length; frame += 1) {
|
|
217
|
+
const set = features[frame] as FeatureSet;
|
|
218
|
+
for (let i = 0; i < set.count; i += 1) {
|
|
219
|
+
const root = rootOf((firstOf[frame] as number) + i);
|
|
220
|
+
const frames = seenFrames.get(root) ?? new Set<number>();
|
|
221
|
+
/* A track that claims two features in one frame is a wrong merge: it is dropped whole. */
|
|
222
|
+
if (frames.has(frame)) {
|
|
223
|
+
dropped.add(root);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
frames.add(frame);
|
|
227
|
+
seenFrames.set(root, frames);
|
|
228
|
+
const track = byRoot.get(root) ?? [];
|
|
229
|
+
track.push({ frame, x: set.x[i] as number, y: set.y[i] as number });
|
|
230
|
+
byRoot.set(root, track);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const tracks: Observation[][] = [];
|
|
234
|
+
for (const [root, track] of byRoot) {
|
|
235
|
+
if (dropped.has(root) || track.length < 2) continue;
|
|
236
|
+
tracks.push(track);
|
|
237
|
+
}
|
|
238
|
+
return tracks;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Where a track is seen in a frame, or null. */
|
|
242
|
+
function seenIn(track: readonly Observation[], frame: number): Observation | null {
|
|
243
|
+
for (const one of track) if (one.frame === frame) return one;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** A camera's pose as six numbers — a rotation vector and a translation — and back. */
|
|
248
|
+
interface Camera {
|
|
249
|
+
readonly rotation: Float64Array;
|
|
250
|
+
readonly translation: Float64Array;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The projection of a point and its derivatives, in the camera's own increments. */
|
|
254
|
+
function project(
|
|
255
|
+
camera: Camera,
|
|
256
|
+
point: ArrayLike<number>,
|
|
257
|
+
intrinsics: readonly [number, number, number, number],
|
|
258
|
+
out: Float64Array,
|
|
259
|
+
dCamera: Float64Array | null,
|
|
260
|
+
dPoint: Float64Array | null,
|
|
261
|
+
): boolean {
|
|
262
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
263
|
+
const r = camera.rotation;
|
|
264
|
+
const turned = [0, 0, 0];
|
|
265
|
+
for (let i = 0; i < 3; i += 1) {
|
|
266
|
+
turned[i] =
|
|
267
|
+
(r[i * 3] as number) * (point[0] as number) +
|
|
268
|
+
(r[i * 3 + 1] as number) * (point[1] as number) +
|
|
269
|
+
(r[i * 3 + 2] as number) * (point[2] as number) +
|
|
270
|
+
(camera.translation[i] as number);
|
|
271
|
+
}
|
|
272
|
+
const z = turned[2] as number;
|
|
273
|
+
if (!(z > 1e-6)) return false;
|
|
274
|
+
out[0] = (fx * (turned[0] as number)) / z + cx;
|
|
275
|
+
out[1] = (fy * (turned[1] as number)) / z + cy;
|
|
276
|
+
if (dCamera !== null) {
|
|
277
|
+
/* The update is a small rotation applied on the left, so its derivative is a cross product. */
|
|
278
|
+
const [X, Y, Z] = turned as [number, number, number];
|
|
279
|
+
const du = [fx / z, 0, (-fx * X) / (z * z)];
|
|
280
|
+
const dv = [0, fy / z, (-fy * Y) / (z * z)];
|
|
281
|
+
const skew = [
|
|
282
|
+
[0, Z, -Y],
|
|
283
|
+
[-Z, 0, X],
|
|
284
|
+
[Y, -X, 0],
|
|
285
|
+
];
|
|
286
|
+
for (let p = 0; p < 3; p += 1) {
|
|
287
|
+
let u = 0;
|
|
288
|
+
let v = 0;
|
|
289
|
+
for (let k = 0; k < 3; k += 1) {
|
|
290
|
+
u += (du[k] as number) * ((skew[k] as number[])[p] as number);
|
|
291
|
+
v += (dv[k] as number) * ((skew[k] as number[])[p] as number);
|
|
292
|
+
}
|
|
293
|
+
dCamera[p] = u;
|
|
294
|
+
dCamera[6 + p] = v;
|
|
295
|
+
dCamera[3 + p] = du[p] as number;
|
|
296
|
+
dCamera[9 + p] = dv[p] as number;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (dPoint !== null) {
|
|
300
|
+
const du = [fx / z, 0, (-fx * (turned[0] as number)) / (z * z)];
|
|
301
|
+
const dv = [0, fy / z, (-fy * (turned[1] as number)) / (z * z)];
|
|
302
|
+
for (let p = 0; p < 3; p += 1) {
|
|
303
|
+
let u = 0;
|
|
304
|
+
let v = 0;
|
|
305
|
+
for (let k = 0; k < 3; k += 1) {
|
|
306
|
+
u += (du[k] as number) * (r[k * 3 + p] as number);
|
|
307
|
+
v += (dv[k] as number) * (r[k * 3 + p] as number);
|
|
308
|
+
}
|
|
309
|
+
dPoint[p] = u;
|
|
310
|
+
dPoint[3 + p] = v;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** A frame's pose from points it can see, by Levenberg–Marquardt over six numbers. */
|
|
317
|
+
function poseFromPoints(
|
|
318
|
+
observations: readonly { readonly point: Float64Array; readonly x: number; readonly y: number }[],
|
|
319
|
+
intrinsics: readonly [number, number, number, number],
|
|
320
|
+
camera: Camera,
|
|
321
|
+
): number {
|
|
322
|
+
const start = new Float64Array(6);
|
|
323
|
+
const rotation = new Float64Array(9);
|
|
324
|
+
const working: Camera = { rotation: new Float64Array(9), translation: new Float64Array(3) };
|
|
325
|
+
const pixel = new Float64Array(2);
|
|
326
|
+
const apply = (x: Float64Array): void => {
|
|
327
|
+
rotationOf(x, rotation);
|
|
328
|
+
compose(rotation, camera.rotation, working.rotation);
|
|
329
|
+
for (let i = 0; i < 3; i += 1) {
|
|
330
|
+
working.translation[i] = (camera.translation[i] as number) + (x[3 + i] as number);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
const problem = {
|
|
334
|
+
residuals: observations.length * 2,
|
|
335
|
+
parameters: 6,
|
|
336
|
+
evaluate(x: Float64Array, residuals: Float64Array, jacobian: Float64Array | null): void {
|
|
337
|
+
apply(x);
|
|
338
|
+
const dCamera = new Float64Array(12);
|
|
339
|
+
observations.forEach((one, at) => {
|
|
340
|
+
const ahead = project(
|
|
341
|
+
working,
|
|
342
|
+
one.point,
|
|
343
|
+
intrinsics,
|
|
344
|
+
pixel,
|
|
345
|
+
jacobian === null ? null : dCamera,
|
|
346
|
+
null,
|
|
347
|
+
);
|
|
348
|
+
residuals[at * 2] = ahead ? (pixel[0] as number) - one.x : 0;
|
|
349
|
+
residuals[at * 2 + 1] = ahead ? (pixel[1] as number) - one.y : 0;
|
|
350
|
+
if (jacobian === null) return;
|
|
351
|
+
for (let p = 0; p < 6; p += 1) {
|
|
352
|
+
jacobian[at * 2 * 6 + p] = ahead ? (dCamera[p] as number) : 0;
|
|
353
|
+
jacobian[(at * 2 + 1) * 6 + p] = ahead ? (dCamera[6 + p] as number) : 0;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
const { cost } = levenbergMarquardt(problem, start, { maxIterations: 30 });
|
|
359
|
+
apply(start);
|
|
360
|
+
camera.rotation.set(working.rotation);
|
|
361
|
+
camera.translation.set(working.translation);
|
|
362
|
+
return cost;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Everything the adjustment moves: the cameras, the points, and which sees which. */
|
|
366
|
+
interface Problem {
|
|
367
|
+
readonly cameras: Camera[];
|
|
368
|
+
readonly points: Float64Array[];
|
|
369
|
+
readonly links: {
|
|
370
|
+
readonly camera: number;
|
|
371
|
+
readonly point: number;
|
|
372
|
+
readonly x: number;
|
|
373
|
+
readonly y: number;
|
|
374
|
+
}[];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** The whole reconstruction scaled so the furthest camera stands `gauge` from the first. */
|
|
378
|
+
function rescale(problem: Problem, gauge: number): void {
|
|
379
|
+
if (!(gauge > 0)) return;
|
|
380
|
+
const first = centreOf(problem.cameras[0] as Camera);
|
|
381
|
+
let furthest = 0;
|
|
382
|
+
let span = 0;
|
|
383
|
+
problem.cameras.forEach((camera, at) => {
|
|
384
|
+
const centre = centreOf(camera);
|
|
385
|
+
const away = Math.sqrt(
|
|
386
|
+
(centre[0] - first[0]) * (centre[0] - first[0]) +
|
|
387
|
+
(centre[1] - first[1]) * (centre[1] - first[1]) +
|
|
388
|
+
(centre[2] - first[2]) * (centre[2] - first[2]),
|
|
389
|
+
);
|
|
390
|
+
if (away > span) {
|
|
391
|
+
span = away;
|
|
392
|
+
furthest = at;
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
void furthest;
|
|
396
|
+
if (!(span > 1e-12)) return;
|
|
397
|
+
const factor = gauge / span;
|
|
398
|
+
if (Math.abs(factor - 1) < 1e-12) return;
|
|
399
|
+
for (const camera of problem.cameras) {
|
|
400
|
+
for (let i = 0; i < 3; i += 1)
|
|
401
|
+
camera.translation[i] = (camera.translation[i] as number) * factor;
|
|
402
|
+
}
|
|
403
|
+
for (const point of problem.points) {
|
|
404
|
+
for (let i = 0; i < 3; i += 1) point[i] = (point[i] as number) * factor;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The reprojection cost of a problem as it stands. */
|
|
409
|
+
function costOf(problem: Problem, intrinsics: readonly [number, number, number, number]): number {
|
|
410
|
+
const pixel = new Float64Array(2);
|
|
411
|
+
let sum = 0;
|
|
412
|
+
for (const link of problem.links) {
|
|
413
|
+
const camera = problem.cameras[link.camera] as Camera;
|
|
414
|
+
const point = problem.points[link.point] as Float64Array;
|
|
415
|
+
if (!project(camera, point, intrinsics, pixel, null, null)) continue;
|
|
416
|
+
const dx = (pixel[0] as number) - link.x;
|
|
417
|
+
const dy = (pixel[1] as number) - link.y;
|
|
418
|
+
sum += dx * dx + dy * dy;
|
|
419
|
+
}
|
|
420
|
+
return sum;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The bundle adjustment: cameras and points together, the points eliminated by the Schur
|
|
425
|
+
* complement. The first camera is held still, since a path has no absolute place of its own.
|
|
426
|
+
*
|
|
427
|
+
* **And its scale is held too, which the first camera alone does not fix.** Shrinking every camera
|
|
428
|
+
* and every point together reprojects to the very same pixels, so the cost cannot see it and the
|
|
429
|
+
* reconstruction drifts — measured, a path that started a tenth of a metre from the truth ended
|
|
430
|
+
* three quarters of one away. So after each accepted step the whole thing is scaled back to the
|
|
431
|
+
* distance `gauge` between the first camera and the furthest one.
|
|
432
|
+
*/
|
|
433
|
+
function adjust(
|
|
434
|
+
problem: Problem,
|
|
435
|
+
intrinsics: readonly [number, number, number, number],
|
|
436
|
+
iterations: number,
|
|
437
|
+
gauge: number,
|
|
438
|
+
): void {
|
|
439
|
+
const cameraCount = problem.cameras.length;
|
|
440
|
+
const pointCount = problem.points.length;
|
|
441
|
+
if (cameraCount < 2 || pointCount === 0) return;
|
|
442
|
+
const size = 6;
|
|
443
|
+
let damping = 1e-4;
|
|
444
|
+
let cost = costOf(problem, intrinsics);
|
|
445
|
+
|
|
446
|
+
const cameraBlocks = new Float64Array(cameraCount * size * size);
|
|
447
|
+
const pointBlocks = new Float64Array(pointCount * 9);
|
|
448
|
+
const links = new Float64Array(problem.links.length * size * 3);
|
|
449
|
+
const linkCamera = Int32Array.from(problem.links.map((link) => link.camera));
|
|
450
|
+
const linkPoint = Int32Array.from(problem.links.map((link) => link.point));
|
|
451
|
+
const cameraGradient = new Float64Array(cameraCount * size);
|
|
452
|
+
const pointGradient = new Float64Array(pointCount * 3);
|
|
453
|
+
const cameraStep = new Float64Array(cameraCount * size);
|
|
454
|
+
const pointStep = new Float64Array(pointCount * 3);
|
|
455
|
+
const pixel = new Float64Array(2);
|
|
456
|
+
const dCamera = new Float64Array(12);
|
|
457
|
+
const dPoint = new Float64Array(6);
|
|
458
|
+
const rotation = new Float64Array(9);
|
|
459
|
+
|
|
460
|
+
for (let round = 0; round < iterations; round += 1) {
|
|
461
|
+
cameraBlocks.fill(0);
|
|
462
|
+
pointBlocks.fill(0);
|
|
463
|
+
links.fill(0);
|
|
464
|
+
cameraGradient.fill(0);
|
|
465
|
+
pointGradient.fill(0);
|
|
466
|
+
|
|
467
|
+
problem.links.forEach((link, at) => {
|
|
468
|
+
const camera = problem.cameras[link.camera] as Camera;
|
|
469
|
+
const point = problem.points[link.point] as Float64Array;
|
|
470
|
+
if (!project(camera, point, intrinsics, pixel, dCamera, dPoint)) return;
|
|
471
|
+
const residual = [(pixel[0] as number) - link.x, (pixel[1] as number) - link.y];
|
|
472
|
+
/* The first camera is the frame everything else is measured in: its rows are zero, so its
|
|
473
|
+
own block is the identity added below and its step comes back as nothing. */
|
|
474
|
+
const fixed = link.camera === 0;
|
|
475
|
+
for (let row = 0; row < 2; row += 1) {
|
|
476
|
+
for (let p = 0; p < size; p += 1) {
|
|
477
|
+
const value = fixed ? 0 : (dCamera[row * 6 + p] as number);
|
|
478
|
+
cameraGradient[link.camera * size + p] =
|
|
479
|
+
(cameraGradient[link.camera * size + p] as number) - value * (residual[row] as number);
|
|
480
|
+
for (let q = 0; q < size; q += 1) {
|
|
481
|
+
const other = fixed ? 0 : (dCamera[row * 6 + q] as number);
|
|
482
|
+
cameraBlocks[(link.camera * size + p) * size + q] =
|
|
483
|
+
(cameraBlocks[(link.camera * size + p) * size + q] as number) + value * other;
|
|
484
|
+
}
|
|
485
|
+
for (let q = 0; q < 3; q += 1) {
|
|
486
|
+
links[(at * size + p) * 3 + q] =
|
|
487
|
+
(links[(at * size + p) * 3 + q] as number) + value * (dPoint[row * 3 + q] as number);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (let p = 0; p < 3; p += 1) {
|
|
491
|
+
const value = dPoint[row * 3 + p] as number;
|
|
492
|
+
pointGradient[link.point * 3 + p] =
|
|
493
|
+
(pointGradient[link.point * 3 + p] as number) - value * (residual[row] as number);
|
|
494
|
+
for (let q = 0; q < 3; q += 1) {
|
|
495
|
+
pointBlocks[(link.point * 3 + p) * 3 + q] =
|
|
496
|
+
(pointBlocks[(link.point * 3 + p) * 3 + q] as number) +
|
|
497
|
+
value * (dPoint[row * 3 + q] as number);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
/* The held camera's block would be singular; one on its diagonal leaves it where it is. */
|
|
503
|
+
for (let p = 0; p < size; p += 1)
|
|
504
|
+
cameraBlocks[p * size + p] = (cameraBlocks[p * size + p] as number) + 1;
|
|
505
|
+
|
|
506
|
+
let taken = false;
|
|
507
|
+
for (let attempt = 0; attempt < 8 && !taken; attempt += 1) {
|
|
508
|
+
const solved = schurSolve(
|
|
509
|
+
{
|
|
510
|
+
cameras: cameraCount,
|
|
511
|
+
cameraSize: size,
|
|
512
|
+
points: pointCount,
|
|
513
|
+
cameraBlocks,
|
|
514
|
+
pointBlocks,
|
|
515
|
+
links,
|
|
516
|
+
linkCamera,
|
|
517
|
+
linkPoint,
|
|
518
|
+
cameraGradient,
|
|
519
|
+
pointGradient,
|
|
520
|
+
},
|
|
521
|
+
damping,
|
|
522
|
+
cameraStep,
|
|
523
|
+
pointStep,
|
|
524
|
+
);
|
|
525
|
+
if (!solved) {
|
|
526
|
+
damping *= 10;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const heldCameras = problem.cameras.map((camera) => ({
|
|
530
|
+
rotation: Float64Array.from(camera.rotation),
|
|
531
|
+
translation: Float64Array.from(camera.translation),
|
|
532
|
+
}));
|
|
533
|
+
const heldPoints = problem.points.map((point) => Float64Array.from(point));
|
|
534
|
+
for (let c = 0; c < cameraCount; c += 1) {
|
|
535
|
+
const camera = problem.cameras[c] as Camera;
|
|
536
|
+
rotationOf(cameraStep.subarray(c * size, c * size + 3), rotation);
|
|
537
|
+
compose(rotation, camera.rotation, camera.rotation);
|
|
538
|
+
for (let i = 0; i < 3; i += 1) {
|
|
539
|
+
camera.translation[i] =
|
|
540
|
+
(camera.translation[i] as number) + (cameraStep[c * size + 3 + i] as number);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
for (let p = 0; p < pointCount; p += 1) {
|
|
544
|
+
const point = problem.points[p] as Float64Array;
|
|
545
|
+
for (let i = 0; i < 3; i += 1)
|
|
546
|
+
point[i] = (point[i] as number) + (pointStep[p * 3 + i] as number);
|
|
547
|
+
}
|
|
548
|
+
rescale(problem, gauge);
|
|
549
|
+
const next = costOf(problem, intrinsics);
|
|
550
|
+
if (next < cost) {
|
|
551
|
+
cost = next;
|
|
552
|
+
damping = Math.max(damping / 10, 1e-10);
|
|
553
|
+
taken = true;
|
|
554
|
+
} else {
|
|
555
|
+
problem.cameras.forEach((camera, c) => {
|
|
556
|
+
camera.rotation.set((heldCameras[c] as Camera).rotation);
|
|
557
|
+
camera.translation.set((heldCameras[c] as Camera).translation);
|
|
558
|
+
});
|
|
559
|
+
problem.points.forEach((point, p) => point.set(heldPoints[p] as Float64Array));
|
|
560
|
+
damping *= 10;
|
|
561
|
+
if (damping > 1e8) return;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!taken) return;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** The clip's camera path, into `out`: 3 × 4 world-to-camera a frame. */
|
|
569
|
+
export function estimatePoses(
|
|
570
|
+
frames: readonly CaptureFrame[],
|
|
571
|
+
options: PoseOptions,
|
|
572
|
+
out: Float32Array,
|
|
573
|
+
): PoseResult {
|
|
574
|
+
const count = frames.length;
|
|
575
|
+
const confidence = new Float32Array(count);
|
|
576
|
+
if (count === 0) return { count: 0, confidence, parallax: false, scale: 'relative' };
|
|
577
|
+
const intrinsics = options.intrinsics;
|
|
578
|
+
const random = options.random ?? mulberryish();
|
|
579
|
+
const budget = options.budget ?? BUDGET;
|
|
580
|
+
|
|
581
|
+
const identity = (): Camera => ({
|
|
582
|
+
rotation: Float64Array.from([1, 0, 0, 0, 1, 0, 0, 0, 1]),
|
|
583
|
+
translation: new Float64Array(3),
|
|
584
|
+
});
|
|
585
|
+
const cameras: Camera[] = Array.from({ length: count }, () => identity());
|
|
586
|
+
writePose(cameras[0]!.rotation, cameras[0]!.translation, out, 0);
|
|
587
|
+
if (count === 1) {
|
|
588
|
+
confidence[0] = 1;
|
|
589
|
+
return { count: 1, confidence, parallax: false, scale: 'relative' };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const { features, descriptors } = look(frames, budget);
|
|
593
|
+
const tracks = follow(features, descriptors, Math.min(4, count - 1));
|
|
594
|
+
|
|
595
|
+
/* Does the clip move at all? Every consecutive pair is asked, and one moving pair is enough. */
|
|
596
|
+
let parallax = false;
|
|
597
|
+
let bestPair = { at: 1, parallax: 0 };
|
|
598
|
+
const scratchRotation = new Float64Array(9);
|
|
599
|
+
const scratchTranslation = new Float64Array(3);
|
|
600
|
+
for (let at = 1; at < count; at += 1) {
|
|
601
|
+
const shared = tracks.filter(
|
|
602
|
+
(track) => seenIn(track, 0) !== null && seenIn(track, at) !== null,
|
|
603
|
+
);
|
|
604
|
+
if (shared.length < 20) continue;
|
|
605
|
+
const a = new Float64Array(shared.length * 2);
|
|
606
|
+
const b = new Float64Array(shared.length * 2);
|
|
607
|
+
shared.forEach((track, i) => {
|
|
608
|
+
const first = seenIn(track, 0) as Observation;
|
|
609
|
+
const second = seenIn(track, at) as Observation;
|
|
610
|
+
a[i * 2] = first.x;
|
|
611
|
+
a[i * 2 + 1] = first.y;
|
|
612
|
+
b[i * 2] = second.x;
|
|
613
|
+
b[i * 2 + 1] = second.y;
|
|
614
|
+
});
|
|
615
|
+
const answer = relativePose(a, b, shared.length, intrinsics, random, {
|
|
616
|
+
rotation: scratchRotation,
|
|
617
|
+
translation: scratchTranslation,
|
|
618
|
+
});
|
|
619
|
+
if (answer.parallax > bestPair.parallax) {
|
|
620
|
+
bestPair = { at, parallax: answer.parallax };
|
|
621
|
+
if (answer.model === 'essential') {
|
|
622
|
+
cameras[at]!.rotation.set(scratchRotation);
|
|
623
|
+
cameras[at]!.translation.set(scratchTranslation);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (answer.model === 'essential' && answer.parallax >= TURNED) parallax = true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const given = options.modelPoses ?? null;
|
|
630
|
+
if (given !== null) {
|
|
631
|
+
/* The model's own cameras, taken as they are and refined below. */
|
|
632
|
+
for (let at = 0; at < count; at += 1) {
|
|
633
|
+
const pose = given[at];
|
|
634
|
+
if (pose === undefined) continue;
|
|
635
|
+
for (let r = 0; r < 3; r += 1) {
|
|
636
|
+
for (let c = 0; c < 3; c += 1) cameras[at]!.rotation[r * 3 + c] = pose[r * 4 + c] as number;
|
|
637
|
+
cameras[at]!.translation[r] = pose[r * 4 + 3] as number;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!parallax) {
|
|
643
|
+
/* A turn: answer the rotations and claim no baseline at all. */
|
|
644
|
+
for (let at = 0; at < count; at += 1) {
|
|
645
|
+
if (at > 0 && given === null) turnOnly(tracks, features, intrinsics, random, at, cameras);
|
|
646
|
+
cameras[at]!.translation.fill(0);
|
|
647
|
+
writePose(cameras[at]!.rotation, cameras[at]!.translation, out, at);
|
|
648
|
+
confidence[at] = at === 0 ? 1 : 0.5;
|
|
649
|
+
}
|
|
650
|
+
return { count, confidence, parallax: false, scale: 'relative' };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* The points, from the pair that moved most, and then every frame that can see them. */
|
|
654
|
+
const points = new Map<number, Float64Array>();
|
|
655
|
+
triangulateTracks(tracks, cameras, intrinsics, 0, bestPair.at, points);
|
|
656
|
+
const placed = new Set<number>([0, bestPair.at]);
|
|
657
|
+
for (let at = 1; at < count; at += 1) {
|
|
658
|
+
if (placed.has(at)) continue;
|
|
659
|
+
const seen: { point: Float64Array; x: number; y: number }[] = [];
|
|
660
|
+
tracks.forEach((track, id) => {
|
|
661
|
+
const point = points.get(id);
|
|
662
|
+
const here = seenIn(track, at);
|
|
663
|
+
if (point !== undefined && here !== null) seen.push({ point, x: here.x, y: here.y });
|
|
664
|
+
});
|
|
665
|
+
if (seen.length >= 6 && given === null) {
|
|
666
|
+
/* A frame with nothing placed yet starts from the nearest placed camera. */
|
|
667
|
+
const nearest = [...placed].reduce((best, one) =>
|
|
668
|
+
Math.abs(one - at) < Math.abs(best - at) ? one : best,
|
|
669
|
+
);
|
|
670
|
+
cameras[at]!.rotation.set(cameras[nearest]!.rotation);
|
|
671
|
+
cameras[at]!.translation.set(cameras[nearest]!.translation);
|
|
672
|
+
poseFromPoints(seen, intrinsics, cameras[at] as Camera);
|
|
673
|
+
}
|
|
674
|
+
placed.add(at);
|
|
675
|
+
triangulateTracks(tracks, cameras, intrinsics, 0, at, points);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/* And everything moves together until the pictures agree. */
|
|
679
|
+
const ids = [...points.keys()];
|
|
680
|
+
const problem: Problem = {
|
|
681
|
+
cameras,
|
|
682
|
+
points: ids.map((id) => points.get(id) as Float64Array),
|
|
683
|
+
links: [],
|
|
684
|
+
};
|
|
685
|
+
ids.forEach((id, index) => {
|
|
686
|
+
for (const one of tracks[id] as Observation[]) {
|
|
687
|
+
problem.links.push({ camera: one.frame, point: index, x: one.x, y: one.y });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
/*
|
|
691
|
+
* **A track merged in error is a point that pulls every camera that sees it.** The window's
|
|
692
|
+
* matching joins tracks across a missed step, which is what makes a clip's tracks long enough to
|
|
693
|
+
* start from, and it joins a few that are not the same point at all. Those land far from where
|
|
694
|
+
* they were seen the moment the first poses exist, so they are dropped before anything is moved —
|
|
695
|
+
* measured on the orbit, keeping them left the answer further from the truth than the start was.
|
|
696
|
+
*/
|
|
697
|
+
{
|
|
698
|
+
const pixel = new Float64Array(2);
|
|
699
|
+
const kept = problem.links.filter((link) => {
|
|
700
|
+
const camera = problem.cameras[link.camera] as Camera;
|
|
701
|
+
const point = problem.points[link.point] as Float64Array;
|
|
702
|
+
if (!project(camera, point, intrinsics, pixel, null, null)) return false;
|
|
703
|
+
const dx = (pixel[0] as number) - link.x;
|
|
704
|
+
const dy = (pixel[1] as number) - link.y;
|
|
705
|
+
return Math.sqrt(dx * dx + dy * dy) < STRAY;
|
|
706
|
+
});
|
|
707
|
+
const seen = new Int32Array(problem.points.length);
|
|
708
|
+
for (const link of kept) seen[link.point] = (seen[link.point] as number) + 1;
|
|
709
|
+
problem.links.length = 0;
|
|
710
|
+
for (const link of kept) if ((seen[link.point] as number) >= 2) problem.links.push(link);
|
|
711
|
+
}
|
|
712
|
+
/* The distance the adjustment holds: the path's own span, which fixes the one free scale. */
|
|
713
|
+
const firstCentre = centreOf(cameras[0] as Camera);
|
|
714
|
+
let gauge = 0;
|
|
715
|
+
for (const camera of cameras) {
|
|
716
|
+
const centre = centreOf(camera);
|
|
717
|
+
gauge = Math.max(
|
|
718
|
+
gauge,
|
|
719
|
+
Math.sqrt(
|
|
720
|
+
(centre[0] - firstCentre[0]) * (centre[0] - firstCentre[0]) +
|
|
721
|
+
(centre[1] - firstCentre[1]) * (centre[1] - firstCentre[1]) +
|
|
722
|
+
(centre[2] - firstCentre[2]) * (centre[2] - firstCentre[2]),
|
|
723
|
+
),
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
adjust(problem, intrinsics, ITERATIONS, gauge);
|
|
727
|
+
|
|
728
|
+
/* What is left over per frame is how sure it is: how many of its points still land where seen. */
|
|
729
|
+
const pixel = new Float64Array(2);
|
|
730
|
+
const seenPer = new Int32Array(count);
|
|
731
|
+
const keptPer = new Int32Array(count);
|
|
732
|
+
for (const link of problem.links) {
|
|
733
|
+
seenPer[link.camera] = (seenPer[link.camera] as number) + 1;
|
|
734
|
+
const camera = problem.cameras[link.camera] as Camera;
|
|
735
|
+
if (!project(camera, problem.points[link.point] as Float64Array, intrinsics, pixel, null, null))
|
|
736
|
+
continue;
|
|
737
|
+
const dx = (pixel[0] as number) - link.x;
|
|
738
|
+
const dy = (pixel[1] as number) - link.y;
|
|
739
|
+
if (Math.sqrt(dx * dx + dy * dy) < OUTLIER)
|
|
740
|
+
keptPer[link.camera] = (keptPer[link.camera] as number) + 1;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
let scale: 'metric' | 'relative' = 'relative';
|
|
744
|
+
const known = options.knownLength ?? null;
|
|
745
|
+
if (known !== null) {
|
|
746
|
+
const from = centreOf(cameras[known.from] as Camera);
|
|
747
|
+
const to = centreOf(cameras[known.to] as Camera);
|
|
748
|
+
const measured = Math.sqrt(
|
|
749
|
+
(to[0] - from[0]) * (to[0] - from[0]) +
|
|
750
|
+
(to[1] - from[1]) * (to[1] - from[1]) +
|
|
751
|
+
(to[2] - from[2]) * (to[2] - from[2]),
|
|
752
|
+
);
|
|
753
|
+
if (measured > 1e-9) {
|
|
754
|
+
const factor = known.metres / measured;
|
|
755
|
+
for (const camera of cameras) {
|
|
756
|
+
for (let i = 0; i < 3; i += 1)
|
|
757
|
+
camera.translation[i] = (camera.translation[i] as number) * factor;
|
|
758
|
+
}
|
|
759
|
+
scale = 'metric';
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
for (let at = 0; at < count; at += 1) {
|
|
764
|
+
writePose(cameras[at]!.rotation, cameras[at]!.translation, out, at);
|
|
765
|
+
const seen = seenPer[at] as number;
|
|
766
|
+
const kept = keptPer[at] as number;
|
|
767
|
+
/* Sure where many points were seen and they still land where they were seen. */
|
|
768
|
+
const share = seen === 0 ? 0 : kept / seen;
|
|
769
|
+
const enough = Math.min(1, seen / 40);
|
|
770
|
+
confidence[at] = at === 0 ? 1 : share * enough;
|
|
771
|
+
}
|
|
772
|
+
return { count, confidence, parallax: true, scale };
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** Where a camera stands: −Rᵀ · t. */
|
|
776
|
+
function centreOf(camera: Camera): [number, number, number] {
|
|
777
|
+
const out: [number, number, number] = [0, 0, 0];
|
|
778
|
+
for (let r = 0; r < 3; r += 1) {
|
|
779
|
+
for (let c = 0; c < 3; c += 1) {
|
|
780
|
+
out[c] -= (camera.rotation[r * 3 + c] as number) * (camera.translation[r] as number);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return out;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** The rotation of a frame that only turned, from its matches with the first. */
|
|
787
|
+
function turnOnly(
|
|
788
|
+
tracks: readonly Observation[][],
|
|
789
|
+
_features: readonly FeatureSet[],
|
|
790
|
+
intrinsics: readonly [number, number, number, number],
|
|
791
|
+
random: () => number,
|
|
792
|
+
at: number,
|
|
793
|
+
cameras: readonly Camera[],
|
|
794
|
+
): void {
|
|
795
|
+
const shared = tracks.filter((track) => seenIn(track, 0) !== null && seenIn(track, at) !== null);
|
|
796
|
+
if (shared.length < 8) return;
|
|
797
|
+
const a = new Float64Array(shared.length * 2);
|
|
798
|
+
const b = new Float64Array(shared.length * 2);
|
|
799
|
+
shared.forEach((track, i) => {
|
|
800
|
+
const first = seenIn(track, 0) as Observation;
|
|
801
|
+
const second = seenIn(track, at) as Observation;
|
|
802
|
+
a[i * 2] = first.x;
|
|
803
|
+
a[i * 2 + 1] = first.y;
|
|
804
|
+
b[i * 2] = second.x;
|
|
805
|
+
b[i * 2 + 1] = second.y;
|
|
806
|
+
});
|
|
807
|
+
relativePose(a, b, shared.length, intrinsics, random, {
|
|
808
|
+
rotation: cameras[at]!.rotation,
|
|
809
|
+
translation: new Float64Array(3),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** Points for the tracks two placed frames both see, kept where they land in front of both. */
|
|
814
|
+
function triangulateTracks(
|
|
815
|
+
tracks: readonly Observation[][],
|
|
816
|
+
cameras: readonly Camera[],
|
|
817
|
+
intrinsics: readonly [number, number, number, number],
|
|
818
|
+
first: number,
|
|
819
|
+
second: number,
|
|
820
|
+
points: Map<number, Float64Array>,
|
|
821
|
+
): void {
|
|
822
|
+
const ids: number[] = [];
|
|
823
|
+
const a: number[] = [];
|
|
824
|
+
const b: number[] = [];
|
|
825
|
+
tracks.forEach((track, id) => {
|
|
826
|
+
if (points.has(id)) return;
|
|
827
|
+
const one = seenIn(track, first);
|
|
828
|
+
const other = seenIn(track, second);
|
|
829
|
+
if (one === null || other === null) return;
|
|
830
|
+
ids.push(id);
|
|
831
|
+
a.push(one.x, one.y);
|
|
832
|
+
b.push(other.x, other.y);
|
|
833
|
+
});
|
|
834
|
+
if (ids.length === 0) return;
|
|
835
|
+
/* The second camera, measured from the first, which is what the linear method takes. */
|
|
836
|
+
const rotation = new Float64Array(9);
|
|
837
|
+
const translation = new Float64Array(3);
|
|
838
|
+
const firstCamera = cameras[first] as Camera;
|
|
839
|
+
const secondCamera = cameras[second] as Camera;
|
|
840
|
+
for (let r = 0; r < 3; r += 1) {
|
|
841
|
+
for (let c = 0; c < 3; c += 1) {
|
|
842
|
+
let sum = 0;
|
|
843
|
+
for (let k = 0; k < 3; k += 1) {
|
|
844
|
+
sum +=
|
|
845
|
+
(secondCamera.rotation[r * 3 + k] as number) *
|
|
846
|
+
(firstCamera.rotation[c * 3 + k] as number);
|
|
847
|
+
}
|
|
848
|
+
rotation[r * 3 + c] = sum;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
for (let r = 0; r < 3; r += 1) {
|
|
852
|
+
let sum = secondCamera.translation[r] as number;
|
|
853
|
+
for (let k = 0; k < 3; k += 1)
|
|
854
|
+
sum -= (rotation[r * 3 + k] as number) * (firstCamera.translation[k] as number);
|
|
855
|
+
translation[r] = sum;
|
|
856
|
+
}
|
|
857
|
+
const found = new Float64Array(ids.length * 3);
|
|
858
|
+
triangulate(
|
|
859
|
+
Float64Array.from(a),
|
|
860
|
+
Float64Array.from(b),
|
|
861
|
+
ids.length,
|
|
862
|
+
intrinsics,
|
|
863
|
+
rotation,
|
|
864
|
+
translation,
|
|
865
|
+
found,
|
|
866
|
+
);
|
|
867
|
+
ids.forEach((id, at) => {
|
|
868
|
+
const z = found[at * 3 + 2] as number;
|
|
869
|
+
if (!(z > 0)) return;
|
|
870
|
+
/* Back into the world the first camera is measured in. */
|
|
871
|
+
const world = new Float64Array(3);
|
|
872
|
+
for (let c = 0; c < 3; c += 1) {
|
|
873
|
+
let sum = 0;
|
|
874
|
+
for (let r = 0; r < 3; r += 1) {
|
|
875
|
+
sum +=
|
|
876
|
+
(firstCamera.rotation[r * 3 + c] as number) *
|
|
877
|
+
((found[at * 3 + r] as number) - (firstCamera.translation[r] as number));
|
|
878
|
+
}
|
|
879
|
+
world[c] = sum;
|
|
880
|
+
}
|
|
881
|
+
points.set(id, world);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** A generator for a caller that gave none: seeded, so the answer is still the same twice. */
|
|
886
|
+
function mulberryish(): () => number {
|
|
887
|
+
let state = 0x9e3779b9;
|
|
888
|
+
return () => {
|
|
889
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
890
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state);
|
|
891
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
892
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
893
|
+
};
|
|
894
|
+
}
|