@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.
- package/CHANGELOG.md +49 -0
- package/README.md +20 -11
- package/dist/index.d.ts +56 -2
- package/dist/index.js +2 -2
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioPackager.js +49 -15
- package/dist/packages/adapters/mediabunny/src/MediabunnyCodecReader.js +3 -1
- package/dist/packages/adapters/mediabunny/src/decoderConfigurations.js +2 -2
- package/dist/packages/adapters/mediabunny/src/trackColour.js +21 -0
- package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +12 -1
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +3 -0
- package/dist/packages/adapters/three/src/conversionUniforms.js +61 -0
- package/dist/packages/adapters/three/src/glslLiterals.js +10 -0
- package/dist/packages/adapters/three/src/matrixCorrections.js +35 -0
- package/dist/packages/adapters/three/src/rendererUniforms.js +33 -12
- package/dist/packages/adapters/three/src/shaderPrograms.js +4 -2
- package/dist/packages/adapters/three/src/shaders/analysis.frag.js +1 -1
- package/dist/packages/adapters/three/src/shaders/displayConversion.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensModels.js +1 -1
- package/dist/packages/adapters/three/src/shaders/lensSampling.js +1 -1
- package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +1 -1
- package/dist/packages/adapters/three/src/shaders/stitch.frag.js +1 -1
- package/dist/packages/adapters/three/src/threeMatrix.js +10 -0
- package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +16 -5
- package/dist/packages/adapters/webcodecs/src/colorSpaceOf.js +32 -0
- package/dist/packages/core/src/application/playback/probeDecoding.js +2 -2
- package/dist/packages/core/src/application/recording/frameTimesOf.js +0 -1
- package/dist/packages/core/src/domain/colour/DisplayConversion.js +137 -0
- package/dist/packages/core/src/domain/colour/TrackColour.js +38 -0
- package/dist/packages/core/src/domain/colour/matrixCorrection.js +76 -0
- package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +7 -4
- package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +1 -1
- package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +1 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +109 -35
- package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +1 -0
- package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +33 -4
- package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +22 -17
- package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +28 -15
- package/dist/packages/core/src/domain/format/calibration/v6TermReading.js +20 -0
- package/dist/packages/core/src/domain/format/info/calibrationSources.js +27 -0
- package/dist/packages/core/src/domain/format/info/infoFields.js +8 -2
- package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +11 -6
- package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +17 -8
- package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +1 -3
- package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +0 -3
- package/dist/packages/core/src/domain/optics/MeiDistortion.js +43 -0
- package/dist/packages/core/src/domain/optics/MeiModel.js +6 -14
- package/dist/packages/core/src/domain/optics/scaledProjection.js +30 -0
- package/dist/packages/core/src/domain/stitching/StitchingSetup.js +13 -4
- package/dist/packages/core/src/index.js +7 -3
- package/dist/packages/core/src/shared/errors/GyroViewError.js +55 -3
- package/dist/packages/core/src/shared/index.js +11 -0
- package/dist/packages/core/src/shared/mapRecord.js +11 -0
- package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +12 -26
- package/dist/packages/core/src/shared/protobuf/wireFormat.js +16 -0
- package/dist/packages/player/src/composition/buildPipeline.js +2 -1
- package/dist/packages/player/src/composition/openInputs.js +4 -1
- package/dist/packages/player/src/controls/messages.js +2 -1
- package/dist/standalone.js +211 -100
- 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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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 };
|
package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
15
|
-
*
|
|
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
|
|
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(
|
|
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(
|
|
34
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
86
|
-
*
|
|
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
|
|
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
|
|
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 };
|