点云压缩RAHT改进版MATLAB实现:熵编码优化与MSE评估
简介这份代码是区域自适应层次变换RAHT的 Matlab 工程实现对应 Queiroz 与 Chou 提出的 RAHT-rlgr 压缩方案面向点云压缩、三维数据处理等方向的研究者与工程人员。压缩包共 11 个文件以 C 源文件、头文件为核心另含 Matlab 的 install.m 安装脚本、License 与说明文档整体仅 13KB轻量易读便于快速编译和验证。资源实现了 RAHT_cod 编码器与 RAHT_dec 解码器的改进版本可直接对体素化点云的顶点坐标和颜色属性进行变换、量化并输出码流同时配有编译步骤、可执行文件用法和测试说明适合想深入理解 RAHT 原理或在自有项目中接入该算法的读者参考。已有 347 人学习下载。 点云压缩写多了之后你会发现一个很有意思的现象很多论文里PSNR高得吓人可一旦自己动手实现重建点云的均方误差MSE怎么都对不上。我这次把Queiroz和Chou在2016年提出的RAHTRegion Adaptive Hierarchical Transform编码器在MATLAB里完整重写了一遍并基于原版RLGR熵编码器做了三处针对性改进最后用MSE对重建质量做了量化评估。整个过程踩了不少坑也搞清楚了很多论文里一笔带过的细节。这篇文章就围绕这套RAHT-rlgr改进版编码器的MATLAB实现展开把变换原理、熵编码改动、代码结构和MSE评估方法一次性说透适合正在做点云压缩、3D编码或者想入门G-PCC的朋友参考。1. RAHT不是点云DCT区域自适应分层变换的原理拆解1.1 为什么点云压缩需要RAHT这种东西点云和普通图像最大的差异在于数据结构完全不确定。图像是规则网格像素之间天然存在空间邻域关系DCT或者小波变换可以直接套用而点云里的点是散布在三维空间中的密度不均匀甚至可能存在大片的空洞区域。这种非规则结构让传统变换编码基本失效因为变换要求输入是规则采样信号。RAHT的思路很直接把点云放进八叉树里让树结构自己去适应点云的分布。父节点里的点沿八叉树逐层向上传递每一层只对同一父节点下的子节点做变换。这样变换的基函数实际上是随八叉树结构变化的点密的区域变换层级深点稀的区域提前合并既避免了固定网格变换带来的空洞浪费又保证了变换的正交性。这也是它名字里自适应的由来。1.2 自底向上的类Haar变换到底在算什么如果你熟悉一维Haar小波RAHT的每一层变换可以理解成三维版本的Haar推广。假设某个八叉树节点下有k个子节点被占据RAHT不是一次性对k个点做k维变换而是按配对的方式逐层做2x2正交变换。每一层的变换矩阵是[ a ] [ w1 w2 ] [ a1 ] [ d ] [ w2 -w1 ] [ a2 ]其中w1和w2根据两个子节点的点数加权保证变换后直流分量a的能量和原始两个节点总能量一致。d就是高频细节系数是RAHT真正要编码的东西。直流分量继续向上传递高频分量送到熵编码器。从最底层到根节点每层只做局部配对变换计算量是线性的这是RAHT能够实际部署的重要原因。我最初犯过一个理解错误以为RAHT是对点云坐标做变换但实际上RAHT变换的是体素占据信息或者属性值比如颜色而不是几何坐标本身。在处理几何信息时配合八叉树管理能获得整体的压缩处理颜色属性时几何结构已经确定利用同样的树层逐级变换颜色系数即可。MSE评估的就是重建属性值与原始属性值之间的差异。2. RLGR熵编码与改进版的关键改动2.1 RLGR是什么为什么不用算术编码RLGR是Run-Length Golomb-Rice的缩写本质上是游程编码和Golomb-Rice编码的组合。RAHT产生的系数经过量化后会出现大量连续的零值这正是游程编码擅长的场景。而非零系数的幅度分布通常呈指数衰减Golomb-Rice编码又是处理这种分布的最优选择之一。把两者结合起来就得到了一个结构简单但效率不错的熵编码器。相比算术编码RLGR的实现复杂度低很多状态管理简单MATLAB这类脚本语言也能跑出不错的效率。它的自适应性主要体现在参数k的更新上编码器根据近期符号运行情况不断调整Golomb-Rice的阶数k。如果连续遇到零值k会适当增大让游程编码更高效如果遇到大的非零系数k会减小避免过长的Rice码。这个动态调整机制让RLGR对非平稳信源也有一定鲁棒性。原版Queiroz和Chou框架里的RLGR实现比较朴素游程长度和Rice编码使用同一个k参数。这意味着当数据中既有长零游程又有大幅值系数时k的调整会互相打架导致两个部分都不够优化。2.2 改进版动了三处我这次的改进版本主要动了三个地方。第一是游程和非零系数的k参数分开管理。游程编码器和Rice幅度编码器各自维护一份k状态零值游程长的时候游程的k增大不会影响非零系数的Rice编码精度。仅这一点在点云比较稀疏、零值占比高的场景下就能带来稳定的码率下降。第二是量化系数扫描顺序调整。原版按八叉树节点的深度优先顺序直接送熵编码器但RAHT的高频系数在空间上存在明显的相关性同一父节点下的兄弟高频系数往往同时为零或者同时非零。我改成按层级优先的顺序先扫描同层所有节点的高频系数再进入下一层。这样相邻符号的相关性更强游程编码的效率明显改善。第三是MSE导向的量化参数自适应。原版对每个层级使用相同的量化步长但RAHT中越往高层的系数对应的是点云中越低频的信息视觉影响更大。改进版按层级加权的均方误差贡献来分配量化步长对低频层使用更细的量化对高频层适当放宽。这个改动在码率几乎不变的情况下显著降低了重建点云的整体MSE。这个思路来自实际观察也符合变换编码中低频精细量化、高频粗量化的一般原则。3. MATLAB代码实现核心模块与可复现步骤3.1 整体架构这套MATLAB实现的整体流程如下原始点云经过体素化生成八叉树结构和占据信息对占据信息或者颜色属性做RAHT变换变换系数按层级重排量化后送入RLGR熵编码器解码端执行完全对称的逆过程。为了控制篇幅这里重点讲RAHT变换和RLGR编码两个核心模块的可复现逻辑。我建议把代码拆成四个独立函数文件便于单步调试raht_forward.m做正向变换、raht_inverse.m做逆向重建、rlgr_encode.m和rlgr_decode.m做熵编解码。另外准备一个quantize.m专门处理量化与反量化。这样MSE测试时可以单独替换量化模块不用改主流程。3.2 RAHT变换的核心MATLAB片段下面的代码展示正向RAHT中单个层级的处理逻辑。为了可读性我做了简化假设当前层的节点列表已经按八叉树索引组织好每个节点的子节点信息存放在children矩阵中。function coeff raht_forward_level(occupancy, coeff, children) % occupancy: 当前层的占据标志 % coeff: 当前层的变换系数直流部分 % children: 每个节点对应的子节点索引 maxNode size(children, 1); newCoeff zeros(maxNode, 1); for idx 1:maxNode kids children(idx, :); kids kids(kids 0); % 只取有效子节点 occupied kids(occupancy(kids) 0); k length(occupied); if k 2 a1 coeff(occupied(1)); a2 coeff(occupied(2)); % 等权重的正交变换保证能量守恒 newCoeff(idx) (a1 a2) / sqrt(2); % 高频系数送入熵编码器省略存放逻辑 high(idx) (a1 - a2) / sqrt(2); elseif k 1 newCoeff(idx) coeff(occupied(1)); else newCoeff(idx) 0; end end coeff newCoeff; end这里需要特别注意如果两个子节点的点数不相同权重不能直接取1/sqrt(2)否则变换矩阵不是正交的逆变换后能量会有损失。处理方法是根据点数比例构造加权旋转矩阵n1 weight(occupied(1)); n2 weight(occupied(2)); w1 sqrt(n1 / (n1 n2)); w2 sqrt(n2 / (n1 n2)); newCoeff(idx) w1 * a1 w2 * a2; high(idx) w2 * a1 - w1 * a2;上面这段是RAHT实现里最容易出错的地方。如果不加权重逆变换虽然也能重建点云但MSE会莫名其妙偏高而且不同密度的点云之间数值不可比。我第一次写的时候就直接用了等权重结果在点云密度不均匀的数据上MSE比预期高出一个数量级排查了很久才意识到是变换正交性被破坏了。3.3 RLGR改进版编码的MATLAB片段RLGR编码器改进后的结构如下游程和幅度分别使用独立的kfunction bits rlgr_encode_improved(symbols) kRun 1; kMag 1; run 0; bits []; for i 1:length(symbols) if symbols(i) 0 run run 1; else % 编码游程使用独立的kRun bits [bits golombRice(run, kRun)]; % 编码非零幅度使用独立的kMag bits [bits golombRice(abs(symbols(i)), kMag)]; run 0; % 分别更新两个k kRun updateKRun(kRun, 0); % 遇到非零游程的k适当减小 kMag updateKMag(kMag, abs(symbols(i))); end end % 处理结尾的零游程 if run 0 bits [bits golombRice(run, kRun)]; end endupdateKRun和updateKMag的更新规则参考了经典的RLGR自适应逻辑当实际编码所需的码长偏长时k增大偏短时k减小。具体幅度调节可以按步长1来实际测试下来步长0.5配合四舍五入的效果更平滑码率波动更小。在MATLAB里做位级操作我强烈建议用logical数组存比特流而不是用字符串拼接。字符串拼接在测试小数据时方便但点云数据一上来就是几百万个符号字符串操作会直接把内存打爆。用logical数组配合预分配处理百万级系数是没问题的。4. 图像均方误差的评估方法与常见坑4.1 点云场景下MSE的两种计算口径标题里提到的图像的均方误差在RAHT压缩场景下需要区分两种口径。如果压缩的是点云的属性信息比如颜色那么八叉树结构在编解码前后保持不变每个点的数量相同位置对应关系明确MSE直接按属性值的均方误差计算即可。公式就是最朴素的形式function mse computeAttrMSE(orig, recon) diff orig - recon; mse mean(diff(:) .^ 2); end如果压缩的是几何信息点云重建后点的数量、顺序都可能变化这时直接逐点计算MSE就不行了。常规做法是对每个重建点在原始点云中找最近邻计算两点之间的欧氏距离平方再对所有重建点取平均。MATLAB里可以这样写function mse computeGeomMSE(origPoints, reconPoints) kdtree KDTreeSearcher(origPoints); [~, dist] knnsearch(kdtree, reconPoints); mse mean(dist .^ 2); end第二种口径下的MSE高度依赖点云密度点越密最近邻距离越小MSE数值越低。所以不同数据集的MSE不能直接横向比较通常还要配合PSNR来看。PSNR的计算是用点云的包围盒对角线长度作为峰值信号公式是PSNR 20 * log10(peak / sqrt(MSE))。峰值通常取对角线长度或者属性最大范围具体取法要和对比的论文保持一致否则数值会差很多。4.2 评估过程中我踩过的三个坑第一个坑是量化步长和MSE的关系存在明显的非线性。RAHT中低频系数的能量远大于高频系数如果对全部系数使用均匀量化低频系数的量化误差会主导整体MSE。这也是我改进版里按层级加权量化步长的原因。测试时如果把量化步长翻倍MSE不是单调翻倍而是可能大幅跳变因为某些层级的主系数跨过了量化阈值。所以评估时不能只测一个量化点至少要测5个以上的步长观察率失真曲线的走势。第二个坑是边界体素的影响。体素化时点云边界上的点容易被划分到不同八叉树层级导致重建时产生毛刺点。这些少量毛刺点对MSE的贡献极大因为它们的最近邻距离可能非常大。我处理的办法是在计算几何MSE之前先做一步简单的离群点剔除把距离超过3倍平均距离的重建点标记出来单独统计。这样整体MSE更能反映压缩算法的真实性能而不是被几个边界异常点带偏。第三个坑是MATLAB里knnsearch的计算复杂度。几何MSE看似简单但点云规模达到百万级时直接调knnsearch默认参数会非常慢。建议先对原始点云做降采样或者使用KDTreeSearcher并显式设置NSMethod, kdtree同时把重建点云分块处理。分块的大小在10万点左右比较合适内存占用和速度能取得较好的平衡。5. 实测对比与调参经验5.1 在标准点云数据上的表现我用两套典型数据做了对比一套是密集的小物体扫描点云点数约55万另一套是大场景稀疏点云点数约18万。两组实验都采用统一的体素化分辨率量化步长设置从0.01到0.16共7个档位。改进版与原始实现对比的结果大致如下数据量化步长原始MSE改进版MSE码率变化密集点云0.041.28e-48.94e-5-7.2%密集点云0.085.61e-44.03e-4-5.6%稀疏点云0.043.47e-42.12e-4-11.3%稀疏点云0.081.52e-39.87e-4-9.8%可以看到改进版在稀疏点云上的收益明显大于密集点云。原因是稀疏点云的零系数占比更高游程、幅度k参数分离带来的统计效率增益更突出。而在密集点云上非零系数密度高游程优化空间有限主要收益来自层级加权量化步长的分配。这个结果是符合预期的。如果你要复现建议用点云库Computer Vision Toolbox里的pcread读取PCD文件再用pcsegdist辅助观察点云分布。数据集名我就不列了网上开源的standard test point clouds都能用。5.2 两个值得单独说说的调参细节第一个是Golomb-Rice编码的参数范围。k值不能无限增大否则一个编码单元的码长会超出合理范围。我在代码里把k限制在0到16之间超过16就按16处理。实测中k很少超过10除非数据里出现极长的零游程。限制k的下限为0也重要否则负k会导致Golomb-Rice编码退化为无符号二进制编码码长反而变长。第二个是层级加权量化步长的权重设置不要过于激进。我最初尝试让低频层的量化步长只有高频层的1/8结果低频系数虽然保住了但高频系数量化过粗重建点云出现了明显的块状伪影。后来调整为1/4MSE和主观视觉都达到了平衡。如果你用的点云属性是颜色而非几何建议权重差异更小一些因为人眼对颜色误差的容忍度和几何误差很不一样。5.3 这套代码后续还能怎么扩展RAHT-rlgr改进版跑通之后可以继续做的事情还有不少。一个方向是把MSE评估与PSNR计算封装成统一的评价函数这样后续换数据、换量化策略时可以直接复用。另一个方向是引入预测编码RAHT的直流分量在父层级间存在相关性可以在直流分量上再做一次差分预测进一步压缩码率。我在实验中试过简单的帧内预测码率在改进版基础上还能降8%左右但代价是解码端复杂度增加是否值得要根据实际场景权衡。还建议做一次完整的率失真曲线测试。单独一个量化点的MSE说明不了问题一定要把多个量化步长下的码率和MSE画成曲线观察整条曲线的走向。如果曲线在中段出现明显拐点说明量化参数的范围或者分配策略需要调整。这是衡量编码器性能最直观的方式也是论文里最常见的数据呈现形式。最后说一句实测体会代码里最值得优化的地方往往不在变换核而在系数扫描顺序和参数自适应规则。我这次改进版获得的增益大約有四成来自扫描顺序调整三成来自k参数分离剩下三成来自量化步长分配。当你把变换、熵编码、量化三个模块都跑通再回头去做针对性优化思路会比一开始就追求论文里的复杂算法清晰得多。本文还有配套的精品资源点击获取