@bvdmitri/gyroview 0.9.0-cutaway.1 → 0.9.0-cutaway.3
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 +10 -0
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +9 -0
- package/dist/packages/adapters/three/src/rendererUniforms.js +3 -0
- package/dist/packages/adapters/three/src/rowCorrections.js +18 -0
- package/dist/packages/adapters/three/src/shaders/lensSampling.js +1 -1
- package/dist/packages/core/src/application/recording/motionOf.js +3 -1
- package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +5 -1
- package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +13 -1
- package/dist/packages/core/src/domain/motion/rollingShutter/RollingShutter.js +46 -0
- package/dist/packages/player/src/composition/buildPipeline.js +2 -1
- package/dist/standalone.js +111 -90
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,13 @@ Added (ADR 0045, a recording is rendered frame by frame at the pace its consumer
|
|
|
16
16
|
a multiple of four, or a span that holds no frame, is refused with `invalid-argument`. Its types,
|
|
17
17
|
`Rendition` and `RenditionOptions`, are exported.
|
|
18
18
|
|
|
19
|
+
Added (ADR 0046, each row is turned for the time its sensor read it):
|
|
20
|
+
|
|
21
|
+
- An Insta360 ONE R recording has each row of its lenses turned by the camera's turn between the
|
|
22
|
+
frame's time and the time its sensor read that row, in every stabilization mode: a fast turn no
|
|
23
|
+
longer skews the top and bottom of the picture against its middle. Other cameras turn whole
|
|
24
|
+
until their sensors' row order is measured.
|
|
25
|
+
|
|
19
26
|
Fixed:
|
|
20
27
|
|
|
21
28
|
- A clip trimmed in the Insta360 app is stabilized with the gyro of its own frames. The app keeps
|
|
@@ -24,6 +31,9 @@ Fixed:
|
|
|
24
31
|
the clip begins.
|
|
25
32
|
- The two files of a split-lens clip trimmed in the Insta360 app (`VID_..._00_002-trim.insv`) are
|
|
26
33
|
found as a pair: the name's suffix after the sequence number is kept for the other lens's file.
|
|
34
|
+
- An Insta360 ONE R recording is stabilized in its own IMU frame. Its info record names it
|
|
35
|
+
"Insta360 OneR", which no entry matched, so it was stabilized under the aligned guess and lock
|
|
36
|
+
turned its picture more than leaving it alone; `ONE_R_IMU_FRAME` is measured (ADR 0009).
|
|
27
37
|
|
|
28
38
|
## 0.8.1 (2026-10-08)
|
|
29
39
|
|
|
@@ -9,6 +9,7 @@ import { SAMPLING_STRATEGIES } from "./samplingStrategies.js";
|
|
|
9
9
|
import { applyLensGain, applyPicture, applyShaderSampling, applyStabilization } from "./rendererUniforms.js";
|
|
10
10
|
import { SeamMeterPass } from "./seamMeter/SeamMeterPass.js";
|
|
11
11
|
import { PLAYER_PICTURES, compileAgain, openRendererParts } from "./rendererParts.js";
|
|
12
|
+
import { applyRowCorrections } from "./rowCorrections.js";
|
|
12
13
|
import { scissorBoxOf } from "./scissorBox.js";
|
|
13
14
|
//#region ../../packages/adapters/three/src/ThreeFrameRenderer.ts
|
|
14
15
|
/**
|
|
@@ -97,6 +98,14 @@ var ThreeFrameRenderer = class {
|
|
|
97
98
|
this.ensureLive();
|
|
98
99
|
applyStabilization(this.parts.uniforms, rotation);
|
|
99
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Turns each lens row by its rolling-shutter correction, or every row with the frame. Does not
|
|
103
|
+
* redraw by itself, as {@link setStabilization} does not.
|
|
104
|
+
*/
|
|
105
|
+
setRowCorrections(corrections) {
|
|
106
|
+
this.ensureLive();
|
|
107
|
+
applyRowCorrections(this.parts.uniforms, corrections);
|
|
108
|
+
}
|
|
100
109
|
present(presentation) {
|
|
101
110
|
this.ensureLive();
|
|
102
111
|
const { frames } = presentation.pair;
|
|
@@ -18,6 +18,7 @@ var SHADER_DEFINES = Object.fromEntries([
|
|
|
18
18
|
["MEI_RADIAL_TERMS", MEI_TERM_CAPACITY.radial],
|
|
19
19
|
["MEI_TANGENTIAL_ORDERS", MEI_TERM_CAPACITY.tangential],
|
|
20
20
|
["MEI_THIN_PRISM_ORDERS", MEI_TERM_CAPACITY.thinPrism],
|
|
21
|
+
["ROW_CORRECTIONS", 9],
|
|
21
22
|
...CONVERSION_DEFINES,
|
|
22
23
|
["SAMPLING_BILINEAR", 0],
|
|
23
24
|
["SAMPLING_TRILINEAR", 1],
|
|
@@ -39,6 +40,8 @@ function viewUniforms(setup) {
|
|
|
39
40
|
uLensCount: { value: setup.lenses.length },
|
|
40
41
|
uViewRotation: { value: new Matrix3() },
|
|
41
42
|
uStabilization: { value: new Matrix3() },
|
|
43
|
+
uRowCorrected: { value: false },
|
|
44
|
+
uRowCorrection: { value: Array.from({ length: 9 }, () => new Matrix3()) },
|
|
42
45
|
uPlaneHalfExtent: { value: 1 },
|
|
43
46
|
uPictureAspect: { value: 1 },
|
|
44
47
|
uScreenArea: { value: Array.from({ length: 2 }, () => new Vector4(0, 0, 1, 1)) },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ensureInvariant } from "../../../core/src/shared/errors/GyroViewError.js";
|
|
2
|
+
import { toThreeMatrix } from "./threeMatrix.js";
|
|
3
|
+
//#region ../../packages/adapters/three/src/rowCorrections.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sets the rows' rolling-shutter corrections, from the first row of a lens's window to its last,
|
|
6
|
+
* or turns every row with the frame when there are none.
|
|
7
|
+
*/
|
|
8
|
+
function applyRowCorrections(uniforms, corrections) {
|
|
9
|
+
if (!corrections) {
|
|
10
|
+
uniforms.uRowCorrected.value = false;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
ensureInvariant(corrections.length === 9, `a frame's rows take 9 corrections, not ${corrections.length}`);
|
|
14
|
+
uniforms.uRowCorrection.value = corrections.map((rotation) => toThreeMatrix(rotation));
|
|
15
|
+
uniforms.uRowCorrected.value = true;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { applyRowCorrections };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
//#region ../../packages/adapters/three/src/shaders/lensSampling.glsl?raw
|
|
2
|
-
var lensSampling_default = "// How a body direction becomes a texel of one lens, for the stitch and the seam meters: the\n// pose and field as uniforms, and the sampling through each lens model, the texel as its\n// exposure signal. Needs lensTextures.glsl, displayConversion.glsl and lensModels.glsl.\nuniform vec2 uFeather;\nuniform mat3 uLensRotation[MAX_LENSES];\nuniform int uLensKind[MAX_LENSES];\nuniform float uLensHalfFov[MAX_LENSES];\nuniform vec4 uLensWindow[MAX_LENSES];\n\nstruct LensSample {\n bool isImaged;\n // Angle from the lens's optical axis.\n float theta;\n // The texel where the lens's exposure is a factor (displayConversion.glsl).\n vec3 signal;\n};\n\n// Where lens i images direction d on its calibration canvas; the principal point, and not\n// known, for a kind of lens this shader does not know.\nvec2 canvasPixel(int i, vec3 d, float theta, out bool isKnown) {\n isKnown = true;\n if (uLensKind[i] == LENS_MEI) {\n return projectMei(i, d);\n }\n if (uLensKind[i] == LENS_RADIAL_POLYNOMIAL) {\n return projectRadialPolynomial(i, d, theta);\n }\n isKnown = false;\n return uLensPrincipalPoint[i];\n}\n\n// What lens i shows in a body direction, read as `sampling` says, if it images it at all:\n// outside its field, or beyond the window the frame shows, nothing. The footprint is taken and\n// the texture read in every fragment before the gates: a read inside a branch that some pixels\n// of a 2x2 quad skip took a far too coarse footprint on Apple GPUs (rawLenses.frag.glsl).\nLensSample sampleLensWith(int i, vec3 dirBody, int sampling) {\n vec3 d = uLensRotation[i] * dirBody;\n // atan keeps its precision near the axis where acos loses it.\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n vec2 windowUv = (pixel - uLensWindow[i].xy) / uLensWindow[i].zw;\n Footprint footprint = footprintOf(i, windowUv);\n bool isInWindow =\n all(greaterThanEqual(windowUv, vec2(0.0))) && all(lessThanEqual(windowUv, vec2(1.0)));\n vec3 texel = sampleLens(uLensTexture[i], footprint, sampling).rgb;\n LensSample result;\n result.theta = theta;\n result.isImaged = isKnown && theta < uLensHalfFov[i] && isInWindow;\n result.signal = result.isImaged ? exposureSignalOf(i, texel) : vec3(0.0);\n return result;\n}\n\n// What lens i shows in a body direction, read as the picture is.\nLensSample sampleLensAt(int i, vec3 dirBody) {\n return sampleLensWith(i, dirBody, uSampling);\n}\n";
|
|
2
|
+
var lensSampling_default = "// How a body direction becomes a texel of one lens, for the stitch and the seam meters: the\n// pose and field as uniforms, and the sampling through each lens model, the texel as its\n// exposure signal. Needs lensTextures.glsl, displayConversion.glsl and lensModels.glsl.\nuniform vec2 uFeather;\nuniform mat3 uLensRotation[MAX_LENSES];\nuniform int uLensKind[MAX_LENSES];\nuniform float uLensHalfFov[MAX_LENSES];\nuniform vec4 uLensWindow[MAX_LENSES];\n// Rolling-shutter corrections (ADR 0046): the body as it was when each row was read, from the\n// body at the frame's time, for rows from the top of every lens's window to its bottom.\nuniform bool uRowCorrected;\nuniform mat3 uRowCorrection[ROW_CORRECTIONS];\n\nstruct LensSample {\n bool isImaged;\n // Angle from the lens's optical axis.\n float theta;\n // The texel where the lens's exposure is a factor (displayConversion.glsl).\n vec3 signal;\n};\n\n// Where lens i images direction d on its calibration canvas; the principal point, and not\n// known, for a kind of lens this shader does not know.\nvec2 canvasPixel(int i, vec3 d, float theta, out bool isKnown) {\n isKnown = true;\n if (uLensKind[i] == LENS_MEI) {\n return projectMei(i, d);\n }\n if (uLensKind[i] == LENS_RADIAL_POLYNOMIAL) {\n return projectRadialPolynomial(i, d, theta);\n }\n isKnown = false;\n return uLensPrincipalPoint[i];\n}\n\n// The body direction as it was when lens i read the row it images the direction in: found by\n// where the direction lands at the frame's time, which the correction moves by a few rows at\n// most, so one step is enough.\nvec3 rowCorrected(int i, vec3 dirBody) {\n if (!uRowCorrected) return dirBody;\n vec3 d = uLensRotation[i] * dirBody;\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n float row = clamp((pixel.y - uLensWindow[i].y) / uLensWindow[i].w, 0.0, 1.0);\n float step = row * float(ROW_CORRECTIONS - 1);\n int below = min(int(floor(step)), ROW_CORRECTIONS - 2);\n vec3 above = uRowCorrection[below] * dirBody;\n vec3 next = uRowCorrection[below + 1] * dirBody;\n return normalize(mix(above, next, step - float(below)));\n}\n\n// What lens i shows in a body direction, read as `sampling` says, if it images it at all:\n// outside its field, or beyond the window the frame shows, nothing. The footprint is taken and\n// the texture read in every fragment before the gates: a read inside a branch that some pixels\n// of a 2x2 quad skip took a far too coarse footprint on Apple GPUs (rawLenses.frag.glsl).\nLensSample sampleLensWith(int i, vec3 dirBody, int sampling) {\n vec3 d = uLensRotation[i] * rowCorrected(i, dirBody);\n // atan keeps its precision near the axis where acos loses it.\n float theta = atan(length(d.xy), d.z);\n bool isKnown;\n vec2 pixel = canvasPixel(i, d, theta, isKnown);\n vec2 windowUv = (pixel - uLensWindow[i].xy) / uLensWindow[i].zw;\n Footprint footprint = footprintOf(i, windowUv);\n bool isInWindow =\n all(greaterThanEqual(windowUv, vec2(0.0))) && all(lessThanEqual(windowUv, vec2(1.0)));\n vec3 texel = sampleLens(uLensTexture[i], footprint, sampling).rgb;\n LensSample result;\n result.theta = theta;\n result.isImaged = isKnown && theta < uLensHalfFov[i] && isInWindow;\n result.signal = result.isImaged ? exposureSignalOf(i, texel) : vec3(0.0);\n return result;\n}\n\n// What lens i shows in a body direction, read as the picture is.\nLensSample sampleLensAt(int i, vec3 dirBody) {\n return sampleLensWith(i, dirBody, uSampling);\n}\n";
|
|
3
3
|
//#endregion
|
|
4
4
|
export { lensSampling_default as default };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { imuFrameFor } from "../../domain/motion/imu/ImuFrame.js";
|
|
2
2
|
import { mountingOf, uprightImuFrame } from "../../domain/motion/mounting/Mounting.js";
|
|
3
3
|
import { OrientationTrack } from "../../domain/motion/orientation/OrientationTrack.js";
|
|
4
|
+
import { rollingShutterFor } from "../../domain/motion/rollingShutter/RollingShutter.js";
|
|
4
5
|
//#region ../../packages/core/src/application/recording/motionOf.ts
|
|
5
6
|
var NO_GYRO_WARNING = "the recording has no gyro record; stabilization is unavailable";
|
|
6
7
|
/**
|
|
@@ -27,7 +28,8 @@ async function motionOf(recording, clock) {
|
|
|
27
28
|
setup: {
|
|
28
29
|
orientations,
|
|
29
30
|
mounting,
|
|
30
|
-
imuFrame
|
|
31
|
+
imuFrame,
|
|
32
|
+
rollingShutter: rollingShutterFor(recording.info)
|
|
31
33
|
},
|
|
32
34
|
warnings
|
|
33
35
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { rotationIntoBody } from "../../domain/motion/mounting/Mounting.js";
|
|
2
|
+
import { rowCorrectionsAt } from "../../domain/motion/rollingShutter/RollingShutter.js";
|
|
2
3
|
import { OffStabilization } from "../../domain/motion/stabilization/stabilizers.js";
|
|
3
4
|
//#region ../../packages/core/src/application/stabilization/StabilizingFrameSink.ts
|
|
4
5
|
/**
|
|
5
6
|
* Use case: before every presentation, looks up the camera's orientation at the frame's
|
|
6
|
-
* exposure and tells the sink how to turn the picture for the current stabilization mode
|
|
7
|
+
* exposure and tells the sink how to turn the picture for the current stabilization mode, and,
|
|
8
|
+
* for a camera whose rolling shutter is known, how to turn each row for the time it was read.
|
|
7
9
|
*/
|
|
8
10
|
var StabilizingFrameSink = class {
|
|
9
11
|
parts;
|
|
@@ -19,6 +21,8 @@ var StabilizingFrameSink = class {
|
|
|
19
21
|
const { orientations, mounting } = this.parts.motion;
|
|
20
22
|
const rotation = this.stabilizer.nextRotation(orientations.orientationAt(time), time);
|
|
21
23
|
this.parts.sink.setStabilization(rotationIntoBody(mounting, rotation));
|
|
24
|
+
const { rollingShutter } = this.parts;
|
|
25
|
+
if (rollingShutter) this.parts.sink.setRowCorrections(rowCorrectionsAt(this.parts.motion, time, rollingShutter));
|
|
22
26
|
this.parts.sink.present(presentation);
|
|
23
27
|
}
|
|
24
28
|
/**
|
|
@@ -123,6 +123,17 @@ var A1_IMU_FRAME = measuredImuFrame("A1", [
|
|
|
123
123
|
*/
|
|
124
124
|
var X3_IMU_FRAME = measuredImuFrame("X3", QUARTER_TURN_ABOUT_LATERAL);
|
|
125
125
|
/**
|
|
126
|
+
* Measured on one ONE R recording with the 5.7K 360 lens module, a skydive that turns throughout
|
|
127
|
+
* (ADR 0009): the stillness ranking puts it first in Chromium and WebKit, 41.0 against 52.1
|
|
128
|
+
* unstabilized and 46.8 for the next. The X5's arrangement turned a half turn about the lateral
|
|
129
|
+
* axis. The ONE R names itself "Insta360 OneR", another model than the ONE RS.
|
|
130
|
+
*/
|
|
131
|
+
var ONE_R_IMU_FRAME = measuredImuFrame("ONE R", [
|
|
132
|
+
"x",
|
|
133
|
+
"-z",
|
|
134
|
+
"y"
|
|
135
|
+
]);
|
|
136
|
+
/**
|
|
126
137
|
* Assumed for the ONE RS until a recording that turns is ranked (ADR 0009). On one tripod
|
|
127
138
|
* recording of the 1-inch 360 edition, standing upright, gravity lies along the IMU's x, so the
|
|
128
139
|
* body's down is the IMU's x; of the four arrangements that agree, the levelling ranking puts
|
|
@@ -153,6 +164,7 @@ var IMU_FRAMES_BY_MODEL = /* @__PURE__ */ new Map([
|
|
|
153
164
|
["Insta360 X4 Air", X4_AIR_IMU_FRAME],
|
|
154
165
|
["Insta360 X5", X5_IMU_FRAME],
|
|
155
166
|
["Insta360 X6", X6_IMU_FRAME],
|
|
167
|
+
["Insta360 OneR", ONE_R_IMU_FRAME],
|
|
156
168
|
["Insta360 OneRS", ONE_RS_IMU_FRAME]
|
|
157
169
|
]);
|
|
158
170
|
/**
|
|
@@ -163,4 +175,4 @@ function imuFrameFor(hints) {
|
|
|
163
175
|
return IMU_FRAMES_BY_MODEL.get(hints.model?.trim() ?? "") ?? ALIGNED_IMU_FRAME;
|
|
164
176
|
}
|
|
165
177
|
//#endregion
|
|
166
|
-
export { A1_IMU_FRAME, ALIGNED_IMU_FRAME, ONE_RS_IMU_FRAME, X3_IMU_FRAME, X4_AIR_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
178
|
+
export { A1_IMU_FRAME, ALIGNED_IMU_FRAME, ONE_RS_IMU_FRAME, ONE_R_IMU_FRAME, X3_IMU_FRAME, X4_AIR_IMU_FRAME, X5_IMU_FRAME, X6_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { seconds } from "../../../shared/units/time.js";
|
|
2
|
+
import { multiplyMatrices, transposeMatrix } from "../../../shared/math/Matrix3.js";
|
|
3
|
+
import { quaternionToMatrix } from "../../../shared/math/Quaternion.js";
|
|
4
|
+
/**
|
|
5
|
+
* Where a frame's time lies down its rows: the middle row's (ADR 0034).
|
|
6
|
+
*/
|
|
7
|
+
var FRAME_TIME_ROW = .5;
|
|
8
|
+
/**
|
|
9
|
+
* Keyed by the whole model name, as the IMU frames are: a camera's row order is measured on a
|
|
10
|
+
* recording of it (ADR 0046), never assumed.
|
|
11
|
+
*/
|
|
12
|
+
var ROW_ORDERS_BY_MODEL = /* @__PURE__ */ new Map([["Insta360 OneR", "top-first"]]);
|
|
13
|
+
/**
|
|
14
|
+
* The rolling shutter to correct, for a camera whose row order is measured and whose info record
|
|
15
|
+
* states its readout; nothing otherwise, and its frames turn whole.
|
|
16
|
+
*/
|
|
17
|
+
function rollingShutterFor(info) {
|
|
18
|
+
const rowOrder = ROW_ORDERS_BY_MODEL.get(info.model?.trim() ?? "");
|
|
19
|
+
const readout = info.readoutTime;
|
|
20
|
+
return rowOrder && readout !== void 0 && readout > 0 ? {
|
|
21
|
+
readout,
|
|
22
|
+
rowOrder
|
|
23
|
+
} : void 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* For each of {@link ROW_CORRECTION_COUNT} rows down a lens's window, the rotation taking a
|
|
27
|
+
* direction in the body as it was at the frame's `time` into the body as it was when that row was
|
|
28
|
+
* read: the camera turned between the two, so the world a row shows lies that much elsewhere.
|
|
29
|
+
* Every stabilization mode turns the picture at the frame's time, so the corrections apply after
|
|
30
|
+
* it, whatever the mode.
|
|
31
|
+
*/
|
|
32
|
+
function rowCorrectionsAt(motion, time, shutter) {
|
|
33
|
+
const { orientations, mounting } = motion;
|
|
34
|
+
const atTime = quaternionToMatrix(orientations.orientationAt(time));
|
|
35
|
+
const fromBody = transposeMatrix(mounting.toBody);
|
|
36
|
+
const last = 8;
|
|
37
|
+
return Array.from({ length: 9 }, (_, index) => {
|
|
38
|
+
const step = shutter.rowOrder === "top-first" ? index : last - index;
|
|
39
|
+
const rowTime = seconds(time + (step / last - FRAME_TIME_ROW) * shutter.readout);
|
|
40
|
+
const atRow = quaternionToMatrix(orientations.orientationAt(rowTime));
|
|
41
|
+
const uprightTurn = multiplyMatrices(transposeMatrix(atRow), atTime);
|
|
42
|
+
return multiplyMatrices(mounting.toBody, multiplyMatrices(uprightTurn, fromBody));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { rollingShutterFor, rowCorrectionsAt };
|
|
@@ -154,7 +154,8 @@ function sinkOver(renderer, opened) {
|
|
|
154
154
|
const stabilizing = new StabilizingFrameSink({
|
|
155
155
|
sink: renderer,
|
|
156
156
|
motion: opened.motion,
|
|
157
|
-
frameTimes: opened.frameTimes
|
|
157
|
+
frameTimes: opened.frameTimes,
|
|
158
|
+
rollingShutter: opened.motion.rollingShutter
|
|
158
159
|
});
|
|
159
160
|
return {
|
|
160
161
|
sink: stabilizing,
|