@bubo-squared/gyroview 0.3.0 → 0.4.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +20 -11
  3. package/dist/index.d.ts +56 -2
  4. package/dist/index.js +2 -2
  5. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +49 -15
  6. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +3 -1
  7. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +2 -2
  8. package/dist/packages/adapters/mediabunny/src/trackColour.js +21 -0
  9. package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +12 -1
  10. package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +3 -0
  11. package/dist/packages/adapters/three/src/conversionUniforms.js +61 -0
  12. package/dist/packages/adapters/three/src/glslLiterals.js +10 -0
  13. package/dist/packages/adapters/three/src/matrixCorrections.js +35 -0
  14. package/dist/packages/adapters/three/src/rendererUniforms.js +33 -12
  15. package/dist/packages/adapters/three/src/shaderPrograms.js +4 -2
  16. package/dist/packages/adapters/three/src/shaders/analysis.frag.js +1 -1
  17. package/dist/packages/adapters/three/src/shaders/displayConversion.js +4 -0
  18. package/dist/packages/adapters/three/src/shaders/lensModels.js +1 -1
  19. package/dist/packages/adapters/three/src/shaders/lensSampling.js +1 -1
  20. package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
  21. package/dist/packages/adapters/three/src/shaders/stitch.frag.js +1 -1
  22. package/dist/packages/adapters/three/src/threeMatrix.js +10 -0
  23. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +16 -5
  24. package/dist/packages/adapters/webcodecs/src/colorSpaceOf.js +32 -0
  25. package/dist/packages/core/src/application/playback/probeDecoding.js +2 -2
  26. package/dist/packages/core/src/application/recording/frameTimesOf.js +0 -1
  27. package/dist/packages/core/src/domain/colour/DisplayConversion.js +137 -0
  28. package/dist/packages/core/src/domain/colour/TrackColour.js +38 -0
  29. package/dist/packages/core/src/domain/colour/matrixCorrection.js +76 -0
  30. package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +7 -4
  31. package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +1 -1
  32. package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +1 -0
  33. package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +109 -35
  34. package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +1 -0
  35. package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +33 -4
  36. package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +22 -17
  37. package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +28 -15
  38. package/dist/packages/core/src/domain/format/calibration/v6TermReading.js +20 -0
  39. package/dist/packages/core/src/domain/format/info/calibrationSources.js +27 -0
  40. package/dist/packages/core/src/domain/format/info/infoFields.js +8 -2
  41. package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +11 -6
  42. package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +17 -8
  43. package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +1 -3
  44. package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +0 -3
  45. package/dist/packages/core/src/domain/optics/MeiDistortion.js +43 -0
  46. package/dist/packages/core/src/domain/optics/MeiModel.js +6 -14
  47. package/dist/packages/core/src/domain/optics/scaledProjection.js +30 -0
  48. package/dist/packages/core/src/domain/stitching/StitchingSetup.js +13 -4
  49. package/dist/packages/core/src/index.js +7 -3
  50. package/dist/packages/core/src/shared/errors/GyroViewError.js +55 -3
  51. package/dist/packages/core/src/shared/index.js +11 -0
  52. package/dist/packages/core/src/shared/mapRecord.js +11 -0
  53. package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +12 -26
  54. package/dist/packages/core/src/shared/protobuf/wireFormat.js +16 -0
  55. package/dist/packages/player/src/composition/buildPipeline.js +2 -1
  56. package/dist/packages/player/src/composition/openInputs.js +4 -1
  57. package/dist/packages/player/src/controls/messages.js +2 -1
  58. package/dist/standalone.js +211 -100
  59. package/package.json +4 -4
@@ -1,45 +1,119 @@
1
- import { V3Token } from "../offsetTokens.js";
1
+ import { MeiToken, V3Token, V6Token } from "../offsetTokens.js";
2
2
  import { canvasOfFirstBlock, eulerDegrees, versionWordMismatch } from "./CalibrationStringLayout.js";
3
3
  import { CalibrationVersion } from "../CalibrationVersion.js";
4
4
  import { MeiModel } from "../../../optics/MeiModel.js";
5
+ import { RADIAL_AND_FIRST_PAIR } from "../v6TermReading.js";
5
6
  //#region ../../packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.ts
6
7
  /**
7
- * `offset_v3`: `xi fx fy cx cy yaw pitch roll tx ty tz k1 k2 k3 p1 p2 width height type` per
8
- * lens, then a version word with 3 in its high 16 bits.
8
+ * The layout of a Mei string version, a version word after its lens blocks.
9
9
  */
10
- var MEI_CALIBRATION_LAYOUT = {
10
+ function meiLayout(format) {
11
+ return {
12
+ version: format.version,
13
+ lensTokens: format.lensTokens,
14
+ trailingTokens: 1,
15
+ radialScale: format.radialScale,
16
+ versionWordProblem: (versionWord) => versionWordMismatch(versionWord, format.version),
17
+ parseLens: (block, lensIndex) => meiLensOf(block, lensIndex, format),
18
+ canvasOf: (_numbers, blocks) => canvasOfFirstBlock(blocks, format.canvasTokens.width, format.canvasTokens.height)
19
+ };
20
+ }
21
+ function meiLensOf(block, lensIndex, format) {
22
+ return {
23
+ lensIndex,
24
+ model: new MeiModel({
25
+ xi: block(MeiToken.Xi),
26
+ focal: [block(MeiToken.FocalX), block(MeiToken.FocalY)],
27
+ principalPoint: {
28
+ x: block(MeiToken.CenterX),
29
+ y: block(MeiToken.CenterY)
30
+ },
31
+ distortion: format.distortionOf(block)
32
+ }),
33
+ orientation: eulerDegrees(block(MeiToken.Yaw), block(MeiToken.Pitch), block(MeiToken.Roll)),
34
+ translation: [
35
+ block(MeiToken.TranslationX),
36
+ block(MeiToken.TranslationY),
37
+ block(MeiToken.TranslationZ)
38
+ ]
39
+ };
40
+ }
41
+ /**
42
+ * `offset_v3`: the Mei tokens, then `k1 k2 k3 p1 p2 width height type` per lens, and a version
43
+ * word with 3 in its high 16 bits. Drawn as read: the X5, the only camera seen to write it,
44
+ * stitches through its legacy string (ADR 0023).
45
+ */
46
+ var MEI_CALIBRATION_LAYOUT = meiLayout({
11
47
  version: CalibrationVersion.Mei,
12
48
  lensTokens: 19,
13
- trailingTokens: 1,
14
- versionWordProblem: (versionWord) => versionWordMismatch(versionWord, CalibrationVersion.Mei),
15
- parseLens(block, lensIndex) {
16
- return {
17
- lensIndex,
18
- model: new MeiModel({
19
- xi: block(V3Token.Xi),
20
- focal: [block(V3Token.FocalX), block(V3Token.FocalY)],
21
- principalPoint: {
22
- x: block(V3Token.CenterX),
23
- y: block(V3Token.CenterY)
24
- },
25
- radial: [
26
- block(V3Token.K1),
27
- block(V3Token.K2),
28
- block(V3Token.K3)
29
- ],
30
- tangential: [block(V3Token.P1), block(V3Token.P2)]
31
- }),
32
- orientation: eulerDegrees(block(V3Token.Yaw), block(V3Token.Pitch), block(V3Token.Roll)),
33
- translation: [
34
- block(V3Token.TranslationX),
35
- block(V3Token.TranslationY),
36
- block(V3Token.TranslationZ)
37
- ]
38
- };
49
+ canvasTokens: {
50
+ width: V3Token.CanvasWidth,
51
+ height: V3Token.CanvasHeight
39
52
  },
40
- canvasOf(_numbers, blocks) {
41
- return canvasOfFirstBlock(blocks, V3Token.CanvasWidth, V3Token.CanvasHeight);
42
- }
43
- };
53
+ distortionOf: (block) => ({
54
+ radial: [
55
+ block(V3Token.K1),
56
+ block(V3Token.K2),
57
+ block(V3Token.K3)
58
+ ],
59
+ tangential: [{
60
+ p1: block(V3Token.P1),
61
+ p2: block(V3Token.P2)
62
+ }],
63
+ thinPrism: []
64
+ }),
65
+ radialScale: 1
66
+ });
67
+ /**
68
+ * The radial scale a v6 reading is drawn at, measured on the X6, the one camera seen to stitch
69
+ * through it: Insta360 Studio's far field and the seam's far bins agree on it to 0.3 percent
70
+ * (ADR 0023). Provisional, from one unit; the X5's own v6 strings would want 1.02 to 1.04, but
71
+ * the X5 stitches through its legacy string.
72
+ */
73
+ var V6_RADIAL_SCALE = 1.008;
74
+ /**
75
+ * `offset_v6`: the Mei tokens, then `k1..k5 p1..p4 s1..s4 width height type` per lens, and a
76
+ * version word with 6 in its high 16 bits; its distortion tokens read by `reading` (ADR 0032).
77
+ */
78
+ function extendedMeiLayout(reading) {
79
+ return meiLayout({
80
+ version: CalibrationVersion.ExtendedMei,
81
+ lensTokens: 27,
82
+ canvasTokens: {
83
+ width: V6Token.CanvasWidth,
84
+ height: V6Token.CanvasHeight
85
+ },
86
+ distortionOf: (block) => reading(v6DistortionTokensOf(block)),
87
+ radialScale: V6_RADIAL_SCALE
88
+ });
89
+ }
90
+ /**
91
+ * `offset_v6` as the player reads it: its radial terms and first tangential pair (ADR 0032,
92
+ * provisional).
93
+ */
94
+ var EXTENDED_MEI_CALIBRATION_LAYOUT = extendedMeiLayout(RADIAL_AND_FIRST_PAIR);
95
+ function v6DistortionTokensOf(block) {
96
+ return {
97
+ radial: [
98
+ block(V6Token.K1),
99
+ block(V6Token.K2),
100
+ block(V6Token.K3),
101
+ block(V6Token.K4),
102
+ block(V6Token.K5)
103
+ ],
104
+ tangential: [
105
+ block(V6Token.P1),
106
+ block(V6Token.P2),
107
+ block(V6Token.P3),
108
+ block(V6Token.P4)
109
+ ],
110
+ thinPrism: [
111
+ block(V6Token.S1),
112
+ block(V6Token.S2),
113
+ block(V6Token.S3),
114
+ block(V6Token.S4)
115
+ ]
116
+ };
117
+ }
44
118
  //#endregion
45
- export { MEI_CALIBRATION_LAYOUT };
119
+ export { EXTENDED_MEI_CALIBRATION_LAYOUT, MEI_CALIBRATION_LAYOUT, extendedMeiLayout, meiLayout };
@@ -11,6 +11,7 @@ var POLYNOMIAL_CALIBRATION_LAYOUT = {
11
11
  version: CalibrationVersion.Polynomial,
12
12
  lensTokens: 16,
13
13
  trailingTokens: 1,
14
+ radialScale: 1,
14
15
  versionWordProblem: (versionWord) => versionWordMismatch(versionWord, CalibrationVersion.Polynomial),
15
16
  parseLens(block, lensIndex) {
16
17
  return {
@@ -31,7 +31,10 @@ var V2Token = {
31
31
  CanvasHeight: 14,
32
32
  LensType: 15
33
33
  };
34
- var V3Token = {
34
+ /**
35
+ * The tokens every Mei string (v3, v6) starts its lens block with; each version's own follow.
36
+ */
37
+ var MeiToken = {
35
38
  Xi: 0,
36
39
  FocalX: 1,
37
40
  FocalY: 2,
@@ -42,7 +45,9 @@ var V3Token = {
42
45
  Roll: 7,
43
46
  TranslationX: 8,
44
47
  TranslationY: 9,
45
- TranslationZ: 10,
48
+ TranslationZ: 10
49
+ };
50
+ var V3Token = {
46
51
  K1: 11,
47
52
  K2: 12,
48
53
  K3: 13,
@@ -53,8 +58,32 @@ var V3Token = {
53
58
  LensType: 18
54
59
  };
55
60
  /**
61
+ * `offset_v6` after the Mei tokens: five radial terms, four tangential and four thin-prism
62
+ * tokens, then the canvas and the lens type. The order is insta360-rs's (`docs/INSV_FORMAT.md`),
63
+ * confirmed on the X5, whose v6 and v3 strings agree on the Mei tokens, the canvas and the lens
64
+ * type (ADR 0032).
65
+ */
66
+ var V6Token = {
67
+ K1: 11,
68
+ K2: 12,
69
+ K3: 13,
70
+ K4: 14,
71
+ K5: 15,
72
+ P1: 16,
73
+ P2: 17,
74
+ P3: 18,
75
+ P4: 19,
76
+ S1: 20,
77
+ S2: 21,
78
+ S3: 22,
79
+ S4: 23,
80
+ CanvasWidth: 24,
81
+ CanvasHeight: 25,
82
+ LensType: 26
83
+ };
84
+ /**
56
85
  * Tokens after the lens blocks. v1 ends with canvas width, canvas height and a version word;
57
- * v2 and v3 end with the version word only.
86
+ * v2, v3 and v6 end with the version word only.
58
87
  */
59
88
  var V1Trailing = {
60
89
  CanvasWidth: 0,
@@ -62,4 +91,4 @@ var V1Trailing = {
62
91
  VersionWord: 2
63
92
  };
64
93
  //#endregion
65
- export { V1Token, V1Trailing, V2Token, V3Token };
94
+ export { MeiToken, V1Token, V1Trailing, V2Token, V3Token, V6Token };
@@ -1,40 +1,45 @@
1
1
  import "./layouts/CalibrationStringLayout.js";
2
2
  import { LEGACY_CALIBRATION_LAYOUT } from "./layouts/LegacyCalibrationLayout.js";
3
- import { MEI_CALIBRATION_LAYOUT } from "./layouts/MeiCalibrationLayout.js";
4
- import { POLYNOMIAL_CALIBRATION_LAYOUT } from "./layouts/PolynomialCalibrationLayout.js";
5
3
  import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
4
+ import { EXTENDED_MEI_CALIBRATION_LAYOUT, MEI_CALIBRATION_LAYOUT } from "./layouts/MeiCalibrationLayout.js";
5
+ import { POLYNOMIAL_CALIBRATION_LAYOUT } from "./layouts/PolynomialCalibrationLayout.js";
6
6
  //#region ../../packages/core/src/domain/format/calibration/parseOffsetString.ts
7
7
  var TOKEN_SEPARATOR = "_";
8
- var LAYOUTS = [
8
+ /**
9
+ * Every calibration string version the player reads; their token counts never coincide for a
10
+ * lens count.
11
+ */
12
+ var CALIBRATION_LAYOUTS = [
9
13
  LEGACY_CALIBRATION_LAYOUT,
10
14
  POLYNOMIAL_CALIBRATION_LAYOUT,
11
- MEI_CALIBRATION_LAYOUT
15
+ MEI_CALIBRATION_LAYOUT,
16
+ EXTENDED_MEI_CALIBRATION_LAYOUT
12
17
  ];
13
18
  /**
14
- * Parses `offset`, `offset_v2` and `offset_v3` strings into a calibration set with its version. The
15
- * layout is detected from the token count; the v6 layout (13 distortion coefficients per lens) is
16
- * recognised and rejected explicitly.
19
+ * Parses a calibration string of any version into a calibration set with its version, the layout
20
+ * detected from the token count among `layouts`: the player's, or a measurement's own readings.
17
21
  */
18
- function parseOffsetString(text) {
19
- const numbers = text.split(TOKEN_SEPARATOR).map((token) => parseNumber(token, text));
22
+ function parseOffsetString(text, layouts = CALIBRATION_LAYOUTS) {
23
+ const numbers = text.split(TOKEN_SEPARATOR).map((token) => parseNumber(token));
20
24
  const lensCount = numbers[0] ?? 0;
21
- const layout = detectLayout(numbers.length, lensCount, text);
25
+ const layout = detectLayout(layouts, numbers);
22
26
  ensureVersionWordFits(layout, numbers.at(-1) ?? 0);
23
27
  const blocks = Array.from({ length: lensCount }, (_unused, lensIndex) => lensBlock(numbers, 1 + lensIndex * layout.lensTokens));
24
28
  return {
25
29
  version: layout.version,
26
30
  canvas: layout.canvasOf(numbers, blocks),
27
- lenses: blocks.map((block, lensIndex) => layout.parseLens(block, lensIndex))
31
+ lenses: blocks.map((block, lensIndex) => layout.parseLens(block, lensIndex)),
32
+ radialScale: layout.radialScale
28
33
  };
29
34
  }
30
35
  function tokenCountFor(lensCount, lensTokens, trailingTokens) {
31
36
  return 1 + lensCount * lensTokens + trailingTokens;
32
37
  }
33
- function detectLayout(tokenCount, lensCount, text) {
34
- const layout = LAYOUTS.find((candidate) => tokenCount === tokenCountFor(lensCount, candidate.lensTokens, candidate.trailingTokens));
38
+ function detectLayout(layouts, numbers) {
39
+ const lensCount = numbers[0] ?? 0;
40
+ const layout = layouts.find((candidate) => numbers.length === tokenCountFor(lensCount, candidate.lensTokens, candidate.trailingTokens));
35
41
  if (layout) return layout;
36
- if (tokenCount === tokenCountFor(lensCount, 27, 1)) throw new GyroViewError("unsupported-calibration", "calibration string uses the v6 layout (13 distortion coefficients), which is not supported yet");
37
- throw new GyroViewError("invalid-calibration", `calibration string has ${tokenCount} tokens for ${lensCount} lenses, matching no known layout: ${text}`);
42
+ throw new GyroViewError("invalid-calibration", `calibration string has ${numbers.length} tokens for ${lensCount} lenses, matching no known layout`);
38
43
  }
39
44
  function ensureVersionWordFits(layout, versionWord) {
40
45
  const problem = layout.versionWordProblem?.(versionWord);
@@ -43,9 +48,9 @@ function ensureVersionWordFits(layout, versionWord) {
43
48
  function lensBlock(numbers, start) {
44
49
  return (token) => numbers[start + token] ?? NaN;
45
50
  }
46
- function parseNumber(token, text) {
51
+ function parseNumber(token) {
47
52
  const value = Number(token);
48
- if (token.trim() === "" || !Number.isFinite(value)) throw new GyroViewError("invalid-calibration", `calibration token "${token}" is not a finite number in: ${text}`);
53
+ if (token.trim() === "" || !Number.isFinite(value)) throw new GyroViewError("invalid-calibration", `calibration token "${token}" is not a finite number`);
49
54
  return value;
50
55
  }
51
56
  //#endregion
@@ -1,5 +1,6 @@
1
1
  import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
2
2
  import { parseOffsetString } from "./parseOffsetString.js";
3
+ import { CALIBRATION_SOURCES } from "../info/calibrationSources.js";
3
4
  //#region ../../packages/core/src/domain/format/calibration/selectCalibration.ts
4
5
  /**
5
6
  * Only these failures mean "this string is unusable, try the next one"; anything else is a bug
@@ -7,22 +8,25 @@ import { parseOffsetString } from "./parseOffsetString.js";
7
8
  */
8
9
  var SKIPPABLE = /* @__PURE__ */ new Set(["invalid-calibration", "unsupported-calibration"]);
9
10
  /**
10
- * Picks the calibration a recording is stitched through: the legacy string first, whose
11
- * equidistant reading is the one Insta360's own stitch agrees with on the far field
12
- * (ADR 0023), then the Mei (v3) and polynomial (v2) strings for a recording without it. Strings
13
- * that cannot be used are reported, not silently dropped; having none is reported as absence,
14
- * not as a failure.
11
+ * The order the calibration strings are tried in: the legacy string first, whose equidistant
12
+ * reading is the one Insta360's own stitch agrees with on the far field (ADR 0023), then, for a
13
+ * recording without it, the Mei strings, the newer fit (v6) before the older (v3), and the
14
+ * polynomial (v2) last.
15
+ */
16
+ var CALIBRATION_PREFERENCE = [
17
+ "offset",
18
+ "offsetV6",
19
+ "offsetV3",
20
+ "offsetV2"
21
+ ];
22
+ /**
23
+ * Picks the calibration a recording is stitched through, the first usable string in
24
+ * {@link CALIBRATION_PREFERENCE}. Strings that cannot be used are reported, not silently
25
+ * dropped; having none is reported as absence, not as a failure.
15
26
  */
16
27
  function selectCalibration(strings) {
17
- const candidates = [
18
- ["offset", strings.offset],
19
- ["offset_v3", strings.offsetV3],
20
- ["offset_v2", strings.offsetV2]
21
- ];
22
28
  const warnings = [];
23
- for (const [name, text] of candidates) {
24
- if (text === void 0) continue;
25
- const attempt = tryParse(name, text);
29
+ for (const attempt of attemptsOf(strings)) {
26
30
  if ("calibration" in attempt) return {
27
31
  calibration: attempt.calibration,
28
32
  warnings
@@ -34,7 +38,16 @@ function selectCalibration(strings) {
34
38
  warnings
35
39
  };
36
40
  }
37
- function tryParse(name, text) {
41
+ /**
42
+ * The strings the recording carries, each parsed when asked for, in preference order.
43
+ */
44
+ function* attemptsOf(strings) {
45
+ for (const key of CALIBRATION_PREFERENCE) {
46
+ const text = strings[key];
47
+ if (text !== void 0) yield tryParse(CALIBRATION_SOURCES[key], text);
48
+ }
49
+ }
50
+ function tryParse({ name }, text) {
38
51
  try {
39
52
  return { calibration: parseOffsetString(text) };
40
53
  } catch (error) {
@@ -43,4 +56,4 @@ function tryParse(name, text) {
43
56
  }
44
57
  }
45
58
  //#endregion
46
- export { selectCalibration };
59
+ export { CALIBRATION_PREFERENCE, selectCalibration };
@@ -0,0 +1,20 @@
1
+ //#region ../../packages/core/src/domain/format/calibration/v6TermReading.ts
2
+ /**
3
+ * The five radial terms and the first tangential pair, the terms v3 carries too and reads the
4
+ * same way: on the X5, whose v3 and v6 strings describe the same lenses, this reading draws
5
+ * v3's image to about two canvas pixels. `p3`, `p4` and `s1` to `s4` are left out until real
6
+ * footage says how Insta360's stitch reads them.
7
+ */
8
+ var RADIAL_AND_FIRST_PAIR = ({ radial, tangential }) => {
9
+ const [p1, p2] = tangential;
10
+ return {
11
+ radial,
12
+ tangential: [{
13
+ p1,
14
+ p2
15
+ }],
16
+ thinPrism: []
17
+ };
18
+ };
19
+ //#endregion
20
+ export { RADIAL_AND_FIRST_PAIR };
@@ -0,0 +1,27 @@
1
+ import { InfoField } from "./infoFields.js";
2
+ //#region ../../packages/core/src/domain/format/info/calibrationSources.ts
3
+ /**
4
+ * Every calibration string the player reads, declared once: the parser reads the strings by it,
5
+ * and the calibration's selection names them by it. The record's type makes the compiler refuse
6
+ * a string of {@link CalibrationStrings} without a source.
7
+ */
8
+ var CALIBRATION_SOURCES = {
9
+ offset: {
10
+ field: InfoField.Offset,
11
+ name: "offset"
12
+ },
13
+ offsetV2: {
14
+ field: InfoField.OffsetV2,
15
+ name: "offset_v2"
16
+ },
17
+ offsetV3: {
18
+ field: InfoField.OffsetV3,
19
+ name: "offset_v3"
20
+ },
21
+ offsetV6: {
22
+ field: InfoField.OffsetV6,
23
+ name: "offset_v6"
24
+ }
25
+ };
26
+ //#endregion
27
+ export { CALIBRATION_SOURCES };
@@ -45,7 +45,7 @@ var InfoField = {
45
45
  */
46
46
  OffsetV2: 53,
47
47
  /**
48
- Unified (MEI) camera model calibration string (v3, or v6 with more coefficients).
48
+ Unified (MEI) camera model calibration string (v3).
49
49
  */
50
50
  OffsetV3: 54,
51
51
  IsRawGyro: 62,
@@ -64,7 +64,13 @@ var InfoField = {
64
64
  /**
65
65
  Observed (insta360-rs): 1 = track 0 is stream 10, 2 = track 0 is stream 00.
66
66
  */
67
- TrackOrder: 80
67
+ TrackOrder: 80,
68
+ /**
69
+ Unified (MEI) camera model calibration string with more distortion terms (v6, 27 tokens a
70
+ lens). Named "current V6 calibration" by insta360-rs; observed beside v1 to v3 on the X5, and
71
+ as the only calibration string of the X6.
72
+ */
73
+ OffsetV6: 111
68
74
  };
69
75
  var DimensionField = {
70
76
  Width: 1,
@@ -1,7 +1,9 @@
1
1
  import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
2
2
  import { DimensionField, FileLayoutValue, GyroConfigField, InfoField, PtsType, TrackOrderValue, WindowCropField } from "./infoFields.js";
3
+ import { CALIBRATION_SOURCES } from "./calibrationSources.js";
3
4
  import { InfoRecordFormat } from "../constants.js";
4
5
  import { ProtobufMessage } from "../../../shared/protobuf/ProtobufMessage.js";
6
+ import { mapRecord } from "../../../shared/mapRecord.js";
5
7
  import { milliseconds, millisecondsToSeconds } from "../../../shared/units/time.js";
6
8
  //#region ../../packages/core/src/domain/format/info/parseInfoRecord.ts
7
9
  /**
@@ -22,7 +24,7 @@ function parseInfoRecord(payload, format) {
22
24
  firstFrameTimestamp: message.varint(InfoField.FirstFrameTimestamp),
23
25
  readoutTime: secondsFromMilliseconds(message.double(InfoField.RollingShutterTimeMs)),
24
26
  windowCrop: windowCropOf(message.message(InfoField.WindowCropInfo)),
25
- gyroOffset: optionalMilliseconds(message.double(InfoField.GyroTimestampMs)),
27
+ gyroOffset: gyroOffsetOf(message),
26
28
  gyroType: message.varint(InfoField.GyroType),
27
29
  isRawGyro: message.boolean(InfoField.IsRawGyro),
28
30
  preferredFrameTimeSource: frameTimeSourceOf(message.varint(InfoField.PtsType)),
@@ -34,6 +36,13 @@ function parseInfoRecord(payload, format) {
34
36
  function secondsFromMilliseconds(value) {
35
37
  return value === void 0 ? void 0 : millisecondsToSeconds(milliseconds(value));
36
38
  }
39
+ /**
40
+ * The gyro's offset from the frames, only where the camera says it measured one (field 29), as
41
+ * telemetry-parser reads it: a value in field 28 without that is not one.
42
+ */
43
+ function gyroOffsetOf(message) {
44
+ return message.boolean(InfoField.HasGyroTimestamp) === true ? optionalMilliseconds(message.double(InfoField.GyroTimestampMs)) : void 0;
45
+ }
37
46
  function optionalMilliseconds(value) {
38
47
  return value === void 0 ? void 0 : milliseconds(value);
39
48
  }
@@ -56,11 +65,7 @@ function trackOrderOf(value) {
56
65
  return value === TrackOrderValue.Stream00First ? "stream-00-first" : void 0;
57
66
  }
58
67
  function calibrationStringsOf(message) {
59
- return {
60
- offset: message.string(InfoField.Offset),
61
- offsetV2: message.string(InfoField.OffsetV2),
62
- offsetV3: message.string(InfoField.OffsetV3)
63
- };
68
+ return mapRecord(CALIBRATION_SOURCES, (source) => message.string(source.field));
64
69
  }
65
70
  function dimensionOf(message) {
66
71
  const width = message?.varint(DimensionField.Width);
@@ -82,16 +82,25 @@ function toBodyFrame(frame, imuVector) {
82
82
  return transformVector(frame.toBody, imuVector);
83
83
  }
84
84
  /**
85
- * Measured on two X5 recordings (firmware 1.7 and 1.11, ADR 0009): the IMU sits rotated a quarter
86
- * turn about the camera's lateral axis, so the body's down is the IMU's z and the body's forward
87
- * is the IMU's minus y. Chosen by the world-stillness ranking in
88
- * `tools/integration/src/measure/imuFrameRanking.test.ts`, which every other arrangement loses.
85
+ * The IMU rotated a quarter turn about the camera's lateral axis: the body's down is the IMU's z
86
+ * and the body's forward is the IMU's minus y.
89
87
  */
90
- var X5_IMU_FRAME = measuredImuFrame("X5", [
88
+ var QUARTER_TURN_ABOUT_LATERAL = [
91
89
  "x",
92
90
  "z",
93
91
  "-y"
94
- ]);
92
+ ];
93
+ /**
94
+ * Measured on two X5 recordings (firmware 1.7 and 1.11, ADR 0009), chosen by the world-stillness
95
+ * ranking in `tools/integration/src/measure/imuFrameRanking.test.ts`, which every other
96
+ * arrangement loses.
97
+ */
98
+ var X5_IMU_FRAME = measuredImuFrame("X5", QUARTER_TURN_ABOUT_LATERAL);
99
+ /**
100
+ * Measured on one X6 recording (ADR 0009): the X5's arrangement, which the same ranking puts
101
+ * first by half the unstabilized movement, the runner-up close to none.
102
+ */
103
+ var X6_IMU_FRAME = measuredImuFrame("X6", QUARTER_TURN_ABOUT_LATERAL);
95
104
  /**
96
105
  * Until a recording proves otherwise, the IMU is assumed aligned with the body.
97
106
  */
@@ -100,7 +109,7 @@ var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
100
109
  "y",
101
110
  "z"
102
111
  ]);
103
- var IMU_FRAMES_BY_MODEL = [["Insta360 X5", X5_IMU_FRAME]];
112
+ var IMU_FRAMES_BY_MODEL = [["Insta360 X5", X5_IMU_FRAME], ["Insta360 X6", X6_IMU_FRAME]];
104
113
  /**
105
114
  * The IMU frame for a recording, chosen from the camera model the file names. A starting point
106
115
  * the recording's own data can be checked against with the ranking test, never the last word.
@@ -109,4 +118,4 @@ function imuFrameFor(hints) {
109
118
  return IMU_FRAMES_BY_MODEL.find(([prefix]) => hints.model?.startsWith(prefix))?.[1] ?? ALIGNED_IMU_FRAME;
110
119
  }
111
120
  //#endregion
112
- export { ALIGNED_IMU_FRAME, X5_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
121
+ export { ALIGNED_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
@@ -11,7 +11,6 @@ var GRID_TOLERANCE_FRAMES = .01;
11
11
  * Capture timing of every encoded frame, in frame order.
12
12
  */
13
13
  var FrameTimes = class FrameTimes {
14
- readoutTime;
15
14
  clock;
16
15
  captureTimes;
17
16
  shutterTimes;
@@ -21,7 +20,6 @@ var FrameTimes = class FrameTimes {
21
20
  this.clock = parts.clock;
22
21
  this.captureTimes = parts.captureTimes;
23
22
  this.shutterTimes = parts.shutterTimes;
24
- this.readoutTime = parts.readoutTime ?? seconds(0);
25
23
  this.frameDuration = parts.frameDuration;
26
24
  }
27
25
  /**
@@ -47,7 +45,7 @@ var FrameTimes = class FrameTimes {
47
45
  captureTime,
48
46
  videoTime,
49
47
  shutterTime,
50
- midExposureVideoTime: seconds(videoTime + (shutterTime ?? 0) / 2 + this.readoutTime / 2)
48
+ midExposureVideoTime: seconds(videoTime + (shutterTime ?? 0) / 2)
51
49
  };
52
50
  }
53
51
  /**
@@ -15,7 +15,6 @@ function fromExposureRecord(context) {
15
15
  clock,
16
16
  captureTimes: Float64Array.from(frames.captureTimes),
17
17
  shutterTimes: Float64Array.from(frames.shutterTimes),
18
- readoutTime: context.readoutTime,
19
18
  frameDuration: context.frameDuration
20
19
  });
21
20
  }
@@ -30,7 +29,6 @@ function fromTrackTimestamps(context) {
30
29
  return FrameTimes.withoutShutterTimes({
31
30
  clock,
32
31
  captureTimes,
33
- readoutTime: context.readoutTime,
34
32
  frameDuration: context.frameDuration
35
33
  });
36
34
  }
@@ -45,7 +43,6 @@ function fromNominalRate(context) {
45
43
  return FrameTimes.withoutShutterTimes({
46
44
  clock,
47
45
  captureTimes,
48
- readoutTime: context.readoutTime,
49
46
  frameDuration: context.frameDuration
50
47
  });
51
48
  }
@@ -0,0 +1,43 @@
1
+ import { ensureInvariant } from "../../shared/errors/GyroViewError.js";
2
+ //#region ../../packages/core/src/domain/optics/MeiDistortion.ts
3
+ /**
4
+ * The most terms of each family a calibration string carries (`offset_v6`: five radial terms,
5
+ * two tangential pairs, two thin-prism orders): what a renderer must hold room for.
6
+ */
7
+ var MEI_TERM_CAPACITY = {
8
+ radial: 5,
9
+ tangential: 2,
10
+ thinPrism: 2
11
+ };
12
+ /**
13
+ * Refuses a distortion with more terms of a family than {@link MEI_TERM_CAPACITY} holds: a
14
+ * defect of the reading that made it, found where the string is read rather than when a
15
+ * renderer packs it.
16
+ */
17
+ function ensureWithinTermCapacity(distortion) {
18
+ ensureFamilyFits("radial", distortion.radial.length);
19
+ ensureFamilyFits("tangential", distortion.tangential.length);
20
+ ensureFamilyFits("thinPrism", distortion.thinPrism.length);
21
+ }
22
+ function ensureFamilyFits(family, count) {
23
+ const capacity = MEI_TERM_CAPACITY[family];
24
+ ensureInvariant(count <= capacity, `a Mei distortion has ${count} ${family} terms, more than the ${capacity} a string carries`);
25
+ }
26
+ function distortMei(distortion, point) {
27
+ const [x, y] = point;
28
+ const r2 = x * x + y * y;
29
+ const radial = 1 + r2 * seriesIn(r2, distortion.radial, (k) => k);
30
+ const p1 = seriesIn(r2, distortion.tangential, (pair) => pair.p1);
31
+ const p2 = seriesIn(r2, distortion.tangential, (pair) => pair.p2);
32
+ const prismX = r2 * seriesIn(r2, distortion.thinPrism, (term) => term.x);
33
+ const prismY = r2 * seriesIn(r2, distortion.thinPrism, (term) => term.y);
34
+ return [radial * x + 2 * p1 * x * y + p2 * (r2 + 2 * x * x) + prismX, radial * y + p1 * (r2 + 2 * y * y) + 2 * p2 * x * y + prismY];
35
+ }
36
+ /**
37
+ * `c0 + c1 r² + c2 r⁴ + ...` of the terms' coefficients, by Horner's rule.
38
+ */
39
+ function seriesIn(r2, terms, coefficientOf) {
40
+ return terms.reduceRight((sum, term) => sum * r2 + coefficientOf(term), 0);
41
+ }
42
+ //#endregion
43
+ export { MEI_TERM_CAPACITY, distortMei, ensureWithinTermCapacity };