简介:离散分数余弦变换(DFrCT)作为传统DCT的分数阶扩展,可引入自由阶次参数以获得更精细、可调的频率分辨率,是处理非平稳信号与局部特征提取的重要工具,这份MATLAB代码资源面向信号处理、图像压缩、语音识别及生物医学信号分析等方向的研究者与学生,能直接解决分数阶余弦变换算法实现与验证的需求。压缩包共3个文件,均为m脚本/函数,体积仅1KB,代码量小但逻辑完整,适合研读与二次开发;其中make_EC.m负责生成示例信号,Disfrct.m实现核心变换计算,dFRCT.m提供变体或辅助处理逻辑,三者配合覆盖从数据构造、分数阶次选择、复数运算到结果后处理的完整流程。已有249人学习下载。通过学习这三个文件,可快速掌握DFrCT的分数阶次调整方法与变换步骤,省去从零推导公式和搭建测试环境的精力,便于将算法迁移到图像压缩、生物医学信号分析等实际项目中,也可作为相关课程设计的参考实现。
1. 从 DFRFT 函数说起:discrete fractional cosine transform 到底解决什么问题
拿到一段线性调频信号,想把它转到某个中间频率轴上再处理,你大概率会先找离散分数阶傅里叶变换(DFRFT 函数)的现成实现。可如果信号本身是实信号,边界要求又比较苛刻,直接上 DFRFT 往往有一半计算浪费在对称分量上。这时候,discrete fractional cosine transform 才是更贴合的算子:它把分数阶变换限定在余弦基里,保留实数域处理习惯,又继承了分数阶变换“旋转时频平面”的核心能力。它不替代 DFRFT,而是把 DFRFT 的偶对称投影抽出来,做成一个更快、更稳定、更容易嵌入现有 DCT 流程的函数。
这套变换适合三类人:在时频域里做滤波和参数估计的,做图像或音频掩码的,以及在 MATLAB、Python 里写过 DCT 又不想引入复数中间量的人。实现上不需要去背复杂的积分公式,最常见的工程路径是先构造 DCT-II 矩阵,再对整个矩阵做分数次幂。你只要把alpha从 0 拨到 1,就能看到信号从原始时域形态,平滑过渡到标准 DCT 谱的形态。
2. 连续积分到离散矩阵:分数阶余弦变换与 DFRFT 函数差在哪一步
2.1 分数阶傅里叶的偶对称投影,就是分数阶余弦变换
在很多资料里,分数阶余弦变换被称为 Fractional Cosine Transform,它并不算一个独立发明,而是分数阶傅里叶变换的偶对称版本。FRFT 的连续定义是:
F_α{x}(u) = B_α ∫ x(t) exp(jπ(t²+u²)cot α − 2jπtu csc α) dt
其中B_α = sqrt(1 − j cot α),α 是旋转角度。当 α = π/2 时,它退化成普通傅里叶变换;α = 0 时是恒等变换。
如果输入信号在时间轴上是对称的,FRFT 的积分核里cos部分会被单独保留下来,于是就有了分数阶余弦变换的连续积分形式。常见写法是:
X_α(u) = A_α ∫ x(t) exp(jπ(t²+u²)cot α) cos(2π t u csc α) dt
注意不同文献里这个积分可能差 2π 系数或常数因子,这并不影响离散实现,因为一旦切到矩阵分数化路线,常数因子会自动消解。你只需要理解一件事:分数阶余弦变换处理的是偶对称投影,它把 DFRFT 里那一堆复数运算压缩成实数余弦基,代价是输入信号被当作偶函数对待。
2.2 工程上为什么不直接采样连续积分
我见过不少初学者拿连续积分公式直接做数值逼近:先选采样区间,再算t和u的网格,最后用trapz积分。这种做法不是不行,而是坑太多。分数阶变换的旋转特性对采样间隔、信号带宽和边界截断都非常敏感,积分网格稍微取错,旋转后的谱就完全变形。更麻烦的是,连续定义是按无穷区间设计的,离散信号本身没有“无穷区间”可言,硬套积分公式得到的结果和 DCT 快速度实现对不上,调试起来像在解一个黑匣子。
真正一线工程里最稳的做法,是先构造一个离散余弦变换矩阵,再对这个正交矩阵做分数次幂。设C是 N×N 的 DCT-II 矩阵,满足Cᵀ C = I,那么离散分数阶余弦变换的核矩阵就是:
K_α = C^α
当 α = 0 时,K_0 = I,变换结果等于原信号;当 α = 1 时,K_1 = C,变换结果就是标准 DCT-II。α 在 0 到 1 中间取值时,信号在原始时域和余弦谱域之间做连续旋转。这个定义保持了线性、可逆和阶数叠加这三个最重要的工程性质,而且绕开了连续积分里那些让人头疼的归一化问题。
2.3 alpha 阶数的含义与使用时频旋转的边界
alpha 在这个变换里不是频率,而是“旋转量”。它对应的物理意义可以粗略理解成:把信号在二维平面上转α·90°。α = 0 不转,α = 1 转 90° 到 DCT 谱域,α = 2 转 180° 而相当于连续做两次 DCT-II。这个“旋转”和 DFRFT 的时频旋转不完全一样,它是投影到余弦基之后的旋转,信息只保留在实部对应的一侧。
实际使用时要记住一个边界:alpha 不是越大越好。旋转到特定角度时,信号能量会集中在少数几个系数上,这是做滤波和参数估计的好时机;但一旦越过这个角度,能量又会被打散回整个域。所以扫 alpha 找峰值,比固定 alpha 看谱线更有工程价值。后面会专门给出一段扫阶数的检测流程。
3. 用 Python 复现一个可用的 DFRFT 函数:DCT 矩阵分数化与关键参数
3.1 先构造 DCT-II 矩阵,这是整个变换的地基
不直接计算连续积分,而是先把 N 阶 DCT-II 变换矩阵构造出来。意思很直白:矩阵的每一列,就是对应位置单位脉冲的 DCT 结果。这个概念搞清楚了,后面矩阵分数化才立得住。
import numpy as np from scipy.fftpack import dct from scipy.linalg import schur def dct2_matrix(N): """构造 N×N DCT-II 矩阵,列向量单位正交。""" C = np.zeros((N, N)) for n in range(N): unit = np.zeros(N) unit[n] = 1.0 C[:, n] = dct(unit, type=2, norm="ortho") return C这段代码的逻辑是用单位脉冲逐一通过 scipy 的dct函数,把输出结果填入矩阵列。norm="ortho"是必须的,只有正交归一化才能保证Cᵀ C = I,否则后面做分数幂时矩阵性质不成立。
N 是变换长度,通常和信号长度保持一致。如果信号长度接近 2 的幂,可以不补齐;强行补零会改变 DCT 的边界相位,分数阶旋转的结果也会跟着偏,这一点和普通 FFT 补零完全是两码事。
3.2 用实 Schur 分解计算矩阵的分数次幂
拿到正交矩阵 C 后,下一步是对它做分数次幂。这里最容易踩坑的是直接调numpy.linalg.eig然后对特征值取指数。实际上 DCT-II 矩阵不是对称矩阵,特征向量数值稳定性差,直接特征分解后恢复出来的矩阵往往不正交。我惯用的方式是对正交矩阵做实数 Schur 分解,把矩阵化成 1×1 和 2×2 的块,再对每个块单独取分数次幂。
def orthogonal_matrix_power(C, alpha): """对正交矩阵 C 计算 C^α,使用实 Schur 分块取幂。""" T, Q = schur(C, output="real") n = T.shape[0] Tp = np.zeros((n, n), dtype=complex) i = 0 while i < n: if i == n - 1 or abs(T[i + 1, i]) < 1e-12: lam = round(T[i, i].real) if lam == 1: Tp[i, i] = 1.0 elif lam == -1: Tp[i, i] = np.exp(1j * np.pi * alpha) i += 1 else: a = T[i, i] c = T[i + 1, i] theta = np.arctan2(c, a) ct = np.cos(alpha * theta) st = np.sin(alpha * theta) Tp[i:i+2, i:i+2] = [[ct, -st], [st, ct]] i += 2 return Q @ Tp @ Q.T这段代码的核心逻辑分三步。第一步,schur得到准上三角矩阵 T 和正交矩阵 Q,满足C = Q @ T @ Qᵀ。第二步,遇到 2×2 块时,块本身就是一个旋转矩阵,其旋转角由arctan2(c, a)提取,分数幂就是把这个角度乘以 alpha。第三步,遇到 1×1 块时,特征值只可能是 1 或 -1,分别做 1 的 alpha 次幂和 -1 的 alpha 次幂,后者的结果落在复数域。
3.3 封装成 DFRFT 函数族的调用入口
这一步封装两个函数:一个负责返回核矩阵,一个负责对信号做变换。实际工程中如果要在同一 alpha 下处理几百段信号,缓存核矩阵能省掉大量重复计算。
def dfrct_matrix(N, alpha): C = dct2_matrix(N) return orthogonal_matrix_power(C, alpha) def dfrct(x, alpha, K=None): x = np.asarray(x, dtype=np.float64) if K is None: K = dfrct_matrix(len(x), alpha) return K @ x调用时只需要提供信号和 alpha。验证方式很简单:alpha 取 0 时输出等于原信号,alpha 取 1 时输出等于dct(x, type=2, norm="ortho")。在我的机器上,这两个退化点的误差都稳定在 1e-12 量级,说明核矩阵的构造和分数化过程是自洽的。
| 参数 | 取值 | 选型说明 |
|---|---|---|
N | 与信号长度一致 | 补零会改变 DCT 边界语义,除非有特殊理由否则不补 |
alpha | 0 到 2 | 0 为原信号,1 为 DCT,2 为两次 DCT,负值为逆变换 |
norm | "ortho" | 保证矩阵正交,是分数化的前提 |
K | 缓存核矩阵 | 同 alpha 批量处理时务必传入,节省大量耗时 |
4. 实现 DFRFT 函数的四个坑:DCT 不对称与特征分解的坑
4.1 直接调 eigh 报错:DCT-II 矩阵不是对称矩阵
现象:你在写矩阵分数化时想走“对称矩阵特征分解”的老路,调用scipy.linalg.eigh,结果要么报出矩阵不是 Hermitian 的错误,要么强行运行后得到的结果连Cᵀ C = I都验证不过去。
原因:DCT-II 的变换核里有归一化因子,行和列上的权重不一样,所以C[k,n]不等于C[n,k]。很多资料里画 DCT 矩阵时看起来对称,实际用正交归一化展开后并不对称。这种情况eigh是算不了的,只能用一般特征分解或者 Schur 分解。
解决:不要试图“修”矩阵,直接改算法。文章里给出的orthogonal_matrix_power用实 Schur 分解,自动把正交矩阵按块拆开,完全避开对称性要求。如果你坚持用特征分解,就用scipy.linalg.eig,但一定要在分解后验证V @ V.conj().T是否接近单位阵,避免数值误差积累。
4.2 alpha=1 后复原的 DCT 对不上
现象:dfrct(x, 1.0)跑出来的结果和dct(x, type=2, norm="ortho")差别很大,甚至差出一个量级。
原因:最常见的元凶是构造 DCT-II 矩阵时用了norm="ortho",但验证时调用dct没有指定同样的归一化,或者反过来了。其次,Schur 分解里 2×2 旋转块的 theta 提取得和矩阵定义方向一致,如果 theta 符号取反,alpha=1 时会差在旋转方向上。
解决:先用一段随机信号做自检,确认dfrct(x, 0)和dfrct(x, 1)两个退化点都过了,再去做实际滤波。测试代码就三行,但值得每次改完算法都跑一遍:
x = np.random.default_rng(0).standard_normal(64) assert np.allclose(dfrct(x, 0.0), x, atol=1e-10) assert np.allclose(dfrct(x, 1.0), dct(x, type=2, norm="ortho"), atol=1e-10)这两条断言也是甄别“矩阵分解是玄学”和“代码真的有 bug”的最快手段。
4.3 alpha 不是整数时输出变复数,取实部还是取模
现象:输入是实信号,alpha = 0.3 时变换结果是复数序列。有人为了后续处理方便直接取实部,结果发现能量不守恒,取模之后峰又变宽了,找不准阶数。
原因:DCT-II 矩阵的特征值里有 -1,而 -1 的 0.3 次幂本身就是一个复数exp(j·0.3π)。正交矩阵的分数化天然会把部分特征值推到复数域,这不是 bug,而是分数阶变换的固有属性。分数阶余弦变换名为“余弦”,但中间状态并不保证实值。
解决:分析阶段保留复数,取模看能量分布;可视化阶段再取实部,但心里要清楚这只是一种投影。如果需要严格保持实数输出的工程链路,可以只取核矩阵的实部K.real,但代价是损失严格的阶数叠加性质,不建议在需要精确旋转角度的场景里这么干。
4.4 N 增大后内存和耗时双双失控
现象:N=512 时秒级出结果,N=8192 时构造矩阵加 Schur 分解跑了很久,内存占用飙升到接近瓶颈。
原因:dfrct_matrix返回的是稠密复数矩阵,单是 N=8192 的核矩阵就有 8192×8192×16 字节,也就是大约 1GB 内存。Schur 分解本身又是 O(N³) 的量级,内存和时间都很难看。
解决:矩阵法只适合 N ≤ 2048 的原型验证和中小批处理。N 再大就要换路线:一是信道化降采样后再做分数阶变换;二是改用基于线性调频分解的 DFRFT 近似算法,也就是把分数阶 Fourier 变换用 chirp 乘积和 FFT 逼近,避免构造满矩阵。工程里我一般把矩阵法的 N 上限卡在 1024,超过就考虑分块或换近似,不硬扛。
5. 用扫阶数找线性调频:DFRFT 函数的一种验证与用法
5.1 构造被噪声盖住的 chirp,扫 alpha 找峰值阶数
线性调频信号在某个特定 alpha 下会聚集出一个尖锐的谱峰,这是分数阶变换最经典的用法之一。我把这种方法当验证函数用:如果能稳定找到正确的阶数,说明前面的 DFRFT 函数实现没毛病。
fs = 1000 t = np.arange(256) / fs x = np.cos(2 * np.pi * (50 * t + 120 * t**2)) x = x + 0.5 * np.random.default_rng(1).standard_normal(256) alpha_grid = np.linspace(0.0, 1.2, 121) peaks = [] for a in alpha_grid: y = dfrct(x, a) peaks.append(np.max(np.abs(y))) best = alpha_grid[int(np.argmax(peaks))]这段代码先造一个带噪声的 chirp,再在 0 到 1.2 上均匀扫 121 个阶数,每个阶数做一次变换并取谱峰最大模值。峰值最高的阶数就是信号能量聚集最集中的位置,一般对应 chirp 的调频斜率。如果结果落在网格边界,说明真实阶数出界了,把网格范围扩出去重扫。
5.2 在旋转域做带通后反变换,注意边界泄漏
找到最佳阶数后,可以在这个域里把噪声系数置零,再做逆变换恢复时域信号。这样做比直接在时域滤波更符合 chirp 的能量分布结构。
dfrct的逆变换就是让它自己转回负角度,dfrct(x, alpha)的逆是dfrct(x, -alpha),不需要额外求逆矩阵。
要注意边界泄漏:DCT 本身在边界上是偶对称延拓,旋转之后信号的两端依然会留下类似边缘振铃的痕迹。做滤波时峰值 60% 以下的系数直接置零会让边界处出现明显起伏,实际项目里可以加一段淡入淡出窗,或者在旋转域只保留峰周围少量系数,而不是做硬阈值截断。
5.3 我的习惯:缓存核矩阵,alpha 控制在 0 到 2 之间
这套函数我用了快两年,最大的体会是:不要把dfrct_matrix放在循环里反复调用。先算出核矩阵存下来,批量信号处理能快一个数量级;扫阶数时则反过来,alpha 每次不同,没法缓存,那就预处理信号短段,控制在 256 到 1024 之间,保持扫描速度在可接受范围。alpha 的取值也尽量锁定在 0 到 2,超过这个范围虽然也能算,但参数解释和边界效应会变复杂,而且大多数工程问题的旋转角度都落在 0 到 1.5 之间,出界的调参基本属于过度折腾。希望这一套思路能帮你在实现类似函数时少走一点弯路。
本文还有配套的精品资源,点击获取