@bvdmitri/gyroview 0.9.0-cutaway.3 → 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 CHANGED
@@ -23,6 +23,13 @@ Added (ADR 0046, each row is turned for the time its sensor read it):
23
23
  longer skews the top and bottom of the picture against its middle. Other cameras turn whole
24
24
  until their sensors' row order is measured.
25
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
+
26
33
  Fixed:
27
34
 
28
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;
@@ -1,6 +1,6 @@
1
1
  import { rotationIntoBody } from "../../domain/motion/mounting/Mounting.js";
2
2
  import { rowCorrectionsAt } from "../../domain/motion/rollingShutter/RollingShutter.js";
3
- import { OffStabilization } from "../../domain/motion/stabilization/stabilizers.js";
3
+ import { OffStabilization, stabilizerFor } from "../../domain/motion/stabilization/stabilizers.js";
4
4
  //#region ../../packages/core/src/application/stabilization/StabilizingFrameSink.ts
5
5
  /**
6
6
  * Use case: before every presentation, looks up the camera's orientation at the frame's
@@ -16,6 +16,13 @@ var StabilizingFrameSink = class {
16
16
  setStabilizer(stabilizer) {
17
17
  this.stabilizer = stabilizer;
18
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
+ }
19
26
  present(presentation) {
20
27
  const time = this.exposureTimeOf(presentation);
21
28
  const { orientations, mounting } = this.parts.motion;
@@ -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,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 @@ var STABILIZATION_MODES = [
3
3
  "off",
4
4
  "lock",
5
5
  "horizon",
6
+ "smooth",
6
7
  "follow"
7
8
  ];
8
9
  /**
@@ -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?.setStabilizer(stabilizerFor(mode));
89
+ drawing.stabilizing?.setStabilizationMode(mode);
91
90
  session.redraw();
92
91
  },
93
92
  setQuality: (quality) => {
@@ -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?.setStabilizer(stabilizerFor(request.stabilization ?? "lock"));
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) {