iOS:同步来自相机和运动数据的帧

iOS: Synchronizing frames from camera and motion data

我正在尝试从相机和相关运动数据中捕捉帧。 对于同步,我使用时间戳。视频和动作被写入文件,然后进行处理。在那个过程中,我可以计算每个视频的运动帧偏移量。

事实证明,相同时间戳的运动数据和视频数据彼此偏移了不同的时间,从 0.2 秒到 0.3 秒不等。 此偏移量对于一个视频是恒定的,但因视频而异。 如果每次都具有相同的偏移量,我将能够减去一些校准值,但事实并非如此。

有什么同步时间戳的好方法吗? 也许我没有正确记录它们? 有没有更好的办法让他们在同一个参照系下?

CoreMotion returns 相对于系统正常运行时间的时间戳,所以我添加偏移量以获得 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];

为了获得高精度的帧时间戳,我使用了 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;
    ...
}

我找到的解决这个问题的最佳方法是 运行 录制视频上的特征跟踪器,选择一个强大的特征并绘制它沿 X 轴移动的速度,然后将此图与加速度计 Y 数据相关联。

当有 2 个相似的图沿横坐标相互偏移时,有一种称为 cross-correlation 的技术可以找到偏移量。

这种方法有一个明显的缺点 - 它很慢,因为它需要一些视频处理。

关联这些时间戳实际上非常简单 - 虽然没有明确记录,但相机帧和运动数据时间戳均基于 mach_absolute_time() 时基。

这是一个在启动时重置的单调计时器,但重要的是当设备处于睡眠状态时也会停止计数。所以没有简单的方法将其转换为标准的“挂钟”时间。

谢天谢地,您不需要这样做,因为时间戳可以直接比较 - motion.timestamp 以秒为单位,您可以在回调中注销 mach_absolute_time() 以查看它是相同的时基。我的快速测试显示,运动时间戳通常在处理程序中 mach_absolute_time 之前大约 2 毫秒,这对于将数据报告给应用程序可能需要多长时间似乎是正确的。

注意mach_absolute_time()是滴答单位,需要转换成纳秒;在 iOS 10 及更高版本上,您可以使用等效的 clock_gettime_nsec_np(CLOCK_UPTIME_RAW);,其中 does the same thing.

    [_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);
    }];

对于相机,时基也是一样的:

// 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);

这里时间戳和被调用的处理程序之间的延迟有点大,通常在 10 毫秒。

我们现在需要考虑相机帧上的时间戳的实际含义 - 这里有两个问题;有限的曝光时间和滚动快门。

滚动快门意味着并非图像的所有扫描线实际上都同时被捕获 - 首先捕获顶行,最后捕获底行。这种数据的滚动读出分布在整个帧时间内,因此在 30 FPS 相机模式下,最终扫描线的曝光 start/end 时间几乎恰好在第一条扫描线的相应 start/end 时间之后的 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;

在室内设置中,曝光通常会在整个帧时间结束,因此上面的 midFrameTime 最终与 frameTime 相同。对于您通常从明亮的室外场景中获得的短曝光,差异很明显(在极快的运动下)。

为什么原来的方法有不同的偏移量

我认为你偏移的主要原因是你假设第一帧的时间戳是处理程序运行的时间 - 即它没有考虑捕获数据和它被传递到你的之间的任何延迟应用程序。特别是如果您正在为这些处理程序使用主队列,我可以想象第一帧的回调会被您提到的 0.2-0.3 秒延迟。