把一张频谱图PNG重新变成可以播放的WAV,最大的麻烦不是图片像素到数值的转换,而是补回丢失的相位。短时傅里叶变换得到的频谱通常只保留幅度或功率,相位谱被丢弃。单靠幅度谱无法唯一确定原始波形,因为不同初始化会产生不同的时域信号,但重建的目标是让结果的幅度谱尽量接近原始图像。下面从图像解析、幅度重建、相位恢复和WAV写入四个环节展开,给出可运行代码。

整个流程可以概括为:图像像素映射为幅度谱,迭代恢复相位得到复数频谱,最后通过逆短时傅里叶变换生成时域波形并保存为WAV。参数的选择与原始频谱图生成时是否一致,直接决定重建效果。
一、频谱图PNG与STFT幅度谱的映射
音频频谱图通常以图像形式保存,常见有灰度图、彩色热力图和dB域图。PNG图像每个像素的灰度值或RGB值经过颜色映射后,对应某一时刻某一频率上的幅度或功率。读取时先统一转成单通道,再把像素值归一化到0到1之间。如果原图是dB谱,还需要做dB到线性幅度的转换,否则重建出的音频动态范围会明显失真。
使用PIL读取图像并转为灰度数组的代码如下:
import numpy as np
from PIL import Image
def png_to_magnitude(png_path):
img = Image.open(png_path).convert("L")
arr = np.asarray(img, dtype=np.float32) / 255.0
return arr
这里的归一化假设原始幅度已经映射到0到255。若图像是彩色的,需要先提取亮度或按色彩映射表还原。也可以直接用matplotlib的colormap反函数,但实现复杂。工程上更推荐在生成频谱图时同时保存幅度矩阵为.npy,PNG仅作为可视化和辅助重建。
图像的方向也不能忽略。很多绘图库默认把低频放底部,高频放顶部,但也有上下翻转的情况。拿到PNG后最好先确认频率轴方向,必要时使用np.flipud翻转。图像的高度对应频点数,宽度对应时间帧数,这两个尺寸会直接影响后续STFT参数的设置。
二、Griffin-Lim迭代与相位恢复
逆短时傅里叶变换需要复数值的频谱,包含幅度和相位。只有幅度谱时,最简单有效的相位恢复算法是Griffin-Lim。它在每次迭代中先给频谱配上当前估计的相位或随机相位,然后做逆STFT得到时域波形;接着再对波形做正向STFT,用原始目标幅度替换新频谱的幅度,保留新得到的相位,再进行下一轮逆变换。
这个过程实际上是在时域连续性与频域幅度约束之间交替投影。窗函数和帧移会造成冗余表示,使约束不一致,因此Griffin-Lim不是一次求解,而是逐步收敛。迭代次数通常50到200次,重建质量提升会逐渐变缓。
下面是基于librosa的Griffin-Lim实现:
import librosa
import numpy as np
def griffin_lim(mag, n_fft=2048, hop_length=512, n_iter=100):
# mag: shape (1+n_fft//2, frames) 的线性幅度谱
phase = np.exp(2j * np.pi * np.random.rand(*mag.shape))
stft = mag.astype(np.complex64) * phase
y = librosa.istft(stft, hop_length=hop_length)
for i in range(n_iter):
stft = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)
_, new_phase = librosa.magphase(stft)
stft = mag.astype(np.complex64) * new_phase
y = librosa.istft(stft, hop_length=hop_length)
return y
注意相位初始化对结果有影响。随机相位会让早期重建有噪声,但最终幅度谱会被硬性替换为输入幅度。更稳的做法是先使用零相位或者用上一次正向STFT得到的相位作为下一次的输入,如代码所示。Griffin-Lim适合离线批量重建,实时场景则需要更轻量的模型。
三、完整重建流程:PNG到WAV
完整的逆重建需要确定几个关键参数:采样率、窗长、帧移、FFT长度以及频谱图的方向。多数频谱图纵轴是频率,横轴是时间,原点在左下角。如果原图保存时上下翻转了,读取后需要np.flipud。
图像尺寸决定了频率帧数和时间帧数。FFT长度要根据频率轴大小设置,例如频率轴有1025个频点,那么n_fft等于2048。时间轴帧数与hop_length相关,实际波形长度可以按公式估算:len_in_seconds = frames * hop_length / sr。如果重建的目标是最接近原图,那么这些参数必须与生成频谱图时一致,否则重建出的音频长度和音色都会出现偏差。
下面把读取PNG、相位恢复和写入WAV串起来:
import numpy as np
import librosa
import soundfile as sf
from PIL import Image
def spectrogram_png_to_wav(png_path, out_wav_path,
sr=22050, n_fft=2048, hop_length=512,
n_iter=100):
img = Image.open(png_path).convert("L")
arr = np.asarray(img, dtype=np.float32) / 255.0
# 若图像轴上下翻转,取消下一行注释
# arr = np.flipud(arr)
phase = np.exp(2j * np.pi * np.random.rand(*arr.shape))
stft = arr.astype(np.complex64) * phase
y = librosa.istft(stft, hop_length=hop_length)
for _ in range(n_iter):
stft = librosa.stft(y, n_fft=n_fft, hop_length=hop_length)
_, new_phase = librosa.magphase(stft)
stft = arr.astype(np.complex64) * new_phase
y = librosa.istft(stft, hop_length=hop_length)
y = np.clip(y, -1.0, 1.0)
sf.write(out_wav_path, y, sr)
return y
实际使用中,如果PNG是dB谱,需要先做 10 ** (db / 20) 转换。同时要注意十六位WAV写入时的量化噪声,可以先归一化到-1到1,再用soundfile的subtype='PCM_16'输出。语音场景采样率通常设为16000或22050,音乐建议44100。
四、相位恢复质量提升与常见误区
Griffin-Lim虽然稳定,但收敛速度慢,恢复出来的音频往往缺少高频细节和瞬态冲击感。一个常见误区是增加迭代次数就能无限提升质量,实际上频谱一致性与时域自然性之间存在折中。改进方案包括RAAR算法,它比Griffin-Lim有更快的收敛速度;还有基于神经网络的相位估计模型,能直接从幅度谱预测相位或者波形。
另一个容易忽略的环节是图像颜色映射。很多频谱图使用magma、jet等色彩映射,直接灰度化无法还原原始幅度。此时应记录生成频谱图时的归一化参数和colormap,将RGB转换回能量值。对于黑白频谱图,要注意0是背景还是静音,如果背景像素值不是0,静音段会重建出底噪。
评估重建质量时,可以用重建波形的频谱图与原PNG做均方误差,也可以主观试听瞬态部分。若音乐信号里鼓点变得模糊,说明相位恢复尚未触及高频瞬态,此时可以减小hop_length或增加n_fft来提升频率分辨率,代价是计算量上升。参数选择并没有一套通用值,需要结合实际音频长度和内容做多次试验。

