@bubo-squared/gyroview 0.1.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 +32 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.js +4 -0
- package/dist/index.d.ts +959 -0
- package/dist/index.js +13 -0
- package/dist/packages/adapters/blob/src/BlobRandomAccessSource.js +37 -0
- package/dist/packages/adapters/blob/src/index.js +2 -0
- package/dist/packages/adapters/fetch/src/HttpRangeSource.js +194 -0
- package/dist/packages/adapters/fetch/src/HttpResourceLocator.js +35 -0
- package/dist/packages/adapters/fetch/src/httpRequest.js +126 -0
- package/dist/packages/adapters/fetch/src/index.js +3 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyAudioSegments.js +133 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyDemuxer.js +63 -0
- package/dist/packages/adapters/mediabunny/src/MediabunnyVideoTrackReader.js +84 -0
- package/dist/packages/adapters/mediabunny/src/SegmentChannel.js +84 -0
- package/dist/packages/adapters/mediabunny/src/bufferSources.js +12 -0
- package/dist/packages/adapters/mediabunny/src/index.js +2 -0
- package/dist/packages/adapters/mse-audio/src/MediaSourceAudioClock.js +139 -0
- package/dist/packages/adapters/mse-audio/src/SourceBufferFeeder.js +174 -0
- package/dist/packages/adapters/mse-audio/src/events.js +26 -0
- package/dist/packages/adapters/mse-audio/src/index.js +2 -0
- package/dist/packages/adapters/mse-audio/src/mediaSourceSupport.js +82 -0
- package/dist/packages/adapters/three/src/ThreeFrameRenderer.js +274 -0
- package/dist/packages/adapters/three/src/compileAndProve.js +14 -0
- package/dist/packages/adapters/three/src/fullscreenPass.js +52 -0
- package/dist/packages/adapters/three/src/index.js +2 -0
- package/dist/packages/adapters/three/src/lensTextures.js +25 -0
- package/dist/packages/adapters/three/src/pictureMaterials.js +44 -0
- package/dist/packages/adapters/three/src/renderInto.js +15 -0
- package/dist/packages/adapters/three/src/rendererUniforms.js +157 -0
- package/dist/packages/adapters/three/src/samplingStrategies.js +26 -0
- package/dist/packages/adapters/three/src/seamMeter/SeamMeterPass.js +79 -0
- package/dist/packages/adapters/three/src/seamMeter/rowMeans.js +38 -0
- package/dist/packages/adapters/three/src/shaderPrograms.js +66 -0
- package/dist/packages/adapters/three/src/shaders/analysis.frag.js +4 -0
- package/dist/packages/adapters/three/src/shaders/equirectangularRays.js +4 -0
- package/dist/packages/adapters/three/src/shaders/fixedJoin.js +4 -0
- package/dist/packages/adapters/three/src/shaders/fullscreen.vert.js +4 -0
- package/dist/packages/adapters/three/src/shaders/header.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensModels.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensSampling.js +4 -0
- package/dist/packages/adapters/three/src/shaders/lensTextures.js +4 -0
- package/dist/packages/adapters/three/src/shaders/rawLenses.frag.js +4 -0
- package/dist/packages/adapters/three/src/shaders/rectilinearRays.js +4 -0
- package/dist/packages/adapters/three/src/shaders/screenAreas.js +4 -0
- package/dist/packages/adapters/three/src/shaders/stitch.frag.js +4 -0
- package/dist/packages/adapters/three/src/webglRenderer.js +37 -0
- package/dist/packages/adapters/webcodecs/src/WebCodecsVideoDecoderPort.js +135 -0
- package/dist/packages/adapters/webcodecs/src/index.js +2 -0
- package/dist/packages/core/src/application/gainMatching/GainMatching.js +66 -0
- package/dist/packages/core/src/application/gainMatching/GainMatchingFrameSink.js +62 -0
- package/dist/packages/core/src/application/playback/ClockWatch.js +76 -0
- package/dist/packages/core/src/application/playback/DecodePipeline.js +188 -0
- package/dist/packages/core/src/application/playback/DecodeRun.js +93 -0
- package/dist/packages/core/src/application/playback/FramePairQueue.js +93 -0
- package/dist/packages/core/src/application/playback/FramePairer.js +60 -0
- package/dist/packages/core/src/application/playback/PlaybackSession.js +417 -0
- package/dist/packages/core/src/application/playback/SeekOrder.js +36 -0
- package/dist/packages/core/src/application/playback/SessionLifecycle.js +42 -0
- package/dist/packages/core/src/application/playback/StartGate.js +55 -0
- package/dist/packages/core/src/application/playback/TimeUpdates.js +31 -0
- package/dist/packages/core/src/application/playback/WallClock.js +46 -0
- package/dist/packages/core/src/application/playback/keyframeTimeAt.js +15 -0
- package/dist/packages/core/src/application/playback/probeDecoding.js +168 -0
- package/dist/packages/core/src/application/recording/frameTimesOf.js +22 -0
- package/dist/packages/core/src/application/recording/inspectLayout.js +21 -0
- package/dist/packages/core/src/application/recording/inspectRecording.js +124 -0
- package/dist/packages/core/src/application/recording/locateOtherLensFile.js +18 -0
- package/dist/packages/core/src/application/recording/motionOf.js +39 -0
- package/dist/packages/core/src/application/recording/readRecording.js +31 -0
- package/dist/packages/core/src/application/recording/timeRecording.js +57 -0
- package/dist/packages/core/src/application/stabilization/StabilizingFrameSink.js +31 -0
- package/dist/packages/core/src/domain/format/boxes/BoxLayout.js +7 -0
- package/dist/packages/core/src/domain/format/boxes/boxConstants.js +16 -0
- package/dist/packages/core/src/domain/format/boxes/scanBoxes.js +59 -0
- package/dist/packages/core/src/domain/format/calibration/CalibrationVersion.js +13 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/CalibrationStringLayout.js +34 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/LegacyCalibrationLayout.js +44 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/MeiCalibrationLayout.js +45 -0
- package/dist/packages/core/src/domain/format/calibration/layouts/PolynomialCalibrationLayout.js +44 -0
- package/dist/packages/core/src/domain/format/calibration/offsetTokens.js +65 -0
- package/dist/packages/core/src/domain/format/calibration/parseOffsetString.js +52 -0
- package/dist/packages/core/src/domain/format/calibration/selectCalibration.js +46 -0
- package/dist/packages/core/src/domain/format/captureOrigin.js +20 -0
- package/dist/packages/core/src/domain/format/constants.js +33 -0
- package/dist/packages/core/src/domain/format/info/infoFields.js +110 -0
- package/dist/packages/core/src/domain/format/info/parseInfoRecord.js +90 -0
- package/dist/packages/core/src/domain/format/info/readInfoRecord.js +14 -0
- package/dist/packages/core/src/domain/format/layout/detectLensLayout.js +122 -0
- package/dist/packages/core/src/domain/format/naming/RecordingFileName.js +67 -0
- package/dist/packages/core/src/domain/format/records/TrailerRecords.js +83 -0
- package/dist/packages/core/src/domain/format/records/exposure/parseExposureRecord.js +48 -0
- package/dist/packages/core/src/domain/format/records/gyro/FloatGyroSampleLayout.js +39 -0
- package/dist/packages/core/src/domain/format/records/gyro/RawGyroSampleLayout.js +55 -0
- package/dist/packages/core/src/domain/format/records/gyro/SampleColumns.js +82 -0
- package/dist/packages/core/src/domain/format/records/gyro/gyroLayouts.js +12 -0
- package/dist/packages/core/src/domain/format/records/gyro/parseGyroRecord.js +129 -0
- package/dist/packages/core/src/domain/format/trailer/RecordHeader.js +23 -0
- package/dist/packages/core/src/domain/format/trailer/Trailer.js +23 -0
- package/dist/packages/core/src/domain/format/trailer/TrailerFooter.js +27 -0
- package/dist/packages/core/src/domain/format/trailer/locateRecords.js +80 -0
- package/dist/packages/core/src/domain/format/trailer/parseRecordIndex.js +28 -0
- package/dist/packages/core/src/domain/format/trailer/readTrailer.js +32 -0
- package/dist/packages/core/src/domain/motion/exposure/ExposureRecord.js +53 -0
- package/dist/packages/core/src/domain/motion/gyro/GyroTrack.js +60 -0
- package/dist/packages/core/src/domain/motion/gyro/medianStep.js +11 -0
- package/dist/packages/core/src/domain/motion/gyro/repairedTimeline.js +101 -0
- package/dist/packages/core/src/domain/motion/imu/ImuFrame.js +112 -0
- package/dist/packages/core/src/domain/motion/orientation/OrientationTrack.js +146 -0
- package/dist/packages/core/src/domain/motion/orientation/estimateGyroBias.js +64 -0
- package/dist/packages/core/src/domain/motion/orientation/gravity.js +97 -0
- package/dist/packages/core/src/domain/motion/stabilization/Stabilizer.js +13 -0
- package/dist/packages/core/src/domain/motion/stabilization/stabilizers.js +86 -0
- package/dist/packages/core/src/domain/motion/timing/CaptureClock.js +41 -0
- package/dist/packages/core/src/domain/motion/timing/FrameTimes.js +92 -0
- package/dist/packages/core/src/domain/motion/timing/frameTimeSources.js +61 -0
- package/dist/packages/core/src/domain/motion/timing/resolveFrameTimes.js +68 -0
- package/dist/packages/core/src/domain/optics/EquidistantModel.js +39 -0
- package/dist/packages/core/src/domain/optics/MeiModel.js +45 -0
- package/dist/packages/core/src/domain/optics/PolynomialModel.js +44 -0
- package/dist/packages/core/src/domain/optics/gainMatch.js +62 -0
- package/dist/packages/core/src/domain/optics/lensGeometry.js +18 -0
- package/dist/packages/core/src/domain/optics/lensPose.js +35 -0
- package/dist/packages/core/src/domain/optics/opticsConstants.js +5 -0
- package/dist/packages/core/src/domain/playback/PlayerState.js +83 -0
- package/dist/packages/core/src/domain/stitching/LensLayout.js +21 -0
- package/dist/packages/core/src/domain/stitching/StitchingSetup.js +78 -0
- package/dist/packages/core/src/domain/stitching/seamDisparity.js +4 -0
- package/dist/packages/core/src/domain/stitching/seamDisparityField.js +5 -0
- package/dist/packages/core/src/domain/stitching/seamJoin.js +16 -0
- package/dist/packages/core/src/domain/stitching/seamMismatch.js +1 -0
- package/dist/packages/core/src/domain/stitching/seamRing.js +4 -0
- package/dist/packages/core/src/domain/stitching/seamStrip.js +26 -0
- package/dist/packages/core/src/domain/view/Framing.js +31 -0
- package/dist/packages/core/src/domain/view/PictureQuality.js +26 -0
- package/dist/packages/core/src/domain/view/ViewMode.js +9 -0
- package/dist/packages/core/src/domain/view/ViewState.js +48 -0
- package/dist/packages/core/src/domain/view/lensTilesView.js +63 -0
- package/dist/packages/core/src/domain/view/magnification.js +103 -0
- package/dist/packages/core/src/domain/view/normalView.js +29 -0
- package/dist/packages/core/src/domain/view/panoramaView.js +99 -0
- package/dist/packages/core/src/domain/view/rectilinear.js +40 -0
- package/dist/packages/core/src/domain/view/screenLayout.js +113 -0
- package/dist/packages/core/src/domain/view/viewGestures.js +98 -0
- package/dist/packages/core/src/domain/view/viewModes.js +14 -0
- package/dist/packages/core/src/index.js +59 -0
- package/dist/packages/core/src/ports/FramePair.js +6 -0
- package/dist/packages/core/src/shared/async/Deferred.js +42 -0
- package/dist/packages/core/src/shared/async/RunStop.js +40 -0
- package/dist/packages/core/src/shared/binary/ByteRange.js +38 -0
- package/dist/packages/core/src/shared/binary/ByteReader.js +77 -0
- package/dist/packages/core/src/shared/errors/GyroViewError.js +81 -0
- package/dist/packages/core/src/shared/errors/reportLater.js +11 -0
- package/dist/packages/core/src/shared/events/Outbox.js +68 -0
- package/dist/packages/core/src/shared/events/TypedEmitter.js +55 -0
- package/dist/packages/core/src/shared/keysOf.js +10 -0
- package/dist/packages/core/src/shared/lazy.js +11 -0
- package/dist/packages/core/src/shared/math/Matrix3.js +93 -0
- package/dist/packages/core/src/shared/math/Quaternion.js +133 -0
- package/dist/packages/core/src/shared/math/Vector3.js +54 -0
- package/dist/packages/core/src/shared/math/clamp.js +9 -0
- package/dist/packages/core/src/shared/protobuf/ProtobufMessage.js +157 -0
- package/dist/packages/core/src/shared/text/urlPath.js +31 -0
- package/dist/packages/core/src/shared/text/utf8.js +12 -0
- package/dist/packages/core/src/shared/units/angle.js +22 -0
- package/dist/packages/core/src/shared/units/time.js +13 -0
- package/dist/packages/player/src/PlayerSource.js +15 -0
- package/dist/packages/player/src/browserPlayer.js +18 -0
- package/dist/packages/player/src/choices.js +25 -0
- package/dist/packages/player/src/composition/Disposables.js +31 -0
- package/dist/packages/player/src/composition/browserPorts.js +46 -0
- package/dist/packages/player/src/composition/buildPipeline.js +153 -0
- package/dist/packages/player/src/composition/ensureDecodable.js +63 -0
- package/dist/packages/player/src/composition/openInputs.js +179 -0
- package/dist/packages/player/src/composition/openRecording.js +42 -0
- package/dist/packages/player/src/controls/ChoiceMenu.js +113 -0
- package/dist/packages/player/src/controls/PictureMenus.js +47 -0
- package/dist/packages/player/src/controls/SeekBar.js +125 -0
- package/dist/packages/player/src/controls/SoundControls.js +41 -0
- package/dist/packages/player/src/controls/TransportButtons.js +31 -0
- package/dist/packages/player/src/controls/ViewGestures.js +136 -0
- package/dist/packages/player/src/controls/Wording.js +78 -0
- package/dist/packages/player/src/controls/controlParts.js +36 -0
- package/dist/packages/player/src/controls/controls.js +4 -0
- package/dist/packages/player/src/controls/controlsBar.js +23 -0
- package/dist/packages/player/src/controls/controlsMarkup.js +85 -0
- package/dist/packages/player/src/controls/customControls.js +59 -0
- package/dist/packages/player/src/controls/formatTime.js +19 -0
- package/dist/packages/player/src/controls/icons.js +40 -0
- package/dist/packages/player/src/controls/keyboard.js +118 -0
- package/dist/packages/player/src/controls/messages.js +94 -0
- package/dist/packages/player/src/controls/parseMarkup.js +31 -0
- package/dist/packages/player/src/controls/viewButtons.js +14 -0
- package/dist/packages/player/src/element/ElementLoads.js +119 -0
- package/dist/packages/player/src/element/FullscreenToggle.js +39 -0
- package/dist/packages/player/src/element/GyroViewElement.js +313 -0
- package/dist/packages/player/src/element/IdleWatcher.js +71 -0
- package/dist/packages/player/src/element/TypedEventElement.js +22 -0
- package/dist/packages/player/src/element/accessibleRegion.js +19 -0
- package/dist/packages/player/src/element/attributeNames.js +51 -0
- package/dist/packages/player/src/element/attributes.js +101 -0
- package/dist/packages/player/src/element/defineGyroView.js +21 -0
- package/dist/packages/player/src/element/earlyProperties.js +36 -0
- package/dist/packages/player/src/element/elementSource.js +23 -0
- package/dist/packages/player/src/element/liveSettings.js +138 -0
- package/dist/packages/player/src/element/playbackAttributes.js +53 -0
- package/dist/packages/player/src/element/playerEventMirror.js +33 -0
- package/dist/packages/player/src/element/reflectedProperties.js +78 -0
- package/dist/packages/player/src/element/styles.js +4 -0
- package/dist/packages/player/src/element/template.js +47 -0
- package/dist/packages/player/src/index.js +10 -0
- package/dist/packages/player/src/inspectRecording.js +22 -0
- package/dist/packages/player/src/player/DrawingBufferFit.js +77 -0
- package/dist/packages/player/src/player/FrameLoop.js +40 -0
- package/dist/packages/player/src/player/PictureSettings.js +60 -0
- package/dist/packages/player/src/player/Player.js +377 -0
- package/dist/packages/player/src/player/PlayerEvents.js +28 -0
- package/dist/packages/player/src/player/PlayerPhase.js +21 -0
- package/dist/packages/player/src/player/PlayerSound.js +61 -0
- package/dist/packages/player/src/player/PlayerView.js +113 -0
- package/dist/packages/player/src/player/SessionRelay.js +66 -0
- package/dist/packages/player/src/player/ensureFinite.js +26 -0
- package/dist/packages/player/src/player/loadRecording.js +65 -0
- package/dist/packages/player/src/player/transportEvents.js +29 -0
- package/dist/standalone.js +4919 -0
- package/package.json +73 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/format/trailer/Trailer.ts
|
|
2
|
+
/**
|
|
3
|
+
* The table of contents of an Insta360 trailer: which records exist and where their payloads
|
|
4
|
+
* lie. Pure data; reading payloads is the caller's job because some are megabytes large.
|
|
5
|
+
*/
|
|
6
|
+
var Trailer = class {
|
|
7
|
+
footer;
|
|
8
|
+
payloadStart;
|
|
9
|
+
byId;
|
|
10
|
+
constructor(footer, payloadStart, records) {
|
|
11
|
+
this.footer = footer;
|
|
12
|
+
this.payloadStart = payloadStart;
|
|
13
|
+
this.byId = new Map(records.map((record) => [record.id, record]));
|
|
14
|
+
}
|
|
15
|
+
get records() {
|
|
16
|
+
return [...this.byId.values()];
|
|
17
|
+
}
|
|
18
|
+
locationOf(id) {
|
|
19
|
+
return this.byId.get(id);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { Trailer };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import "../constants.js";
|
|
3
|
+
import { ByteReader } from "../../../shared/binary/ByteReader.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/format/trailer/TrailerFooter.ts
|
|
5
|
+
/**
|
|
6
|
+
* The fixed 72-byte block that closes an Insta360 file. Its presence identifies the format;
|
|
7
|
+
* its size field tells where the trailer payload begins.
|
|
8
|
+
*/
|
|
9
|
+
var TrailerFooter = class TrailerFooter {
|
|
10
|
+
trailerSize;
|
|
11
|
+
version;
|
|
12
|
+
constructor(trailerSize, version) {
|
|
13
|
+
this.trailerSize = trailerSize;
|
|
14
|
+
this.version = version;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parses exactly the last {@link TRAILER_FOOTER_SIZE} bytes of a file.
|
|
18
|
+
*/
|
|
19
|
+
static parse(bytes) {
|
|
20
|
+
if (bytes.byteLength !== 72) throw new GyroViewError("invalid-trailer", `footer must be 72 bytes, got ${bytes.byteLength}`);
|
|
21
|
+
const reader = new ByteReader(bytes);
|
|
22
|
+
if (reader.asciiAt(40, 32) !== "8db42d694ccc418790edff439fe026bf") throw new GyroViewError("invalid-trailer", "file does not end with the Insta360 magic");
|
|
23
|
+
return new TrailerFooter(reader.uint32LeAt(32), reader.uint32LeAt(36));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { TrailerFooter };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { RecordType } from "../constants.js";
|
|
3
|
+
import { ByteRange } from "../../../shared/binary/ByteRange.js";
|
|
4
|
+
import { RecordHeader } from "./RecordHeader.js";
|
|
5
|
+
import { parseRecordIndex } from "./parseRecordIndex.js";
|
|
6
|
+
//#region ../../packages/core/src/domain/format/trailer/locateRecords.ts
|
|
7
|
+
/**
|
|
8
|
+
* Finds every record payload. Newer firmware writes an index record before the footer and pads
|
|
9
|
+
* records to aligned offsets; older firmware writes records back to back, which can only be
|
|
10
|
+
* walked backwards from the footer. The header before the footer tells which case applies.
|
|
11
|
+
*/
|
|
12
|
+
function locateRecords(source, layout) {
|
|
13
|
+
return layout.lastHeader.id === RecordType.Index ? locateThroughIndex(source, layout) : locateByWalkingBackwards(source, layout);
|
|
14
|
+
}
|
|
15
|
+
async function locateThroughIndex(source, layout) {
|
|
16
|
+
const indexOffset = layout.fileSize - 72 - 6 - layout.lastHeader.payloadSize;
|
|
17
|
+
ensureNotBeforePayloadStart(indexOffset, layout.lastHeader, layout.payloadStart);
|
|
18
|
+
const indexRange = ByteRange.of(indexOffset, layout.lastHeader.payloadSize);
|
|
19
|
+
const records = parseRecordIndex(await source.read(indexRange), layout.payloadStart);
|
|
20
|
+
for (const record of records) ensureBeforeIndex(record, indexRange.offset);
|
|
21
|
+
return records;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The index counts every offset from the payload start, so only a record's end can stray: past
|
|
25
|
+
* the index, into the footer or beyond the file.
|
|
26
|
+
*/
|
|
27
|
+
function ensureBeforeIndex(record, indexStart) {
|
|
28
|
+
if (record.payload.end > indexStart) throw new GyroViewError("invalid-trailer", `record ${record.id} at ${record.payload.offset}+${record.payload.length} lies outside the trailer`);
|
|
29
|
+
}
|
|
30
|
+
async function locateByWalkingBackwards(source, layout) {
|
|
31
|
+
const records = [];
|
|
32
|
+
const chunks = new BackwardChunks(source, layout.payloadStart);
|
|
33
|
+
let recordEnd = layout.fileSize - 72;
|
|
34
|
+
while (recordEnd > layout.payloadStart) {
|
|
35
|
+
const header = RecordHeader.parse(await chunks.bytesEndingAt(recordEnd, 6));
|
|
36
|
+
const payloadOffset = recordEnd - 6 - header.payloadSize;
|
|
37
|
+
ensureNotBeforePayloadStart(payloadOffset, header, layout.payloadStart);
|
|
38
|
+
records.push({
|
|
39
|
+
id: header.id,
|
|
40
|
+
format: header.format,
|
|
41
|
+
payload: ByteRange.of(payloadOffset, header.payloadSize)
|
|
42
|
+
});
|
|
43
|
+
recordEnd = payloadOffset;
|
|
44
|
+
}
|
|
45
|
+
return records.toReversed();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* How much of the trailer one read of the backward walk takes: the headers of the small records
|
|
49
|
+
* near the footer share one, and only a large payload (the gyro record) sends the walk past it.
|
|
50
|
+
*/
|
|
51
|
+
var WALK_CHUNK_SIZE = 65536;
|
|
52
|
+
/**
|
|
53
|
+
* Reads the trailer backwards a chunk at a time, so walking a bare trailer costs a round trip
|
|
54
|
+
* per chunk rather than per record, each read being an HTTP request.
|
|
55
|
+
*/
|
|
56
|
+
var BackwardChunks = class {
|
|
57
|
+
source;
|
|
58
|
+
trailerStart;
|
|
59
|
+
chunkStart = 0;
|
|
60
|
+
chunk = /* @__PURE__ */ new Uint8Array();
|
|
61
|
+
constructor(source, trailerStart) {
|
|
62
|
+
this.source = source;
|
|
63
|
+
this.trailerStart = trailerStart;
|
|
64
|
+
}
|
|
65
|
+
async bytesEndingAt(end, length) {
|
|
66
|
+
const start = end - length;
|
|
67
|
+
if (start < this.trailerStart) throw new GyroViewError("invalid-trailer", `a record header at ${start} lies before the trailer`);
|
|
68
|
+
if (!(start >= this.chunkStart && end <= this.chunkStart + this.chunk.byteLength)) await this.readChunkEndingAt(end);
|
|
69
|
+
return this.chunk.subarray(start - this.chunkStart, end - this.chunkStart);
|
|
70
|
+
}
|
|
71
|
+
async readChunkEndingAt(end) {
|
|
72
|
+
this.chunkStart = Math.max(this.trailerStart, end - WALK_CHUNK_SIZE);
|
|
73
|
+
this.chunk = await this.source.read(ByteRange.of(this.chunkStart, end - this.chunkStart));
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
function ensureNotBeforePayloadStart(payloadOffset, header, payloadStart) {
|
|
77
|
+
if (payloadOffset < payloadStart) throw new GyroViewError("invalid-trailer", `record ${header.id} (${header.payloadSize} bytes) runs past the trailer start; records are not contiguous`);
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
export { WALK_CHUNK_SIZE, locateRecords };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { ByteReader } from "../../../shared/binary/ByteReader.js";
|
|
3
|
+
import { ByteRange } from "../../../shared/binary/ByteRange.js";
|
|
4
|
+
//#region ../../packages/core/src/domain/format/trailer/parseRecordIndex.ts
|
|
5
|
+
/**
|
|
6
|
+
* Parses the index record (id 0) into record locations. Slot offsets are relative to the
|
|
7
|
+
* trailer payload start, so the caller supplies that absolute position.
|
|
8
|
+
*/
|
|
9
|
+
function parseRecordIndex(bytes, payloadStart) {
|
|
10
|
+
if (bytes.byteLength % 10 !== 0) throw new GyroViewError("invalid-trailer", `index size ${bytes.byteLength} is not a multiple of 10`);
|
|
11
|
+
const reader = new ByteReader(bytes);
|
|
12
|
+
const records = [];
|
|
13
|
+
for (let slotOffset = 0; slotOffset < bytes.byteLength; slotOffset += 10) {
|
|
14
|
+
const location = slotAt(reader, slotOffset, payloadStart);
|
|
15
|
+
if (location) records.push(location);
|
|
16
|
+
}
|
|
17
|
+
return records;
|
|
18
|
+
}
|
|
19
|
+
function slotAt(reader, slotOffset, payloadStart) {
|
|
20
|
+
const size = reader.uint32LeAt(slotOffset + 2);
|
|
21
|
+
return size === 0 ? void 0 : {
|
|
22
|
+
id: reader.uint8At(slotOffset + 0),
|
|
23
|
+
format: reader.uint8At(slotOffset + 1),
|
|
24
|
+
payload: ByteRange.of(payloadStart + reader.uint32LeAt(slotOffset + 6), size)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { parseRecordIndex };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { GyroViewError } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { ByteReader } from "../../../shared/binary/ByteReader.js";
|
|
3
|
+
import { ByteRange } from "../../../shared/binary/ByteRange.js";
|
|
4
|
+
import { RecordHeader } from "./RecordHeader.js";
|
|
5
|
+
import { locateRecords } from "./locateRecords.js";
|
|
6
|
+
import { Trailer } from "./Trailer.js";
|
|
7
|
+
import { TrailerFooter } from "./TrailerFooter.js";
|
|
8
|
+
//#region ../../packages/core/src/domain/format/trailer/readTrailer.ts
|
|
9
|
+
var TAIL_SIZE = 78;
|
|
10
|
+
/**
|
|
11
|
+
* Reads the trailer's table of contents with as few round trips as possible: one read for the
|
|
12
|
+
* footer plus last header, then whatever locating the records needs. `fileSize` is passed in so
|
|
13
|
+
* the caller can share one size lookup between several readers of the same source.
|
|
14
|
+
*/
|
|
15
|
+
async function readTrailer(source, fileSize) {
|
|
16
|
+
if (fileSize < TAIL_SIZE) throw new GyroViewError("invalid-trailer", `file of ${fileSize} bytes is too small to hold a trailer`);
|
|
17
|
+
const layout = parseLayout(await source.read(ByteRange.lastOf(fileSize, TAIL_SIZE)), fileSize);
|
|
18
|
+
return new Trailer(layout.footer, layout.payloadStart, await locateRecords(source, layout));
|
|
19
|
+
}
|
|
20
|
+
function parseLayout(tail, fileSize) {
|
|
21
|
+
const reader = new ByteReader(tail);
|
|
22
|
+
const footer = TrailerFooter.parse(reader.bytesAt(6, 72));
|
|
23
|
+
if (footer.trailerSize > fileSize) throw new GyroViewError("invalid-trailer", `trailer size ${footer.trailerSize} exceeds file size ${fileSize}`);
|
|
24
|
+
return {
|
|
25
|
+
fileSize,
|
|
26
|
+
payloadStart: fileSize - footer.trailerSize,
|
|
27
|
+
footer,
|
|
28
|
+
lastHeader: RecordHeader.parse(reader.bytesAt(0, 6))
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { readTrailer };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ensureIndexInRange, ensureInvariant } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { microseconds, seconds } from "../../../shared/units/time.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/motion/exposure/ExposureRecord.ts
|
|
4
|
+
/**
|
|
5
|
+
* Capture times and shutter durations of every frame the sensor produced, in order.
|
|
6
|
+
*/
|
|
7
|
+
var ExposureRecord = class ExposureRecord {
|
|
8
|
+
captureTimeStore;
|
|
9
|
+
shutterTimeStore;
|
|
10
|
+
constructor(captureTimeStore, shutterTimeStore) {
|
|
11
|
+
this.captureTimeStore = captureTimeStore;
|
|
12
|
+
this.shutterTimeStore = shutterTimeStore;
|
|
13
|
+
ensureInvariant(captureTimeStore.length === shutterTimeStore.length, "exposure record arrays disagree on the entry count");
|
|
14
|
+
}
|
|
15
|
+
get length() {
|
|
16
|
+
return this.captureTimeStore.length;
|
|
17
|
+
}
|
|
18
|
+
get captureTimes() {
|
|
19
|
+
return this.captureTimeStore;
|
|
20
|
+
}
|
|
21
|
+
get shutterTimes() {
|
|
22
|
+
return this.shutterTimeStore;
|
|
23
|
+
}
|
|
24
|
+
entryAt(index) {
|
|
25
|
+
ensureIndexInRange(index, this.length, "exposure entry");
|
|
26
|
+
return {
|
|
27
|
+
captureTime: microseconds(this.captureTimeStore[index] ?? 0),
|
|
28
|
+
shutterTime: seconds(this.shutterTimeStore[index] ?? 0)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Index of the first entry captured at or after `captureTime`, or the length when none is.
|
|
33
|
+
*/
|
|
34
|
+
indexAtOrAfter(captureTime) {
|
|
35
|
+
let low = 0;
|
|
36
|
+
let high = this.captureTimeStore.length;
|
|
37
|
+
while (low < high) {
|
|
38
|
+
const middle = low + high >>> 1;
|
|
39
|
+
if ((this.captureTimeStore[middle] ?? Infinity) < captureTime) low = middle + 1;
|
|
40
|
+
else high = middle;
|
|
41
|
+
}
|
|
42
|
+
return low;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The `count` entries starting at `start`, as their own record.
|
|
46
|
+
*/
|
|
47
|
+
slice(start, count) {
|
|
48
|
+
ensureInvariant(start >= 0 && count >= 0 && start + count <= this.length, "exposure slice out of range");
|
|
49
|
+
return new ExposureRecord(this.captureTimeStore.slice(start, start + count), this.shutterTimeStore.slice(start, start + count));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { ExposureRecord };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ensureIndexInRange, ensureInvariant } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { microseconds } from "../../../shared/units/time.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/motion/gyro/GyroTrack.ts
|
|
4
|
+
/**
|
|
5
|
+
* Structure-of-arrays storage for a whole recording's IMU samples (a 4-minute X5 clip has a
|
|
6
|
+
* quarter of a million), with a per-sample view for callers that prefer objects. Its times never
|
|
7
|
+
* go back: the orientation lookup searches them and the estimation windows measure them, so
|
|
8
|
+
* recorded stamps come through {@link repairedTimeline}.
|
|
9
|
+
*/
|
|
10
|
+
var GyroTrack = class {
|
|
11
|
+
captureTimeStore;
|
|
12
|
+
accelerationStore;
|
|
13
|
+
angularVelocityStore;
|
|
14
|
+
constructor(captureTimeStore, accelerationStore, angularVelocityStore) {
|
|
15
|
+
this.captureTimeStore = captureTimeStore;
|
|
16
|
+
this.accelerationStore = accelerationStore;
|
|
17
|
+
this.angularVelocityStore = angularVelocityStore;
|
|
18
|
+
const expected = captureTimeStore.length * 3;
|
|
19
|
+
ensureInvariant(accelerationStore.length === expected && angularVelocityStore.length === expected, "gyro track arrays disagree on the sample count");
|
|
20
|
+
ensureInvariant(isNonDecreasing(captureTimeStore), "gyro sample times go back");
|
|
21
|
+
}
|
|
22
|
+
get length() {
|
|
23
|
+
return this.captureTimeStore.length;
|
|
24
|
+
}
|
|
25
|
+
get isEmpty() {
|
|
26
|
+
return this.captureTimeStore.length === 0;
|
|
27
|
+
}
|
|
28
|
+
get captureTimes() {
|
|
29
|
+
return this.captureTimeStore;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Mean spacing between consecutive samples, or undefined for fewer than two samples.
|
|
33
|
+
*/
|
|
34
|
+
get meanSampleInterval() {
|
|
35
|
+
if (this.length < 2) return void 0;
|
|
36
|
+
const span = (this.captureTimeStore[this.length - 1] ?? 0) - (this.captureTimeStore[0] ?? 0);
|
|
37
|
+
return microseconds(span / (this.length - 1));
|
|
38
|
+
}
|
|
39
|
+
sampleAt(index) {
|
|
40
|
+
ensureIndexInRange(index, this.length, "gyro sample");
|
|
41
|
+
const base = index * 3;
|
|
42
|
+
return {
|
|
43
|
+
captureTime: microseconds(this.captureTimeStore[index] ?? 0),
|
|
44
|
+
acceleration: this.vectorAt(this.accelerationStore, base),
|
|
45
|
+
angularVelocity: this.vectorAt(this.angularVelocityStore, base)
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
vectorAt(store, base) {
|
|
49
|
+
return [
|
|
50
|
+
store[base] ?? NaN,
|
|
51
|
+
store[base + 1] ?? NaN,
|
|
52
|
+
store[base + 2] ?? NaN
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
function isNonDecreasing(times) {
|
|
57
|
+
return times.every((time, index) => index === 0 || time >= (times[index - 1] ?? time));
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
export { GyroTrack };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region ../../packages/core/src/domain/motion/gyro/medianStep.ts
|
|
2
|
+
/**
|
|
3
|
+
* The median spacing of consecutive stamps: a stray among them moves it by no more than one
|
|
4
|
+
* place, so a few glitched or unset stamps do not decide it. Zero for fewer than two stamps.
|
|
5
|
+
*/
|
|
6
|
+
function medianStep(stamps) {
|
|
7
|
+
const sorted = stamps.subarray(1).map((stamp, index) => stamp - (stamps[index] ?? stamp)).toSorted();
|
|
8
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { medianStep };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { medianStep } from "./medianStep.js";
|
|
2
|
+
//#region ../../packages/core/src/domain/motion/gyro/repairedTimeline.ts
|
|
3
|
+
/**
|
|
4
|
+
* How far a stamp may stray from where its neighbours put it, in typical sample intervals,
|
|
5
|
+
* before it is taken for a glitch: far beyond an IMU's jitter, far below what a flipped bit
|
|
6
|
+
* leaves.
|
|
7
|
+
*/
|
|
8
|
+
var STRAY_INTERVALS = 50;
|
|
9
|
+
/**
|
|
10
|
+
* The least stray tolerance, in microseconds, for a record whose stamps barely move.
|
|
11
|
+
*/
|
|
12
|
+
var MIN_STRAY_TOLERANCE_US = 1e4;
|
|
13
|
+
/**
|
|
14
|
+
* A stretch of stamps in step this long is trusted: longer than any run of strays the repair
|
|
15
|
+
* mends (a clock glitch, a flipped bit, a few uninitialised stamps), so strays never outvote it.
|
|
16
|
+
*/
|
|
17
|
+
var TRUSTED_STRETCH = 17;
|
|
18
|
+
/**
|
|
19
|
+
* Recorded stamps made fit to integrate over and to search. Stretches of stamps in step are
|
|
20
|
+
* trusted when they are long (and the longest always is); every stamp outside them is placed on
|
|
21
|
+
* the line from its nearest trusted neighbour, keeping its own time when that is near. A real gap
|
|
22
|
+
* lies between two trusted stretches and stays. No stamp is then let go back.
|
|
23
|
+
*/
|
|
24
|
+
function repairedTimeline(recorded) {
|
|
25
|
+
const interval = typicalInterval(recorded);
|
|
26
|
+
const line = {
|
|
27
|
+
recorded,
|
|
28
|
+
interval,
|
|
29
|
+
tolerance: toleranceOf(interval)
|
|
30
|
+
};
|
|
31
|
+
const times = Float64Array.from(recorded);
|
|
32
|
+
placeOutsideTrusted(times, trustedStretches(steadyStretches(line)), line);
|
|
33
|
+
const mended = times.filter((time, index) => time !== recorded[index]).length;
|
|
34
|
+
for (let index = 1; index < times.length; index += 1) times[index] = Math.max(times[index] ?? 0, times[index - 1] ?? 0);
|
|
35
|
+
return {
|
|
36
|
+
times,
|
|
37
|
+
mended
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function toleranceOf(interval) {
|
|
41
|
+
return Math.max(STRAY_INTERVALS * interval, MIN_STRAY_TOLERANCE_US);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The median spacing of the first samples.
|
|
45
|
+
*/
|
|
46
|
+
function typicalInterval(times) {
|
|
47
|
+
return medianStep(times.subarray(0, 1025));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The maximal runs of stamps each a step after the one before (not back, not far ahead).
|
|
51
|
+
*/
|
|
52
|
+
function steadyStretches({ recorded, tolerance }) {
|
|
53
|
+
const stretches = [];
|
|
54
|
+
let start = 0;
|
|
55
|
+
for (let index = 1; index <= recorded.length; index += 1) {
|
|
56
|
+
const step = (recorded[index] ?? -Infinity) - (recorded[index - 1] ?? 0);
|
|
57
|
+
if (step >= 0 && step <= tolerance) continue;
|
|
58
|
+
stretches.push({
|
|
59
|
+
start,
|
|
60
|
+
end: index
|
|
61
|
+
});
|
|
62
|
+
start = index;
|
|
63
|
+
}
|
|
64
|
+
return stretches;
|
|
65
|
+
}
|
|
66
|
+
function trustedStretches(stretches) {
|
|
67
|
+
let longest = 0;
|
|
68
|
+
for (const stretch of stretches) longest = Math.max(longest, lengthOf(stretch));
|
|
69
|
+
return stretches.filter((stretch) => {
|
|
70
|
+
const length = lengthOf(stretch);
|
|
71
|
+
return length >= TRUSTED_STRETCH || length === longest;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function lengthOf(stretch) {
|
|
75
|
+
return stretch.end - stretch.start;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Each stamp between trusted stretches, or before the first or after the last, placed on the
|
|
79
|
+
* line from the trusted stamp before it (or after it, ahead of the first stretch).
|
|
80
|
+
*/
|
|
81
|
+
function placeOutsideTrusted(times, trusted, line) {
|
|
82
|
+
const [first] = trusted;
|
|
83
|
+
if (!first) return;
|
|
84
|
+
for (let index = first.start - 1; index >= 0; index -= 1) times[index] = placed(index, (times[index + 1] ?? 0) - line.interval, line);
|
|
85
|
+
for (const [position, stretch] of trusted.entries()) placeAfter(times, {
|
|
86
|
+
start: stretch.end,
|
|
87
|
+
end: trusted[position + 1]?.start ?? times.length
|
|
88
|
+
}, line);
|
|
89
|
+
}
|
|
90
|
+
function placeAfter(times, untrusted, line) {
|
|
91
|
+
for (let index = untrusted.start; index < untrusted.end; index += 1) times[index] = placed(index, (times[index - 1] ?? 0) + line.interval, line);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The stamp's own time when it is near where its neighbour puts it, that place otherwise.
|
|
95
|
+
*/
|
|
96
|
+
function placed(index, expected, line) {
|
|
97
|
+
const recorded = line.recorded[index] ?? expected;
|
|
98
|
+
return Math.abs(recorded - expected) <= line.tolerance ? recorded : expected;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { repairedTimeline };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { ensureInvariant } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { determinantOf, transformVector } from "../../../shared/math/Matrix3.js";
|
|
3
|
+
//#region ../../packages/core/src/domain/motion/imu/ImuFrame.ts
|
|
4
|
+
var AXIS_VECTORS = /* @__PURE__ */ new Map([
|
|
5
|
+
["x", [
|
|
6
|
+
1,
|
|
7
|
+
0,
|
|
8
|
+
0
|
|
9
|
+
]],
|
|
10
|
+
["y", [
|
|
11
|
+
0,
|
|
12
|
+
1,
|
|
13
|
+
0
|
|
14
|
+
]],
|
|
15
|
+
["z", [
|
|
16
|
+
0,
|
|
17
|
+
0,
|
|
18
|
+
1
|
|
19
|
+
]],
|
|
20
|
+
["-x", [
|
|
21
|
+
-1,
|
|
22
|
+
0,
|
|
23
|
+
0
|
|
24
|
+
]],
|
|
25
|
+
["-y", [
|
|
26
|
+
0,
|
|
27
|
+
-1,
|
|
28
|
+
0
|
|
29
|
+
]],
|
|
30
|
+
["-z", [
|
|
31
|
+
0,
|
|
32
|
+
0,
|
|
33
|
+
-1
|
|
34
|
+
]]
|
|
35
|
+
]);
|
|
36
|
+
function axisVector(axis) {
|
|
37
|
+
const vector = AXIS_VECTORS.get(axis);
|
|
38
|
+
ensureInvariant(vector !== void 0, `unknown IMU axis ${axis}`);
|
|
39
|
+
return vector;
|
|
40
|
+
}
|
|
41
|
+
function toBodyMatrixOf(bodyAxes) {
|
|
42
|
+
const [rowX, rowY, rowZ] = bodyAxes.map((axis) => axisVector(axis));
|
|
43
|
+
return [
|
|
44
|
+
...rowX,
|
|
45
|
+
...rowY,
|
|
46
|
+
...rowZ
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Only a proper rotation is a frame: a repeated axis is singular and a reflection would
|
|
51
|
+
* integrate a mirrored world.
|
|
52
|
+
*/
|
|
53
|
+
function isProperRotation(bodyAxes) {
|
|
54
|
+
return determinantOf(toBodyMatrixOf(bodyAxes)) === 1;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A frame measured on a recording of the camera it is for.
|
|
58
|
+
*/
|
|
59
|
+
function measuredImuFrame(name, bodyAxes) {
|
|
60
|
+
return {
|
|
61
|
+
...frameOf(name, bodyAxes),
|
|
62
|
+
isVerified: true
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A frame assumed until a recording proves it; it still needs a real file.
|
|
67
|
+
*/
|
|
68
|
+
function assumedImuFrame(name, bodyAxes) {
|
|
69
|
+
return {
|
|
70
|
+
...frameOf(name, bodyAxes),
|
|
71
|
+
isVerified: false
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function frameOf(name, bodyAxes) {
|
|
75
|
+
ensureInvariant(isProperRotation(bodyAxes), `IMU axes ${bodyAxes.join(", ")} are not a rotation`);
|
|
76
|
+
return {
|
|
77
|
+
name,
|
|
78
|
+
toBody: toBodyMatrixOf(bodyAxes)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function toBodyFrame(frame, imuVector) {
|
|
82
|
+
return transformVector(frame.toBody, imuVector);
|
|
83
|
+
}
|
|
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.
|
|
89
|
+
*/
|
|
90
|
+
var X5_IMU_FRAME = measuredImuFrame("X5", [
|
|
91
|
+
"x",
|
|
92
|
+
"z",
|
|
93
|
+
"-y"
|
|
94
|
+
]);
|
|
95
|
+
/**
|
|
96
|
+
* Until a recording proves otherwise, the IMU is assumed aligned with the body.
|
|
97
|
+
*/
|
|
98
|
+
var ALIGNED_IMU_FRAME = assumedImuFrame("aligned (unverified)", [
|
|
99
|
+
"x",
|
|
100
|
+
"y",
|
|
101
|
+
"z"
|
|
102
|
+
]);
|
|
103
|
+
var IMU_FRAMES_BY_MODEL = [["Insta360 X5", X5_IMU_FRAME]];
|
|
104
|
+
/**
|
|
105
|
+
* The IMU frame for a recording, chosen from the camera model the file names. A starting point
|
|
106
|
+
* the recording's own data can be checked against with the ranking test, never the last word.
|
|
107
|
+
*/
|
|
108
|
+
function imuFrameFor(hints) {
|
|
109
|
+
return IMU_FRAMES_BY_MODEL.find(([prefix]) => hints.model?.startsWith(prefix))?.[1] ?? ALIGNED_IMU_FRAME;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { ALIGNED_IMU_FRAME, X5_IMU_FRAME, assumedImuFrame, imuFrameFor, isProperRotation, toBodyFrame };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ensureInvariant } from "../../../shared/errors/GyroViewError.js";
|
|
2
|
+
import { seconds } from "../../../shared/units/time.js";
|
|
3
|
+
import { ZERO_VECTOR3, addVectors, isFiniteVector, scaleVector, subtractVectors } from "../../../shared/math/Vector3.js";
|
|
4
|
+
import { clamp } from "../../../shared/math/clamp.js";
|
|
5
|
+
import { toBodyFrame } from "../imu/ImuFrame.js";
|
|
6
|
+
import { estimateGyroBias, stillestWindow } from "./estimateGyroBias.js";
|
|
7
|
+
import { IDENTITY_QUATERNION, multiplyQuaternions, normalizeQuaternion, quaternionFromRotationVector, slerpQuaternions } from "../../../shared/math/Quaternion.js";
|
|
8
|
+
import { gravityCorrection, initialOrientation } from "./gravity.js";
|
|
9
|
+
//#region ../../packages/core/src/domain/motion/orientation/OrientationTrack.ts
|
|
10
|
+
var DEFAULT_GRAVITY_GAIN = .2;
|
|
11
|
+
/**
|
|
12
|
+
* Length of the stillest window the gyro bias is estimated from.
|
|
13
|
+
*/
|
|
14
|
+
var BIAS_WINDOW_SECONDS = .5;
|
|
15
|
+
/**
|
|
16
|
+
* Length of the opening window whose mean gravity levels the initial pose: half a second of
|
|
17
|
+
* accelerometer readings averages out hand shake at the start.
|
|
18
|
+
*/
|
|
19
|
+
var LEVELLING_WINDOW_SECONDS = .5;
|
|
20
|
+
var QUATERNION_COMPONENTS = 4;
|
|
21
|
+
/**
|
|
22
|
+
* Gaps longer than this between samples are integrated as if they were this long: a dropout
|
|
23
|
+
* must not spin the estimate by a whole missing second.
|
|
24
|
+
*/
|
|
25
|
+
var MAX_STEP_SECONDS = .05;
|
|
26
|
+
/**
|
|
27
|
+
* The camera's orientation over the whole recording, body frame to world frame, one quaternion
|
|
28
|
+
* per gyro sample. World: y down along gravity, z along the body's forward axis at the start.
|
|
29
|
+
*/
|
|
30
|
+
var OrientationTrack = class OrientationTrack {
|
|
31
|
+
videoTimes;
|
|
32
|
+
quaternions;
|
|
33
|
+
constructor(videoTimes, quaternions) {
|
|
34
|
+
this.videoTimes = videoTimes;
|
|
35
|
+
this.quaternions = quaternions;
|
|
36
|
+
ensureInvariant(quaternions.length === videoTimes.length * QUATERNION_COMPONENTS, "orientation track arrays disagree on the sample count");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Integrates the gyro, bias-corrected and pulled towards gravity, from an initial pose that
|
|
40
|
+
* levels the opening window's gravity.
|
|
41
|
+
*/
|
|
42
|
+
static integrate(parts) {
|
|
43
|
+
const { gyro, clock, frame } = parts;
|
|
44
|
+
const videoTimes = new Float64Array(gyro.length);
|
|
45
|
+
const quaternions = new Float32Array(gyro.length * QUATERNION_COMPONENTS);
|
|
46
|
+
if (gyro.isEmpty) return new OrientationTrack(videoTimes, quaternions);
|
|
47
|
+
const context = {
|
|
48
|
+
frame,
|
|
49
|
+
bias: estimateGyroBias(gyro, frame, stillestWindow(gyro, frame, seconds(BIAS_WINDOW_SECONDS))),
|
|
50
|
+
gravityGain: parts.options?.gravityGain ?? DEFAULT_GRAVITY_GAIN
|
|
51
|
+
};
|
|
52
|
+
videoTimes.set(sampleVideoTimes(gyro, clock));
|
|
53
|
+
let state = {
|
|
54
|
+
orientation: initialOrientation(gyro, frame, seconds(LEVELLING_WINDOW_SECONDS)),
|
|
55
|
+
time: seconds(videoTimes[0] ?? 0),
|
|
56
|
+
rate: ZERO_VECTOR3
|
|
57
|
+
};
|
|
58
|
+
for (let index = 0; index < gyro.length; index += 1) {
|
|
59
|
+
const time = seconds(videoTimes[index] ?? state.time);
|
|
60
|
+
state = stepTo(state, {
|
|
61
|
+
sample: finiteSample(gyro, index),
|
|
62
|
+
time
|
|
63
|
+
}, context);
|
|
64
|
+
quaternions.set(state.orientation, index * QUATERNION_COMPONENTS);
|
|
65
|
+
}
|
|
66
|
+
return new OrientationTrack(videoTimes, quaternions);
|
|
67
|
+
}
|
|
68
|
+
get length() {
|
|
69
|
+
return this.videoTimes.length;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The orientation at a video time, interpolated between samples and held at the ends.
|
|
73
|
+
*/
|
|
74
|
+
orientationAt(videoTime) {
|
|
75
|
+
if (this.length === 0) return IDENTITY_QUATERNION;
|
|
76
|
+
const after = this.firstIndexAfter(videoTime);
|
|
77
|
+
if (after === 0) return this.quaternionAt(0);
|
|
78
|
+
if (after >= this.length) return this.quaternionAt(this.length - 1);
|
|
79
|
+
const before = after - 1;
|
|
80
|
+
const span = (this.videoTimes[after] ?? 0) - (this.videoTimes[before] ?? 0);
|
|
81
|
+
const fraction = span > 0 ? (videoTime - (this.videoTimes[before] ?? 0)) / span : 0;
|
|
82
|
+
return slerpQuaternions(this.quaternionAt(before), this.quaternionAt(after), fraction);
|
|
83
|
+
}
|
|
84
|
+
quaternionAt(index) {
|
|
85
|
+
const base = index * QUATERNION_COMPONENTS;
|
|
86
|
+
const [x = 0, y = 0, z = 0, w = 1] = this.quaternions.subarray(base, base + QUATERNION_COMPONENTS);
|
|
87
|
+
return [
|
|
88
|
+
x,
|
|
89
|
+
y,
|
|
90
|
+
z,
|
|
91
|
+
w
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
firstIndexAfter(videoTime) {
|
|
95
|
+
let low = 0;
|
|
96
|
+
let high = this.length;
|
|
97
|
+
while (low < high) {
|
|
98
|
+
const middle = Math.floor((low + high) / 2);
|
|
99
|
+
if ((this.videoTimes[middle] ?? Infinity) <= videoTime) low = middle + 1;
|
|
100
|
+
else high = middle;
|
|
101
|
+
}
|
|
102
|
+
return low;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The samples' video times, never going back as the track's capture times never do, so the
|
|
107
|
+
* lookup's binary search holds.
|
|
108
|
+
*/
|
|
109
|
+
function sampleVideoTimes(gyro, clock) {
|
|
110
|
+
return Float64Array.from({ length: gyro.length }, (_unused, index) => clock.gyroVideoTimeOf(gyro.sampleAt(index).captureTime));
|
|
111
|
+
}
|
|
112
|
+
function finiteSample(gyro, index) {
|
|
113
|
+
const sample = gyro.sampleAt(index);
|
|
114
|
+
ensureInvariant(isFiniteVector(sample.acceleration) && isFiniteVector(sample.angularVelocity), `gyro sample ${index} is not finite`);
|
|
115
|
+
return sample;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The state at the next sample: the body turned at the previous rate, pulled towards the gravity
|
|
119
|
+
* this sample measures, for the time since the previous sample (clamped, see MAX_STEP_SECONDS).
|
|
120
|
+
*/
|
|
121
|
+
function stepTo(state, { sample, time }, context) {
|
|
122
|
+
const { frame, bias, gravityGain } = context;
|
|
123
|
+
const step = clamp(time - state.time, 0, MAX_STEP_SECONDS);
|
|
124
|
+
const correction = gravityCorrection(state.orientation, toBodyFrame(frame, sample.acceleration));
|
|
125
|
+
return {
|
|
126
|
+
orientation: advance(state.orientation, corrected(state.rate, correction, gravityGain), step),
|
|
127
|
+
time,
|
|
128
|
+
rate: subtractVectors(toBodyFrame(frame, sample.angularVelocity), bias)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The gyro rate with the gravity pull added, radians per second in the body frame.
|
|
133
|
+
*/
|
|
134
|
+
function corrected(rate, correction, gain) {
|
|
135
|
+
return addVectors(rate, scaleVector(correction, gain));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* One integration step: the body turns at `rate` for `step` seconds. The rate is the one
|
|
139
|
+
* measured at the start of the interval, so a turn beginning at a sample begins exactly there.
|
|
140
|
+
*/
|
|
141
|
+
function advance(orientation, rate, step) {
|
|
142
|
+
const turn = quaternionFromRotationVector(scaleVector(rate, step));
|
|
143
|
+
return normalizeQuaternion(multiplyQuaternions(orientation, turn));
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { OrientationTrack };
|