@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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytic scenes rendered on the CPU, so a capture's stages can be held to geometry that is known
|
|
3
|
+
* before anything runs.
|
|
4
|
+
*
|
|
5
|
+
* **It ships rather than sitting in a test**, as `@driftengine/nav`'s test field does: a consumer
|
|
6
|
+
* porting this, or checking a device against the reference, needs the same scenes the engine's own
|
|
7
|
+
* tests use, and a fixture that exists only under `*.test.ts` cannot be imported.
|
|
8
|
+
*
|
|
9
|
+
* **A ray cast rather than a rasteriser.** Every pixel's answer is the distance along its own ray,
|
|
10
|
+
* so the depth written beside the colour is the truth rather than an interpolation — which is what
|
|
11
|
+
* makes it a fixture for depth, for triangulation and for the pose that comes out of them. The
|
|
12
|
+
* shapes are boxes and planes, which intersect in closed form, and the surfaces are a procedural
|
|
13
|
+
* checker with a seeded value noise on top: **a corner detector needs something to find**, and a
|
|
14
|
+
* flat colour would make every later test pass for the wrong reason.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: no shadows, no reflections, one directional light. Nothing in a capture
|
|
17
|
+
* cares — what is measured is where things are, not how they look.
|
|
18
|
+
*/
|
|
19
|
+
/** Where the light comes from, normalised, and how much of the surface is lit without it. */
|
|
20
|
+
const LIGHT = [0.4, 0.8, -0.45];
|
|
21
|
+
const AMBIENT = 0.35;
|
|
22
|
+
/**
|
|
23
|
+
* A world-to-camera for an eye looking at a target, with the world's up.
|
|
24
|
+
*
|
|
25
|
+
* **The camera's axes are the ones an image has**: x to the right, **y downward** and z forward, so
|
|
26
|
+
* row zero of a frame is the top of it. Taking y upward instead renders every scene upside down,
|
|
27
|
+
* which a test of a corner's position finds and a test of a colour does not.
|
|
28
|
+
*/
|
|
29
|
+
export function lookAt(eye, target, up = [0, 1, 0]) {
|
|
30
|
+
const forward = normalise([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
|
|
31
|
+
const sideways = cross(up, forward);
|
|
32
|
+
/*
|
|
33
|
+
* **A camera looking straight along its own up vector is refused rather than answered.** The
|
|
34
|
+
* cross product is zero there and `normalise` would hand back a zero row, so the pose comes out
|
|
35
|
+
* rank-deficient: every point in the world projects to the middle of the frame and the depth map
|
|
36
|
+
* behind it is nonsense that looks like a picture. Found by a fusion whose two vertical views
|
|
37
|
+
* quietly agreed the sphere was somewhere else.
|
|
38
|
+
*/
|
|
39
|
+
const span = Math.sqrt(sideways[0] * sideways[0] + sideways[1] * sideways[1] + sideways[2] * sideways[2]);
|
|
40
|
+
if (!(span > 1e-9)) {
|
|
41
|
+
throw new RangeError('capture: a camera cannot look along its own up vector — pass an up it is not parallel to');
|
|
42
|
+
}
|
|
43
|
+
const right = normalise(sideways);
|
|
44
|
+
const down = cross(right, forward);
|
|
45
|
+
const out = new Float64Array(12);
|
|
46
|
+
const rows = [right, down, forward];
|
|
47
|
+
for (let r = 0; r < 3; r += 1) {
|
|
48
|
+
const row = rows[r];
|
|
49
|
+
out[r * 4] = row[0];
|
|
50
|
+
out[r * 4 + 1] = row[1];
|
|
51
|
+
out[r * 4 + 2] = row[2];
|
|
52
|
+
out[r * 4 + 3] = -(row[0] * eye[0] + row[1] * eye[1] + row[2] * eye[2]);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
function cross(a, b) {
|
|
57
|
+
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
|
|
58
|
+
}
|
|
59
|
+
function normalise(v) {
|
|
60
|
+
/* `Math.hypot` is one of the twenty-two the determinism gate refuses; a root of a sum is not. */
|
|
61
|
+
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) || 1;
|
|
62
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
63
|
+
}
|
|
64
|
+
/*
|
|
65
|
+
* A value noise over the surface, from an integer hash rather than a sine: `Math.sin` is one of the
|
|
66
|
+
* twenty-two functions no two engines are obliged to agree on, and this package is inside the
|
|
67
|
+
* determinism gate — a fixture that renders differently elsewhere is not a fixture.
|
|
68
|
+
*/
|
|
69
|
+
function noise(x, y, z, seed) {
|
|
70
|
+
let h = (Math.imul(x | 0, 0x27d4eb2d) ^
|
|
71
|
+
Math.imul(y | 0, 0x165667b1) ^
|
|
72
|
+
Math.imul(z | 0, 0x9e3779b1) ^
|
|
73
|
+
seed) >>>
|
|
74
|
+
0;
|
|
75
|
+
h = Math.imul(h ^ (h >>> 15), 0x2c1b3c6d) >>> 0;
|
|
76
|
+
h = Math.imul(h ^ (h >>> 12), 0x297a2d39) >>> 0;
|
|
77
|
+
return ((h ^ (h >>> 15)) >>> 0) / 4294967296;
|
|
78
|
+
}
|
|
79
|
+
/** The colour of a point on a surface: a checker, a seeded speckle, and the light on it. */
|
|
80
|
+
function shade(point, normal, seed, out, at) {
|
|
81
|
+
const checker = (Math.floor(point[0] * 2) + Math.floor(point[1] * 2) + Math.floor(point[2] * 2)) % 2 === 0
|
|
82
|
+
? 0.75
|
|
83
|
+
: 0.35;
|
|
84
|
+
const speckle = noise(Math.floor(point[0] * 8), Math.floor(point[1] * 8), Math.floor(point[2] * 8), seed);
|
|
85
|
+
const lambert = Math.max(0, normal[0] * LIGHT[0] + normal[1] * LIGHT[1] + normal[2] * LIGHT[2]);
|
|
86
|
+
const light = AMBIENT + (1 - AMBIENT) * lambert;
|
|
87
|
+
const tone = (checker * 0.7 + speckle * 0.3) * light;
|
|
88
|
+
const tint = [1, 0.92, 0.86];
|
|
89
|
+
for (let c = 0; c < 3; c += 1) {
|
|
90
|
+
out[at + c] = Math.max(0, Math.min(255, Math.round(tone * 255 * tint[c])));
|
|
91
|
+
}
|
|
92
|
+
out[at + 3] = 255;
|
|
93
|
+
}
|
|
94
|
+
/** The nearest hit along a ray, or null: `[distance, normal]`. */
|
|
95
|
+
function hitBox(origin, direction, box) {
|
|
96
|
+
let near = -Infinity;
|
|
97
|
+
let far = Infinity;
|
|
98
|
+
let axis = 0;
|
|
99
|
+
let sign = 1;
|
|
100
|
+
for (let a = 0; a < 3; a += 1) {
|
|
101
|
+
const d = direction[a];
|
|
102
|
+
const o = origin[a];
|
|
103
|
+
const low = box.min[a];
|
|
104
|
+
const high = box.max[a];
|
|
105
|
+
if (Math.abs(d) < 1e-12) {
|
|
106
|
+
if (o < low || o > high)
|
|
107
|
+
return null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
let first = (low - o) / d;
|
|
111
|
+
let second = (high - o) / d;
|
|
112
|
+
let towards = -1;
|
|
113
|
+
if (first > second) {
|
|
114
|
+
[first, second] = [second, first];
|
|
115
|
+
towards = 1;
|
|
116
|
+
}
|
|
117
|
+
if (first > near) {
|
|
118
|
+
near = first;
|
|
119
|
+
axis = a;
|
|
120
|
+
sign = towards;
|
|
121
|
+
}
|
|
122
|
+
far = Math.min(far, second);
|
|
123
|
+
if (near > far)
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
if (near <= 1e-6 || near === -Infinity)
|
|
127
|
+
return null;
|
|
128
|
+
const normal = [0, 0, 0];
|
|
129
|
+
normal[axis] = sign;
|
|
130
|
+
return { distance: near, normal };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* `scene` through `camera` into `out`, RGBA, and the distance along the camera's own axis into
|
|
134
|
+
* `depth` where one
|
|
135
|
+
* is given — zero where the ray met nothing.
|
|
136
|
+
*/
|
|
137
|
+
export function renderTestScene(scene, camera, out, depth) {
|
|
138
|
+
const { width, height } = camera;
|
|
139
|
+
const [fx, fy, cx, cy] = camera.intrinsics;
|
|
140
|
+
const m = camera.worldToCamera;
|
|
141
|
+
/* The eye and the camera's axes in the world: the rotation is orthonormal, so its inverse is its transpose. */
|
|
142
|
+
const eye = [0, 0, 0];
|
|
143
|
+
for (let r = 0; r < 3; r += 1) {
|
|
144
|
+
for (let c = 0; c < 3; c += 1) {
|
|
145
|
+
eye[c] -= m[r * 4 + c] * m[r * 4 + 3];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const point = [0, 0, 0];
|
|
149
|
+
const direction = [0, 0, 0];
|
|
150
|
+
for (let y = 0; y < height; y += 1) {
|
|
151
|
+
for (let x = 0; x < width; x += 1) {
|
|
152
|
+
const a = (x + 0.5 - cx) / fx;
|
|
153
|
+
const b = (y + 0.5 - cy) / fy;
|
|
154
|
+
for (let c = 0; c < 3; c += 1) {
|
|
155
|
+
direction[c] = a * m[c] + b * m[4 + c] + m[8 + c];
|
|
156
|
+
}
|
|
157
|
+
const length = Math.sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]);
|
|
158
|
+
for (let c = 0; c < 3; c += 1)
|
|
159
|
+
direction[c] = direction[c] / length;
|
|
160
|
+
let nearest = Infinity;
|
|
161
|
+
let normal = null;
|
|
162
|
+
let seed = 0;
|
|
163
|
+
for (const box of scene.boxes) {
|
|
164
|
+
const hit = hitBox(eye, direction, box);
|
|
165
|
+
if (hit !== null && hit.distance < nearest) {
|
|
166
|
+
nearest = hit.distance;
|
|
167
|
+
normal = hit.normal;
|
|
168
|
+
seed = box.seed;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const plane of scene.planes) {
|
|
172
|
+
const denominator = plane.normal[0] * direction[0] +
|
|
173
|
+
plane.normal[1] * direction[1] +
|
|
174
|
+
plane.normal[2] * direction[2];
|
|
175
|
+
if (Math.abs(denominator) < 1e-12)
|
|
176
|
+
continue;
|
|
177
|
+
const distance = (plane.offset -
|
|
178
|
+
(plane.normal[0] * eye[0] +
|
|
179
|
+
plane.normal[1] * eye[1] +
|
|
180
|
+
plane.normal[2] * eye[2])) /
|
|
181
|
+
denominator;
|
|
182
|
+
if (distance > 1e-6 && distance < nearest) {
|
|
183
|
+
nearest = distance;
|
|
184
|
+
normal = [
|
|
185
|
+
denominator > 0 ? -plane.normal[0] : plane.normal[0],
|
|
186
|
+
denominator > 0 ? -plane.normal[1] : plane.normal[1],
|
|
187
|
+
denominator > 0 ? -plane.normal[2] : plane.normal[2],
|
|
188
|
+
];
|
|
189
|
+
seed = plane.seed;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const at = (y * width + x) * 4;
|
|
193
|
+
if (normal === null) {
|
|
194
|
+
out[at] = 0;
|
|
195
|
+
out[at + 1] = 0;
|
|
196
|
+
out[at + 2] = 0;
|
|
197
|
+
out[at + 3] = 0;
|
|
198
|
+
if (depth !== undefined)
|
|
199
|
+
depth[y * width + x] = 0;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (let c = 0; c < 3; c += 1)
|
|
203
|
+
point[c] = eye[c] + nearest * direction[c];
|
|
204
|
+
shade(point, normal, seed, out, at);
|
|
205
|
+
/* The depth a camera means is along its axis, not along the ray. */
|
|
206
|
+
if (depth !== undefined) {
|
|
207
|
+
const along = (point[0] - eye[0]) * m[8] +
|
|
208
|
+
(point[1] - eye[1]) * m[9] +
|
|
209
|
+
(point[2] - eye[2]) * m[10];
|
|
210
|
+
depth[y * width + x] = along;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RelativePoseOut {
|
|
2
|
+
/** 3 × 3 row-major: the second camera's rotation relative to the first. */
|
|
3
|
+
readonly rotation: Float64Array;
|
|
4
|
+
/** A unit direction, or zeros where the pair has no parallax to fix one. */
|
|
5
|
+
readonly translation: Float64Array;
|
|
6
|
+
}
|
|
7
|
+
export interface RelativePoseResult {
|
|
8
|
+
readonly inliers: number;
|
|
9
|
+
readonly model: 'essential' | 'homography';
|
|
10
|
+
/** The median angle between a point's two rays, in degrees. */
|
|
11
|
+
readonly parallax: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The points behind `count` correspondences, in the first camera's frame, into `out` (3 each), and
|
|
15
|
+
* how many landed in front of both cameras. A point that did not is left at zero.
|
|
16
|
+
*/
|
|
17
|
+
export declare function triangulate(a: Float64Array, b: Float64Array, count: number, intrinsics: readonly [number, number, number, number], rotation: Float64Array, translation: Float64Array, out: Float64Array): number;
|
|
18
|
+
/**
|
|
19
|
+
* The pose of the second view relative to the first, from `count` correspondences in pixels.
|
|
20
|
+
* `random` is the caller's seeded generator: a capture answers the same twice.
|
|
21
|
+
*/
|
|
22
|
+
export declare function relativePose(a: Float64Array, b: Float64Array, count: number, intrinsics: readonly [number, number, number, number], random: () => number, out: RelativePoseOut): RelativePoseResult;
|
package/dist/twoView.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pose between two views, and the points it puts in front of them.
|
|
3
|
+
*
|
|
4
|
+
* **Two models are fitted, not one.** An essential matrix explains a pair that moved; a homography
|
|
5
|
+
* explains a pair that turned, or one looking at a plane. Both are fitted by RANSAC and scored the
|
|
6
|
+
* same way, and the homography wins where it explains what the essential explains — because a
|
|
7
|
+
* camera that only turned can be fitted with a translation that reproduces both pictures exactly
|
|
8
|
+
* and means nothing. **That is the failure this exists to prevent**: the translation would go into
|
|
9
|
+
* the reconstruction as a baseline, every triangulated point would take its scale from it, and
|
|
10
|
+
* nothing downstream could tell.
|
|
11
|
+
*
|
|
12
|
+
* **So parallax is measured and reported**, as the median angle between the two rays to a point.
|
|
13
|
+
* Below a threshold the pair is a turn: the rotation is answered, the translation is zeroed, and
|
|
14
|
+
* the caller is told which model spoke.
|
|
15
|
+
*
|
|
16
|
+
* **RANSAC takes its randomness as an argument.** A capture must give the same answer twice, so the
|
|
17
|
+
* caller passes the generator — a seeded one — rather than this reaching for `Math.random`.
|
|
18
|
+
*/
|
|
19
|
+
import { exactAcos } from '@driftengine/core';
|
|
20
|
+
import { cholesky, choleskySolve, svd3, svdN } from './math/dense.js';
|
|
21
|
+
/** How many samples RANSAC draws, and how far a point may sit from its epipolar line. */
|
|
22
|
+
const TRIALS = 200;
|
|
23
|
+
const THRESHOLD = 1.5;
|
|
24
|
+
/** A pair with less parallax than this, in degrees, is a turn rather than a move. */
|
|
25
|
+
const TURNED = 1;
|
|
26
|
+
/** The homography wins when it explains at least this share of what both models explain. */
|
|
27
|
+
const PLANAR = 0.45;
|
|
28
|
+
/** The last right-singular vector of `rows × cols`: the least-squares null direction. */
|
|
29
|
+
function nullVector(rows, count, cols) {
|
|
30
|
+
const u = new Float64Array(count * cols);
|
|
31
|
+
const s = new Float64Array(cols);
|
|
32
|
+
const v = new Float64Array(cols * cols);
|
|
33
|
+
svdN(rows, count, cols, u, s, v);
|
|
34
|
+
const out = new Float64Array(cols);
|
|
35
|
+
for (let r = 0; r < cols; r += 1)
|
|
36
|
+
out[r] = v[r * cols + cols - 1];
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** An essential matrix from eight or more normalised correspondences. */
|
|
40
|
+
function essentialFrom(a, b, picks) {
|
|
41
|
+
const rows = new Float64Array(Math.max(9, picks.length) * 9);
|
|
42
|
+
picks.forEach((at, row) => {
|
|
43
|
+
const [x1, y1] = [a[at * 2], a[at * 2 + 1]];
|
|
44
|
+
const [x2, y2] = [b[at * 2], b[at * 2 + 1]];
|
|
45
|
+
const terms = [x2 * x1, x2 * y1, x2, y2 * x1, y2 * y1, y2, x1, y1, 1];
|
|
46
|
+
for (let c = 0; c < 9; c += 1)
|
|
47
|
+
rows[row * 9 + c] = terms[c];
|
|
48
|
+
});
|
|
49
|
+
const flat = nullVector(rows, Math.max(9, picks.length), 9);
|
|
50
|
+
/* An essential matrix has two equal singular values and a third of zero: make it one. */
|
|
51
|
+
const u = new Float64Array(9);
|
|
52
|
+
const s = new Float64Array(3);
|
|
53
|
+
const v = new Float64Array(9);
|
|
54
|
+
svd3(flat, u, s, v);
|
|
55
|
+
const scale = (s[0] + s[1]) / 2;
|
|
56
|
+
const out = new Float64Array(9);
|
|
57
|
+
for (let r = 0; r < 3; r += 1) {
|
|
58
|
+
for (let c = 0; c < 3; c += 1) {
|
|
59
|
+
out[r * 3 + c] =
|
|
60
|
+
scale *
|
|
61
|
+
(u[r * 3] * v[c * 3] +
|
|
62
|
+
u[r * 3 + 1] * v[c * 3 + 1]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
/** A homography from four or more normalised correspondences. */
|
|
68
|
+
function homographyFrom(a, b, picks) {
|
|
69
|
+
const count = Math.max(9, picks.length * 2);
|
|
70
|
+
const rows = new Float64Array(count * 9);
|
|
71
|
+
picks.forEach((at, pick) => {
|
|
72
|
+
const [x1, y1] = [a[at * 2], a[at * 2 + 1]];
|
|
73
|
+
const [x2, y2] = [b[at * 2], b[at * 2 + 1]];
|
|
74
|
+
const first = [-x1, -y1, -1, 0, 0, 0, x2 * x1, x2 * y1, x2];
|
|
75
|
+
const second = [0, 0, 0, -x1, -y1, -1, y2 * x1, y2 * y1, y2];
|
|
76
|
+
for (let c = 0; c < 9; c += 1) {
|
|
77
|
+
rows[pick * 2 * 9 + c] = first[c];
|
|
78
|
+
rows[(pick * 2 + 1) * 9 + c] = second[c];
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return nullVector(rows, count, 9);
|
|
82
|
+
}
|
|
83
|
+
/** Sampson's distance from an essential matrix, in pixels of the first camera. */
|
|
84
|
+
function essentialError(e, a, b, at, focal) {
|
|
85
|
+
const x1 = [a[at * 2], a[at * 2 + 1], 1];
|
|
86
|
+
const x2 = [b[at * 2], b[at * 2 + 1], 1];
|
|
87
|
+
const ex1 = [0, 0, 0];
|
|
88
|
+
const etx2 = [0, 0, 0];
|
|
89
|
+
for (let r = 0; r < 3; r += 1) {
|
|
90
|
+
for (let c = 0; c < 3; c += 1) {
|
|
91
|
+
ex1[r] = ex1[r] + e[r * 3 + c] * x1[c];
|
|
92
|
+
etx2[c] = etx2[c] + e[r * 3 + c] * x2[r];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const numerator = x2[0] * ex1[0] +
|
|
96
|
+
x2[1] * ex1[1] +
|
|
97
|
+
x2[2] * ex1[2];
|
|
98
|
+
const denominator = ex1[0] * ex1[0] +
|
|
99
|
+
ex1[1] * ex1[1] +
|
|
100
|
+
etx2[0] * etx2[0] +
|
|
101
|
+
etx2[1] * etx2[1];
|
|
102
|
+
if (denominator === 0)
|
|
103
|
+
return Infinity;
|
|
104
|
+
return (Math.abs(numerator) / Math.sqrt(denominator)) * focal;
|
|
105
|
+
}
|
|
106
|
+
/** The symmetric transfer error of a homography, in pixels. */
|
|
107
|
+
function homographyError(h, a, b, at, focal) {
|
|
108
|
+
const apply = (m, x, y) => {
|
|
109
|
+
const u = m[0] * x + m[1] * y + m[2];
|
|
110
|
+
const v = m[3] * x + m[4] * y + m[5];
|
|
111
|
+
const w = m[6] * x + m[7] * y + m[8];
|
|
112
|
+
return w === 0 ? null : [u / w, v / w];
|
|
113
|
+
};
|
|
114
|
+
const inverse = invert3(h);
|
|
115
|
+
if (inverse === null)
|
|
116
|
+
return Infinity;
|
|
117
|
+
const forward = apply(h, a[at * 2], a[at * 2 + 1]);
|
|
118
|
+
const backward = apply(inverse, b[at * 2], b[at * 2 + 1]);
|
|
119
|
+
if (forward === null || backward === null)
|
|
120
|
+
return Infinity;
|
|
121
|
+
/* A root of a sum, not `Math.hypot`, which the determinism gate refuses. */
|
|
122
|
+
const forwardX = forward[0] - b[at * 2];
|
|
123
|
+
const forwardY = forward[1] - b[at * 2 + 1];
|
|
124
|
+
const backwardX = backward[0] - a[at * 2];
|
|
125
|
+
const backwardY = backward[1] - a[at * 2 + 1];
|
|
126
|
+
const first = Math.sqrt(forwardX * forwardX + forwardY * forwardY);
|
|
127
|
+
const second = Math.sqrt(backwardX * backwardX + backwardY * backwardY);
|
|
128
|
+
return ((first + second) / 2) * focal;
|
|
129
|
+
}
|
|
130
|
+
function invert3(m) {
|
|
131
|
+
const a = m[0];
|
|
132
|
+
const b = m[1];
|
|
133
|
+
const c = m[2];
|
|
134
|
+
const d = m[3];
|
|
135
|
+
const e = m[4];
|
|
136
|
+
const f = m[5];
|
|
137
|
+
const g = m[6];
|
|
138
|
+
const h = m[7];
|
|
139
|
+
const i = m[8];
|
|
140
|
+
const determinant = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
|
|
141
|
+
if (Math.abs(determinant) < 1e-14)
|
|
142
|
+
return null;
|
|
143
|
+
const out = new Float64Array(9);
|
|
144
|
+
out[0] = (e * i - f * h) / determinant;
|
|
145
|
+
out[1] = (c * h - b * i) / determinant;
|
|
146
|
+
out[2] = (b * f - c * e) / determinant;
|
|
147
|
+
out[3] = (f * g - d * i) / determinant;
|
|
148
|
+
out[4] = (a * i - c * g) / determinant;
|
|
149
|
+
out[5] = (c * d - a * f) / determinant;
|
|
150
|
+
out[6] = (d * h - e * g) / determinant;
|
|
151
|
+
out[7] = (b * g - a * h) / determinant;
|
|
152
|
+
out[8] = (a * e - b * d) / determinant;
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** One point from a pair of rays, by the linear method, into `out` at `at`. */
|
|
156
|
+
function triangulatePoint(ax, ay, bx, by, rotation, translation, out, at) {
|
|
157
|
+
/* Four rows of `x × (P · X) = 0`, two from each camera, solved by their normal equations. */
|
|
158
|
+
const rows = [
|
|
159
|
+
[-1, 0, ax, 0],
|
|
160
|
+
[0, -1, ay, 0],
|
|
161
|
+
[
|
|
162
|
+
rotation[0] - bx * rotation[6],
|
|
163
|
+
rotation[1] - bx * rotation[7],
|
|
164
|
+
rotation[2] - bx * rotation[8],
|
|
165
|
+
translation[0] - bx * translation[2],
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
rotation[3] - by * rotation[6],
|
|
169
|
+
rotation[4] - by * rotation[7],
|
|
170
|
+
rotation[5] - by * rotation[8],
|
|
171
|
+
translation[1] - by * translation[2],
|
|
172
|
+
],
|
|
173
|
+
];
|
|
174
|
+
const normals = new Float64Array(9);
|
|
175
|
+
const right = new Float64Array(3);
|
|
176
|
+
for (const row of rows) {
|
|
177
|
+
for (let r = 0; r < 3; r += 1) {
|
|
178
|
+
right[r] = right[r] - row[r] * row[3];
|
|
179
|
+
for (let c = 0; c < 3; c += 1) {
|
|
180
|
+
normals[r * 3 + c] =
|
|
181
|
+
normals[r * 3 + c] + row[r] * row[c];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const factor = new Float64Array(9);
|
|
186
|
+
if (!cholesky(normals, 3, factor))
|
|
187
|
+
return false;
|
|
188
|
+
const point = Float64Array.from(right);
|
|
189
|
+
choleskySolve(factor, 3, point);
|
|
190
|
+
out[at * 3] = point[0];
|
|
191
|
+
out[at * 3 + 1] = point[1];
|
|
192
|
+
out[at * 3 + 2] = point[2];
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The points behind `count` correspondences, in the first camera's frame, into `out` (3 each), and
|
|
197
|
+
* how many landed in front of both cameras. A point that did not is left at zero.
|
|
198
|
+
*/
|
|
199
|
+
export function triangulate(a, b, count, intrinsics, rotation, translation, out) {
|
|
200
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
201
|
+
let ahead = 0;
|
|
202
|
+
for (let at = 0; at < count; at += 1) {
|
|
203
|
+
const ax = (a[at * 2] - cx) / fx;
|
|
204
|
+
const ay = (a[at * 2 + 1] - cy) / fy;
|
|
205
|
+
const bx = (b[at * 2] - cx) / fx;
|
|
206
|
+
const by = (b[at * 2 + 1] - cy) / fy;
|
|
207
|
+
out[at * 3] = 0;
|
|
208
|
+
out[at * 3 + 1] = 0;
|
|
209
|
+
out[at * 3 + 2] = 0;
|
|
210
|
+
if (!triangulatePoint(ax, ay, bx, by, rotation, translation, out, at))
|
|
211
|
+
continue;
|
|
212
|
+
const z = out[at * 3 + 2];
|
|
213
|
+
if (!(z > 0)) {
|
|
214
|
+
out[at * 3] = 0;
|
|
215
|
+
out[at * 3 + 1] = 0;
|
|
216
|
+
out[at * 3 + 2] = 0;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
/* And in front of the second camera too, which is what a wrong decomposition fails. */
|
|
220
|
+
let second = translation[2];
|
|
221
|
+
for (let k = 0; k < 3; k += 1) {
|
|
222
|
+
second += rotation[6 + k] * out[at * 3 + k];
|
|
223
|
+
}
|
|
224
|
+
if (second > 0)
|
|
225
|
+
ahead += 1;
|
|
226
|
+
else {
|
|
227
|
+
out[at * 3] = 0;
|
|
228
|
+
out[at * 3 + 1] = 0;
|
|
229
|
+
out[at * 3 + 2] = 0;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return ahead;
|
|
233
|
+
}
|
|
234
|
+
/** The four poses an essential matrix allows, and the one its points stand in front of. */
|
|
235
|
+
function decompose(e, a, b, picks, out) {
|
|
236
|
+
const u = new Float64Array(9);
|
|
237
|
+
const s = new Float64Array(3);
|
|
238
|
+
const v = new Float64Array(9);
|
|
239
|
+
svd3(e, u, s, v);
|
|
240
|
+
const w = Float64Array.from([0, -1, 0, 1, 0, 0, 0, 0, 1]);
|
|
241
|
+
const build = (turned) => {
|
|
242
|
+
/* U · W · Vᵀ, or U · Wᵀ · Vᵀ: the two rotations an essential matrix allows. */
|
|
243
|
+
const out2 = new Float64Array(9);
|
|
244
|
+
const uw = new Float64Array(9);
|
|
245
|
+
for (let r = 0; r < 3; r += 1) {
|
|
246
|
+
for (let c = 0; c < 3; c += 1) {
|
|
247
|
+
let sum = 0;
|
|
248
|
+
for (let k = 0; k < 3; k += 1) {
|
|
249
|
+
sum +=
|
|
250
|
+
u[r * 3 + k] *
|
|
251
|
+
(turned ? w[c * 3 + k] : w[k * 3 + c]);
|
|
252
|
+
}
|
|
253
|
+
uw[r * 3 + c] = sum;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (let r = 0; r < 3; r += 1) {
|
|
257
|
+
for (let c = 0; c < 3; c += 1) {
|
|
258
|
+
let sum = 0;
|
|
259
|
+
for (let k = 0; k < 3; k += 1)
|
|
260
|
+
sum += uw[r * 3 + k] * v[c * 3 + k];
|
|
261
|
+
out2[r * 3 + c] = sum;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/* A rotation has determinant one; the other sign is a reflection. */
|
|
265
|
+
const determinant = out2[0] *
|
|
266
|
+
(out2[4] * out2[8] - out2[5] * out2[7]) -
|
|
267
|
+
out2[1] *
|
|
268
|
+
(out2[3] * out2[8] - out2[5] * out2[6]) +
|
|
269
|
+
out2[2] *
|
|
270
|
+
(out2[3] * out2[7] - out2[4] * out2[6]);
|
|
271
|
+
if (determinant < 0)
|
|
272
|
+
for (let i = 0; i < 9; i += 1)
|
|
273
|
+
out2[i] = -out2[i];
|
|
274
|
+
return out2;
|
|
275
|
+
};
|
|
276
|
+
const direction = Float64Array.from([u[2], u[5], u[8]]);
|
|
277
|
+
const scratch = new Float64Array(picks.length * 3);
|
|
278
|
+
const subA = new Float64Array(picks.length * 2);
|
|
279
|
+
const subB = new Float64Array(picks.length * 2);
|
|
280
|
+
picks.forEach((at, i) => {
|
|
281
|
+
subA[i * 2] = a[at * 2];
|
|
282
|
+
subA[i * 2 + 1] = a[at * 2 + 1];
|
|
283
|
+
subB[i * 2] = b[at * 2];
|
|
284
|
+
subB[i * 2 + 1] = b[at * 2 + 1];
|
|
285
|
+
});
|
|
286
|
+
let best = -1;
|
|
287
|
+
for (const turned of [false, true]) {
|
|
288
|
+
const rotation = build(turned);
|
|
289
|
+
for (const sign of [1, -1]) {
|
|
290
|
+
const translation = Float64Array.from([
|
|
291
|
+
sign * direction[0],
|
|
292
|
+
sign * direction[1],
|
|
293
|
+
sign * direction[2],
|
|
294
|
+
]);
|
|
295
|
+
const ahead = triangulate(subA, subB, picks.length, [1, 1, 0, 0], rotation, translation, scratch);
|
|
296
|
+
if (ahead > best) {
|
|
297
|
+
best = ahead;
|
|
298
|
+
out.rotation.set(rotation);
|
|
299
|
+
out.translation.set(translation);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return best;
|
|
304
|
+
}
|
|
305
|
+
/** The median angle between the two rays to each point, in degrees. */
|
|
306
|
+
function parallaxOf(a, b, picks, rotation, translation) {
|
|
307
|
+
const angles = [];
|
|
308
|
+
const point = new Float64Array(3);
|
|
309
|
+
const centre = [0, 0, 0];
|
|
310
|
+
for (let k = 0; k < 3; k += 1) {
|
|
311
|
+
/* The second camera's centre in the first's frame: −Rᵀ · t. */
|
|
312
|
+
centre[k] = -(rotation[k] * translation[0] +
|
|
313
|
+
rotation[3 + k] * translation[1] +
|
|
314
|
+
rotation[6 + k] * translation[2]);
|
|
315
|
+
}
|
|
316
|
+
for (const at of picks) {
|
|
317
|
+
if (!triangulatePoint(a[at * 2], a[at * 2 + 1], b[at * 2], b[at * 2 + 1], rotation, translation, point, 0)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const first = [point[0], point[1], point[2]];
|
|
321
|
+
const second = [
|
|
322
|
+
first[0] - centre[0],
|
|
323
|
+
first[1] - centre[1],
|
|
324
|
+
first[2] - centre[2],
|
|
325
|
+
];
|
|
326
|
+
const dot = first[0] * second[0] + first[1] * second[1] + first[2] * second[2];
|
|
327
|
+
const lengths = Math.sqrt(first[0] * first[0] + first[1] * first[1] + first[2] * first[2]) *
|
|
328
|
+
Math.sqrt(second[0] * second[0] + second[1] * second[1] + second[2] * second[2]);
|
|
329
|
+
if (lengths === 0)
|
|
330
|
+
continue;
|
|
331
|
+
angles.push((exactAcos(Math.min(1, Math.max(-1, dot / lengths))) * 180) / Math.PI);
|
|
332
|
+
}
|
|
333
|
+
if (angles.length === 0)
|
|
334
|
+
return 0;
|
|
335
|
+
angles.sort((p, q) => p - q);
|
|
336
|
+
return angles[Math.floor(angles.length / 2)];
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* The pose of the second view relative to the first, from `count` correspondences in pixels.
|
|
340
|
+
* `random` is the caller's seeded generator: a capture answers the same twice.
|
|
341
|
+
*/
|
|
342
|
+
export function relativePose(a, b, count, intrinsics, random, out) {
|
|
343
|
+
const [fx, fy, cx, cy] = intrinsics;
|
|
344
|
+
const focal = (fx + fy) / 2;
|
|
345
|
+
const normalA = new Float64Array(count * 2);
|
|
346
|
+
const normalB = new Float64Array(count * 2);
|
|
347
|
+
for (let at = 0; at < count; at += 1) {
|
|
348
|
+
normalA[at * 2] = (a[at * 2] - cx) / fx;
|
|
349
|
+
normalA[at * 2 + 1] = (a[at * 2 + 1] - cy) / fy;
|
|
350
|
+
normalB[at * 2] = (b[at * 2] - cx) / fx;
|
|
351
|
+
normalB[at * 2 + 1] = (b[at * 2 + 1] - cy) / fy;
|
|
352
|
+
}
|
|
353
|
+
const sample = (size) => {
|
|
354
|
+
const picks = [];
|
|
355
|
+
while (picks.length < size) {
|
|
356
|
+
const at = Math.min(count - 1, Math.floor(random() * count));
|
|
357
|
+
if (!picks.includes(at))
|
|
358
|
+
picks.push(at);
|
|
359
|
+
}
|
|
360
|
+
return picks;
|
|
361
|
+
};
|
|
362
|
+
const score = (fit, error, size) => {
|
|
363
|
+
let best = null;
|
|
364
|
+
let bestInliers = [];
|
|
365
|
+
let bestScore = -1;
|
|
366
|
+
for (let trial = 0; trial < TRIALS; trial += 1) {
|
|
367
|
+
const model = fit(sample(size));
|
|
368
|
+
let sum = 0;
|
|
369
|
+
const inliers = [];
|
|
370
|
+
for (let at = 0; at < count; at += 1) {
|
|
371
|
+
const off = error(model, at);
|
|
372
|
+
if (off < THRESHOLD) {
|
|
373
|
+
inliers.push(at);
|
|
374
|
+
/* A score that rewards a close fit rather than counting alone. */
|
|
375
|
+
sum += THRESHOLD - off;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (sum > bestScore) {
|
|
379
|
+
bestScore = sum;
|
|
380
|
+
best = model;
|
|
381
|
+
bestInliers = inliers;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { model: best, inliers: bestInliers, score: bestScore };
|
|
385
|
+
};
|
|
386
|
+
const essential = score((picks) => essentialFrom(normalA, normalB, picks), (model, at) => essentialError(model, normalA, normalB, at, focal), 8);
|
|
387
|
+
const homography = score((picks) => homographyFrom(normalA, normalB, picks), (model, at) => homographyError(model, normalA, normalB, at, focal), 4);
|
|
388
|
+
const total = essential.score + homography.score;
|
|
389
|
+
const planar = total > 0 && homography.score / total > PLANAR;
|
|
390
|
+
out.rotation.set([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
391
|
+
out.translation.set([0, 0, 0]);
|
|
392
|
+
if (!planar && essential.model !== null && essential.inliers.length >= 8) {
|
|
393
|
+
const refined = essentialFrom(normalA, normalB, essential.inliers);
|
|
394
|
+
decompose(refined, normalA, normalB, essential.inliers, out);
|
|
395
|
+
const parallax = parallaxOf(normalA, normalB, essential.inliers, out.rotation, out.translation);
|
|
396
|
+
if (parallax >= TURNED) {
|
|
397
|
+
return { inliers: essential.inliers.length, model: 'essential', parallax };
|
|
398
|
+
}
|
|
399
|
+
/* It moved too little to say where: answer the turn and claim no baseline. */
|
|
400
|
+
out.translation.set([0, 0, 0]);
|
|
401
|
+
return { inliers: essential.inliers.length, model: 'homography', parallax };
|
|
402
|
+
}
|
|
403
|
+
if (homography.model !== null) {
|
|
404
|
+
/* A turn's homography is `R` itself once the intrinsics are out of it; scale it to a rotation. */
|
|
405
|
+
const h = homography.model;
|
|
406
|
+
const u = new Float64Array(9);
|
|
407
|
+
const s = new Float64Array(3);
|
|
408
|
+
const v = new Float64Array(9);
|
|
409
|
+
svd3(h, u, s, v);
|
|
410
|
+
const rotation = new Float64Array(9);
|
|
411
|
+
for (let r = 0; r < 3; r += 1) {
|
|
412
|
+
for (let c = 0; c < 3; c += 1) {
|
|
413
|
+
let sum = 0;
|
|
414
|
+
for (let k = 0; k < 3; k += 1)
|
|
415
|
+
sum += u[r * 3 + k] * v[c * 3 + k];
|
|
416
|
+
rotation[r * 3 + c] = sum;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const determinant = rotation[0] *
|
|
420
|
+
(rotation[4] * rotation[8] -
|
|
421
|
+
rotation[5] * rotation[7]) -
|
|
422
|
+
rotation[1] *
|
|
423
|
+
(rotation[3] * rotation[8] -
|
|
424
|
+
rotation[5] * rotation[6]) +
|
|
425
|
+
rotation[2] *
|
|
426
|
+
(rotation[3] * rotation[7] -
|
|
427
|
+
rotation[4] * rotation[6]);
|
|
428
|
+
if (determinant < 0)
|
|
429
|
+
for (let i = 0; i < 9; i += 1)
|
|
430
|
+
rotation[i] = -rotation[i];
|
|
431
|
+
out.rotation.set(rotation);
|
|
432
|
+
return { inliers: homography.inliers.length, model: 'homography', parallax: 0 };
|
|
433
|
+
}
|
|
434
|
+
return { inliers: 0, model: 'homography', parallax: 0 };
|
|
435
|
+
}
|