简介:这份资源面向计算机视觉学习者与三维重建入门者,提供一套基于MATLAB的空间雕刻(Space Carving)三维重构完整实现,用于从多视角序列图像中恢复物体三维几何形状,适合具备一定图像处理基础、希望动手理解体素雕刻原理的读者。压缩包共99个文件,约44.68MB,包含39张jpg与37个ppm序列图像、17个m脚本、2个mat数据文件及2份pdf参考文档,覆盖图像序列、算法代码与体素数据等模块。资源围绕视图配准、遮挡分析、体素模型构建与后处理等环节展开,配有恐龙模型的连续视图数据,可对照代码理解可见性信息如何逐步雕刻出物体轮廓。目前已有786人学习下载,适合作为三维重建课程实验或自学练手项目,帮助读者打通从序列图像处理到体素建模的完整链路。
1. 序列图像做三维重构,为什么我最后选了 Space Carving 而不是泊松重建
手里有一组围绕目标物体拍摄的序列图像,相机内参外参都标定好了,想把它重建成带纹理的三维模型——这个需求在文物数字化、零件逆向、电商展示里都很常见。很多人第一反应是上泊松重建或者神经辐射场,但如果你要的是「轮廓可控、能直接看到体素雕刻过程、代码量小到能塞进一个 MATLAB 工程」的方案,Space Carving(空间雕刻)反而是最稳的起点。它的核心逻辑很朴素:先假设整个空间被一个立方体包住,然后从每张图像的轮廓反投影出锥体,所有锥体的交集就是物体的可视外壳,把外壳外的体素一层层「雕」掉,剩下的就是重建结果。
这套思路对序列图像的要求不高,不需要稠密匹配,不需要 GPU,MATLAB 里几百行就能跑通。适合谁?适合手头有标定好的多视角图像、想快速验证三维重建流程、又不想被深度学习框架绑架的从业者。下面我把这份 MATLAB 实现拆开讲,从体素初始化到轮廓反投影,再到可视化,每一步都落到能抄的代码和参数上。
2. Space Carving 的数学底子:可视外壳、体素与投影矩阵怎么对上
2.1 可视外壳不是真表面,但够用
Space Carving 重建出来的是「可视外壳」(Visual Hull),它是真实物体表面的一个外包围,凹面会被填平。这不是缺陷,而是这个方法的边界——你拿到的模型在轮廓精度上没问题,但内凹结构(比如杯子把手内侧)会丢失。理解这一点,后面调参时就不会纠结「为什么这里鼓出来了」。
数学上,可视外壳等于所有相机锥体的交集。每个相机锥体由相机光心和该视角下物体的轮廓多边形定义。体素如果落在任意一个锥体外面,就说明它在这个视角下投影到了轮廓外,必然不属于物体,直接删掉。迭代所有视角,剩下的体素集合就是外壳。
2.2 体素网格初始化:分辨率决定一切
体素网格是重建的「画布」。分辨率太低,模型像马赛克;太高,内存和计算时间爆炸。我一般先用 64³ 跑通流程,确认投影矩阵没问题后再上 128³ 或 256³。
% 体素网格初始化 % voxelSize: 体素物理边长,单位与标定一致 % gridSize: 每个维度的体素数,常用 64 / 128 / 256 gridSize = 128; voxelSize = 0.005; % 5mm,根据物体实际尺寸调整 % 以世界坐标原点为中心构建网格 halfRange = gridSize * voxelSize / 2; xRange = linspace(-halfRange, halfRange, gridSize); yRange = linspace(-halfRange, halfRange, gridSize); zRange = linspace(-halfRange, halfRange, gridSize); [X, Y, Z] = meshgrid(xRange, yRange, zRange); voxelCoords = [X(:), Y(:), Z(:), ones(numel(X), 1)]; % 齐次坐标 N×4 % 初始全部标记为占用 occupied = true(gridSize, gridSize, gridSize);这段代码的关键在voxelCoords的齐次坐标构造。Space Carving 的每一步都要把体素投影到图像平面,齐次坐标能让投影变成一次矩阵乘法。voxelSize要和相机标定的世界单位一致,如果标定用的是毫米,这里就填毫米,别混。
提示:
gridSize从 64 跳到 128,体素数量是 8 倍增长,内存占用和循环时间同步翻 8 倍。256³ 在 16GB 内存的机器上就要小心了,建议先做一次内存预估。
2.3 投影矩阵与轮廓反投影
每个视角需要两样东西:投影矩阵 P(3×4)和二值轮廓图。投影矩阵来自相机标定,轮廓图可以用背景减除或手动抠图得到。核心操作是把体素投影到图像上,查该位置的轮廓值。
% 对每个视角执行雕刻 for viewIdx = 1:numViews P = projectionMatrices{viewIdx}; % 3×4 投影矩阵 silhouette = silhouettes{viewIdx}; % 二值轮廓,1 为物体 [imgH, imgW] = size(silhouette); % 体素投影到图像平面 projCoords = (P * voxelCoords')'; % N×3 u = projCoords(:,1) ./ projCoords(:,3); v = projCoords(:,2) ./ projCoords(:,3); % 最近邻查轮廓,越界视为背景 uRound = round(u); vRound = round(v); valid = uRound >= 1 & uRound <= imgW & vRound >= 1 & vRound <= imgH; insideSilhouette = false(size(uRound)); idx = sub2ind([imgH, imgW], vRound(valid), uRound(valid)); insideSilhouette(valid) = silhouette(idx) > 0; % 投影到轮廓外的体素删除 occupied(insideSilhouette == false) = false; end逻辑说明:P * voxelCoords'得到的是齐次投影坐标,除以第三维才是像素坐标。round做最近邻采样,这里可以换成双线性插值,但对二值轮廓来说最近邻足够。valid处理投影落在图像外的体素,这些体素直接判为背景删除。sub2ind把二维索引转成一维,比双重循环快一个数量级。
参数上,projectionMatrices的格式要和标定工具输出对齐。常见做法是用棋盘格标定得到每张图的旋转和平移,再组装成 3×4 矩阵。如果投影结果整体偏移,八成是矩阵转置或者坐标系方向反了,先拿一个已知世界点验证投影位置。
3. 从轮廓图到体素雕刻:完整流程与参数调优
3.1 轮廓提取:背景减除的阈值怎么定
轮廓质量直接决定重建质量。我一般用背景减除:先拍一张纯背景图,然后每张图减去背景,阈值化得到轮廓。
% 背景减除提取轮廓 background = imread('background.png'); backgroundGray = rgb2gray(background); silhouettes = cell(numViews, 1); for i = 1:numViews img = imread(sprintf('view_%02d.png', i)); imgGray = rgb2gray(img); % 差分并阈值化 diff = abs(double(imgGray) - double(backgroundGray)); threshold = 30; % 经验值,根据光照稳定性调整 mask = diff > threshold; % 形态学清理:去噪点、填小洞 mask = bwareaopen(mask, 50); % 去掉小于 50 像素的连通域 mask = imfill(mask, 'holes'); % 填充内部空洞 mask = imopen(mask, strel('disk', 2)); % 平滑边缘 silhouettes{i} = mask; endthreshold是最需要调的参数。光照稳定时 20~30 够用,有阴影或反光时可能要上 50。bwareaopen的 50 是面积阈值,图像分辨率高就调大。imopen的 disk 半径 2 能去掉边缘毛刺,但太大会腐蚀轮廓,导致重建模型偏瘦。
注意:轮廓必须闭合且无内部空洞,否则雕刻时会误删物体内部体素。跑完提取后建议把每张轮廓叠加显示一遍,肉眼确认。
3.2 雕刻顺序与提前终止
雕刻是迭代过程,每处理一个视角就删一批体素。视角顺序不影响最终结果,但影响中间过程的体素数量。我一般把轮廓面积小的视角放前面,先删掉大量明显在外的体素,后续视角处理的数据量就小了。
% 按轮廓面积升序排列视角,加速收敛 areas = cellfun(@(m) sum(m(:)), silhouettes); [~, order] = sort(areas, 'ascend'); for k = 1:numViews viewIdx = order(k); % ... 执行上面的雕刻逻辑 ... % 提前终止:如果剩余体素太少,可能参数有误 remaining = sum(occupied(:)); if remaining < 100 warning('体素剩余过少,检查投影矩阵或轮廓质量'); break; end end提前终止是个保险。正常情况下 128³ 网格雕刻完应该剩几千到几万个体素。如果只剩几百个,要么投影矩阵错了,要么轮廓几乎全黑。
3.3 参数对照表
| 参数 | 常用值 | 影响 | 调整方向 |
|---|---|---|---|
| gridSize | 64 / 128 / 256 | 模型精度与内存 | 先 64 验证,再 128 出结果 |
| voxelSize | 0.002~0.01 | 物理尺度 | 与标定单位一致 |
| threshold | 20~50 | 轮廓胖瘦 | 轮廓缺失就调低,噪点多就调高 |
| bwareaopen 面积 | 30~100 | 去噪强度 | 分辨率高就调大 |
| imopen 半径 | 1~3 | 边缘平滑 | 太大会腐蚀轮廓 |
这张表建议存下来,每次换数据集先按这个范围试。
4. 避坑与排查:投影偏移、轮廓空洞、内存爆掉怎么救
4.1 重建结果整体偏移或镜像
现象:雕刻完的模型不在网格中心,或者左右颠倒。
原因:投影矩阵的坐标系定义和体素网格的世界坐标系不一致。常见的是标定输出用 OpenCV 坐标系(Y 向下),而 MATLAB 网格用 Y 向上;或者投影矩阵转置了。
解决:拿一个已知世界坐标点(比如标定板的角点),手动算它的投影像素位置,和实际图像里的位置对比。如果 U 方向对但 V 方向反了,就是 Y 轴方向问题,在投影矩阵前乘一个翻转矩阵修正。
4.2 轮廓内部有空洞导致模型被掏空
现象:重建模型表面有洞,或者内部被挖空。
原因:轮廓提取时阈值过高,物体表面暗部被当成背景,轮廓内部出现空洞。雕刻时这些空洞对应的体素被误删。
解决:在轮廓提取后加imfill(mask, 'holes'),并检查每张轮廓的连通域数量。正常应该只有一个主连通域,如果出现多个,说明阈值或光照有问题。
4.3 内存不足或循环太慢
现象:MATLAB 报 Out of Memory,或者跑一晚上没结果。
原因:256³ 网格的体素坐标矩阵是 1600 万行 × 4 列,double 类型占 512MB,加上投影中间变量轻松超过 4GB。而且全量投影是 N×4 乘 4×3,每次循环都在操作千万级矩阵。
解决:分块处理。把体素网格按 Z 轴切成若干片,每次只加载一片做投影和雕刻。或者用single类型代替double,内存减半,精度对体素雕刻足够。
% 分块雕刻示例:按 Z 轴切片 sliceSize = 16; % 每次处理 16 层 for zStart = 1:sliceSize:gridSize zEnd = min(zStart + sliceSize - 1, gridSize); % 只提取这一片的体素坐标 % ... 投影和雕刻逻辑 ... end4.4 轮廓与图像不对齐
现象:投影查轮廓时,物体边缘的体素被错误保留或删除,模型边缘毛糙。
原因:轮廓图和投影矩阵来自不同分辨率的图像,或者轮廓提取时做了缩放但没同步更新内参。
解决:确保轮廓图和标定用的图像同分辨率。如果必须缩放,投影矩阵的内参部分要按比例修改。我一般不做缩放,直接在原图上提轮廓。
5. 进阶技巧:从可视外壳到带纹理模型,以及验证重建质量
5.1 用 marching cubes 提取表面网格
雕刻完得到的是体素占用网格,要变成可用的三维模型,需要提取等值面。MATLAB 自带isosurface就是 marching cubes 的实现。
% 从占用网格提取表面 % 先构造平滑的标量场:占用为 1,空为 0,做一次高斯平滑 occupancyField = double(occupied); occupancyField = smooth3(occupancyField, 'gaussian', 5); % 提取等值面,阈值取 0.5 fv = isosurface(X, Y, Z, occupancyField, 0.5); % 简化网格(可选) fv = reducepatch(fv, 0.5); % 保留 50% 面片 % 导出为 STL stlwrite('reconstructed.stl', fv);smooth3的窗口大小 5 是经验值,太小表面粗糙,太大细节丢失。reducepatch的 0.5 表示保留一半面片,用于减小文件体积,对精度要求高就别简化。
5.2 纹理映射:把序列图像贴回模型
有了网格和相机参数,纹理映射就是反投影。对每个面片,找到它可见的视角,把该视角的图像像素采样到面片顶点上。
% 简单纹理映射:选一个主视角做投影 % 实际工程中会做多视角融合,这里演示单视角 mainView = 1; P = projectionMatrices{mainView}; img = imread(sprintf('view_%02d.png', mainView)); % 面片顶点投影到图像 vertices = fv.vertices; verticesH = [vertices, ones(size(vertices,1), 1)]; projCoords = (P * verticesH')'; u = projCoords(:,1) ./ projCoords(:,3); v = projCoords(:,2) ./ projCoords(:,3); % 采样颜色 colors = zeros(size(vertices,1), 3); for i = 1:size(vertices,1) ui = round(u(i)); vi = round(v(i)); if ui >= 1 && ui <= size(img,2) && vi >= 1 && vi <= size(img,1) colors(i,:) = img(vi, ui, :); end end % 写入 PLY 带顶点色 plywrite('textured.ply', fv.faces, fv.vertices, colors);单视角映射在背面会有拉伸,实际项目里我一般选 3~5 个视角做加权融合,权重用面片法向和视角方向的夹角余弦。
5.3 验证重建质量:三个可量化的指标
重建完不能只看截图,要有量化验证。我常用三个指标:
| 指标 | 计算方法 | 合格范围 |
|---|---|---|
| 轮廓重投影误差 | 把重建模型投影回各视角,和原轮廓算 IoU | IoU > 0.85 |
| 体素占用率 | 剩余体素数 / 初始体素数 | 1%~10% |
| 表面平滑度 | 相邻面片法向夹角均值 | < 15° |
轮廓重投影误差最能反映投影矩阵和雕刻逻辑是否正确。如果某个视角 IoU 明显低,回去检查那张图的轮廓质量或投影矩阵。
5.4 一个我踩过的坑
早期跑 256³ 网格时,我没做分块,直接全量投影,MATLAB 跑到第三个视角就卡死。后来改成按 Z 轴切片,每片 16 层,内存峰值降到 1GB 以内,总时间反而短了——因为缓存命中率高了。从那以后我每次上高分辨率网格前,都强制先跑一遍 64³ 验证流程,确认投影矩阵和轮廓没问题再放大。这个习惯帮我省了至少几十次无效等待。
希望帮到你。
本文还有配套的精品资源,点击获取