python开发中,常使用绘图工具matplotlib绘制带有坐标轴或者colorbar的图像,比如下图是结合librosa进行频谱分析的梅尔频谱图。
当matplotlib绘制的图形只有部分是我们需要的,或者当我们没有条件将其生成的图像保存到本地,需要将像素数据放入内存中时,就会想到是否有一种办法可以将matplotlib绘制的图片部分/全部像素点提取出来,以备使用。
下面按照小编的思维方式介绍一下走弯路的过程,如果需要直接查看正确答案,移步至文章最后的代码,如有需要勘误之处,请移步评论区。
首先介绍业务需求,基于如下的代码,将频谱图中的频谱窗口(中间的绘图部分,不包括外侧色度条、空白区域和坐标轴)的像素点提取出来,且不能保存图像到本地进行二次读取操作(没有保存到本地的条件,需要到其他环境中运行,只能在内存中操作):
import matplotlib.pyplot as plt
import librosa
s = librosa.feature.melspectrogram(y=x, sr=fs)
fig, ax = plt.subplots()
s1 = librosa.power_to_db(s, ref=np.max)
img = librosa.display.specshow(s1, x_axis=xxx, y_axis='mel', sr=fs, ax=ax)
# 生成右侧的渐变色板条
fig.colorbar(img, ax=ax, format='%+2.0f dB')
保存到本地的图片如下:
第一次尝试,想到了plt.subplots()或者librosa.display.specshow()返回的对象是否提供rgb三通道像素点的API?反复查看了几遍,发现有一个get_array(),获取代码如下:
img = librosa.display.specshow(S_dB, x_axis='time', y_axis='mel', sr=fs, ax=ax)
a = img.get_array()
a其实就是s1,并不是什么像素点了。
第二次尝试,想到是否可以定位到频谱区域的位置进行截图,丢弃坐标、色板条和空白部分。
考虑使用PIL的截图API,具体代码如下:
imageObject = Image.open(‘xxx’)
cropped = imageObject.crop((x1,y1,x2,y2))
cropped.save('xxx')
(x1,y1)和(x2,y2)分别是截图区域的左上角和右下角,但是发现,需要将图像存储到本地,不符合小编的要求。
需要注意的是,librosa 的所有绘图功能都依赖于 matplotlib,一般需要导入 matplotlib 的 pyplot API。
最后查阅资料发现,可以通过matplotlib.figure的canvas获取像素数据,具体地通过tostring_rgb()方法,查看tostring_rgb()方法的源码,发现仅有一行:
return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
可以见得,是将self._renderer中的部分维度、部分位置的数据提取出来了,并不是直接解析某对象得到的。
由于目前还需要对像素数据进行截取,所以确定了[57:429,79:578:,]这个截取范围,至于确定范围的方法,需要根据图像调整,但是基本可以确定的是,在前面绘图参数和数据的尺寸不变的情况下,频谱图窗口的位置相对于整个图像是不变的,也就是说截取范围可以不变。
最后为了准确,使用PIL Image对象,将像素点填充进去,并show()出来,以查看截取的部分是否能够正常成像。
import matplotlib.pyplot as plt
from PIL import Image
import librosa
def test():
# ---------原始频谱图------------------
# 1、加载音频文件的时域信号、采样率到内存中
# 2、根据时域信号、采样率等参数生成梅尔频谱
s = librosa.feature.melspectrogram(y=x, sr=fs)
# 3、matplotlib构建figure和一组子图,返回图形(matplotlib.figure)和坐标轴(matplotlib.axes.Axes)
fig, ax = plt.subplots()
# 4、频谱转成分贝单位的值
S_dB = librosa.power_to_db(s, ref=np.max)
# 5、显示频谱图像
img = librosa.display.specshow(S_dB, x_axis='time', y_axis='mel', sr=fs, ax=ax)
# ---------利用canvas对象获取像素点------------------
# 6、绘制figure的边界框
fig.canvas.draw()
# 7、提取rgb数据,返回字节流对象
buf = fig.canvas.tostring_rgb()
# 8、获取canvas的宽度、高度
ncols, nrows = fig.canvas.get_width_height()
# 9、PIL创建图片对象,确定要截取的区域像素点的数量,这里是499×372
im = Image.new("RGB",(499,372))
# 10、将字节流转成numpy数组,并形状重置
d = np.fromstring(buf, dtype=np.uint8).reshape(nrows, ncols, 3)
# 11、截取目标区域的rgb像素点
dd = d[57:429,79:578:,]
# ---------测试:查看像素点对应的图像------------------
# 12、赋值给图片对象的像素点,每一个像素点都由rgb三通道组成
for i in range(0,372):
for j in range(0,499):
im.putpixel((j,i),(int(dd[i][j][0]),int(dd[i][j][1]),int(dd[i][j][2])))
# 13、查看截取的图像
im.show()