@bubo-squared/gyroview 0.3.1 → 0.4.1

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 (55) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +3 -3
  3. package/dist/index.d.ts +36 -0
  4. package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +49 -15
  5. package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +3 -1
  6. package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +2 -2
  7. package/dist/packages/adapters/mediabunny/src/trackColour.js +21 -0
  8. package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +12 -1
  9. package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +3 -0
  10. package/dist/packages/adapters/three/src/conversionUniforms.js +61 -0
  11. package/dist/packages/adapters/three/src/glslLiterals.js +10 -0
  12. package/dist/packages/adapters/three/src/matrixCorrections.js +35 -0
  13. package/dist/packages/adapters/three/src/rendererUniforms.js +33 -12
  14. package/dist/packages/adapters/three/src/shaderPrograms.js +4 -2
  15. package/dist/packages/adapters/three/src/shaders/analysis.frag.js +1 -1
  16. package/dist/packages/adapters/three/src/shaders/displayConversion.js +4 -0
  17. package/dist/packages/adapters/three/src/shaders/lensModels.js +1 -1
  18. package/dist/packages/adapters/three/src/shaders/lensSampling.js +1 -1
  19. package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
  20. package/dist/packages/adapters/three/src/shaders/stitch.frag.js +1 -1
  21. package/dist/packages/adapters/three/src/threeMatrix.js +10 -0
  22. package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +4 -2
  23. package/dist/packages/adapters/webcodecs/src/colorSpaceOf.js +32 -0
  24. package/dist/packages/core/src/application/recording/frameTimesOf.js +0 -1
  25. package/dist/packages/core/src/domain/colour/DisplayConversion.js +137 -0
  26. package/dist/packages/core/src/domain/colour/TrackColour.js +38 -0
  27. package/dist/packages/core/src/domain/colour/matrixCorrection.js +76 -0
  28. package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +7 -4
  29. package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +1 -1
  30. package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +1 -0
  31. package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +109 -35
  32. package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +1 -0
  33. package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +33 -4
  34. package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +22 -17
  35. package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +28 -15
  36. package/dist/packages/core/src/domain/format/calibration/v6TermReading.js +20 -0
  37. package/dist/packages/core/src/domain/format/info/calibrationSources.js +27 -0
  38. package/dist/packages/core/src/domain/format/info/infoFields.js +8 -2
  39. package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +11 -6
  40. package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +31 -9
  41. package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +1 -3
  42. package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +0 -3
  43. package/dist/packages/core/src/domain/optics/MeiDistortion.js +43 -0
  44. package/dist/packages/core/src/domain/optics/MeiModel.js +6 -14
  45. package/dist/packages/core/src/domain/optics/scaledProjection.js +30 -0
  46. package/dist/packages/core/src/domain/stitching/StitchingSetup.js +13 -4
  47. package/dist/packages/core/src/index.js +6 -2
  48. package/dist/packages/core/src/shared/index.js +11 -0
  49. package/dist/packages/core/src/shared/mapRecord.js +11 -0
  50. package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +12 -26
  51. package/dist/packages/core/src/shared/protobuf/wireFormat.js +16 -0
  52. package/dist/packages/player/src/composition/buildPipeline.js +2 -1
  53. package/dist/packages/player/src/composition/openInputs.js +4 -1
  54. package/dist/standalone.js +211 -100
  55. package/package.json +2 -2
@@ -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,30 @@ 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);
104
+ /**
105
+ * Measured on one X4 Air recording (ADR 0009): the X5's arrangement again, which the ranking puts
106
+ * first, the aligned frame third.
107
+ */
108
+ var X4_AIR_IMU_FRAME = measuredImuFrame("X4 Air", QUARTER_TURN_ABOUT_LATERAL);
95
109
  /**
96
110
  * Until a recording proves otherwise, the IMU is assumed aligned with the body.
97
111
  */
@@ -100,13 +114,21 @@ var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
100
114
  "y",
101
115
  "z"
102
116
  ]);
103
- var IMU_FRAMES_BY_MODEL = [["Insta360 X5", X5_IMU_FRAME]];
117
+ /**
118
+ * Keyed by the whole model name: a name that extends a measured one ("X4 Air" of "X4", an "X5
119
+ * Pro") is another camera, unmeasured until a recording of it is ranked.
120
+ */
121
+ var IMU_FRAMES_BY_MODEL = /* @__PURE__ */ new Map([
122
+ ["Insta360 X4 Air", X4_AIR_IMU_FRAME],
123
+ ["Insta360 X5", X5_IMU_FRAME],
124
+ ["Insta360 X6", X6_IMU_FRAME]
125
+ ]);
104
126
  /**
105
127
  * The IMU frame for a recording, chosen from the camera model the file names. A starting point
106
128
  * the recording's own data can be checked against with the ranking test, never the last word.
107
129
  */
108
130
  function imuFrameFor(hints) {
109
- return IMU_FRAMES_BY_MODEL.find(([prefix]) => hints.model?.startsWith(prefix))?.[1] ?? ALIGNED_IMU_FRAME;
131
+ return IMU_FRAMES_BY_MODEL.get(hints.model?.trim() ?? "") ?? ALIGNED_IMU_FRAME;
110
132
  }
111
133
  //#endregion
112
- export { ALIGNED_IMU_FRAME, X5_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
134
+ export { ALIGNED_IMU_FRAME, X4_AIR_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 };
@@ -1,9 +1,10 @@
1
1
  import { HALF_FIELD_OF_VIEW } from "./opticsConstants.js";
2
+ import { distortMei, ensureWithinTermCapacity } from "./MeiDistortion.js";
2
3
  //#region ../../packages/core/src/domain/optics/MeiModel.ts
3
4
  /**
4
- * Unified (Mei) omnidirectional camera model with radial-tangential distortion, as stored in
5
- * `offset_v3`. A direction is projected onto the unit sphere, then from a point `xi` behind the
6
- * sphere centre onto the normalised image plane, then distorted and scaled.
5
+ * Unified (Mei) omnidirectional camera model with radial, tangential and thin-prism distortion,
6
+ * as stored in `offset_v3` and `offset_v6`. A direction is projected onto the unit sphere, then from a point
7
+ * `xi` behind the sphere centre onto the normalised image plane, then distorted and scaled.
7
8
  */
8
9
  var MeiModel = class {
9
10
  parameters;
@@ -12,6 +13,7 @@ var MeiModel = class {
12
13
  principalPoint;
13
14
  constructor(parameters) {
14
15
  this.parameters = parameters;
16
+ ensureWithinTermCapacity(parameters.distortion);
15
17
  this.principalPoint = parameters.principalPoint;
16
18
  }
17
19
  get projection() {
@@ -24,22 +26,12 @@ var MeiModel = class {
24
26
  const [x, y, z] = direction;
25
27
  const depth = z + this.parameters.xi;
26
28
  if (depth <= 0 || Math.atan2(Math.hypot(x, y), z) > this.halfFieldOfView) return void 0;
27
- const normalised = [x / depth, y / depth];
28
- const [distortedX, distortedY] = this.distort(normalised);
29
+ const [distortedX, distortedY] = distortMei(this.parameters.distortion, [x / depth, y / depth]);
29
30
  return {
30
31
  x: this.parameters.focal[0] * distortedX + this.principalPoint.x,
31
32
  y: this.parameters.focal[1] * distortedY + this.principalPoint.y
32
33
  };
33
34
  }
34
- distort([mx, my]) {
35
- const [k1, k2, k3] = this.parameters.radial;
36
- const [p1, p2] = this.parameters.tangential;
37
- const r2 = mx * mx + my * my;
38
- const radial = 1 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2;
39
- const tangentialX = 2 * p1 * mx * my + p2 * (r2 + 2 * mx * mx);
40
- const tangentialY = p1 * (r2 + 2 * my * my) + 2 * p2 * mx * my;
41
- return [radial * mx + tangentialX, radial * my + tangentialY];
42
- }
43
35
  };
44
36
  //#endregion
45
37
  export { MeiModel };
@@ -0,0 +1,30 @@
1
+ //#region ../../packages/core/src/domain/optics/scaledProjection.ts
2
+ /**
3
+ * The projection with every image radius multiplied by `scale` about the principal point: the
4
+ * Mei model's focal lengths, or the radial polynomial's coefficients. Exact at a scale of 1.
5
+ */
6
+ function scaledProjection(projection, scale) {
7
+ switch (projection.kind) {
8
+ case "mei": {
9
+ const [fx, fy] = projection.focal;
10
+ return {
11
+ ...projection,
12
+ focal: [fx * scale, fy * scale]
13
+ };
14
+ }
15
+ case "radial-polynomial": {
16
+ const [c1, c2, c3, c4] = projection.coefficients;
17
+ return {
18
+ ...projection,
19
+ coefficients: [
20
+ c1 * scale,
21
+ c2 * scale,
22
+ c3 * scale,
23
+ c4 * scale
24
+ ]
25
+ };
26
+ }
27
+ }
28
+ }
29
+ //#endregion
30
+ export { scaledProjection };
@@ -1,7 +1,8 @@
1
1
  import { degrees, degreesToRadians } from "../../shared/units/angle.js";
2
- import { GyroViewError } from "../../shared/errors/GyroViewError.js";
2
+ import { GyroViewError, ensureInvariant } from "../../shared/errors/GyroViewError.js";
3
3
  import { lensRotation } from "../optics/lensPose.js";
4
4
  import { SEAM_RING_ANGLE } from "./seamRing.js";
5
+ import { scaledProjection } from "../optics/scaledProjection.js";
5
6
  //#region ../../packages/core/src/domain/stitching/StitchingSetup.ts
6
7
  /**
7
8
  * Two 200-degree lenses overlap between 80 and 100 degrees from their axes; the blend spans 5
@@ -33,23 +34,31 @@ function buildStitchingSetup(inputs) {
33
34
  const { calibration, layout } = inputs;
34
35
  if (calibration.lenses.length !== layout.sources.length) throw new GyroViewError("unsupported-layout", `the calibration describes ${calibration.lenses.length} lenses but the layout has ${layout.sources.length}`);
35
36
  const frames = lensFrameOrder(layout);
37
+ ensureInvariant(inputs.displayConversions.length === frames.length, `${inputs.displayConversions.length} display conversions for ${frames.length} frame sources`);
36
38
  return {
37
39
  lenses: layout.sources.map((source) => {
38
40
  const lens = lensOf(calibration, source.lensIndex);
41
+ const frameSlot = frames.findIndex((key) => isSameSource(key, source));
39
42
  return {
40
43
  lensIndex: source.lensIndex,
41
- frameSlot: frames.findIndex((key) => isSameSource(key, source)),
44
+ frameSlot,
42
45
  region: source.region,
43
46
  window: canvasWindowOf(calibration, lens),
44
47
  rotation: lensRotation(lens),
45
- projection: lens.model.projection,
46
- halfFieldOfView: lens.model.halfFieldOfView
48
+ projection: scaledProjection(lens.model.projection, calibration.radialScale),
49
+ halfFieldOfView: lens.model.halfFieldOfView,
50
+ displayConversion: conversionAt(inputs.displayConversions, frameSlot)
47
51
  };
48
52
  }),
49
53
  frameSlotCount: frames.length,
50
54
  feather: DEFAULT_FEATHER
51
55
  };
52
56
  }
57
+ function conversionAt(conversions, frameSlot) {
58
+ const conversion = conversions[frameSlot];
59
+ ensureInvariant(conversion !== void 0, `no display conversion for frame slot ${frameSlot}`);
60
+ return conversion;
61
+ }
53
62
  function isSameSource(key, source) {
54
63
  return key.inputIndex === source.inputIndex && key.trackIndex === source.trackIndex;
55
64
  }
@@ -1,7 +1,7 @@
1
1
  import { degrees } from "./shared/units/angle.js";
2
- import "./domain/optics/EquidistantModel.js";
3
2
  import "./domain/format/calibration/CalibrationVersion.js";
4
3
  import { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf } from "./shared/errors/GyroViewError.js";
4
+ import { MEI_TERM_CAPACITY } from "./domain/optics/MeiDistortion.js";
5
5
  import { lazy } from "./shared/lazy.js";
6
6
  import { microseconds, microsecondsToSeconds, milliseconds, seconds, secondsToMicroseconds } from "./shared/units/time.js";
7
7
  import "./domain/motion/exposure/ExposureRecord.js";
@@ -48,8 +48,12 @@ import "./domain/stitching/seamMismatch.js";
48
48
  import "./domain/stitching/seamDisparity.js";
49
49
  import "./domain/stitching/seamDisparityField.js";
50
50
  import "./domain/stitching/seamJoin.js";
51
+ import { COLOUR_PRIMARIES, MATRIX_COEFFICIENTS, TRANSFER_CHARACTERISTICS, namedOrUnspecified } from "./domain/colour/TrackColour.js";
52
+ import { BT709_LUMINANCE, HLG_OETF, displayConversionsOf } from "./domain/colour/DisplayConversion.js";
53
+ import { matrixCorrectionOf } from "./domain/colour/matrixCorrection.js";
51
54
  import { keysOf } from "./shared/keysOf.js";
52
55
  import { Outbox } from "./shared/events/Outbox.js";
56
+ import "./shared/index.js";
53
57
  import { Ending, ITERATION_END } from "./shared/async/iteration.js";
54
58
  import { SourceByteStream } from "./application/download/SourceByteStream.js";
55
59
  import { startFileDownload } from "./application/download/startFileDownload.js";
@@ -64,4 +68,4 @@ import { WallClock } from "./application/playback/WallClock.js";
64
68
  import { isFlowing } from "./domain/playback/PlayerState.js";
65
69
  import { TypedEmitter } from "./shared/events/TypedEmitter.js";
66
70
  import { PlaybackSession } from "./application/playback/PlaybackSession.js";
67
- export { ByteRange, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, IDENTITY_MATRIX3, ITERATION_END, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, messageOf, microseconds, microsecondsToSeconds, milliseconds, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
71
+ export { BT709_LUMINANCE, ByteRange, COLOUR_PRIMARIES, DEFAULT_FRAMING, DEFAULT_PICTURE_QUALITY, DEFAULT_STABILIZATION_MODE, DEFAULT_VIEW_MODE, Deferred, Ending, GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GainMatchingFrameSink, GyroViewError, HLG_OETF, IDENTITY_MATRIX3, ITERATION_END, MATRIX_COEFFICIENTS, MEI_TERM_CAPACITY, Outbox, PICTURE_QUALITIES, PlaybackSession, RecordingBuffer, RunStop, SCREEN_CENTRE, STABILIZATION_MODES, STOPPED, SourceByteStream, StabilizingFrameSink, TRANSFER_CHARACTERISTICS, TypedEmitter, VIEW_MODES, WallClock, asGyroViewError, aspectOf, aspectOfArea, buildStitchingSetup, clamp, clampView, degrees, detectLensLayout, displayConversionsOf, downloadPolicyFor, ensureIndexInRange, ensureInvariant, fileNameOfUrl, hasErrorCode, inspectRecording, isAbortError, isFlowing, isGyroViewErrorCode, isSameFraming, isSameView, keysOf, lazy, lensFrameOrder, locateOtherLensFile, lookAt, matrixCorrectionOf, messageOf, microseconds, microsecondsToSeconds, milliseconds, namedOrUnspecified, pixelRatioCapOf, planeHalfExtentOf, probeDecoding, readRecording, readSampleTable, seconds, secondsToMicroseconds, stabilizerFor, startFileDownload, timeRecording, viewModeRulesFor, zoomStepsForPinch };
@@ -0,0 +1,11 @@
1
+ import { degrees } from "./units/angle.js";
2
+ import { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, asGyroViewError, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, messageOf } from "./errors/GyroViewError.js";
3
+ import { lazy } from "./lazy.js";
4
+ import { microseconds, microsecondsToSeconds, milliseconds, seconds, secondsToMicroseconds } from "./units/time.js";
5
+ import "./math/Vector3.js";
6
+ import { clamp } from "./math/clamp.js";
7
+ import { IDENTITY_MATRIX3 } from "./math/Matrix3.js";
8
+ import "./math/Quaternion.js";
9
+ import { keysOf } from "./keysOf.js";
10
+ import { Outbox } from "./events/Outbox.js";
11
+ export { GYRO_VIEW_ERROR_CATEGORIES, GYRO_VIEW_ERROR_CODES, GyroViewError, IDENTITY_MATRIX3, Outbox, asGyroViewError, clamp, degrees, ensureIndexInRange, ensureInvariant, hasErrorCode, isAbortError, isGyroViewErrorCode, keysOf, lazy, messageOf, microseconds, microsecondsToSeconds, milliseconds, seconds, secondsToMicroseconds };
@@ -0,0 +1,11 @@
1
+ //#region ../../packages/core/src/shared/mapRecord.ts
2
+ /**
3
+ * A record with each value of `every` turned by `map`, under the same keys: a table declared once
4
+ * becomes a record of what it describes, and the record's type keeps every key of the table.
5
+ */
6
+ function mapRecord(every, map) {
7
+ const entries = Object.entries(every).map(([key, value]) => [key, map(value, key)]);
8
+ return Object.fromEntries(entries);
9
+ }
10
+ //#endregion
11
+ export { mapRecord };