ser*_*lex 6 video synchronization avfoundation ios core-motion
我正在尝试从相机和相关运动数据中捕获帧。对于同步,我使用时间戳。视频和动作被写入文件,然后进行处理。在那个过程中,我可以计算每个视频的运动帧偏移。
结果表明,相同时间戳的运动数据和视频数据相互偏移了不同的时间,从 0.2 秒到 0.3 秒不等。该偏移对于一个视频是恒定的,但因视频而异。如果每次都是相同的偏移量,我就可以减去一些校准值,但事实并非如此。
有没有一种同步时间戳的好方法?也许我没有正确记录它们?有没有更好的方法将它们带到相同的参考系?
CoreMotion 返回相对于系统正常运行时间的时间戳,因此我添加偏移量以获得 unix 时间:
uptimeOffset = [[NSDate date] timeIntervalSince1970] -
[NSProcessInfo processInfo].systemUptime;
CMDeviceMotionHandler blk =
^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error){
if(!error){
motionTimestamp = motion.timestamp + uptimeOffset;
...
}
};
[motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXTrueNorthZVertical
toQueue:[NSOperationQueue currentQueue]
withHandler:blk];
Run Code Online (Sandbox Code Playgroud)
为了获得高精度的帧时间戳,我使用了 AVCaptureVideoDataOutputSampleBufferDelegate。它也偏移到 unix 时间:
-(void)captureOutput:(AVCaptureOutput *)captureOutput
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
fromConnection:(AVCaptureConnection *)connection
{
CMTime frameTime = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer);
if(firstFrame)
{
firstFrameTime = CMTimeMake(frameTime.value, frameTime.timescale);
startOfRecording = [[NSDate date] timeIntervalSince1970];
}
CMTime presentationTime = CMTimeSubtract(frameTime, firstFrameTime);
float seconds = CMTimeGetSeconds(presentationTime);
frameTimestamp = seconds + startOfRecording;
...
}
Run Code Online (Sandbox Code Playgroud)
关联这些时间戳实际上非常简单 - 尽管没有明确记录,但相机帧和运动数据时间戳都是基于时基的mach_absolute_time()
。
这是一个单调计时器,在启动时重置,但重要的是,当设备休眠时也会停止计数。因此没有简单的方法将其转换为标准的“挂钟”时间。
值得庆幸的是,您不需要,因为时间戳可以直接比较 - Motion.timestamp 以秒为单位,您可以mach_absolute_time()
在回调中注销以查看它是相同的时基。我的快速测试显示,运动时间戳通常在mach_absolute_time
处理程序之前大约 2 毫秒,这似乎与数据报告给应用程序可能需要的时间差不多。
注意mach_absolute_time()
是刻度单位需要转换为纳秒;在 iOS 10 及更高版本上,您可以使用clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
具有相同功能的等效项。
[_motionManager
startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical
toQueue:[NSOperationQueue currentQueue]
withHandler:^(CMDeviceMotion * _Nullable motion, NSError * _Nullable error) {
// motion.timestamp is in seconds; convert to nanoseconds
uint64_t motionTimestampNs = (uint64_t)(motion.timestamp * 1e9);
// Get conversion factors from ticks to nanoseconds
struct mach_timebase_info timebase;
mach_timebase_info(&timebase);
// mach_absolute_time in nanoseconds
uint64_t ticks = mach_absolute_time();
uint64_t machTimeNs = (ticks * timebase.numer) / timebase.denom;
int64_t difference = machTimeNs - motionTimestampNs;
NSLog(@"Motion timestamp: %llu, machTime: %llu, difference %lli", motionTimestampNs, machTimeNs, difference);
}];
Run Code Online (Sandbox Code Playgroud)
对于相机来说,时基也是相同的:
// In practice gives the same value as the CMSampleBufferGetOutputPresentationTimeStamp
// but this is the media's "source" timestamp which feels more correct
CMTime frameTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
uint64_t frameTimestampNs = (uint64_t)(CMTimeGetSeconds(frameTime) * 1e9);
Run Code Online (Sandbox Code Playgroud)
这里时间戳和被调用的处理程序之间的延迟有点大,通常在 10 毫秒内。
我们现在需要考虑相机帧上的时间戳实际上意味着什么 - 这里有两个问题;有限曝光时间和卷帘快门。
卷帘快门意味着实际上并非同时捕获图像的所有扫描线 - 首先捕获顶行,最后捕获底行。这种数据的滚动读出分布在整个帧时间内,因此在 30 FPS 相机模式下,最终扫描线的曝光开始/结束时间几乎正好是第一条扫描线各自的开始/结束时间之后的 1/30 秒。
我的测试表明 AVFoundation 帧中的呈现时间戳是帧读出的开始 - 即第一条扫描线曝光的结束。因此,最终扫描线的曝光结束是frameDuration
在这之后的几秒,而第一条扫描线的曝光的开始是exposureTime
在此之前的几秒。因此,位于帧曝光中心(图像中间扫描线的曝光中点)的时间戳可以计算为:
const double frameDuration = 1.0/30; // rolling shutter effect, depends on camera mode
const double exposure = avCaptureDevice.exposureDuration;
CMTime frameTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
double midFrameTime = CMTimeGetSeconds(frameTime) - exposure * 0.5 + frameDuration * 0.5;
Run Code Online (Sandbox Code Playgroud)
在室内环境中,无论如何,曝光通常都会以全帧时间结束,因此midFrameTime
上面的结果与frameTime
. 当您通常从明亮的户外场景中获得短曝光时,差异是显而易见的(在极快的运动下)。
我认为偏移的主要原因是您假设第一帧的时间戳是处理程序运行的时间 - 即它没有考虑捕获数据和将其传递到您的应用程序之间的任何延迟。特别是如果您使用这些处理程序的主队列,我可以想象第一帧的回调会延迟您提到的 0.2-0.3 秒。