@bvdmitri/gyroview 0.9.0-cutaway.2 → 0.9.0-cutaway.4
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/CHANGELOG.md +14 -0
- package/dist/index.d.ts +2 -1
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +9 -0
- package/dist/packages/adapters/three/src/rendererUniforms.js +3 -0
- package/dist/packages/adapters/three/src/rowCorrections.js +18 -0
- package/dist/packages/adapters/three/src/shaders/lensSampling.js +1 -1
- package/dist/packages/core/src/application/recording/motionOf.js +3 -1
- package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +13 -2
- package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +9 -0
- package/dist/packages/core/src/domain/motion/rollingShutter/RollingShutter.js +46 -0
- package/dist/packages/core/src/domain/motion/stabilization/SmoothedHeading.js +109 -0
- package/dist/packages/core/src/domain/motion/stabilization/Stabilizer.js +1 -0
- package/dist/packages/core/src/domain/motion/stabilization/stabilizers.js +30 -4
- package/dist/packages/player/src/composition/buildPipeline.js +3 -3
- package/dist/packages/player/src/composition/renditionFrames.js +1 -2
- package/dist/packages/player/src/controls/icons.js +1 -0
- package/dist/packages/player/src/controls/messages.js +2 -0
- package/dist/packages/player/src/element/GyroViewElement.js +1 -1
- package/dist/standalone.js +111 -90
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,20 @@ Added (ADR 0045, a recording is rendered frame by frame at the pace its consumer
|
|
|
16
16
|
a multiple of four, or a span that holds no frame, is refused with `invalid-argument`. Its types,
|
|
17
17
|
`Rendition` and `RenditionOptions`, are exported.
|
|
18
18
|
|
|
19
|
+
Added (ADR 0046, each row is turned for the time its sensor read it):
|
|
20
|
+
|
|
21
|
+
- An Insta360 ONE R recording has each row of its lenses turned by the camera's turn between the
|
|
22
|
+
frame's time and the time its sensor read that row, in every stabilization mode: a fast turn no
|
|
23
|
+
longer skews the top and bottom of the picture against its middle. Other cameras turn whole
|
|
24
|
+
until their sensors' row order is measured.
|
|
25
|
+
|
|
26
|
+
Added (ADR 0047, the smooth mode keeps the horizon level and looks ahead for its heading):
|
|
27
|
+
|
|
28
|
+
- A fifth stabilization mode, `smooth` (`stabilization="smooth"`, `setStabilization('smooth')`,
|
|
29
|
+
the Stabilization menu): the horizon level as in `horizon`, the view facing the camera's heading
|
|
30
|
+
smoothed over a second either way in the recording, so turns are followed without lag and the
|
|
31
|
+
heading's shake is gone. `STABILIZATION_MODES` lists it between `horizon` and `follow`.
|
|
32
|
+
|
|
19
33
|
Fixed:
|
|
20
34
|
|
|
21
35
|
- A clip trimmed in the Insta360 app is stabilized with the gyro of its own frames. The app keeps
|
package/dist/index.d.ts
CHANGED
|
@@ -300,6 +300,7 @@ export declare const STABILIZATION_MODES: readonly [
|
|
|
300
300
|
"off",
|
|
301
301
|
"lock",
|
|
302
302
|
"horizon",
|
|
303
|
+
"smooth",
|
|
303
304
|
"follow"
|
|
304
305
|
];
|
|
305
306
|
export type StabilizationMode = (typeof STABILIZATION_MODES)[number];
|
|
@@ -1106,7 +1107,7 @@ export declare class GyroViewElement extends TypedEventElement implements LiveSe
|
|
|
1106
1107
|
*/
|
|
1107
1108
|
stopMotionLook(): void;
|
|
1108
1109
|
/**
|
|
1109
|
-
* Stabilizes the picture in `mode` (`off`, `lock`, `horizon`, `follow`); any other value,
|
|
1110
|
+
* Stabilizes the picture in `mode` (`off`, `lock`, `horizon`, `smooth`, `follow`); any other value,
|
|
1110
1111
|
* an unset one included, is refused.
|
|
1111
1112
|
*/
|
|
1112
1113
|
setStabilization(mode: StabilizationMode): void;
|
|
@@ -9,6 +9,7 @@ import { SAMPLING_STRATEGIES } from "./samplingStrategies.js";
|
|
|
9
9
|
import { applyLensGain, applyPicture, applyShaderSampling, applyStabilization } from "./rendererUniforms.js";
|
|
10
10
|
import { SeamMeterPass } from "./seamMeter/SeamMeterPass.js";
|
|
11
11
|
import { PLAYER_PICTURES, compileAgain, openRendererParts } from "./rendererParts.js";
|
|
12
|
+
import { applyRowCorrections } from "./rowCorrections.js";
|
|
12
13
|
import { scissorBoxOf } from "./scissorBox.js";
|
|
13
14
|
//#region ../../packages/adapters/three/src/ThreeFrameRenderer.ts
|
|
14
15
|
/**
|
|
@@ -97,6 +98,14 @@ var ThreeFrameRenderer = class {
|
|
|
97
98
|
this.ensureLive();
|
|
98
99
|
applyStabilization(this.parts.uniforms, rotation);
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Turns each lens row by its rolling-shutter correction, or every row with the frame. Does not
|
|
103
|
+
* redraw by itself, as {@link setStabilization} does not.
|
|
104
|
+
*/
|
|
105
|
+
setRowCorrections(corrections) {
|
|
106
|
+
this.ensureLive();
|
|
107
|
+
applyRowCorrections(this.parts.uniforms, corrections);
|
|
108
|
+
}
|
|
100
109
|
present(presentation) {
|
|
101
110
|
this.ensureLive();
|
|
102
111
|
const { frames } = presentation.pair;
|
|
@@ -18,6 +18,7 @@ var SHADER_DEFINES = Object.fromEntries([
|
|
|
18
18
|
["MEI_RADIAL_TERMS", MEI_TERM_CAPACITY.radial],
|
|
19
19
|
["MEI_TANGENTIAL_ORDERS", MEI_TERM_CAPACITY.tangential],
|
|
20
20
|
["MEI_THIN_PRISM_ORDERS", MEI_TERM_CAPACITY.thinPrism],
|
|
21
|
+
["ROW_CORRECTIONS", 9],
|
|
21
22
|
...CONVERSION_DEFINES,
|
|
22
23
|
["SAMPLING_BILINEAR", 0],
|
|
23
24
|
["SAMPLING_TRILINEAR", 1],
|
|
@@ -39,6 +40,8 @@ function viewUniforms(setup) {
|
|
|
39
40
|
uLensCount: { value: setup.lenses.length },
|
|
40
41
|
uViewRotation: { value: new Matrix3() },
|
|
41
42
|
uStabilization: { value: new Matrix3() },
|
|
43
|
+
uRowCorrected: { value: false },
|
|
44
|
+
uRowCorrection: { value: Array.from({ length: 9 }, () => new Matrix3()) },
|
|
42
45
|
uPlaneHalfExtent: { value: 1 },
|
|
43
46
|
uPictureAspect: { value: 1 },
|
|
44
47
|
uScreenArea: { value: Array.from({ length: 2 }, () => new Vector4(0, 0, 1, 1)) },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import { toThreeMatrix } from "./threeMatrix.js";
|
|
3
|
+
//#region ../../packages/adapters/three/src/rowCorrections.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sets the rows' rolling-shutter corrections, from the first row of a lens's window to its last,
|
|
6
|
+
* or turns every row with the frame when there are none.
|
|
7
|
+
*/
|
|
8
|
+
function applyRowCorrections(uniforms, corrections) {
|
|
9
|
+
if (!corrections) {
|
|
10
|
+
uniforms.uRowCorrected.value = false;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
ensureInvariant(corrections.length === 9, `a frame's rows take 9 corrections, not ${corrections.length}`);
|
|
14
|
+
uniforms.uRowCorrection.value = corrections.map((rotation) => toThreeMatrix(rotation));
|
|
15
|
+
uniforms.uRowCorrected.value = true;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { applyRowCorrections };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
//#region ../../packages/adapters/three/src/shaders/lensSampling.glsl?raw
|
|
2
|
-
var lensSampling_default = "// How a body direction becomes a texel of one lens, for the stitch and the seam meters: the\n// pose and field as uniforms, and the sampling through each lens model, the texel as its\n// exposure signal. Needs lensTextures.glsl, displayConversion.glsl and lensModels.glsl.\nuniform vec2 uFeather;\nuniform mat3 uLensRotation[MAX_LENSES];\nuniform int uLensKind[MAX_LENSES];\nuniform float uLensHalfFov[MAX_LENSES];\nuniform vec4 uLensWindow[MAX_LENSES];\n\nstruct LensSample {\n bool isImaged;\n // Angle from the lens's optical axis.\n float theta;\n // The texel where the lens's exposure is a factor (displayConversion.glsl).\n vec3 signal;\n};\n\n// Where lens i images direction d on its calibration canvas; the principal point, and not\n// known, for a kind of lens this shader does not know.\nvec2 canvasPixel(int i, vec3 d, float theta, out bool isKnown) {\n isKnown = true;\n if (uLensKind[i] == LENS_MEI) {\n return projectMei(i, d);\n }\n if (uLensKind[i] == LENS_RADIAL_POLYNOMIAL) {\n return projectRadialPolynomial(i, d, theta);\n }\n isKnown = false;\n return uLensPrincipalPoint[i];\n}\n\n// What lens i shows in a body direction, read as `sampling` says, if it images it at all:\n// outside its field, or beyond the window the frame shows, nothing. The footprint is taken and\n// the texture read in every fragment before the gates: a read inside a branch that some pixels\n// of a 2x2 quad skip took a far too coarse footprint on Apple GPUs (rawLenses.frag.glsl).\nLensSample sampleLensWith(int i, vec3 dirBody, int sampling) {\n vec3 d = uLensRotation[i] * dirBody;\n // atan keeps its precision near the axis where acos loses it.\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n vec2 windowUv = (pixel - uLensWindow[i].xy) / uLensWindow[i].zw;\n Footprint footprint = footprintOf(i, windowUv);\n bool isInWindow =\n all(greaterThanEqual(windowUv, vec2(0.0))) && all(lessThanEqual(windowUv, vec2(1.0)));\n vec3 texel = sampleLens(uLensTexture[i], footprint, sampling).rgb;\n LensSample result;\n result.theta = theta;\n result.isImaged = isKnown && theta < uLensHalfFov[i] && isInWindow;\n result.signal = result.isImaged ? exposureSignalOf(i, texel) : vec3(0.0);\n return result;\n}\n\n// What lens i shows in a body direction, read as the picture is.\nLensSample sampleLensAt(int i, vec3 dirBody) {\n return sampleLensWith(i, dirBody, uSampling);\n}\n";
|
|
2
|
+
var lensSampling_default = "// How a body direction becomes a texel of one lens, for the stitch and the seam meters: the\n// pose and field as uniforms, and the sampling through each lens model, the texel as its\n// exposure signal. Needs lensTextures.glsl, displayConversion.glsl and lensModels.glsl.\nuniform vec2 uFeather;\nuniform mat3 uLensRotation[MAX_LENSES];\nuniform int uLensKind[MAX_LENSES];\nuniform float uLensHalfFov[MAX_LENSES];\nuniform vec4 uLensWindow[MAX_LENSES];\n// Rolling-shutter corrections (ADR 0046): the body as it was when each row was read, from the\n// body at the frame's time, for rows from the top of every lens's window to its bottom.\nuniform bool uRowCorrected;\nuniform mat3 uRowCorrection[ROW_CORRECTIONS];\n\nstruct LensSample {\n bool isImaged;\n // Angle from the lens's optical axis.\n float theta;\n // The texel where the lens's exposure is a factor (displayConversion.glsl).\n vec3 signal;\n};\n\n// Where lens i images direction d on its calibration canvas; the principal point, and not\n// known, for a kind of lens this shader does not know.\nvec2 canvasPixel(int i, vec3 d, float theta, out bool isKnown) {\n isKnown = true;\n if (uLensKind[i] == LENS_MEI) {\n return projectMei(i, d);\n }\n if (uLensKind[i] == LENS_RADIAL_POLYNOMIAL) {\n return projectRadialPolynomial(i, d, theta);\n }\n isKnown = false;\n return uLensPrincipalPoint[i];\n}\n\n// The body direction as it was when lens i read the row it images the direction in: found by\n// where the direction lands at the frame's time, which the correction moves by a few rows at\n// most, so one step is enough.\nvec3 rowCorrected(int i, vec3 dirBody) {\n if (!uRowCorrected) return dirBody;\n vec3 d = uLensRotation[i] * dirBody;\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n float row = clamp((pixel.y - uLensWindow[i].y) / uLensWindow[i].w, 0.0, 1.0);\n float step = row * float(ROW_CORRECTIONS - 1);\n int below = min(int(floor(step)), ROW_CORRECTIONS - 2);\n vec3 above = uRowCorrection[below] * dirBody;\n vec3 next = uRowCorrection[below + 1] * dirBody;\n return normalize(mix(above, next, step - float(below)));\n}\n\n// What lens i shows in a body direction, read as `sampling` says, if it images it at all:\n// outside its field, or beyond the window the frame shows, nothing. The footprint is taken and\n// the texture read in every fragment before the gates: a read inside a branch that some pixels\n// of a 2x2 quad skip took a far too coarse footprint on Apple GPUs (rawLenses.frag.glsl).\nLensSample sampleLensWith(int i, vec3 dirBody, int sampling) {\n vec3 d = uLensRotation[i] * rowCorrected(i, dirBody);\n // atan keeps its precision near the axis where acos loses it.\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n vec2 windowUv = (pixel - uLensWindow[i].xy) / uLensWindow[i].zw;\n Footprint footprint = footprintOf(i, windowUv);\n bool isInWindow =\n all(greaterThanEqual(windowUv, vec2(0.0))) && all(lessThanEqual(windowUv, vec2(1.0)));\n vec3 texel = sampleLens(uLensTexture[i], footprint, sampling).rgb;\n LensSample result;\n result.theta = theta;\n result.isImaged = isKnown && theta < uLensHalfFov[i] && isInWindow;\n result.signal = result.isImaged ? exposureSignalOf(i, texel) : vec3(0.0);\n return result;\n}\n\n// What lens i shows in a body direction, read as the picture is.\nLensSample sampleLensAt(int i, vec3 dirBody) {\n return sampleLensWith(i, dirBody, uSampling);\n}\n";
|
|
3
3
|
//#endregion
|
|
4
4
|
export { lensSampling_default as default };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { imuFrameFor } from "../../domain/motion/imu/ImuFrame.js";
|
|
2
2
|
import { mountingOf, uprightImuFrame } from "../../domain/motion/mounting/Mounting.js";
|
|
3
3
|
import { OrientationTrack } from "../../domain/motion/orientation/OrientationTrack.js";
|
|
4
|
+
import { rollingShutterFor } from "../../domain/motion/rollingShutter/RollingShutter.js";
|
|
4
5
|
//#region ../../packages/core/src/application/recording/motionOf.ts
|
|
5
6
|
var NO_GYRO_WARNING = "the recording has no gyro record; stabilization is unavailable";
|
|
6
7
|
/**
|
|
@@ -27,7 +28,8 @@ async function motionOf(recording, clock) {
|
|
|
27
28
|
setup: {
|
|
28
29
|
orientations,
|
|
29
30
|
mounting,
|
|
30
|
-
imuFrame
|
|
31
|
+
imuFrame,
|
|
32
|
+
rollingShutter: rollingShutterFor(recording.info)
|
|
31
33
|
},
|
|
32
34
|
warnings
|
|
33
35
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { rotationIntoBody } from "../../domain/motion/mounting/Mounting.js";
|
|
2
|
-
import {
|
|
2
|
+
import { rowCorrectionsAt } from "../../domain/motion/rollingShutter/RollingShutter.js";
|
|
3
|
+
import { OffStabilization, stabilizerFor } from "../../domain/motion/stabilization/stabilizers.js";
|
|
3
4
|
//#region ../../packages/core/src/application/stabilization/StabilizingFrameSink.ts
|
|
4
5
|
/**
|
|
5
6
|
* Use case: before every presentation, looks up the camera's orientation at the frame's
|
|
6
|
-
* exposure and tells the sink how to turn the picture for the current stabilization mode
|
|
7
|
+
* exposure and tells the sink how to turn the picture for the current stabilization mode, and,
|
|
8
|
+
* for a camera whose rolling shutter is known, how to turn each row for the time it was read.
|
|
7
9
|
*/
|
|
8
10
|
var StabilizingFrameSink = class {
|
|
9
11
|
parts;
|
|
@@ -14,11 +16,20 @@ var StabilizingFrameSink = class {
|
|
|
14
16
|
setStabilizer(stabilizer) {
|
|
15
17
|
this.stabilizer = stabilizer;
|
|
16
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Stabilizes in `mode`, its strategy built over the recording's orientations, which the smooth
|
|
21
|
+
* mode looks ahead in.
|
|
22
|
+
*/
|
|
23
|
+
setStabilizationMode(mode) {
|
|
24
|
+
this.stabilizer = stabilizerFor(mode, this.parts.motion.orientations);
|
|
25
|
+
}
|
|
17
26
|
present(presentation) {
|
|
18
27
|
const time = this.exposureTimeOf(presentation);
|
|
19
28
|
const { orientations, mounting } = this.parts.motion;
|
|
20
29
|
const rotation = this.stabilizer.nextRotation(orientations.orientationAt(time), time);
|
|
21
30
|
this.parts.sink.setStabilization(rotationIntoBody(mounting, rotation));
|
|
31
|
+
const { rollingShutter } = this.parts;
|
|
32
|
+
if (rollingShutter) this.parts.sink.setRowCorrections(rowCorrectionsAt(this.parts.motion, time, rollingShutter));
|
|
22
33
|
this.parts.sink.present(presentation);
|
|
23
34
|
}
|
|
24
35
|
/**
|
|
@@ -72,6 +72,15 @@ var OrientationTrack = class OrientationTrack {
|
|
|
72
72
|
return this.videoTimes.length;
|
|
73
73
|
}
|
|
74
74
|
/**
|
|
75
|
+
* The video times of the first and the last sample; undefined for an empty track.
|
|
76
|
+
*/
|
|
77
|
+
get span() {
|
|
78
|
+
return this.length === 0 ? void 0 : {
|
|
79
|
+
start: seconds(this.videoTimes[0] ?? 0),
|
|
80
|
+
end: seconds(this.videoTimes[this.length - 1] ?? 0)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
75
84
|
* The orientation at a video time, interpolated between samples and held at the ends.
|
|
76
85
|
*/
|
|
77
86
|
orientationAt(videoTime) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { seconds } from "../../../shared/units/time.js";
|
|
2
|
+
import { multiplyMatrices, transposeMatrix } from "../../../shared/math/Matrix3.js";
|
|
3
|
+
import { quaternionToMatrix } from "../../../shared/math/Quaternion.js";
|
|
4
|
+
/**
|
|
5
|
+
* Where a frame's time lies down its rows: the middle row's (ADR 0034).
|
|
6
|
+
*/
|
|
7
|
+
var FRAME_TIME_ROW = .5;
|
|
8
|
+
/**
|
|
9
|
+
* Keyed by the whole model name, as the IMU frames are: a camera's row order is measured on a
|
|
10
|
+
* recording of it (ADR 0046), never assumed.
|
|
11
|
+
*/
|
|
12
|
+
var ROW_ORDERS_BY_MODEL = /* @__PURE__ */ new Map([["Insta360 OneR", "top-first"]]);
|
|
13
|
+
/**
|
|
14
|
+
* The rolling shutter to correct, for a camera whose row order is measured and whose info record
|
|
15
|
+
* states its readout; nothing otherwise, and its frames turn whole.
|
|
16
|
+
*/
|
|
17
|
+
function rollingShutterFor(info) {
|
|
18
|
+
const rowOrder = ROW_ORDERS_BY_MODEL.get(info.model?.trim() ?? "");
|
|
19
|
+
const readout = info.readoutTime;
|
|
20
|
+
return rowOrder && readout !== void 0 && readout > 0 ? {
|
|
21
|
+
readout,
|
|
22
|
+
rowOrder
|
|
23
|
+
} : void 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* For each of {@link ROW_CORRECTION_COUNT} rows down a lens's window, the rotation taking a
|
|
27
|
+
* direction in the body as it was at the frame's `time` into the body as it was when that row was
|
|
28
|
+
* read: the camera turned between the two, so the world a row shows lies that much elsewhere.
|
|
29
|
+
* Every stabilization mode turns the picture at the frame's time, so the corrections apply after
|
|
30
|
+
* it, whatever the mode.
|
|
31
|
+
*/
|
|
32
|
+
function rowCorrectionsAt(motion, time, shutter) {
|
|
33
|
+
const { orientations, mounting } = motion;
|
|
34
|
+
const atTime = quaternionToMatrix(orientations.orientationAt(time));
|
|
35
|
+
const fromBody = transposeMatrix(mounting.toBody);
|
|
36
|
+
const last = 8;
|
|
37
|
+
return Array.from({ length: 9 }, (_, index) => {
|
|
38
|
+
const step = shutter.rowOrder === "top-first" ? index : last - index;
|
|
39
|
+
const rowTime = seconds(time + (step / last - FRAME_TIME_ROW) * shutter.readout);
|
|
40
|
+
const atRow = quaternionToMatrix(orientations.orientationAt(rowTime));
|
|
41
|
+
const uprightTurn = multiplyMatrices(transposeMatrix(atRow), atTime);
|
|
42
|
+
return multiplyMatrices(mounting.toBody, multiplyMatrices(uprightTurn, fromBody));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { rollingShutterFor, rowCorrectionsAt };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { radians } from "../../../shared/units/angle.js";
|
|
2
|
+
import { seconds } from "../../../shared/units/time.js";
|
|
3
|
+
import { rotateVector } from "../../../shared/math/Quaternion.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/motion/stabilization/SmoothedHeading.ts
|
|
5
|
+
/**
|
|
6
|
+
* The step of the grid the heading is smoothed on: far finer than any turn the smoothing keeps.
|
|
7
|
+
*/
|
|
8
|
+
var GRID_STEP_SECONDS = .01;
|
|
9
|
+
/**
|
|
10
|
+
* The Gaussian's reach, in standard deviations: what lies further weighs under one percent of
|
|
11
|
+
* one percent.
|
|
12
|
+
*/
|
|
13
|
+
var KERNEL_REACH = 3;
|
|
14
|
+
/**
|
|
15
|
+
* The camera's heading over a whole recording, smoothed both ways in time, so a steady turn is
|
|
16
|
+
* followed without lag and a quick shake is averaged away.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* The heading is the twist about the vertical horizon reads, `atan2(m13 - m31, m11 + m33)` of
|
|
20
|
+
* the orientation's matrix, but averaged as that vector rather than as an angle. The vector's
|
|
21
|
+
* length is how well the heading is defined: twice the cosine of half the camera's tilt from
|
|
22
|
+
* upright, nothing where it has turned half over about a horizontal axis. Averaged as vectors,
|
|
23
|
+
* the moments a skydiver's camera tumbles through that count for little, where an angle would
|
|
24
|
+
* swing through a half turn at random and, unwrapped, leave a whole turn too many. Near the ends
|
|
25
|
+
* the Gaussian is cut off and its weights taken in proportion.
|
|
26
|
+
*/
|
|
27
|
+
var SmoothedHeading = class SmoothedHeading {
|
|
28
|
+
start;
|
|
29
|
+
sines;
|
|
30
|
+
cosines;
|
|
31
|
+
constructor(start, sines, cosines) {
|
|
32
|
+
this.start = start;
|
|
33
|
+
this.sines = sines;
|
|
34
|
+
this.cosines = cosines;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Smooths the heading of the track's orientations with a Gaussian of standard deviation
|
|
38
|
+
* `spread`; undefined for an empty track.
|
|
39
|
+
*/
|
|
40
|
+
static of(track, spread) {
|
|
41
|
+
const span = track.span;
|
|
42
|
+
if (!span) return void 0;
|
|
43
|
+
const count = Math.max(1, Math.floor((span.end - span.start) / GRID_STEP_SECONDS) + 1);
|
|
44
|
+
const sines = new Float64Array(count);
|
|
45
|
+
const cosines = new Float64Array(count);
|
|
46
|
+
for (let index = 0; index < count; index += 1) {
|
|
47
|
+
const [sine, cosine] = headingVectorOf(track.orientationAt(seconds(span.start + index * GRID_STEP_SECONDS)));
|
|
48
|
+
sines[index] = sine;
|
|
49
|
+
cosines[index] = cosine;
|
|
50
|
+
}
|
|
51
|
+
const samples = spread / GRID_STEP_SECONDS;
|
|
52
|
+
return new SmoothedHeading(span.start, gaussianOf(sines, samples), gaussianOf(cosines, samples));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The smoothed heading at a video time, interpolated on the grid and held at its ends.
|
|
56
|
+
*/
|
|
57
|
+
at(time) {
|
|
58
|
+
const position = (time - this.start) / GRID_STEP_SECONDS;
|
|
59
|
+
const last = this.sines.length - 1;
|
|
60
|
+
const below = Math.min(Math.max(Math.floor(position), 0), last);
|
|
61
|
+
const above = Math.min(below + 1, last);
|
|
62
|
+
const fraction = Math.min(Math.max(position - below, 0), 1);
|
|
63
|
+
const between = (values) => {
|
|
64
|
+
const low = values[below] ?? 0;
|
|
65
|
+
return low + ((values[above] ?? low) - low) * fraction;
|
|
66
|
+
};
|
|
67
|
+
return radians(Math.atan2(between(this.sines), between(this.cosines)));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* The twist about the vertical as a vector, its length how well the twist is defined.
|
|
72
|
+
*/
|
|
73
|
+
function headingVectorOf(orientation) {
|
|
74
|
+
const [xx, , xz] = rotateVector(orientation, [
|
|
75
|
+
1,
|
|
76
|
+
0,
|
|
77
|
+
0
|
|
78
|
+
]);
|
|
79
|
+
const [zx, , zz] = rotateVector(orientation, [
|
|
80
|
+
0,
|
|
81
|
+
0,
|
|
82
|
+
1
|
|
83
|
+
]);
|
|
84
|
+
return [zx - xz, xx + zz];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The values smoothed by a Gaussian of `spread` samples, cut off at its reach and renormalized
|
|
88
|
+
* where the ends cut it shorter.
|
|
89
|
+
*/
|
|
90
|
+
function gaussianOf(values, spread) {
|
|
91
|
+
const reach = Math.ceil(spread * KERNEL_REACH);
|
|
92
|
+
const weights = Float64Array.from({ length: reach + 1 }, (_, offset) => Math.exp(-(offset * offset) / (2 * spread * spread)));
|
|
93
|
+
const smoothed = new Float64Array(values.length);
|
|
94
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
95
|
+
let sum = 0;
|
|
96
|
+
let weightSum = 0;
|
|
97
|
+
const from = Math.max(0, index - reach);
|
|
98
|
+
const to = Math.min(values.length - 1, index + reach);
|
|
99
|
+
for (let other = from; other <= to; other += 1) {
|
|
100
|
+
const weight = weights[Math.abs(other - index)] ?? 0;
|
|
101
|
+
sum += weight * (values[other] ?? 0);
|
|
102
|
+
weightSum += weight;
|
|
103
|
+
}
|
|
104
|
+
smoothed[index] = sum / weightSum;
|
|
105
|
+
}
|
|
106
|
+
return smoothed;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
export { SmoothedHeading };
|
|
@@ -3,6 +3,7 @@ import { seconds } from "../../../shared/units/time.js";
|
|
|
3
3
|
import { IDENTITY_MATRIX3, multiplyMatrices } from "../../../shared/math/Matrix3.js";
|
|
4
4
|
import { conjugateQuaternion, quaternionFromAxisAngle, quaternionToMatrix, slerpQuaternions } from "../../../shared/math/Quaternion.js";
|
|
5
5
|
import { WORLD_DOWN } from "../orientation/gravity.js";
|
|
6
|
+
import { SmoothedHeading } from "./SmoothedHeading.js";
|
|
6
7
|
//#region ../../packages/core/src/domain/motion/stabilization/stabilizers.ts
|
|
7
8
|
/**
|
|
8
9
|
* The picture moves with the camera, as recorded.
|
|
@@ -29,6 +30,26 @@ var HorizonStabilization = class {
|
|
|
29
30
|
return multiplyMatrices(quaternionToMatrix(conjugateQuaternion(orientation)), quaternionToMatrix(heading));
|
|
30
31
|
}
|
|
31
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* How far either way in time the smooth mode's heading is averaged: the standard deviation of its
|
|
35
|
+
* Gaussian. A head's shake, a few hertz, is gone; a turn of a few seconds is kept.
|
|
36
|
+
*/
|
|
37
|
+
var SMOOTH_HEADING_SPREAD_SECONDS = 1;
|
|
38
|
+
/**
|
|
39
|
+
* The horizon stays level and the view faces the camera's heading smoothed both ways in time
|
|
40
|
+
* over the whole recording: turns are followed without lag, shake in the heading is gone.
|
|
41
|
+
* Stateless: the smoothing is done once, when the strategy is built.
|
|
42
|
+
*/
|
|
43
|
+
var SmoothStabilization = class {
|
|
44
|
+
heading;
|
|
45
|
+
constructor(heading) {
|
|
46
|
+
this.heading = heading;
|
|
47
|
+
}
|
|
48
|
+
nextRotation(orientation, videoTime) {
|
|
49
|
+
const facing = quaternionFromAxisAngle(WORLD_DOWN, this.heading.at(videoTime));
|
|
50
|
+
return multiplyMatrices(quaternionToMatrix(conjugateQuaternion(orientation)), quaternionToMatrix(facing));
|
|
51
|
+
}
|
|
52
|
+
};
|
|
32
53
|
var DEFAULT_FOLLOW_OPTIONS = {
|
|
33
54
|
timeConstant: seconds(1.5),
|
|
34
55
|
maxContinuousGap: seconds(1)
|
|
@@ -64,13 +85,18 @@ var STABILIZERS = {
|
|
|
64
85
|
off: () => new OffStabilization(),
|
|
65
86
|
lock: () => new LockStabilization(),
|
|
66
87
|
horizon: () => new HorizonStabilization(),
|
|
88
|
+
smooth: (orientations) => {
|
|
89
|
+
const heading = orientations && SmoothedHeading.of(orientations, seconds(SMOOTH_HEADING_SPREAD_SECONDS));
|
|
90
|
+
return heading ? new SmoothStabilization(heading) : new HorizonStabilization();
|
|
91
|
+
},
|
|
67
92
|
follow: () => new FollowStabilization()
|
|
68
93
|
};
|
|
69
94
|
/**
|
|
70
|
-
* A fresh strategy for the mode; stateful ones start from nothing.
|
|
95
|
+
* A fresh strategy for the mode; stateful ones start from nothing. The smooth mode looks ahead in
|
|
96
|
+
* the recording's `orientations`; without them it is horizon.
|
|
71
97
|
*/
|
|
72
|
-
function stabilizerFor(mode) {
|
|
73
|
-
return STABILIZERS[mode]();
|
|
98
|
+
function stabilizerFor(mode, orientations) {
|
|
99
|
+
return STABILIZERS[mode](orientations);
|
|
74
100
|
}
|
|
75
101
|
/**
|
|
76
102
|
* The camera's heading: the twist of its orientation about the world's vertical axis (the
|
|
@@ -83,4 +109,4 @@ function headingOf(orientation) {
|
|
|
83
109
|
return radians(2 * Math.atan2(y, w));
|
|
84
110
|
}
|
|
85
111
|
//#endregion
|
|
86
|
-
export { FollowStabilization, HorizonStabilization, LockStabilization, OffStabilization, stabilizerFor };
|
|
112
|
+
export { FollowStabilization, HorizonStabilization, LockStabilization, OffStabilization, SmoothStabilization, stabilizerFor };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { messageOf } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
2
|
import { milliseconds, seconds } from "../../../core/src/shared/units/time.js";
|
|
3
3
|
import { UPRIGHT_MOUNTING } from "../../../core/src/domain/motion/mounting/Mounting.js";
|
|
4
|
-
import { stabilizerFor } from "../../../core/src/domain/motion/stabilization/stabilizers.js";
|
|
5
4
|
import { buildStitchingSetup } from "../../../core/src/domain/stitching/StitchingSetup.js";
|
|
6
5
|
import { StabilizingFrameSink } from "../../../core/src/application/stabilization/StabilizingFrameSink.js";
|
|
7
6
|
import { GainMatchingFrameSink } from "../../../core/src/application/gainMatching/GainMatchingFrameSink.js";
|
|
@@ -87,7 +86,7 @@ function drawingFor(parts, disposables) {
|
|
|
87
86
|
function pictureSettingsOf(drawing, session) {
|
|
88
87
|
return {
|
|
89
88
|
setStabilization: (mode) => {
|
|
90
|
-
drawing.stabilizing?.
|
|
89
|
+
drawing.stabilizing?.setStabilizationMode(mode);
|
|
91
90
|
session.redraw();
|
|
92
91
|
},
|
|
93
92
|
setQuality: (quality) => {
|
|
@@ -154,7 +153,8 @@ function sinkOver(renderer, opened) {
|
|
|
154
153
|
const stabilizing = new StabilizingFrameSink({
|
|
155
154
|
sink: renderer,
|
|
156
155
|
motion: opened.motion,
|
|
157
|
-
frameTimes: opened.frameTimes
|
|
156
|
+
frameTimes: opened.frameTimes,
|
|
157
|
+
rollingShutter: opened.motion.rollingShutter
|
|
158
158
|
});
|
|
159
159
|
return {
|
|
160
160
|
sink: stabilizing,
|
|
@@ -2,7 +2,6 @@ import { ensureInvariant, messageOf } from "../../../core/src/shared/errors/Gyro
|
|
|
2
2
|
import { seconds } from "../../../core/src/shared/units/time.js";
|
|
3
3
|
import { UPRIGHT_MOUNTING } from "../../../core/src/domain/motion/mounting/Mounting.js";
|
|
4
4
|
import "../../../core/src/domain/motion/stabilization/Stabilizer.js";
|
|
5
|
-
import { stabilizerFor } from "../../../core/src/domain/motion/stabilization/stabilizers.js";
|
|
6
5
|
import "../../../core/src/domain/view/PictureQuality.js";
|
|
7
6
|
import { RenditionSpan, renditionSizeOf } from "../../../core/src/domain/rendition/RenditionSpan.js";
|
|
8
7
|
import { GainMatchingFrameSink } from "../../../core/src/application/gainMatching/GainMatchingFrameSink.js";
|
|
@@ -92,7 +91,7 @@ var RenditionRun = class {
|
|
|
92
91
|
sinksOver(request) {
|
|
93
92
|
const { opened } = this.parts;
|
|
94
93
|
const { sink, stabilizing } = sinkOver(this.renderer, opened);
|
|
95
|
-
stabilizing?.
|
|
94
|
+
stabilizing?.setStabilizationMode(request.stabilization ?? "lock");
|
|
96
95
|
const mounting = opened.motion?.mounting ?? UPRIGHT_MOUNTING;
|
|
97
96
|
const lenses = stitchingSetupOf(opened).lenses;
|
|
98
97
|
const matching = new GainMatchingFrameSink(sink, this.renderer, referenceLensOf(lenses, mounting));
|
|
@@ -39,6 +39,7 @@ var STABILIZATION_ICONS = {
|
|
|
39
39
|
off: fromTwentyUnitGrid("<path d=\"M2 11l2.5-3 3 4.5 3-6 3 5 2-2.5L18 11\"/>"),
|
|
40
40
|
lock: fromTwentyUnitGrid("<rect x=\"4.5\" y=\"9\" width=\"11\" height=\"8\" rx=\"1.5\"/><path d=\"M7 9V6.5a3 3 0 016 0V9\"/>"),
|
|
41
41
|
horizon: fromTwentyUnitGrid("<circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M3 10h14M8 13h4\"/>"),
|
|
42
|
+
smooth: fromTwentyUnitGrid("<circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M3 10h14\"/><path d=\"M6 13.5c1.5-1 2.5-1 4 0s2.5 1 4 0\"/>"),
|
|
42
43
|
follow: fromTwentyUnitGrid("<path d=\"M3 15c3.5 0 4-9 8.5-9 2 0 3 1 4.5 1\"/><path d=\"M14 4.5L16.5 7 14 9.5\"/>")
|
|
43
44
|
};
|
|
44
45
|
/**
|
|
@@ -33,12 +33,14 @@ var DEFAULT_MESSAGES = frozenMessages({
|
|
|
33
33
|
off: "Off",
|
|
34
34
|
lock: "Lock",
|
|
35
35
|
horizon: "Horizon",
|
|
36
|
+
smooth: "Smooth",
|
|
36
37
|
follow: "Follow"
|
|
37
38
|
},
|
|
38
39
|
stabilizationModeDescriptions: {
|
|
39
40
|
off: "Footage as the camera moved",
|
|
40
41
|
lock: "Orientation fixed to the world",
|
|
41
42
|
horizon: "Level horizon, follows heading",
|
|
43
|
+
smooth: "Level horizon, heading smoothed",
|
|
42
44
|
follow: "Follows turns, smooths out shake"
|
|
43
45
|
},
|
|
44
46
|
viewModes: {
|
|
@@ -239,7 +239,7 @@ var GyroViewElement = class extends TypedEventElement {
|
|
|
239
239
|
this.player.stopMotionLook();
|
|
240
240
|
}
|
|
241
241
|
/**
|
|
242
|
-
* Stabilizes the picture in `mode` (`off`, `lock`, `horizon`, `follow`); any other value,
|
|
242
|
+
* Stabilizes the picture in `mode` (`off`, `lock`, `horizon`, `smooth`, `follow`); any other value,
|
|
243
243
|
* an unset one included, is refused.
|
|
244
244
|
*/
|
|
245
245
|
setStabilization(mode) {
|