资讯动态

MATLAB实现三维多孔介质LBM3D模拟:从切片重构到D3Q19渗透率

发布时间:2026/9/15 7:38:49 来源:尧图企业网站定制
简介面向三维多孔介质建模与曲面重构需求的MATLAB工具包适用于地质岩层、生物组织、过滤材料等科研与工程场景。压缩包内提供LBM3D.m核心脚本整合了基于Delaunay三角剖分或体素化表面提取的曲面重构流程并借助isosurface、patch等函数重建多孔结构同时也可结合LBM方法扩展孔隙尺度流体流动模拟。资源共1个文件类型为m脚本压缩包仅2KB体量精简、便于二次开发。已有841人学习该资源适合具备一定MATLAB基础、希望快速搭建三维多孔介质模型并量化分析其物理特性的研究者。通过调整脚本参数可控制重构精度与形态表现为地质渗流、过滤效率预测、油气开发等应用中的多孔结构分析提供直接的算法参考与数据支持也便于与实验图像或CT数据衔接帮助研究者更高效地开展孔隙尺度的数值实验。1. 三维多孔介质的LBM3D流程为什么值得自己用MATLAB搭一遍一个CT切出来的三维体数据直接抛给商业CFD软件网格划分这一步就能消耗大半天。基于贴体网格的求解器在孔隙壁面处要么生成大量边界层单元要么在喉道处出现畸变网格而LBM3D这类格子玻尔兹曼实现没有这个烦恼规则笛卡尔网格加反弹格式复杂固体边界只是“标记”而不是“网格”。围绕LBM3D.rar这个命名所指代的常见工作流本博文讲清楚如何用MATLAB把二维切片堆叠成三维多孔介质体数据、完成三维重构跑通一遍D3Q19三维LBM流动模拟再做曲面重构并量化几何特征。这套链路适合用MATLAB做算法验证的研究生以及做岩心、多孔电极、催化剂载体仿真的工程师。要把链路跑通需要的是图像处理、体数据操作和格子模型三块拼图下面按顺序拆开。2. 三维重构从切片序列到可计算的多孔介质体数据三维多孔介质的数值实验起点几乎都是体数据。不管是显微CT、FIB-SEM还是同步辐射原始输出是二维切片序列三维重构的第一步就是把它们按空间顺序装进一个三维数组再做分割和连通性处理。这里用到的全是MATLAB图像处理里最标准的函数但有几个坑会在每个项目里重复出现。2.1 用MATLAB读入切片栈并统一坐标最常见的输入是一组tif切片。MATLAB里最直接的读法是dir加imread循环需要注意文件名排序不是自然序slice_100.tif会排在slice_20.tif前面必须按索引重新排序。folder ./ct_slices; tifFiles dir(fullfile(folder, *.tif)); [~, idx] sort({tifFiles.name}); % 避免字典序导致切片乱序 tifFiles tifFiles(idx); firstImg imread(fullfile(folder, tifFiles(1).name)); [nx, ny] size(firstImg); nz numel(tifFiles); imgStack zeros(nx, ny, nz, uint8); for k 1:nz imgStack(:, :, k) imread(fullfile(folder, tifFiles(k).name)); end这段代码做了两件关键事按自然序读图以及预分配uint8数组。预分配不是性能洁癖252层512×512的切片如果不预分配循环里反复扩容会让读取时间翻倍。如果切片是16bitzeros的类型要改成uint16后面所有阈值函数也要按16bit的数据范围处理。如果拿到的不是切片而是一个.raw体积文件常见做法是用fread按三维尺寸直接读fid fopen(volume_16bit.raw, r); raw16 fread(fid, [nx*ny, nz], uint16uint16, 0, l); % l 小端字节序 fclose(fid); vol permute(reshape(raw16, nx, ny, nz), [2 1 3]);.raw文件没有头信息字节序错了整个体数据就是噪点。这里l指定小端实际遇到字节序问题时的现象是相邻像素剧烈跳变排查时先把一维数据用typecast逐字节看确认高位字节落在哪一端。这本质上就是“十六进制字节流怎么解释成有符号整数”的问题和16位CT数值的符号位处理是同一件事。2.2 阈值分割与孔隙率校验把体数据变成二值孔隙/骨架场最常见的是Otsu全局阈值bwInit imbinarize(imgStack); % 亮区域为1 if mean(bwInit(:)) 0.5 bwInit ~bwInit; % 保证 bwInit1 代表孔隙 end poroTotal mean(bwInit(:)); fprintf(总孔隙率 %.4f\n, poroTotal); [p, edges] histcounts(imgStack(:), 0:256); p p / sum(p); ent -sum(p(p 0) .* log2(p(p 0)));代码里先判段直方图方向再翻转是为了避免不同成像系统对孔隙亮暗的定义不一致。poroTotal是后续所有LBM参数标定的锚点孔隙率如果与压汞法结果对不上后面的一切都没有意义。直方图最后算的是一维香农熵如果对阈值敏感可以先对体数据做medfilt3中值滤波再分割滤波核取[3 3 3]就够核太大会把几微米的喉道直接抹掉。2.3 连通域裁剪与计算域扩展全局阈值分割出来的孔隙往往包含孤立孔洞它们不参与流动却会占据LBM的内存。常见做法是取最大连通域或者更严格地取与入口和出口平面都相交的连通域cc bwconncomp(bwInit, 6); % 6邻域适合孔隙骨架 labels labelmatrix(cc); numPix cellfun(numel, cc.PixelIdxList); [~, idxMax] max(numPix); bwMain false(size(bwInit)); bwMain(cc.PixelIdxList{idxMax}) true; % 只保留同时接触x1和xnx平面的连通域才可能沿x方向贯通 inLabels unique(labels(1, :, :)); outLabels unique(labels(nx, :, :)); flowLabels intersect(inLabels(inLabels 0), outLabels(outLabels 0)); bwFlow ismember(labels, flowLabels);bwMain用于几何统计bwFlow用于LBM流动模拟。区别很重要总孔隙率包含闭孔而Darcy尺度下的渗透率只与连通孔隙有关。如果闭孔占比很大说明成像分辨率不足或分割阈值偏低。下表是不同三维重构输入方式的MATLAB方案汇总。输入类型MATLAB入口典型尺寸注意事项CT/Micro-CT切片序列dir imread循环512³或1024³切片间距可能与面内分辨率不一致需各向异性校正raw/vff体积文件fread reshape与体素尺寸对应注意字节序用typecast逐字节定位FIB-SEM序列imread 对齐z方向间距大z方向通常需要插值或单独处理合成几何球堆、纤维直接构造逻辑数组任意沿主流方向留出周期边界便于LBM驱动3. LBM3D仿真核心D3Q19模型、碰撞迁移与反弹边界的MATLAB实现三维LBM有D3Q15、D3Q19、D3Q27三种常用速度离散。D3Q19在精度和内存之间最均衡是多孔介质渗透率模拟的事实标准。这一章把D3Q19从权重表到可运行代码完整落一遍。3.1 D3Q19速度离散与权重表D3Q19把速度空间离散成19个方向1个静止方向、6个轴向方向、12个面对角线方向。声速平方恒为cs²1/3权重和恒为1。velDir [ 0 0 0; % 1 静止 1 0 0; -1 0 0; 0 1 0; 0 -1 0; 0 0 1; 0 0 -1; % 2~7 轴向 1 1 0; 1 -1 0; -1 1 0; -1 -1 0; % 8~11 面对角线 1 0 1; 1 0 -1; -1 0 1; -1 0 -1; % 12~15 0 1 1; 0 1 -1; 0 -1 1; 0 -1 -1 % 16~19 ]; w [1/3; repmat(1/18, 6, 1); repmat(1/36, 12, 1)]; assert(abs(sum(w) - 1) 1e-15);velDir的每一行是格子速度向量单位是格/时间步。w是权重静止方向权重最大对角线方向权重最小。方向编号顺序在后面的反弹格式里必须固定下来因为反弹需要知道每个方向的“对向方向”编号。Otsu分割出的体数据在这个阶段不参与计算参与计算的是从它提取出的标志场。3.2 从体数据到标志场扩展边界与流体节点索引三维多孔介质的体数据四周通常直接截断在骨架材料上这对LBM是天然壁面。为了让三个方向的邻居索引都能用同一个周期映射计算而不越界常见做法是在体数据外包一圈固体节点[nx, ny, nz] size(bwFlow); bwPad false(nx 2, ny 2, nz 2); bwPad(2:end-1, 2:end-1, 2:end-1) bwFlow; fluidFlag bwPad(:); % 逻辑列向量长度 totalNodes totalNodes numel(fluidFlag); [gx, gy, gz] ndgrid(1:nx 2, 1:ny 2, 1:nz 2); coord [gx(:), gy(:), gz(:)]; velDirInt int32(velDir); neighborIdx zeros(totalNodes, 19, int32); for q 1:19 nc coord velDir(q, :); nc(:, 1) mod(nc(:, 1) - 1, nx 2) 1; nc(:, 2) mod(nc(:, 2) - 1, ny 2) 1; nc(:, 3) mod(nc(:, 3) - 1, nz 2) 1; neighborIdx(:, q) sub2ind([nx 2, ny 2, nz 2], nc(:, 1), nc(:, 2), nc(:, 3)); endfluidFlag是逻辑列向量neighborIdx预计算了一次邻居映射之后每个时间步的迁移只是一次索引取值不再做任何坐标运算。外层padding的实际作用是即使主流方向上体数据某一行恰好以孔隙结尾周期mod也能让迁移落到padding固体节点上接下来反弹格式会把它正确弹回去。如果不做padding边界处的mod会把流体节点映射到另一侧的流体节点侧面开口的孔隙就会被错误地连成周期通道。3.3 BGK碰撞与Guo外力格式的实现碰撞步的任务是把所有分布函数按BGK松弛推向平衡态。多孔介质流动的驱动力不是初始速度而是压差在周期边界条件下最稳妥的驱动方式是沿主流方向施加恒定体积力。tau 0.7; % 松弛时间LB单位 rho ones(totalNodes, 1, single); ux zeros(totalNodes, 1, single); uy zeros(totalNodes, 1, single); uz zeros(totalNodes, 1, single); f repmat(single(w), totalNodes, 1); % 初始化为平衡态 fx 1e-5; % x方向体积力LB单位 for iter 1:maxIter rho sum(f, 2); uv (f * velDir) ./ rho; % 矩阵乘法一次得到三个速度分量 ux uv(:, 1); uy uv(:, 2); uz uv(:, 3); feq zeros(size(f), single); for q 1:19 cu velDir(q, 1) * ux velDir(q, 2) * uy velDir(q, 3) * uz; u2 ux .^ 2 uy .^ 2 uz .^ 2; feq(:, q) rho .* w(q) .* (1 3 * cu 4.5 * cu .^ 2 - 1.5 * u2); end for q 1:19 cu velDir(q, 1) * ux velDir(q, 2) * uy velDir(q, 3) * uz; Fterm (1 - 1 / (2 * tau)) * w(q) * ( ... 3 * ((velDir(q, 1) - ux) * fx) 9 * cu * velDir(q, 1) * fx); f(:, q) f(:, q) - (f(:, q) - feq(:, q)) / tau Fterm; end end碰撞步完全矢量化19个方向的循环在MATLAB里通常比三维数组shift快得多。tau取0.7是经验值数值稳定且粘度适中tau趋近0.5时数值粘度低但容易震荡超过1.0时边界层过厚。fx1e-5这个量级对应LB单位下的低马赫数保证流动处于不可压缩范围稳态平均速度通常在1e-3量级。Guo外力格式里的9倍项让外力对动量方程的二阶矩贡献也符合连续极限只用3倍项算出来的渗透率在小喉道处会有几个百分点的偏差。3.4 迁移与反弹预计算邻居表实现完整反弹碰撞改变的是每个节点的分布函数形状迁移把这些分布函数搬运到邻居节点。多孔介质的特色在于迁移的目标节点可能是固体这时入射分布函数必须反弹回来源节点这正是LBM处理复杂固体边界的方式。fStream zeros(size(f), single); solidFlag ~fluidFlag; for q 1:19 nb neighborIdx(:, q); act fluidFlag ~solidFlag(nb); % 当前是流体邻居也是流体 fStream(nb(act), q) f(act, q); % 正常迁移 reb fluidFlag solidFlag(nb); % 当前是流体邻居是固体 fStream(reb, oppIdx(q)) f(reb, q); % 反弹到对向方向 end f fStream;act和reb都是逻辑掩码nb(act)取出真正发生迁移的目标节点编号fStream(nb(act), q) f(act, q)是一次性批量赋值。反弹行把入射方向q的分布函数写到当前节点的对向方向oppIdx(q)对应half-way bounce-back的贴壁位置。反弹格式的对向方向映射必须与velDir的方向编号严格一致oppIdx [1 3 2 5 4 7 6 11 10 9 8 15 14 13 12 19 18 17 16];这个数组的意思是方向2(1,0,0)的对向是方向3(-1,0,0)方向8(1,1,0)的对向是方向11(-1,-1,0)。写错一个索引迁移后质量就不守恒最直接的表现是总密度随时间线性漂移。3.5 驱动方式选择体积力还是Zou-He压力边界周期边界加体积力最适合做渗透率模拟因为主流方向没有物理入口和出口流场完全由几何和驱动力决定天然满足充分发展条件。如果要模拟有限长度岩心常见做法是换成Zou-He速度边界或压力边界入口给定压力出口给定压力侧向用反弹壁面。Zou-He在三维多孔介质中实现起来比体积力繁琐因为它需要对每个边界节点单独重建缺失的分布函数而且入口处的孔隙面积占比影响实际流速。先跑通体积力方案再换Zou-He是更稳妥的路径。4. 收敛判据、单精度存储与MATLAB参数调优LBM模拟跑起来很快但跑多久才算收敛、用什么精度存储这些看似次要的参数往往决定了结果可不可信。4.1 tau值与格子单位的标定关系LB单位下的运动粘度与松弛时间的关系是nu (2*tau - 1) / 6这个公式在D3Q19下成立因为cs²1/3。tau0.7对应nu0.0667。要对应到物理单位需要先确定格子分辨率若体素边长dx1um时间步dt由粘度匹配决定物理渗透率结果最后通过K_physical K_LB * dx^2换回平方米。雷诺数在孔隙尺度下的定义通常取Re U * D / nu其中U是平均孔隙速度D是平均孔径或水力直径。LBM的稳定性要求格子速度不超过0.1fx1e-5驱动的稳态速度通常在5e-4量级孔隙雷诺数远小于1正好符合Darcy渗流假设。4.2 收敛判据平均速度、入口流量与假收敛不要只看总动能的残差那会掩盖局部回流。我一般在每个固定间隔记录三个量全流体域平均速度、主流方向每个截面的流量、以及流量沿x方向的均匀性。monitorStep 500; avgU zeros(1, maxIter / monitorStep); fluxProfile zeros(1, nx 2); for iter 1:maxIter % ... 碰撞、迁移 ... if mod(iter, monitorStep) 0 idx iter / monitorStep; avgU(idx) mean(ux(fluidFlag)); uxField reshape(ux, nx 2, ny 2, nz 2); fluxProfile squeeze(sum(sum(uxField, 2), 3)) / (ny * nz); end end figure; semilogy(1:numel(avgU), abs(diff(avgU)), o-); xlabel(每500步); ylabel(平均速度变化LB单位); figure; plot((1:nx2) * dx, fluxProfile, o-); xlabel(x方向位置); ylabel(截面流量);收敛条件建议设置为平均速度的变化率连续5000步低于1e-6同时沿x方向的流量分布接近水平。流量分布不平时说明体数据内部有死端孔隙或喉道处的回流尚未稳定这是典型假收敛。如果流量曲线在某个截面突然下降优先检查2.3节的连通域处理看是否遗漏了与主流方向相连的狭窄通道。4.3 256³规模的内存预算与single精度优化256³体数据在LBM里是中等规模内存规划不好会直接OOM。分布函数数组如果用double19个方向乘16.8M节点单个数组就需要2.5GB。数据项类型256³典型占用f 分布函数single约1.28 GBneighborIdxint32约1.28 GBflag/bwPadlogical约16.8 MBrho, ux, uy, uzsingle约268 MBneighborIdx占据的内存和分布函数差不多大这是预计算表换速度的代价。常见优化是改成int32的neighborIdx并用single存储分布函数单精度对孔隙流动足够渗透率计算误差远小于分割带来的几何误差。neighborIdx如果每个方向的邻居编号都差不多也可以考虑只存边界处的映射但那样循环里要多做判断在有padding固体层的实现里反而更慢。GPU加速可以把碰撞步全部放到gpuArray上但迁移步的取值会引入一个难以避免的同步开销128³以下的网格不必上GPU256³以上再考虑。5. 曲面重构与后处理从等值面到流动闭孔验证模拟跑完后体数据本身并不会自动变成可发布的图像或可量化的几何数据。曲面重构的任务是把三维体数据中孔隙与骨架的交界面提取成三角网格用它计算比表面积、孔径分布并且反过来校验LBM的几何假设。5.1 用smooth3isosurface把体数据重构为三角曲面直接对二值体数据调isosurface会得到严重阶梯化的块状曲面。正确做法是先对二值场做距离变换再对距离场做高斯平滑最后在0.5等值面处提取dist bwdist(bwMain); % 每个体素到最近骨架的距离 smoothF smooth3(dist, gaussian, [5 5 5], 1); fv isosurface(smoothF, 0.5); % Marching Cubes fv reducepatch(fv, 5e5); % 限制面片数 figure; patch(fv, FaceColor, [0.8 0.8 0.2], EdgeColor, none); camlight; lighting gouraud; axis equal; view(3);smooth3的高斯核[5 5 5]和标准差1是个保守组合能消除体素锯齿又不会把2~3个格子的喉道磨平。等值面取0.5是因为距离场中0.5恰好对应原始二值界面的位置。reducepatch把面片数压到50万以内否则大模型旋转时MATLAB的OpenGL渲染会明显掉帧。5.2 比表面积计算与闭孔识别三角网格的面积可以直接从顶点坐标算不需要回到体素v fv.vertices; f1 v(fv.faces(:, 2), :) - v(fv.faces(:, 1), :); f2 v(fv.faces(:, 3), :) - v(fv.faces(:, 1), :); crossProd cross(f1, f2, 2); faceArea 0.5 * sqrt(sum(crossProd .^ 2, 2)); surfaceArea sum(faceArea); poreVol sum(bwMain(:)); ssa surfaceArea / poreVol; % 体素单位下的比表面积比表面积的定义要和实验数据约好surfaceArea / poreVol得到的是单位孔隙体积的比表面积如果对方给的是单位质量的比表面积需要再乘固相密度。平滑核大小会显著改变表面积数值核越大表面积越小报告中必须写明平滑参数。与流动解耦的闭孔是可以直接识别出来的。用连通域标记找出所有不与边界相交的孔隙把它们标记为闭孔cc2 bwconncomp(bwMain, 6); labels2 labelmatrix(cc2); faceLabels unique(labels2(1, :, :)); % 接触x1的孔隙标签 openLabels setdiff(faceLabels(:), 0); bwOpen ismember(labels2, openLabels); closedPores bwMain ~bwOpen;5.3 用重构结果反哺LBM闭孔掩膜与渗透率交叉验证closedPores就是LBM里bwFlow应该剔除的部分。把它掩掉后重新跑一遍第3章的模拟对比两次渗透率的差异。如果闭孔占比小渗透率差异应在1%以内这时曲面重构主要服务于可视化如果差异超过5%说明分割阈值或连通域判定标准选得不对需要回到2.2节调整。更进一步的验证是在两种高斯平滑参数下分别提取曲面生成两个平滑版本的几何后各自重跑LBM检查渗透率对曲面重构参数的敏感性。两条渗透率曲线叠在一起后这套三维重构LBM3D曲面重构的链路就可以固化下来作为标准流程使用了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价