MATLAB实现Space Carving:从体素雕刻到多视图三维重建

发布时间:2026/9/7 13:32:50
MATLAB实现Space Carving:从体素雕刻到多视图三维重建 简介基于MATLAB实现的空间雕刻三维重建项目面向计算机视觉学习者与三维重建研究者利用多视角序列图像恢复物体三维几何尤适合理解经典体素雕刻思路。压缩包共99个文件约44.68MB包含39张JPG与37张PPM格式的恐龙模型多视角图像序列17个M脚本实现核心算法2个MAT文件保存体素数据另有2个TXT说明文档与2篇PDF参考文献结构清晰便于按用途查阅即使初学者也能快速定位所需内容。目前已有784人学习下载。资源提供完整可运行演示代码覆盖预处理、视图配准、遮挡分析、体素雕刻及后处理等关键环节配合恐龙数据集可直接观察逐层“雕刻”重建效果适合作为课程设计、毕业设计或三维重建入门的参考实现。重建结果可输出为三维点云或体素模型便于进一步分析与应用。 Space Carving这个算法我第一次看到论文的时候其实没觉得它有多惊艳直到自己用MATLAB完整跑通一遍才真正体会到“一块璞玉被反复雕刻成模型”这八个字的分量。这两年做多视图三维重构试过特征点匹配、稠密重建这些经典流程但真要快速得到封闭、完整的物体表面Space Carving反而是实操中最省心的一条路。它的核心思路非常直白先用一个包围盒把目标物体完全包裹起来然后让每一张视角图像对这个包围盒里的所有体素做“颜色一致性投票”凡是和照片对不上的部分就当成多余物料挖掉反复迭代后剩下的就是三维模型。这篇就把我用MATLAB实现Space Carving的完整过程、关键代码和踩坑记录整理出来适合对多视角三维重构、体素模型以及MATLAB图像处理有一定基础、想自己从零搭一套重构流程的读者参考。1. 先搞懂Space Carving在算什么事1.1 空间雕刻的“一致性”逻辑还是用个生活化的例子解释。想象你站在一个房间中央周围一圈人拿着相机从不同角度拍你。有人从正面拍到你的脸有人从侧面拍到你的轮廓有人从背后只拍到你的背影。现在你手里有一个和你等高、等宽的大号泡沫方块你让所有照片“投票”这个方块上每个小格子如果所有照片在对应位置都应该能看到它那就保留只要有一张照片在这个位置应该看到的是背景而不是你那这个小格子就会被削掉。所有人拍完一圈泡沫方块被削成了一个和你形状接近的雕塑。这就是Space Carving最底层的逻辑——用彩色照片的颜色一致性作为判定标准。在算法实现上每个“小格子”就是一个三维体素Voxel。我们初始化一个覆盖整个场景的大立方体然后对每个体素计算它在所有可见图像中投影位置的像素颜色。如果这些颜色彼此接近说明该体素确实位于物体表面如果颜色差异大说明这个体素很可能是在物体轮廓外部应该被移除。移除的过程叫“雕刻”不断迭代直到没有体素再被删除。这一过程的数学基础是photo consistency约束最早由Kutulakos和Seitz在1999年左右系统化提出后来被广泛应用到多视图立体重建中。1.2 为什么我选Space Carving而不是特征点重建完整跑之前需要先想清楚一个问题什么场景下适合用Space Carving我当时手头的数据是需要重建一个表面光滑、纹理较少的工业零件用经典的SIFT特征匹配加Bundle Adjustment那套流程结果在纹理稀疏区域特征点数量严重不足重建出来的模型有大量破洞。Space Carving的优势就在于它不像特征匹配那样依赖局部纹理只要物体的轮廓和颜色和背景能区分开它就能工作。而且它天然适合封闭物体因为雕刻结束后保留下的体素集合在拓扑上往往是完整闭合的做3D打印前处理会非常省事。当然它也有短板。比如对光照变化非常敏感物体如果是有镜面高光的材质颜色一致性很容易误判另外体素分辨率受限于内存要重建精细细节得消耗巨大的内存。所以在方案选型时我就给自己定了个边界目标是快速搭建一套能从多视角照片得到完整白模的流程不追求微米级细节。这个前提下MATLAB的矩阵运算和图像处理工具箱能大量简化开发非常适合做算法验证和原型实现。2. 项目启动前必须备好的四样东西2.1 MATLAB环境与工具箱配置我用的版本是MATLAB R2023a不过往下兼容到R2019b问题不大。主要用到的工具箱包括Image Processing Toolbox图像读取、灰度转换、插值、Computer Vision Toolbox相机标定、drawChessboardCorners这些函数、Parallel Computing Toolbox可选体素循环多的时候能提速不少。如果没有Parallel Computing Toolbox用普通的for循环也能跑只是体素规模大的时候会等到着急。安装方面没什么特别默认安装就行。需要注意的一点是Camera Calibrator App集成在Computer Vision Toolbox里如果你用旧版MATLAB记得确认工具箱是否完整否则后面标定时会出现函数找不到的报错。2.2 图像采集怎么拍才能刻出完整模型拍照这一步直接影响重构质量很多新手一上来随便拿手机绕一圈就拍最后重建出来缺胳膊少腿还以为是算法问题。我当时踩过一次这个坑后来总结出一套拍摄规范绕物体旋转一周每10到15度拍一张保证相邻视角之间有足够重叠。视角太少雕刻时有些面得不到有效投票模型直接缺块。拍摄时保持物体位置固定相机使用三脚架或手持但尽量稳定。如果物体是可以转动的也可以固定相机、旋转物体但前提是要保证标定板的相对位置和物体一致。背景尽量单一、和物体颜色区分明显纯色背景最好。Space Carving的判断依赖颜色一致性背景越杂乱一致性的误判概率越高。避免强反光表面。如果物体表面有光泽可以在表面喷一层显影剂或者用哑光喷漆处理这样后期一致性判断会稳很多。我在实验中拍的是一个小陶瓷摆件绕摆件拍了32张图单反相机用固定焦距光圈优先模式保证曝光稳定。2.3 相机标定把二维像素和三维世界对齐Space Carving里每个体素需要投影到每张图像上找对应的像素这要求我们知道每台相机的内参K、外参R和t最终合成投影矩阵P K[R | t]。我用MATLAB的Camera Calibrator App完成标定。操作流程是打印一张棋盘格标定板用相机拍20到30张不同角度、不同姿态的标定板照片导入App后设置棋盘格方格尺寸我的是25mm点Calibrate就能得到内参矩阵K和畸变系数然后再针对每一幅实验图像估计外参R和t。有个细节值得强调标定完成后外参是针对标定板的坐标系的而Space Carving希望物体坐标系和世界坐标系统一。所以我一般让标定板在拍摄场景中静止把标定板左上角设为世界坐标系原点物体放在标定板前面。这样每一帧图像对应的相机外参直接就是相对于这个统一世界坐标系的后续体素投影计算会省去很多坐标变换的麻烦。2.4 数据组织构建你的image_set结构体为了方便主循环调用我把每张图像和它的投影矩阵封装成一个结构体数组。这个结构体就像是所有后续操作的“弹药库”每个元素记录一张视图的信息。image_set(num_images).img []; image_set(num_images).P []; image_set(num_images).mask []; for i 1:num_images image_set(i).img imread(fullfile(imgDir, imgNames{i})); image_set(i).P K * [R_cell{i}, t_cell{i}]; image_set(i).mask segmentObject(image_set(i).img); end其中segmentObject函数需要你根据背景和物体的颜色差异写一个简单的前景分割把物体从背景中分离出来生成二值掩膜。后面雕刻时只对掩膜区域内的投影像素做一致性判断能大幅减少误删。3. 从体素网格到颜色投票核心实现细节3.1 包围盒与体素大小怎么定初始化体素网格第一步就是确定包围盒的范围。我建议包围盒比物体最大尺寸外扩10%到20%不要太大否则大量无效体素会被反复投影判断白白消耗算力。你可以从多张图像的掩膜反投影出物体的大致三维包围盒或者直接在数据采集阶段用尺子量一下物体尺寸然后换算到真实世界坐标。体素分辨率则是整个项目中最关键的权衡点之一。体素边长设得小重建细节好但体素数量按立方关系增长。例如一个100mm的物体如果体素边长取2mm那么单方向有50个体素总数量是50^3 125,000个。如果取1mm数量飙升到1,000,000个。每个体素都要对每张图像做一次投影和颜色采样这个数量级在MATLAB里已经不是秒级能完成的了。我第一次做就直接用1mm结果跑了大半个小时。后来我把边长设成2.5mm细节损失肉眼几乎看不出来速度却提升了近10倍。建议你第一次跑通流程时用粗分辨率确认算法没错后再慢慢提高精度。3.2 体素投影到图像的完整计算体素是个小立方体但在实际操作中我简化成用它的中心点代表整个体素。中心点的三维坐标如果是(X, Y, Z)投影到第i张图像上就是function[u, v] projectVoxel(pt3d, P) tmp P * [pt3d; 1]; tmp tmp / tmp(3); u tmp(1); v tmp(2); end这里P就是3×4投影矩阵tmp(3)是齐次坐标里的深度分量必须归一化。得到浮点坐标后需要判断这个坐标是否落在图像有效范围内u在1到cols之间v在1到rows之间。如果越界说明这个体素在这个视角下不可见应当跳过。落到有效范围内的浮点坐标还要进一步采样对应颜色。采样的时候我建议用interp2做双线性插值而不是直接用round取整。因为体素中心投影到像素坐标系很少恰好在整数像素中心直接用最近邻取色会带来轻微的抖动这个抖动在弱纹理区域可能无所谓但在物体边缘很容易造成误判。3.3 一致性判据与雕刻阈值颜色一致性怎么判最直观的做法是取所有可见视图中对应像素的RGB向量计算它们的方差或者平均绝对差。如果颜色差异小于某个阈值认为一致否则认为该体素应在表面之外。但实际中因为光照、曝光、视角不同即使是物体表面的真实体素投影到各图像上的像素颜色也会有偏差。所以阈值不能设太死。我用的判据是先收集这个体素在所有可见图像中的RGB颜色矩阵每行是一个视角的颜色然后计算每个通道的标准差取三个通道标准差的均值作为该体素的颜色不一致分数。如果这个分数小于阈值τ保留体素否则标记为待删除。阈值τ通常设在20到40之间RGB范围0到255。太小呢会把光斑阴影误判为不一致导致表面被削掉太大呢背景颜色融合了也会被保留下来。我的做法是先用一个相对宽松的阈值跑一遍比如35观察雕刻结果如果背景明显没除干净再逐步往下调。function consistent checkConsistency(voxelCenter, image_set, tau) colors []; for i 1:numel(image_set) [u, v] projectVoxel(voxelCenter, image_set(i).P); if u 1 || v 1 || u size(image_set(i).img, 2) || v size(image_set(i).img, 1) continue; end if image_set(i).mask(round(v), round(u)) 0 continue; end % 双线性插值取色 c impixel(image_set(i).img, u, v); colors [colors; c]; end if size(colors, 1) 2 consistent false; return; end score mean(std(double(colors), 0, 1)); consistent score tau; end4. 主循环、表面提取与结果可视化4.1 多轮迭代的雕刻主循环雕刻不能只跑一遍。因为一轮雕刻中删除体素的判断是基于上一轮整体状态删除顺序会影响最终结果所以一般需要多轮迭代。每一轮遍历所有当前保留的体素对每个体素做一致性检查把不一致的体素标记为删除一轮结束后统一更新体素状态直到某一轮没有新的体素被删除为止。我实现的伪代码结构大致如下% 初始化体素网格坐标列表 [X, Y, Z] ndgrid(xs, ys, zs); voxelGrid ones(size(X)); % 1表示保留 while true toRemove false(size(X)); for idx 1:numel(X) if ~voxelGrid(idx) continue; end center [X(idx), Y(idx), Z(idx)]; if ~checkConsistency(center, image_set, tau) toRemove(idx) true; end end if ~any(toRemove) break; end voxelGrid(toRemove) 0; end这个代码是串行版体素多时跑得很慢。后续优化时我改用parfor遍历体素索引同时把对image_set的读取预先加载到worker内存里速度提升明显。但要注意parfor里如果依赖顺序更新状态需要拆成两阶段先用parfor算出toRemove索引集合再统一更新状态。4.2 用isosurface提取表面雕刻完成后voxelGrid里是三维0/1矩阵直接显示体积模型也可以但看起来比较粗糙。要想得到干净的表面网格我习惯用isosurface函数相当于执行了一次Marching Cubes算法。做法是把voxelGrid转成double用smooth3做一次盒式滤波平滑然后设置等值面阈值为0.5因为平滑后内部值接近1外部接近00.5等值面就是表面位置再交给isosurface提取三角网格和顶点最后用patch渲染。smoothed smooth3(double(voxelGrid), box, 3); fv isosurface(X, Y, Z, smoothed, 0.5); patch(fv, FaceColor, [0.8 0.8 0.8], EdgeColor, none); camlight; lighting gouraud;这里的X、Y、Z需要是ndgrid生成的网格坐标且和voxelGrid维度一致。isosurface输出的fv结构体直接包含vertices和faces后续要导出OBJ或STL也很方便用writeOBJ或者MATLAB的stlwrite函数即可。4.3 视觉化检查瑕疵可视化不只是为了好看更是为了调试。我每次雕刻完第一步不是直接导出模型而是从多个新视角渲染出来放大检查表面有没有破洞、隆起、凹陷。常用的检查方向包括转45度观察耳朵、凹陷这些遮挡严重的位置把切片平面切进去看内部是否有残留体素对比原图像看轮廓和物体实际轮廓是否完全贴合。如果发现某一面上有戳出来的锥形凸起多半是背景颜色和物体颜色太接近阈值又不严把背景附近的体素一并保留了。解决办法就是提高前景分割掩膜的准确性或者适当调低τ。如果发现表面坑坑洼洼像月球表面往往是体素分辨率不够或者smooth3平滑强度不够。这时候可以先把体素边长调小再重新雕刻不要指望后期平滑能完全掩盖问题。5. 踩坑记录常见问题与排查技巧速查5.1 内存爆炸与体素分辨率取舍这是MATLAB实现中最容易翻车的地方。三维0/1矩阵看起来不大但如果你用double类型存储1000^3的矩阵直接就是8GB内存没几个人的电脑顶得住。解决方法有几个用logical类型存储voxelGrid单个元素只占1字节内存直接降到原来的1/8。避免一次性生成完整X、Y、Z三个大矩阵可以用meshgrid配合单精度single存储坐标或者只在需要遍历时动态生成坐标索引。分块处理。把包围盒切成若干子块逐块雕刻最后用逻辑或合并结果。这个方法对超大场景几乎必用我后期处理更大物体时就是用这个思路。5.2 重建结果很多凸起或者空洞先查这几处凸起问题我前面说过多半是前景分割不干净或者是τ过高。空洞问题则普遍是视角覆盖不足。某些区域只有在极少数图像中可见比如物体底部的底面这些位置体素很难被有效投票一致性判断根本凑不齐足够的颜色样本所以我会在checkConsistency中用“可见视图少于2个直接视为不一致”的策略宁缺毋滥。但更好的办法是拍摄时把物体垫高让底部也能被多个视角看到。另一个原因可能是标定误差偏大。标定参数如果有几像素的误差投影位置就会偏颜色一致性就容易出问题。解决方式是标定完成后做一次重投影误差检查看看均方根误差是否小于0.5像素。5.3 调参经验阈值、视角数量和拍摄环境我把自己的参数设置整理成一张表方便你对照着试。当然参数和实际场景强相关但可以作为初始值参考参数项我的经验值调整建议体素边长物体尺寸的1/50到1/30太细内存爆炸太粗丢细节雕刻阈值τ30到40RGB标准差背景色与物体色接近时调小视角数30到40张少于20张容易缺块相邻视角角度间隔10到15度间隔过大遮挡区域无法恢复背景与物体颜色差异越大越好差异小时用背景分割加强前景掩膜准确性要求高掩膜不完整直接导致误判在实际操作中我会先做一次低分辨率快速雕刻体素边长设为最终目标的2倍用来检查视角覆盖和标定有没有大问题确认没问题后再用精细分辨率重跑。这个方法能帮你省掉大量等待时间。最后分享一个我自己用顺手的检查技巧算法跑通后最难的不是让代码运行而是判断结果对不对。我习惯把雕刻完成后的体素模型和任意一张输入图像叠加显示把当前视角的相机姿态从投影矩阵P中分解出来再用MATLAB的camera lookAt坐标把体素模型渲染到相同视角半透明叠加在原图上。如果轮廓对得上、边缘干净说明整个流程已经基本可靠如果看到轮廓漂移或锯齿严重就要回头查标定而不是急着调雕刻阈值。这一步检查我几乎每次必做也推荐你试试。如果后续想再往上走一步可以引入多分辨率策略先从粗体素快速雕刻锁定大致形状再把粗体素细分成小体素继续精修。这样既兼顾速度又能让细节明显提升。我用这个方法把原来只能跑2mm精度的模型在内存不变的情况下做到了1mm以内的表面细节。整个做法在MATLAB里也不复杂就是在粗体素保留结果的基础上把每个保留体素切成8个小体素然后在新的小体素网格上重复雕刻迭代直到收敛。说到这里思路其实已经全部铺开了剩下的就是打开MATLAB把你的第一张图像读进来。本文还有配套的精品资源点击获取