
1. 项目速览:把实时三维重建搬到嵌入式设备上 🚀
本项目面向室内静态场景实时三维重建,核心思路是用 Zora P1 嵌入式开发板与 Astra Pro RGB-D 相机组成实时采集端,再结合 InfiniTAM v2 的体素融合重建框架,实现静态室内环境的实时建模。项目还设计了 WiFi 数据传输模块,让嵌入式端负责采集,计算能力更强的上位机负责实时重建,达到“前端轻装上阵,后端认真干活”的效果。
项目文件夹中提供了三份代码:网络传输客户端、网络传输服务端,以及不使用网络、仅依靠 Zora P1 完成实时重建的版本。也就是说,这不是单一路线,而是“板端自力更生”和“板端采集 + 上位机重建”两种路线都试过了,工程味很足。
| 项目要素 | 内容 |
|---|---|
| 来源 | 第一届 3D 视觉创新应用竞赛参赛选手路荣丰 |
| 相机型号 | Astra Pro |
| 支持设备 | Zora P1 开发板 |
| 核心框架 | InfiniTAM v2、OpenNI、OpenGL/GLUT、OpenCV、Socket、CUDA/CPU |
| 主要输出 | 实时渲染预览、深度重建结果、STL 网格模型 |
| 源代码链接 | 3DReconstruction_Indoor-static-scene |
2. 这套系统到底怎么搭?一前一后,分工明确 🧩

项目报告与代码共同指向两种部署方案。第一种方案直接在 Zora P1 上采集并重建,优点是独立性强,缺点是算力和内存压力大;第二种方案通过 WiFi 把深度数据发给上位机处理,牺牲一点网络传输复杂度,换来更好的重建效率与显示体验。
| 方案 | 数据在哪里采集 | 重建在哪里完成 | 特点 |
|---|---|---|---|
| 板端直跑 | Zora P1 + Astra Pro | Zora P1 | 简洁独立,但速度约 2 帧/秒 |
| WiFi 分体式 | Zora P1 + Astra Pro | 上位机 | 更适合实时展示与较复杂计算 |
| 离线/文件模式 | 本地文件 | 上位机 | 便于调试 InfiniTAM 流程 |
对应到源码目录,可以这样理解:
| 目录 | 角色 | 说明 |
|---|---|---|
InfiniTAM_v2 (wifi_client) |
采集客户端 | OpenNI 读取 Astra Pro 深度帧,并通过 socket 发送 |
InfiniTAM_v2(wifi_server) |
重建服务端 | 接收深度帧,送入 InfiniTAM 重建并显示/导出 |
InfiniTAM_v2(only_Zora) |
单机板端版本 | 不走网络,在嵌入式端直接采集与重建 |
OpenNI-Linux-Arm64-2.3.0.65 |
相机 SDK | 面向 Arm64 的 OpenNI 运行环境 |
3. 技术路线:InfiniTAM 负责“长出”三维模型 🧠
InfiniTAM 的核心是体素化的实时三维重建。简单说,它把空间划分成许多小体素,把每一帧深度图融合进一个 TSDF 体素场里;随着相机移动,系统不断估计当前位姿、融合新深度、再通过 raycasting 渲染出当前模型。听起来像魔法,其实是严肃的几何计算,只是它确实有点像“用深度图喂出一个 3D 世界”。
| 阶段 | 输入 | 处理 | 输出 |
|---|---|---|---|
| 采集 | Astra Pro 深度帧 | OpenNI 读取 640×480 深度图 | 原始深度图 |
| 跟踪 | 当前深度图、上一帧模型 | ICP/深度跟踪估计相机位姿 | 新相机姿态 |
| 融合 | 深度图、相机姿态 | TSDF 体素融合 | 稠密场景模型 |
| 渲染 | 体素场 | Raycasting 生成预览图 | 实时重建画面 |
| 导出 | 体素场 | Marching Cubes 生成网格 | mesh.stl |
核心循环在 ITMMainEngine::ProcessFrame() 中非常清晰:先把原始 RGB-D 数据构造成 view,然后跟踪、融合、渲染,三步一气呵成。
// 节选自 ITMMainEngine.cpp:InfiniTAM 单帧处理主流程
void ITMMainEngine::ProcessFrame(
ITMUChar4Image *rgbImage,
ITMShortImage *rawDepthImage,
ITMIMUMeasurement *imuMeasurement)
{
if (imuMeasurement == NULL)
viewBuilder->UpdateView(&view, rgbImage, rawDepthImage,
settings->useBilateralFilter,
settings->modelSensorNoise);
else
viewBuilder->UpdateView(&view, rgbImage, rawDepthImage,
settings->useBilateralFilter,
imuMeasurement);
if (!mainProcessingActive) return;
trackingController->Track(trackingState, view);
if (fusionActive)
denseMapper->ProcessFrame(view, trackingState,
scene, renderState_live);
trackingController->Prepare(trackingState, view, renderState_live);
}
4. 嵌入式采集端:Astra Pro 负责看,Zora P1 负责搬 📷

在 wifi_client 版本中,OpenNI 引擎被改造为带 socket 的采集模块。它启动 Astra Pro 的深度流,读取深度帧,将深度数据复制到 InfiniTAM 的 rawDepthImage,同时把同一帧转成 OpenCV Mat 并发给服务端。
// 节选自 OpenNIEngine.cpp:读取 Astra Pro 深度帧并通过 socket 发送
if (depthAvailable)
{
const openni::DepthPixel* depthImagePix =
(const openni::DepthPixel*)data->depthFrame.getData();
memcpy(depth, depthImagePix,
rawDepthImage->dataSize * sizeof(short));
cv::Mat image(480, 640, CV_16SC1, depth);
if (ni_socket.transmit(image) > 0)
{
// depth frame transmitted
}
else
{
printf("transmit failed");
}
}
报告中也说明,当前官方 OpenNI 模块尚未成功读取 RGB 信息,因此本项目阶段主要使用深度图完成重建,彩色纹理渲染没有完全实现。这一点很关键:它不是“彩色模型没写”,而是相机数据链路里的 RGB 获取还未打通。
5. WiFi 传输:把深度图拆包送到上位机 📡

项目的网络模块没有搞复杂协议,而是用了 TCP socket 传输固定尺寸的深度图。深度帧是 640×480、CV_16SC1,被拆成 2 个包发送。每个包里包含一段深度数组和一个 flag,服务端根据 flag 判断帧是否收完整。
| 传输字段 | 数值/类型 | 作用 |
|---|---|---|
| 图像宽度 | 640 | 与 Astra Pro 深度帧一致 |
| 图像高度 | 480 | 与 Astra Pro 深度帧一致 |
| 图像类型 | CV_16SC1 |
单通道 16 位深度 |
| 分包数 | PACKAGE_NUM = 2 |
降低单次发送数据块大小 |
| 端口 | 6666 |
服务端监听端口 |
客户端连接上位机的 IP 与端口,然后逐包发送深度数据。
// 节选自 SocketMatTransmissionClient.cpp:客户端建立 TCP 连接
int SocketMatTransmissionClient::socketConnect(const char* IP, int PORT)
{
if ((sockClient = socket(AF_INET, SOCK_STREAM, 0)) < 0)
return -1;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
if (inet_pton(AF_INET, IP, &servaddr.sin_addr) <= 0)
return -1;
if (connect(sockClient,
(struct sockaddr*)&servaddr,
sizeof(servaddr)) < 0)
return -1;
}
// 节选自 SocketMatTransmissionClient.cpp:深度图拆成两包发送
for (int k = 0; k < PACKAGE_NUM; k++)
{
int num1 = IMG_HEIGHT / PACKAGE_NUM * k;
for (int i = 0; i < IMG_HEIGHT / PACKAGE_NUM; i++)
{
short *ucdata = image.ptr<short>(i + num1);
for (int j = 0; j < IMG_WIDTH; j++)
data.buf[i * IMG_WIDTH + j] = ucdata[j];
}
data.flag = (k == PACKAGE_NUM - 1) ? 2 : 1;
send(sockClient, (char *)(&data), sizeof(data), 0);
}
服务端负责监听、接收并重组图像。这个部分很像快递分拣:包裹到了不算完,还得按顺序拼回一整张深度图。
// 节选自 SocketMatTransmissionServer.cpp:服务端监听并接收客户端连接
int SocketMatTransmissionServer::socketConnect(int PORT)
{
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(server_sockfd,
(struct sockaddr *)&server_sockaddr,
sizeof(server_sockaddr));
listen(server_sockfd, 5);
sockConn = accept(server_sockfd,
(struct sockaddr*)&client_addr,
&length);
}
// 节选自 SocketMatTransmissionServer.cpp:接收两包并恢复为深度图
cv::Mat img(IMG_HEIGHT, IMG_WIDTH, CV_16SC1, cv::Scalar(0));
for (int i = 0; i < PACKAGE_NUM; i++)
{
int pos = 0;
while (pos < sizeof(recvBuf))
{
int len0 = recv(sockConn,
(char *)(&data) + pos,
sizeof(recvBuf) - pos,
0);
pos += len0;
}
int num1 = IMG_HEIGHT / PACKAGE_NUM * i;
for (int j = 0; j < IMG_HEIGHT / PACKAGE_NUM; j++)
{
short *ucdata = img.ptr<short>(j + num1);
for (int k = 0; k < IMG_WIDTH; k++)
ucdata[k] = data.buf[j * IMG_WIDTH + k];
}
}
6. 上位机重建端:收到深度图后,立刻喂给 InfiniTAM 🖥️
在 wifi_server 版本中,服务端程序先启动 socket 监听,再创建图像源。这里的 ImageFileReader 已不再单纯读本地文件,而是被改造成“从网络接收深度图,再变成 InfiniTAM 输入”的桥梁。
// 节选自 InfiniTAM.cpp:服务端启动 socket,再创建重建输入源
SocketMatTransmissionServer socketMat;
if (socketMat.socketConnect(6666) < 0)
{
return 0;
}
CreateDefaultImageSource(imageSource, imuSource,
socketMat,
arg1, arg2, arg3, arg4);
ImageSourceEngine.cpp 中的 loadIntoCache() 接收图像,把 OpenCV Mat 中的深度值写入 ITMShortImage,随后 InfiniTAM 主循环就能像读取普通深度帧一样处理它。
// 节选自 ImageSourceEngine.cpp:网络深度图写入 InfiniTAM 缓存
cv::Mat image_receive(480, 640, CV_16SC1);
if (engine_socketMat.receive(image_receive) > 0)
{
Vector2i newSize(640, 480);
cached_depth->ChangeDims(newSize);
short *ucdata = image_receive.ptr<short>(0);
for (int j = 0; j < 480 * 640; ++j)
{
cached_depth->GetData(MEMORYDEVICE_CPU)[j] = ucdata[j];
}
}
7. 为什么是静态场景?因为相机动,场景最好别跟着闹 🪑
InfiniTAM 这类深度融合方法默认场景相对静止。系统通过连续深度帧估计相机姿态,并把新的深度观测融合到同一个场景模型里。如果场景里的人、椅子、箱子大范围移动,系统可能会把“移动物体的不同位置”都融合进去,最后模型看起来就会有重影。室内静态场景,就是这套算法比较舒适的工作区。
| 条件 | 对系统的影响 | 建议 |
|---|---|---|
| 场景静止 | 深度融合稳定 | 推荐使用 |
| 相机缓慢移动 | ICP 跟踪更容易收敛 | 推荐使用 |
| 相机快速晃动 | 位姿估计容易失败 | 尽量避免 |
| 大面积动态物体 | 模型可能出现重影 | 采集时保持场景稳定 |
| 弱几何区域 | 深度跟踪约束不足 | 增加扫描角度 |
8. 体素融合:不是存一堆点,而是在空间里“填体素” 🧱

InfiniTAM 的 ITMDenseMapper::ProcessFrame() 展示了典型的体素融合流程:先根据当前深度图分配可见体素块,再把深度观测融合进场景。如果开启 swapping,还可以在 CPU/GPU 之间交换体素数据,用于更大场景。
// 节选自 ITMDenseMapper.cpp:分配体素块并融合深度帧
void ITMDenseMapper<TVoxel,TIndex>::ProcessFrame(
const ITMView *view,
const ITMTrackingState *trackingState,
ITMScene<TVoxel,TIndex> *scene,
ITMRenderState *renderState)
{
sceneRecoEngine->AllocateSceneFromDepth(
scene, view, trackingState, renderState);
sceneRecoEngine->IntegrateIntoScene(
scene, view, trackingState, renderState);
if (swappingEngine != NULL)
{
swappingEngine->IntegrateGlobalIntoLocal(scene, renderState);
swappingEngine->SaveToGlobalMemory(scene, renderState);
}
}
系统默认设置中,场景体素参数和跟踪器也很有信息量:trackerType = TRACKER_ICP,说明位姿估计主要基于深度 ICP;voxelSize = 0.005f,代表体素分辨率为 5 mm 量级。分辨率越细,模型越精致,但内存和速度也会“当场加班”。
// 节选自 ITMLibSettings.cpp:场景参数与跟踪器设置
ITMLibSettings::ITMLibSettings(void)
: sceneParams(0.02f, 100, 0.005f, 0.2f, 3.0f, false)
{
#ifndef COMPILE_WITHOUT_CUDA
deviceType = DEVICE_CUDA;
#else
deviceType = DEVICE_CPU;
#endif
useSwapping = false;
useApproximateRaycast = false;
useBilateralFilter = false;
trackerType = TRACKER_ICP;
}
9. 实时显示与网格导出:能看,也能存 🧊

UI 层使用 OpenGL/GLUT 显示重建结果、原始深度图和渲染视图。按键 w 可以导出 mesh.stl,项目目录中也确实存在生成后的 STL 文件,其中 wifi_server 版本的 mesh.stl 约 75 MB,说明上位机方案已经产出较完整的网格结果。
| 结果文件 | 位置 | 大小 |
|---|---|---|
mesh.stl |
InfiniTAM_v2(wifi_server)/cmake-build-relwithdebinfo |
约 75 MB |
mesh.stl |
InfiniTAM_v2(only_Zora)/InfiniTAM/cmake-build-relwithdebinfo |
约 39 MB |
mesh.stl |
InfiniTAM_v2 (wifi_client)/InfiniTAM/cmake-build-relwithdebinfo |
约 39 MB |
// 节选自 UIEngine.cpp:按 w 导出 STL 网格
case 'w':
printf("saving mesh to disk ...");
uiEngine->SaveSceneToMesh("mesh.stl");
printf(" donen");
break;
10. 工程环境:别小看这些依赖,少一个都可能不开心 🧰
报告中列出的部署环境包括上位机 Ubuntu 18.04、嵌入式端 armbin、CMake 3.2 及以上、OpenNI、OpenGL、libpng、librealsense 等。源码的 CMake 配置也显示,项目针对不同端使用了不同 OpenNI 路径:客户端面向 Arm64 OpenNI,服务端面向 x64 OpenNI。
| 环境项 | 用途 |
|---|---|
| Ubuntu 18.04 | 上位机运行环境 |
| armbin | 嵌入式端系统环境 |
| OpenNI | Astra Pro 深度流读取 |
| OpenGL/GLUT | 实时可视化窗口 |
| CMake | 工程构建 |
| CUDA/CPU | InfiniTAM 计算后端 |
| Socket | WiFi 数据传输 |
# 节选自 CMakeLists.txt:客户端面向 Arm64 OpenNI
set(INC_DIR
/home/orbbec/Desktop/InfiniTAM-infinitam_v2/
OpenNI-Linux-Arm64-2.3.0.65/include)
link_libraries(libOpenNI2.so)
link_libraries(liborbbec.so)
link_libraries(libOniFile.so)
11. 技术亮点:它不是炫技,是把链路跑通了 ✨
| 亮点 | 解读 |
|---|---|
| 嵌入式采集 | Zora P1 与 Astra Pro 组成轻量前端 |
| WiFi 分体架构 | 采集与重建解耦,便于上位机承担重计算 |
| InfiniTAM 移植 | 将成熟体素融合框架用于室内静态重建 |
| 深度 ICP 跟踪 | 依靠深度几何估计相机位姿 |
| STL 导出 | 重建结果可保存为网格模型 |
| 性能测量 | 多处加入时间统计,定位耗时瓶颈 |
12. 当前局限:它会建模,但还没练成完全体 🛠️
项目报告和源码都显示,项目已经实现了基本实时重建,但仍有几个明确边界。最主要的是:嵌入式端内存较小,速度约 2 帧/秒;RGB 读取与彩色纹理未完全实现;没有使用 IMU,因此更适合相机缓慢移动、场景静止的重建条件。
| 局限 | 表现 | 后续方向 |
|---|---|---|
| 嵌入式算力有限 | 板端速度约 2 帧/秒 | 优化分辨率、体素尺度、CUDA/CPU 后端 |
| RGB 未完全打通 | 重建结果缺少彩色纹理 | 完善 OpenNI RGB 读取与配准 |
| 未接入 IMU | 快速移动时鲁棒性不足 | 加入 IMU 辅助位姿预测 |
| 交互未完善 | 板端按钮/控制未完成 | 增加嵌入式端控制界面 |
| 网络传输简单 | 固定分包,缺少完整校验 | 增加帧号、校验和、重传策略 |
13. 后续优化路线:让小板子跑得更稳,让模型长得更像 🧭

| 优化方向 | 具体做法 | 收益 |
|---|---|---|
| 网络协议增强 | 加入帧号、时间戳、校验和 | 降低丢包错帧风险 |
| RGB-D 对齐 | 同步读取 RGB 与 Depth,并做外参配准 | 支持彩色重建 |
| IMU 融合 | 将 IMU 预测作为跟踪初值 | 提升快速移动稳定性 |
| 动态分辨率 | 根据网络与算力调整深度图尺寸 | 平衡帧率与精度 |
| 体素参数调优 | 调整体素大小、截断距离和最大权重 | 优化速度与模型细节 |
| 板端交互 | 增加按钮或轻量控制界面 | 方便独立运行 |
14. 总结:这是一次“让嵌入式设备参与三维重建”的扎实尝试 🎯
这个项目的价值不只在于完成了一个室内静态场景重建 demo,更在于把 RGB-D 相机、Zora P1 嵌入式采集、WiFi 深度传输、InfiniTAM 实时融合、STL 网格导出串成了一条完整工程链路。它既验证了板端直接重建的可行性,也展示了“嵌入式采集 + 上位机重建”这种更实用的协同方案。
如果说传统 PC 端重建像是在实验室里慢慢打磨模型,那么这个项目更像是把三维重建带出工位,让一块小板子也参与到空间建模中。它还不完美,但方向很清楚:让实时三维重建更轻、更灵活,也更接近未来的移动式空间感知应用。