简介:这份代码是区域自适应层次变换(RAHT)的 Matlab 工程实现,对应 Queiroz 与 Chou 提出的 RAHT-rlgr 压缩方案,面向点云压缩、三维数据处理等方向的研究者与工程人员。压缩包共 11 个文件,以 C++ 源文件、头文件为核心,另含 Matlab 的 install.m 安装脚本、License 与说明文档,整体仅 13KB,轻量易读,便于快速编译和验证。资源实现了 RAHT_cod 编码器与 RAHT_dec 解码器的改进版本,可直接对体素化点云的顶点坐标和颜色属性进行变换、量化并输出码流,同时配有编译步骤、可执行文件用法和测试说明,适合想深入理解 RAHT 原理或在自有项目中接入该算法的读者参考。已有 347 人学习下载。 点云压缩写多了之后,你会发现一个很有意思的现象:很多论文里PSNR高得吓人,可一旦自己动手实现,重建点云的均方误差(MSE)怎么都对不上。我这次把Queiroz和Chou在2016年提出的RAHT(Region 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编码器改进后的结构如下,游程和幅度分别使用独立的k:
function 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.04 | 1.28e-4 | 8.94e-5 | -7.2% |
| 密集点云 | 0.08 | 5.61e-4 | 4.03e-4 | -5.6% |
| 稀疏点云 | 0.04 | 3.47e-4 | 2.12e-4 | -11.3% |
| 稀疏点云 | 0.08 | 1.52e-3 | 9.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/4,MSE和主观视觉都达到了平衡。如果你用的点云属性是颜色而非几何,建议权重差异更小一些,因为人眼对颜色误差的容忍度和几何误差很不一样。
5.3 这套代码后续还能怎么扩展
RAHT-rlgr改进版跑通之后,可以继续做的事情还有不少。
一个方向是把MSE评估与PSNR计算封装成统一的评价函数,这样后续换数据、换量化策略时可以直接复用。另一个方向是引入预测编码:RAHT的直流分量在父层级间存在相关性,可以在直流分量上再做一次差分预测,进一步压缩码率。我在实验中试过简单的帧内预测,码率在改进版基础上还能降8%左右,但代价是解码端复杂度增加,是否值得要根据实际场景权衡。
还建议做一次完整的率失真曲线测试。单独一个量化点的MSE说明不了问题,一定要把多个量化步长下的码率和MSE画成曲线,观察整条曲线的走向。如果曲线在中段出现明显拐点,说明量化参数的范围或者分配策略需要调整。这是衡量编码器性能最直观的方式,也是论文里最常见的数据呈现形式。
最后说一句实测体会:代码里最值得优化的地方往往不在变换核,而在系数扫描顺序和参数自适应规则。我这次改进版获得的增益大約有四成来自扫描顺序调整,三成来自k参数分离,剩下三成来自量化步长分配。当你把变换、熵编码、量化三个模块都跑通,再回头去做针对性优化,思路会比一开始就追求论文里的复杂算法清晰得多。
本文还有配套的精品资源,点击获取