
# HoloLens 2 研究模式：计算机视觉研究的工具

Dorin Ungureanu Federica Bogo Silvano Galliani Pooja Sama Xin Duan

Casey Meekhof Jan Stühmer $^{*}$ Thomas J. Cashman Bugra Tekin

Johannes L. Schönberger Pawel Olszta Marc Pollefeys 微软公司

## 摘要

混合现实头戴设备，比如微软HoloLens 2，是具备集成计算能力的强大传感设备，因此堪称计算机视觉研究的理想平台。在这份技术报告中，我们介绍了HoloLens 2研究模式——它是一套API和工具集，能够让用户访问原始传感器数据流。我们概述了该API的功能，并解释了如何利用它来开发基于传感器数据处理的混合现实应用。我们还展示了如何将研究模式的传感器数据与HoloLens 2内置的眼动追踪和手部追踪功能相结合。通过发布研究模式API及一套开源工具，我们希望推动计算机视觉及机器人领域的进一步研究，鼓励科研界贡献智慧。

### 1. 引言

混合现实技术拥有巨大的潜力，能够彻底改变我们与环境、他人以及物理世界的互动方式。像微软HoloLens 1和2这样的混合现实头戴设备已经在多个领域得到广泛应用，尤其是在一线工作者的场景中，涵盖辅助手术、远程协作、任务指引，以及在现实世界叠加数字孪生等。尽管这些领域已经有所应用，但混合现实整体仍处于起步阶段。通常来说，开发新的混合现实应用需要开展基础研究，并将不同传感器进行创新性组合。而能够高效采集原始传感器数据、开发可在设备上运行的新计算机视觉算法的工具，大大降低了该领域计算机视觉研究的入门门槛。

![Image](images/2c1ac409ceaba783a2dfafc2db9984f60c96a1eb58b3c20ab3ba131b0e8d798e.jpg)

2018年发布的第一代HoloLens研究模式，通过在设备上提供对

<div style="text-align: center;">Figure 1. Microsoft HoloLens 2.</div>

所有原始图像传感器数据流（包括深度数据和红外数据）的访问权限，助力计算机视觉研究。该研究模式连同收录辅助工具和示例应用的公共代码库一同发布[^4]，推动了HoloLens作为计算机视觉与机器人研究强大工具的应用[^1]。

2019年发布的HoloLens 2（图1）相比第一代设备在诸多方面有所改进——比如配备了专用的DNN核心、实现了精细的手部追踪和眼动追踪功能[^2]。不过，由于HoloLens 2是基于新硬件打造的全新平台，它与旧版研究模式并不兼容。

在这份技术报告中，我们介绍了2020年推出的第二代HoloLens研究模式。该研究模式提供了一组C++ API和工具，用于访问HoloLens 2的传感器数据流。我们讨论了它相较于旧版本的主要创新点，并展示了基于它开发的一系列应用。如需更多相关资料和API文档，可查阅GitHub代码库[^3]。

研究模式专为在计算机视觉和机器人领域探索新思路的学术界及工业界研究人员设计。它并不适用于面向最终用户部署的应用程序。此外，微软不保证研究模式会在未来的硬件或操作系统更新中继续得到支持。

本技术报告的其余部分安排如下。

---

![Image](images/4c70df15e294a8d878077bc48dbf1c2b1071edebf2b4b353a302f64426cfe341.jpg)

<div style="text-align: center;">Figure 2. HPU and SoC: The HPU (top) is located on the frontal part of the device, near the sensors. The SoC (bottom), a Qualcomm Snapdragon 850, is located on the rear.</div>

第2节介绍HoloLens 2设备，重点阐述其输入流；第3节概述研究模式API；第4节展示几个示例应用程序；最后，第5节总结我们的贡献。

### 2. HoloLens 2

相较于第一代设备，HoloLens 2混合现实头戴设备带来了一系列改进——包括更大的视野范围、定制的DNN核心、全关节手部追踪以及眼动追踪功能。

该设备配备了第二代定制版全息处理单元（HPU 2.0），能够实现低功耗的实时计算机视觉处理。HPU在设备上运行所有计算机视觉算法（如头部追踪、手部追踪、眼动追踪、空间映射等），并承载DNN核心。它位于设备的正面，靠近传感器位置（见图2顶部）。片上系统（SoC）中的CPU（高通骁龙850）仍完全可供应用程序使用，该SoC位于设备背面（见图2底部）。

如图3所示，该设备配备了深度摄像头、RGB摄像头、四台灰度摄像头以及惯性测量单元（IMU）；音频则由五通道麦克风阵列采集。

HoloLens 2的研究模式允许访问以下输入流：

<table border=1 style='margin: auto; word-wrap: break-word;'><tr><td style='text-align: center; word-wrap: break-word;'>Stream</td><td style='text-align: center; word-wrap: break-word;'>Resolution</td><td style='text-align: center; word-wrap: break-word;'>Format</td></tr><tr><td style='text-align: center; word-wrap: break-word;'>VLC</td><td style='text-align: center; word-wrap: break-word;'>$640 \times 480$</td><td style='text-align: center; word-wrap: break-word;'>8-bit</td></tr><tr><td style='text-align: center; word-wrap: break-word;'>Long throw depth</td><td style='text-align: center; word-wrap: break-word;'>$320 \times 288$</td><td style='text-align: center; word-wrap: break-word;'>16-bit</td></tr><tr><td style='text-align: center; word-wrap: break-word;'>Long throw AB</td><td style='text-align: center; word-wrap: break-word;'>$320 \times 288$</td><td style='text-align: center; word-wrap: break-word;'>16-bit</td></tr><tr><td style='text-align: center; word-wrap: break-word;'>AHAT</td><td style='text-align: center; word-wrap: break-word;'>$512 \times 512$</td><td style='text-align: center; word-wrap: break-word;'>16-bit</td></tr><tr><td style='text-align: center; word-wrap: break-word;'>AHAT AB</td><td style='text-align: center; word-wrap: break-word;'>$512 \times 512$</td><td style='text-align: center; word-wrap: break-word;'>16-bit</td></tr></table>

表1. 研究模式的摄像头帧信息。

- 四台可见光追踪摄像头（VLC）：用于系统实时视觉惯性SLAM的灰度摄像头（30帧/秒）。

- 深度摄像头，有两种工作模式：

- AHAT（关节式手部追踪）：用于手部追踪的高帧率（45帧/秒）近距深度感应。由于手部距离设备最远不超过1米，HoloLens 2通过基于相位飞行时间的摄像头仅计算“混叠深度”来节省电量。这意味着信号中仅包含以米为单位的距离的小数部分（见图4）。

- 远距离模式：用于设备端空间映射计算的低帧率（1-5帧/秒）远距离深度感应。

- 红外流的两种深度模式（简称AB，即主动亮度模式）：由同一调制红外信号计算得出深度信息。这些图像由红外线照射，不受环境可见光干扰（见图4右侧）。

惯性测量单元（IMU）：

- 加速度计：用于系统确定沿x、y、z轴的线性加速度以及重力。

- 陀螺仪：用于系统确定旋转情况。

- 磁力计：用于系统估算绝对方向。

对于每种流，研究模式都提供了获取帧及相关信息（如分辨率和时间戳）的接口，并能将传感器与设备及世界坐标系进行映射。表1总结了各（摄像）输入流的主要特性。下一节我们将概述相关API。

### 3. 研究模式API

在本节中，我们将介绍研究模式所提供的主要对象，以及它们如何用于访问传感器。

---

![Image](images/6e376268a2af4c66754ccdb879021b23160e7068b5babea18271a841cb5be10e.jpg)

<div style="text-align: center;">Figure 3. HoloLens 2 input sensors.</div>

![Image](images/1dc72036afad9a15d562684f01a197937157f29d9759abf244a1d636d9a8a004.jpg)

<div style="text-align: center;">Figure 4. Depth in AHAT mode: Depth (left) and Active Brightness (right) images.</div>

输入流。关于详细的API文档，请参阅[^3]。

研究模式的主传感器循环（见第3.1节）首先会创建一个ResearchModeDevice对象，该对象用于获取可用的传感器列表。传感器对象提供了检索和处理帧的方法（见第3.2节），以及确定传感器相对于设备和世界的位置的方法（见第3.3节）。

#### 3.1. 主传感器循环

主传感器处理循环包括识别ResearchModeDevice、获取传感器描述符、打开传感器流并读取帧：

HRESULT hr = S_OK;
IResearchModeSensorDevice *pSensorDevice;
std::vector<ResearchModeSensorDescriptor>
    sensorDescriptors;
size_t sensorCount = 0;

hr = CreateResearchModeSensorDevice(&pSensorDevice);
pSensorDevice->DisableEyeSelection();
hr = pSensorDevice->GetSensorCount(&sensorCount);
sensorDescriptors.resize(sensorCount);
hr = pSensorDevice->GetSensorDescriptors(
    sensorDescriptors.data(),
    sensorDescriptors.size(),
    &sensorCount);

#### 3.2. 传感器与传感器帧

请注意，OpenStream和GetNextBuffer必须在同一线程中调用。由于GetNextBuffer调用是阻塞的，因此每个传感器的帧循环应在独立的线程中运行。这样就能让各传感器以其自身的帧率进行处理。

for (auto& sensorDescriptor : sensorDescriptors)
{
    // Sensor frame read thread
    IResearchModeSensor *pSensor = nullptr;
    size_t sampleBufferSize;
    IResearchModeSensorFrame* pSensorFrame = nullptr;

    hr = pSensorDevice->GetSensor(
        sensorDescriptor.sensorType,
        &pSensor);
    hr = pSensor->GetSampleBufferSize(&sampleBufferSize);
    hr = pSensor->OpenStream();

    for (UINT i = 0; i < 4; i++)
    {
        hr = pSensor->GetNextBuffer(&pSensorFrame);
        if (pSensor->GetSensorType() >= IMU_ACCEL)
            {
                // Process IMU frame
                SaveFrameImu(pSensor, pSensorFrame, i);
            }
        else
        {
            // Process camera frame
            SaveFrameCamera(pSensor, pSensorFrame, i);
        }
        if (pSensorFrame)
            pSensorFrame->Release();
        }
    hr = pSensor->CloseStream();
    if (pSensor)
        pSensor->Release();
}
pSensorDevice->EnableEyeSelection();
pSensorDevice->Release();
return hr;

上一节介绍的IResearchModeSensor接口对研究模式的传感器进行了抽象。它提供了所有传感器共有的方法和属性：OpenStream、CloseStream、GetFriendlyName、GetSensorType、GetNextBuffer。传感器可分为以下类型：

enum ResearchModeSensorType
{
    LEFT_FRONT,
    LEFT_LEFT,
    RIGHT_FRONT,
    RIGHT_RIGHT,
    DEPTH_AHAT,
    DEPTH_LONG_THROW,
    IMU_ACCEL,
    IMU_GYRO,
    IMU_MAG
};

其中前六种类型为摄像头传感器，其余三种为IMU传感器。摄像头传感器和IMU传感器在所提供的方法上存在差异：例如，摄像头传感器提供了将相机空间中的3D点投影到图像空间中2D点的相关方法（见第3.3节），而IMU传感器则没有此类功能。

---

通过调用QueryInterface即可获取传感器的特定实现：

IResearchModeSensor* pSensor;
// ... 初始化pSensor
IResearchModeCameraSensor* pCameraSensor = nullptr;
HRESULT hr = pSensor->QueryInterface(
    IID_PPV_ARGS(&pCameraSensor));

IResearchModeSensorFrame接口与传感器特定实现之间也存在类似的区分。一旦传感器进入流式传输模式，便可通过IResearchModeSensor::GetNextBuffer来获取传感器帧。所有传感器帧都具备共同的IResearchModeSensorFrame接口，可返回各类帧通用的信息：时间戳和字节为单位的样本大小。同样，通过调用QueryInterface即可获取摄像头和IMU帧的特定实现：

IResearchModeSensorFrame* pSensorFrame;
// ... 通过GetNextBuffer获取pSensorFrame

IResearchModeSensorVLCFrame* pVLCFrame = nullptr;
HRESULT hr = pSensorFrame->QueryInterface(
    IID_PPV_ARGS(&pVLCFrame));

利用摄像头帧和IMU帧可以获取以下信息：

摄像头帧提供了分辨率、曝光度、增益等参数的获取方法。此外，

- VLC帧会返回灰度缓冲区；

- 长距深度帧会返回深度缓冲区、sigma缓冲区和有效亮度缓冲区；

- AHAT深度帧会返回深度缓冲区和有效亮度缓冲区。

有效亮度缓冲区会返回所谓的红外读数。纯净红外读数中的像素值与场景反射的光线量成正比。该图像看起来与普通红外图像类似。

长距模式下的sigma缓冲区用于根据深度算法计算出的无效掩码来标记不可靠的深度数据。出于效率考虑，AHAT模式下的无效标记代码直接嵌入到深度通道中。

以下代码演示了如何访问长距深度数据：

void ProcessFrame(IResearchModeSensor *pSensor,
                         IResearchModeSensorFrame *pSensorFrame,
                         int bufferCount)
{
    ResearchModeSensorResolution resolution;
    ResearchModeSensorTimestamp timestamp;
    IResearchModeSensorDepthFrame *pDepthFrame = nullptr;
    UINT16 *pAbImage = nullptr;
    UINT16 *pDepth = nullptr;
}

BYTE *pSigma = nullptr;
// 长距模式的无效掩码
const USHORT mask = 0x80;

HRESULT hr = S_OK;
size_t outBufferCount;

pSensorFrame->GetResolution(&resolution);
pSensorFrame->GetTimeStamp(&timestamp);

hr = pSensorFrame->QueryInterface(
    IID_PPV_ARGS(&pDepthFrame));

if (SUCCEEDED(hr))
{
    hr = pDepthFrame->GetSigmaBuffer(&pSigma,
        &outBufferCount);
    // Process the buffer...
}

if (SUCCEEDED(hr))
{
    // Extract depth buffer
    hr = pDepthFrame->GetBuffer(&pDepth,
        &outBufferCount);
    // Validate depth
    for (size_t i = 0; i < outBufferCount; ++i)
    {
        // the most significant bit of pSigma[i]
        // tells if the pixel is valid
        bool isInvalid = (pSigma[i] & mask) > 0;
        if (isInvalid)
        {
            pDepth[i] = 0;
        }
    }
    // Process the buffer...
}

if (SUCCEEDED(hr))
{
    // Extract active brightness buffer
    hr = pDepthFrame->GetAbDepthBuffer(&pAbImage,
        &outBufferCount);
    // Process the buffer...
}

if (pDepthFrame)
{
    pDepthFrame->Release();

#### 3.3. 传感器坐标系

IMU帧用于存储一批传感器采样数据。加速度计帧存储加速度计测量值和温度；陀螺仪帧存储陀螺仪测量值和温度；磁力计帧存储磁力计测量值。

所有传感器都位于设备定义的坐标系中，该坐标系被定义为rigNode。每个传感器都会返回其相对于rigNode（设备原点）的变换，该变换以外部刚体变换（旋转和平移）的形式表示。在HoloLens 2上，设备原点对应左前可见光摄像头；因此，该传感器返回的变换就是恒等变换。图5展示了相对于rigNode的摄像头坐标系。

摄像头传感器的外部变换可通过以下方式获取：

IResearchModeCameraSensor *pCameraSensor;

---

![Image](images/9170caef034a181eb5010dc4ae36f482895d715dede2dd0096a145b22025d2ec.jpg)

<div style="text-align: center;">Figure 5. Camera coordinate frames relative to the rigNode (device origin – Left Front Visible Light camera).</div>

DirectX::XMFLOAT4X4 cameraPose;
// 初始化摄像头传感器...
// 获取相对于rigNode的外部变换矩阵
pCameraSensor->GetCameraExtrinsicsMatrix(&cameraPose);

为了确定传感器在其他坐标系中的位置（例如“世界坐标系”中），我们会使用HoloLens感知API [^7]。这些API利用HoloLens头部追踪器来确定设备在给定坐标系中的位置（例如应用启动时用户所在位置定义的坐标系）。为了确定rigNode的位置，我们需要用GUID来标识它。该GUID由IResearchModeSensorDevicePerception接口提供：

using namespace winrt::Windows::Perception::Spatial;
using namespace winrt::Windows::Perception::Spatial::Preview;

HRRESULT hr = S_OK;
SpatialLocator locator;
IResearchModeSensorDevicePerception* pSensorDevicePerception;
GUID guid;
hr = m_pSensorDevice->QueryInterface(IID_PPV_ARGS(&pSensorDevicePerception));
if (SUCCEEDED(hr))
{
    hr = pSensorDevicePerception->GetRigNodeId(&guid);
    locator = SpatialGraphInteropPreview::CreateLocatorForNode(guid);
}
// 后续处理，定义另一个坐标系统...
auto location = locator.TryLocateAtTimestamp(timestamp, anotherCoordSystem);

需要注意的是，摄像头传感器并不会直接暴露其内参参数。相反，它们提供了MapImagePointToCameraUnitPlane和MapCameraSpaceToImagePoint方法，用于将摄像头参考系中的3D坐标转换为2D图像坐标，反之亦然。

### 4. HoloLens2ForCV代码库

HoloLens2ForCV代码库[^3]提供了几个UWP（通用Windows平台）示例应用，展示了如何访问和处理研究模式输入流：

![Image](images/bc2c45248f9f5232e0a0b99588384994dfbf4aa7f62111d760bf7992f7d21746.jpg)

<div style="text-align: center;">Figure 6. CameraWithCVAndCalibration app: We use OpenCV to detect aUco markers in the scene and triangulate them.</div>

例如，如何在设备上实时可视化数据流，或者如何记录这些数据流，并将其与手部及眼部追踪数据在时空上进行关联。第4.1节会对该代码库中收录的应用做简要介绍。第4.2节则展示了如何利用研究模式，仅需添加几行代码，就能原型化TSDF体积积分和SLAM等计算机视觉算法。

#### 4.1. 示例应用

在初始版本中，HoloLens2ForCV代码库收录了四个UWP应用。我们计划随着时间推移扩展这一初始集合，也欢迎社区贡献。具体而言，我们提供了示例代码，用于a) 在设备上实时可视化输入流，并使用OpenCV[^5]对其进行处理；b) 将数据流记录到磁盘以便进行离线后处理。

传感器可视化与处理：SensorVisualization应用允许用户在设备上实时可视化深度、VLC（以及可选的IMU）输入流（见图7）。这对于测试不同环境下的传感器性能尤为有用。通过CameraWithCVAndCalibration应用，我们展示了如何在设备上处理这些流：我们使用OpenCV在两个前置VLC摄像头中检测arUco标记，并对检测结果进行三角测量（图6）。CalibrationVisualization应用可用于可视化VLC和深度坐标系。

数据流记录器：StreamRecorder应用可同时捕获研究模式数据流（深度和VLC）、来自HoloLens PV（照片视频）摄像头的RGB流，以及头部、手部和眼部追踪数据。该应用提供了简单的用户界面来启动和停止捕获（图8顶部）。数据流被捕获并存储在设备上，之后可通过下载并利用一组Python脚本进行后处理。具体来说，我们提供了相关脚本，用于将研究模式、PV、头部、手部和眼部追踪数据流在时空中关联起来，

---

![Image](images/ecbbc91ec45e2ec9d660bdcaa09fbaf7e6d8e8a6ff1854267b4b6e8be9bbc333.jpg)

<div style="text-align: center;">Figure 7. Research Mode sensor streams visualization on device: here we show the two frontal VLC cameras and Long Throw Depth.</div>

![Image](images/dd77cb915b81b519108a9c4150a50d0b340c6cff0ff401ff5eeeb1a66098079d.jpg)

<div style="text-align: center;">Figure 8. The StreamRecorder user interface (top). Processed outputs: Long Throw Depth frames (piano reconstruction), hand tracker output (red), eye gaze tracker output (blue sphere).</div>

并统一到同一个坐标系中。这使我们能够重建3D场景，并通过观察用户的视线、头部和手部姿态来分析其与场景的交互情况（图8底部）。也可以添加音频捕获功能，该功能可通过Windows Media API[^6]访问，不过在本文撰写时该功能尚未实现。

值得注意的是，StreamRecorder应用依赖于另一个名为Cannon的库（可在[^3]获取）。Cannon是一组用于使用C++、Direct3D和Windows感知API构建原生混合现实应用的封装器和实用代码。它也可在研究模式之外直接使用，以实现快速简便的原生开发。

![Image](images/24252aec9419db2a48cb0eb9141dd9c5c77b6697cc29e86d0d3d51b7e924a578.jpg)

<div style="text-align: center;">Figure 9. TSDF Volume Integration [^9] result obtained from HoloLens depth and RGB input streams.</div>

它属于Perception API。即便不在Research Mode下，也能直接用于快速便捷的原生开发。

#### 4.2. 利用研究模式进行计算机视觉

借助研究模式，人们可以将HoloLens用作计算机视觉和机器人研究的强大工具。为了展示其易用性，我们提供了两个示例：TSDF体积积分和SLAM。

TSDF体积积分：我们提供了一段脚本，该脚本以长距深度帧、RGB帧以及通过StreamRecorder应用捕获的头部姿态作为输入，来执行TSDF体积积分操作。我们只需将应用输出与一个现成的库[^9]结合即可。图9展示了在将近700帧数据上运行该算法后得到的重建结果（头部轨迹以红色显示）。

SLAM：我们将长距深度帧和RGB帧作为输入，喂给一种新近提出的SLAM方法（BAD SLAM [^8]）。可以将SLAM算法估算出的相机姿态与HoloLens返回的头部姿态进行对比和评估。

### 5. 结论

我们介绍了面向HoloLens 2的研究模式，它是一套API和功能集合，能够让用户访问设备上的原始传感器数据流。随着研究模式的发布，我们还同步开放了一个公共代码库，其中收录了示例应用和案例，用以展示如何利用研究模式基于HoloLens开发计算机视觉应用。借助这些工具，我们希望能推动计算机视觉和机器人领域的进一步研究，并鼓励研究界贡献智慧。

---

## 参考文献

[^1]: Computer Vision Applications for Mixed Reality Headsets Workshop at CVPR 2019. <https://docs.microsoft.com/en-us/windows/mixed-reality/cvpr-2019>. [Online; accessed August 2020].

[^2]: Microsoft HoloLens2. <https://www.microsoft.com/en-us/hololens/hardware>. [Online; accessed August 2020].

[^3]: Microsoft HoloLens2ForCV GitHub repository. <https://github.com/microsoft/HoloLens2ForCV>. [Online; accessed August 2020].

[^4]: Microsoft HoloLensForCV GitHub repository. <https://github.com/microsoft/HoloLensForCV>. [Online; accessed August 2020].

[^5]: OpenCV library. <https://opencv.org/>. [Online; accessed August 2020].

[^6]: Windows Media APIs. <https://docs.microsoft.com/en-us/uwp/api/windows.media.capture.frames?view=winrt-19041>. [Online; accessed August 2020].

[^7]: Windows Perception APIs. <https://docs.microsoft.com/en-us/uwp/api/windows.perception.spatial?view=winrt-19041>. [Online; accessed August 2020].

[^8]: Thomas Schöps, Torsten Sattler, and Marc Pollefeys. BAD SLAM: Bundle adjusted direct RGB-D SLAM. In IEEE CVPR, 2019.

[^9]: Qian-Yi Zhou, Jaesik Park, and Vladlen Koltun. Open3D: A modern library for 3D data processing. arXiv:1801.09847, 2018.
