news 2026/10/14 1:04:53

ScanNet ScannerApp 实战指南:用 Structure Sensor 采集 RGB-D 数据、五种数据格式与上传机制详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ScanNet ScannerApp 实战指南:用 Structure Sensor 采集 RGB-D 数据、五种数据格式与上传机制详解
  • 数据集
  • 计算机视觉
  • 3D视觉

【免费下载链接】ScanNet

项目地址:https://gitcode.com/gh_mirrors/sc/ScanNet
点击查看免费下载

本文基于 ScanNet 仓库中的 ScannerApp/README.md 展开,系统讲解 ScanNet iOS 扫描采集 App 的编译构建流程、五种扫描数据文件(*.txt、*.depth、*.h264、*.cam、*.imu)的格式细节,并结合仓库源码深入剖析深度帧压缩编码、H.264 硬件编码、IMU 二进制布局以及“上传 + 校验”的数据回传链路,帮助读者完整理解 ScanNet 数据集从 iPad 端采集到数据服务器落盘的整条采集链路。

一、ScannerApp 的定位:ScanNet 数据采集链路的起点

ScanNet 是一套包含超过 1500 个场景、250 万帧视图的 RGB-D 视频数据集(见仓库根 README.md)。其原始数据并非凭空产生,而是由配套 iPad 应用 ScanNet Scanner App 采集而来:

The scanner app acquires RGB-D scans using a structure.io sensor, stores color, depth and IMU data on local memory and then uploads to the data staging server for processing.

也就是说,ScannerApp 承担三件事:采集(通过 Structure.io 传感器获取深度帧、通过 iPad 摄像头获取彩色帧、通过 CoreMotion 获取 IMU 数据)、本地存储(把数据写入 iPad Documents 目录)、回传(PUT 上传至数据暂存服务器,由 Server 目录下的上传服务接收并触发后续处理)。仓库根 README 也明确了这一分工:ScannerApp负责用 iPad + Structure.io 传感器采集 RGB-D 序列,Server负责接收这些序列。

从源码结构看,App 的核心状态机定义在 ViewController.h 的ScannerState枚举中,依次为ScannerStateSceneLabelling(填写场景标签)→ScannerStateCubePlacement(放置立方体初始化位姿)→ScannerStateScanning(正式扫描)→ScannerStateViewing(查看本地已有扫描)。扫描开始前用户必须填写 userName、sceneLabel 并选择场景类型,否则 ViewController.mm 中的scanButtonPressed:会弹出 “Incomplete metadata” 提示并阻止进入扫描状态——这保证了每个序列的元数据完整性。

二、编译与构建

2.1 环境前提

README 给出的编译前提(Prerequisites)如下:

  • iPad Air 2(其他 iPad 或 iPhone 也可以运行,但官方未做测试);源码 ViewController.mm 中通过sysctlbyname("hw.machine")识别iPad5,3(Wi-Fi)/iPad5,4(LTE)机型,并对 iPad Air 2 默认开启 30 FPS 高分辨率深度流,其他设备默认关闭以避免帧率过低;
  • Xcode 7.2(可通过 App Store 或 Apple 开发者下载页获取,更新版本可用但可能需要修改工程文件);
  • 仓库内置的外部依赖:Structure.io 传感器 SDK与GPUImage图像滤波库,分别位于 ScannerApp/Frameworks/Structure.framework 和 ScannerApp/Frameworks/GPUImage。

2.2 构建步骤

README 给出的构建流程共 5 步:

  1. 用 Xcode 打开 Scanner.xcodeproj;
  2. 在 Scanner/Config.h 顶部设置数据上传服务器 URL;
  3. 连接 iOS 设备并授权开发机向其安装;
  4. 在顶部工具栏选择 “Scanner” 目标和所连设备,点击 “play” 图标构建到设备;
  5. 断开开发机、把 Structure 传感器接到 iPad 上,运行 Scanner App。

2.3 上传服务器配置(Config.h)

Config.h 是整个 App 唯一的网络配置入口,全部为宏定义:

// Upload server configuration #define SERVER "http://dovahkiin.stanford.edu" #define UPLOAD ":8000/upload" #define VERIFY ":8000/verify" // Upload stream checksum chunking size in bytes #define READ_CHUNK_SIZE 4096 #define DEBUG_MODE
  • SERVER/UPLOAD/VERIFY拼接出上传端点SERVER+UPLOAD(PUT 接收文件)与校验端点SERVER+VERIFY(GET 确认文件完整性);
  • READ_CHUNK_SIZE为 4096 字节,是本地计算 MD5 校验和时的分块读取大小(见 ViewController.mm 的calculateChecksum,按块调用CC_MD5_Update);
  • DEBUG_MODE控制调试期 UI 行为,例如仅在 DEBUG 模式下才在扫描列表行上显示 “Upload”/“Delete” 操作按钮。

对应服务端实现在 Server/upload.py,提供PUT/POST /upload与GET /verify?filename=<name>&checksum=<md5>两个路由,文件写入 staging 目录,扫描全部到齐(scan_done_uploading)后触发后续处理。upload.py头部注释还给出了模拟 iPad 上传的 curl 命令,可用于自行联调:

curl -v -X PUT -H "FILE_NAME: test.h264" --data-binary "@<path_to_some_file>" \ -H "Content-Type: application/ipad_scanner_data" "http://localhost:8000/upload"

三、一次扫描产生的文件组织

当用户按下 Scan 按钮进入扫描状态后,ViewController+Sensor.mm 的startScanningAndOpen会先确认已收到相机内参和deviceId(否则拒绝启动并提示No intrinsics received),然后以时间戳加设备 ID 作为文件前缀:

[dateFormatter setDateFormat:@"yyyy-MM-dd_hh-mm-ss"]; std::string dateString = [[dateFormatter stringFromDate:[NSDate date]] UTF8String]; ... [self openFile:(dateString + "__" + _options.deviceId)];

随后openFile:一次性打开五个输出文件,全部位于 iPad 的 Documents 目录:

文件内容
<prefix>.h264iPad 彩色相机 H.264 码流
<prefix>.depthStructure 传感器压缩深度帧流
<prefix>.txt元数据(分辨率、内外参、场景标签等)
<prefix>.imuIMU 原始二进制数据
<prefix>.camera每帧相机曝光参数

此外,第一帧还会额外导出一张<prefix>.jpg缩略图(ViewController+Sensor.mm 中g_frameCount == 1时写入),供后续 “查看历史扫描” 列表使用。扫描结束时closeFile补齐元数据帧数,并把彩色/深度时间戳数组追加写入.depth文件末尾,再逐一把五个文件关闭。

四、五种数据格式详解(README 核心内容)

4.1 元数据文件(*.txt)

README 给出的元数据文件完整样例:

colorWidth = 1296 colorHeight = 968 depthWidth = 640 depthHeight = 480 fx_color = 1170.187988 fy_color = 1170.187988 mx_color = 647.750000 my_color = 483.750000 fx_depth = 571.623718 fy_depth = 571.623718 mx_depth = 319.500000 my_depth = 239.500000 colorToDepthExtrinsics = 0.999977 0.004401 0.005230 -0.037931 -0.004314 0.999852 -0.016630 -0.003321 -0.005303 0.016607 0.999848 -0.021860 -0.000000 0.000000 -0.000000 1.000000 deviceId = AA408AE6-80BB-4E45-B6BA-5ECC8C17FB2F deviceName = iPad One sceneLabel = 0001 sceneType = Bedroom / Hotel numDepthFrames = 1912 numColorFrames = 1912 numIMUmeasurements = 4185

各字段含义可从 ViewController+Sensor.mm 的writeIntrinsics逐一对应:

  • colorWidth/colorHeight、depthWidth/depthHeight:彩色/深度帧分辨率。注意若启用半分辨率彩色(useHalfResColor),写入的彩色分辨率会减半;样例中 1296×968 说明该场景采集时未减半(iPad 后置相机原分辨率);
  • fx_color/fy_color/mx_color/my_color与fx_depth/...:两组相机内参(焦距与主点),在扫描前的前几帧由 SDK 的glProjectionMatrix反解得到(getIntrinsicsFromGlProj);
  • colorToDepthExtrinsics:4×4 彩色相机到深度相机坐标系的刚体变换(行主序展开为 16 个值)。从源码看,若使用硬件配准深度(useHardwareRegisteredDepth)则写单位阵,否则调用 SDK 的colorCameraPoseInSensorCoordinateFrame:获取真实外参;
  • deviceId/deviceName:iPad 设备 UUID 与名称(用于服务端区分采集设备);
  • sceneLabel/sceneType:用户填写的场景编号与类别,类别候选列表硬编码在 ViewController.mm,包括 Dining Room、Office、Classroom、Bedroom / Hotel、Living room / Lounge、Kitchen、Bookstore / Library、Bathroom、Conference Room、Misc. 共 9 类;
  • numDepthFrames/numColorFrames/numIMUmeasurements:扫描结束时由closeFile统计写入的三流帧数,是下游校验三流时间戳对齐的基础。

源码中还额外写入了 README 样例未列出的userName、appVersionId(形如Scanner.v1.<build>)以及closeFile追加的scanSavedAt(ISO 风格时间戳)字段,实际文件以当前 App 代码输出为准。

4.2 深度数据(*.depth)

README 描述*.depth是 Structure.io 传感器的压缩深度帧流,并指明参考 depth2pgm 代码解析。结合 depth2pgm.cpp 的实现,其文件结构可以完整还原:

  1. 逐帧组织:每帧前是一个 4 字节小端uint32_t的压缩数据长度byteSize,紧随其后是byteSize字节的压缩负载;
  2. 压缩算法:采用 Occipital 的 uplinksimple 图像编解码器(uplinksimple_image-codecs.h,基于 DCT 的位流压缩)。采集端在 ViewController+Sensor.mm 中用uplinksimple::encode压缩,压缩缓冲上限按每像素 2 字节分配((2*width+1)*height),即压缩后体积通常不高于原始数据的 2 倍;
  3. shift → depth 查表:传感器输出的是 16 位 “shift” 值,经 uplinksimple_shift2depth.h 中的 1024 项查找表换算为毫米深度(表值从 0/264mm 起单调递增至 9729mm);0xffff为无效值哨兵,解析时凡大于等于该换算结果的像素一律置 0;
  4. 时间戳尾巴:所有深度帧之后,先写入numDepthFrames个 double(深度帧时间戳,秒),再写入numColorFrames个 double(彩色帧时间戳);depth2pgm的process函数会读出深度时间戳并换算为微秒。

解析示例程序的使用方式(见 depth2pgm/README.md):

# 在 ScannerApp/depth2pgm 目录下编译 make # 用法: depth2pgm input.depth output_basename numDepthFramesToExtract

输出为二进制 PGM 格式(P5,16 位,深度单位 mm)。depth2pgm.cpp 的saveAsPGM注意了一个关键细节:写出前对每个 16 位像素做字节交换(std::swap),把小端数据转成 PGM 规范要求的网络字节序,并注释 “data values are 16-bit each”。默认按 640×480 解析,与 App 中使用的STStreamConfigDepth640x480流配置一致。

4.3 彩色数据(*.h264)

README 描述*.h264是 iPad 相机的 H.264 编码彩色帧流,可用 ffmpeg 转成图片序列:

ffmpeg -i id.h264 %6d.color.png

从 Encoder.mm 与 EncoderConfig.h 看,编码链路的实现事实包括:

  • 编码器基于 VideoToolbox 的VTCompressionSession,源像素格式为kCVPixelFormatType_420YpCbCr8BiPlanarFullRange(与 ViewController+Camera.mm 中AVCaptureVideoDataOutput的采样格式一致),格式类型在 JPEG 与 H.264 间由EncoderConfig::MediaFormat选择,扫描时固定走 H.264;
  • 编码参数在 ViewController+Sensor.mm 组装:EncoderConfig(colorWidth, colorHeight, EncoderConfig::H264, 30, _options.colorEncodeBitrate),即30 fps、码率默认 5000 kbps(Options::colorEncodeBitrate的默认值,见 ViewController.h);
  • 彩色帧在进入编码器前会取colorFrame.halfResolutionColorFrame半分辨率版本,因此元数据里的彩色分辨率与传感器原始分辨率存在 2 倍关系时需注意useHalfResColor的实际状态;
  • 输出为Annex B格式:writeToFile会遍历 AVCC 长度前缀的 NALU,逐个加上 4 字节起始码{0,0,0,1}写出;关键帧时先从CMFormatDescription中取出 SPS/PPS 各加起始码写入,保证文件可独立解码。

深度帧与彩色帧的同步由 SDK 的帧同步机制完成:相机回调captureOutput:didOutputSampleBuffer:把彩色 buffer 直接喂给[_sensorController frameSyncNewColorBuffer:],SDK 随后回调sensorDidOutputSynchronizedDepthFrame:andColorFrame:产出配对的深度/彩色帧(见 ViewController+Camera.mm 与 ViewController+Sensor.mm)。startStructureSensorStreaming会根据是否使用彩色相机选择STFrameSyncDepthAndRgb或STFrameSyncOff。

4.4 相机数据(*.cam)

README 说明*.cam是 iPad 相机设置流,包含曝光信息(README 中格式细节标注为 TODO),但源码给出了确切布局:

  • 文件头先写入 3 字节版本字符串"v1"(ViewController+Sensor.mm);
  • 之后每帧彩色数据写 5 个 32 位 float:白平衡红增益redGain、绿增益greenGain、蓝增益blueGain、曝光时长exposureDuration(秒)、ISO 值(ViewController+Sensor.mm)。

这些参数用于后续重建与标注时的曝光校正参考。App 还提供 UI 支持:扫描前可通过 AWB 开关在自动/锁定曝光白平衡间切换,或在曝光表(auto (low light)、2/5/10/15/20 ms)中手动指定曝光档位(ViewController.mm)。

4.5 IMU 数据(*.imu)

README 给出*.imu的原始二进制布局,约每秒 53 个采样,每个采样为 15 个 8 字节 double(小端、IEEE 754):

--- Repeats approx. 53 times per second --- [8-byte capture device up-time sec. represented in double precision IEEE 754 little-endian] [8-byte x rotation rate (Radians/s, double, RHR)] [8-byte y rotation rate (Radians/s, double, RHR)] [8-byte z rotation rate (Radians/s, double, RHR)] [8-byte x user-caused acceleration vector (G, double, device's reference frame)] [8-byte y user-caused acceleration vector (G, double, device's reference frame)] [8-byte z user-caused acceleration vector (G, double, device's reference frame)] [8-byte x magnetic field (microtesla, double)] [8-byte y magnetic field (microtesla, double)] [8-byte z magnetic field (microtesla, double)] [8-byte roll (Radians, double)] [8-byte pitch (Radians, double)] [8-byte yaw (Radians, double)] [8-byte x gravity vector (G, double, device's reference frame)] [8-byte y gravity vector (G, double, device's reference frame)] [8-byte z gravity vector (G, double, device's reference frame)] ----- REPEAT ------

源码侧与该布局完全吻合:ViewController.mm 的setupIMU通过CMMotionManager以 60 FPS 请求 device motion 事件(代码注释说明实际回调约为 53 FPS),processDeviceMotion:withError:按 “systemUptime → rotationRate ×3 → userAcceleration ×3 → magneticField ×3 → roll/pitch/yaw → gravity ×3” 的顺序逐字段fwrite8 字节并立即fflush,字段顺序与 README 描述一一对应;时间戳字段使用的是系统运行时间(NSProcessInfo.systemUptime),源码注释也坦诚说明这依赖主机小端浮点表示。README 同时提示 IMU 数据语义可参考 Apple CMDeviceMotion API 文档。元数据中的numIMUmeasurements即该回调中维护的计数器g_numIMUmeasurements,样例值 4185 除以 53 Hz 可反推出约 79 秒的扫描时长,与numDepthFrames = 1912(约 30 fps × 64 秒)量级吻合。

五、上传与校验:从 iPad 到数据服务器

README 开篇提到的 “uploads to the data staging server” 在源码中是一条完整的两段式链路:

  1. 上传(PUT):uploadButtonPressed:扫描 Documents 目录(排除.jpg缩略图),为每个文件构造 PUT 请求——FILE_NAME头携带文件名、Content-Type: application/ipad_scanner_data、Content-Length为文件字节数,正文为文件输入流(ViewController.mm)。uploadQueue的并发被限制为 1,即串行上传;进度通过didSendBodyData回调按已传字节/总字节驱动进度条;
  2. 校验(GET verify):单个文件返回 HTTP 200 后,App 本地按 4096 字节块(READ_CHUNK_SIZE)计算 MD5,请求VERIFY?filename=<name>&checksum=<md5>;服务端比对暂存文件的校验和,若返回 200,App 才删除本地副本(ViewController.mm)。verifyQueue并发为 3,允许校验与下一轮上传并行。

服务端对应实现在 Server/upload.py:/upload路由接收 PUT/POST 并把文件写入 staging 目录,判断该扫描的全部文件到齐(scan_done_uploading)后触发处理流程;/verify路由执行校验和比对。这套 “上传成功但校验失败则保留本地文件重试” 的设计,是采集数据在弱网环境下仍能最终一致落盘的关键。

此外,App 会检测 WiFi 连通性(isConnectedToWIFI,实际以能否通过 WiFi 到达服务器主机判断),未连网时隐藏上传按钮——即上传能力与网络状态直接联动。

六、适用前提与注意事项

  • 硬件强依赖:采集必须使用 Structure.io(Occipital)传感器 + iPad,SDK 以闭源 framework 形式内置在 ScannerApp/Frameworks/Structure.framework;传感器断连或低电量时 App 会中止扫描并复位状态(sensorDidDisconnect、sensorBatteryNeedsCharging)。
  • 工具链年代:README 明确以 Xcode 7.2 为基准,工程文件为旧版格式;新版 Xcode 编译前可能需要修改工程设置。
  • 数据解析起点:拿到*.depth数据后,推荐从 ScannerApp/depth2pgm 的示例程序入手理解帧组织与 shift→mm 换算;ScanNet 正式数据发布使用的是统一的*.sens容器格式(见 SensReader 与根 README 的 Data Organization 一节),其字段语义与本文所述的元数据、深度、IMU 布局一脉相承。
  • 服务器端:若要自行搭建接收端,可参考 Server/upload.py 及 Server/start_upload_server.sh 的启动方式,并用文件头部注释中的 curl 命令模拟 iPad 端上传做端到端验证。

读完本文,读者应能独立复现 ScannerApp 的构建配置(Config.h服务器端点)、按二进制布局解析*.depth/*.imu/*.cam五类文件、用 ffmpeg 或 depth2pgm 转换彩色与深度数据,并理解 PUT 上传 + MD5 verify 的可靠回传机制——这正是 ScanNet 数据集采集链路的完整技术全貌。

  • 数据集
  • 计算机视觉
  • 3D视觉

【免费下载链接】ScanNet

项目地址:https://gitcode.com/gh_mirrors/sc/ScanNet
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 1:04:50

易语言画板实战:手把手教你实现圆形进度条

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:04:47

CAN通信学习全攻略:从零基础到总线调试的实战路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:04:47

STM32C5开发实战:从环境搭建到TrustZone与低功耗全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:04:28

VxWorks下IGH EtherCAT主站移植实战:架构设计与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 1:03:04

Arduino常用库文件压缩包:快速搭建开发环境与避坑指南

简介&#xff1a;这份压缩包面向Arduino初学者与进阶开发者&#xff0c;汇集了日常编程中高频使用的库文件&#xff0c;帮助解决传感器驱动、通信协议、图形显示等底层适配问题&#xff0c;让互动电子制作与物联网项目能更快落地。包内共972个文件&#xff0c;以ino示例程序、h…

作者头像 李华
网站建设 2026/10/14 1:02:47

MIPS指令周期与访存边界:从硬件时序理解汇编执行

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华