用颜色渐变绘制矩阵的技术叫做“频谱图”。

4
使用短时傅里叶变换(STFT)后,输出的是一个代表三维图形的矩阵,如下所示:(A[X, Y] = M),其中A是输出矩阵,X表示时间,Y表示频率,第三维M表示振幅,用像素颜色的强度表示。以下图片是示例:

###Spectrogram 1

Spectrogram 2

如何在C#中使用渐变颜色绘制输出矩阵A,就像示例中的图片一样?是否有包含C#谱图控件的库?

更新:
通过对给定算法进行一些修改,我可以绘制出谱图。我没有改变颜色调色板,除了将第一种颜色更改为黑色,但我不知道为什么它非常模糊!

这个表示一个声音说“再见”:

Bye Bye Spectrogram

这是一个纯正的正弦波,所以它几乎一直是相同的频率:

Pure sine wave Spectrogram

输出被接受,它表示输入信号的频率,但我认为有一种方法可以使谱图像示例中的那些一样好地呈现出来,请帮忙检查我的代码并提出修改建议。

以下是事件处理程序:
private void SpectrogramButton_Click(object sender, EventArgs e)
{
    Complex[][] SpectrogramData = Fourier_Transform.STFT(/*signal:*/ samples,  /*windowSize:*/ 512, /*hopSize:*/ 512);
    SpectrogramBox.Image = Spectrogram.DrawSpectrogram(SpectrogramData, /*Interpolation Factor:*/ 1000, /*Height:*/ 256);
}


这是我的修改后的绘图功能:

public static Bitmap DrawSpectrogram(Complex[][] Data, int InterpolationFactor, int Height)
{
    // target size:
    Size sz = new Size(Data.GetLength(0), Height);
    Bitmap bmp = new Bitmap(sz.Width, sz.Height);

    // the data array:
    //double[,] data = new double[222, 222];

    // step sizes:
    float stepX = 1f * sz.Width / Data.GetLength(0);
    float stepY = 1f * sz.Height / Data[0].GetLength(0);

    // create a few stop colors:
    List<Color> baseColors = new List<Color>();  // create a color list
    baseColors.Add(Color.Black);
    baseColors.Add(Color.LightSkyBlue);
    baseColors.Add(Color.LightGreen);
    baseColors.Add(Color.Yellow);
    baseColors.Add(Color.Orange);
    baseColors.Add(Color.Red);


    // and the interpolate a larger number of grdient colors:
    List<Color> colors = interpolateColors(baseColors, InterpolationFactor);

    // a few boring test data
    //Random rnd = new Random(1);
    //for (int x = 0; x < data.GetLength(0); x++)
    //    for (int y = 0; y < data.GetLength(1); y++)
    //    {
    //        //data[x, y] = rnd.Next((int)(300 + Math.Sin(x * y / 999) * 200)) +
    //        //                rnd.Next(x + y + 111);
    //        data[x, y] = 0;
    //    }

    // now draw the data:
    float Max = Complex.Max(Data);
    using (Graphics G = Graphics.FromImage(bmp))
        for (int x = 0; x < Data.GetLength(0); x++)
            for (int y = 0; y < Data[0].GetLength(0); y++)
            {
                int Val = (int)Math.Ceiling((Data[x][y].Magnitude / Max) * (InterpolationFactor - 1));
                using (SolidBrush brush = new SolidBrush(colors[(int)Val]))
                    G.FillRectangle(brush, x * stepX, (Data[0].GetLength(0) - y) * stepY, stepX, stepY);
            }

    // and display the result
    return bmp;
}

很抱歉,我对你在回答中提到的“log”一事并不太理解,我的知识有限。



更新:
这是加入log10对幅度进行计算后的输出结果(忽略负值):

  1. 这是之前的“再见”音频:

enter image description here

  1. 霰弹枪声:

enter image description here

  1. 音乐盒声音:

enter image description here

我认为这个输出结果是可以接受的,虽然不同于我之前提供的例子,但我认为它更好。


我现在有点忙,稍后会添加一些内容来演示如何使用对数刻度,因为它通常对于声波数据是必要的。但是我认为第一张图片看起来很不错,所以你没有问题。如果你将第一个颜色改成黑色,你可能应该在它之后插入一个深蓝色和一个中等蓝色的停止颜色。不过,可以尝试调整颜色以便更好地感受它们!你可以使用任意数量的颜色。此外:你需要知道你的值的范围!它是声波的正常范围(16-16k)吗?如果是... - TaW
直接映射到线性颜色列表将无法很好地工作。该列表仅具有1000种颜色,即使您将其扩展到16k或20k,它仍然不正确。相反,您需要对它们进行对数查找。 - TaW
我没有直接使用“Magnitude”,而是使用了这个公式“Val = 20d * Math.Log10(Data[x][y].Magnitude)”。我不知道这是否符合您的意思,但我必须忽略所有负值,这些负值来自于“Magnitude values < 1”,这些值似乎是原始图像中非常暗的区域。因此,我考虑将“log”后的整个值域映射到我拥有的颜色范围内,以确保显示所有值。 - Mohamed Hosnie
你所说的fft数据是什么意思?关于标签,通过我的搜索,我发现了一个方程式,它可以为矩阵中的每一行提供频率值,该方程式为fr = sampleRate / # samples,然后通过方程式f = n * fr计算索引或行n的频率值f。正如你所看到的,它与采样速率和样本数有关,这些参数因文件而异,我不确定它是否正确,但没有尝试过。 - Mohamed Hosnie
你不会将FFT结果(Complex [] [] SpectrogramData)存储起来,还是总是重新计算它们?序列化它们应该是更好的选择...它们是否包含线性数据,例如每个Hz一行,还是数据已经按对数方案排列,意味着每个八度有相同数量的行/频率?请仔细查看第一张图片和我链接中的图片:标签不是线性的,而是对数的。 - TaW
显示剩余14条评论
2个回答

1
没有我知道的开箱即用的控件。当然,可能会有你可以购买的外部库,但是嘘,你不能在SO上询问它们。
理论上,您可以使用或者我应该说滥用一个图表控件来实现这一点。但是,由于数据点是相当昂贵的对象,或者至少比它们看起来更昂贵,因此这似乎不可取。
相反,您可以自己将图形绘制到位图中。
第一步是决定一种颜色渐变。请参见interpolateColors function here的示例!
然后,您只需使用浮点数进行步长和像素大小的双重循环,并在那里使用Graphics.FillRectangle。
这是一个使用 GDI+ 创建 BitmapWinforms PictureBox 进行显示的简单示例。它不会为图形添加任何轴,并完全填充它。
首先创建一些样本数据和一个带有 1000 种颜色的渐变。然后将其绘制到 Bitmap 中并显示结果:

enter image description here

private void button6_Click(object sender, EventArgs e)
{
    // target size:
    Size sz = pictureBox1.ClientSize;
    Bitmap bmp = new Bitmap(sz.Width, sz.Height);

    // the data array:
    double[,] data = new double[222, 222];

    // step sizes:
    float stepX = 1f * sz.Width / data.GetLength(0);
    float stepY = 1f * sz.Height / data.GetLength(1);

    // create a few stop colors:
    List<Color> baseColors = new List<Color>();  // create a color list
    baseColors.Add(Color.RoyalBlue);
    baseColors.Add(Color.LightSkyBlue);
    baseColors.Add(Color.LightGreen);
    baseColors.Add(Color.Yellow);
    baseColors.Add(Color.Orange);
    baseColors.Add(Color.Red);
    // and the interpolate a larger number of grdient colors:
    List<Color> colors = interpolateColors(baseColors, 1000);

    // a few boring test data
    Random rnd = new Random(1);
    for (int x = 0; x < data.GetLength(0); x++)
    for (int y = 0; y < data.GetLength(1); y++)
    {
        data[x, y] = rnd.Next( (int) (300 + Math.Sin(x * y / 999) * 200 )) +
                        rnd.Next(  x +  y + 111);
    }

    // now draw the data:
    using (Graphics G = Graphics.FromImage(bmp))
    for (int x = 0; x < data.GetLength(0); x++)
        for (int y = 0; y < data.GetLength(1); y++)
        {
            using (SolidBrush brush = new SolidBrush(colors[(int)data[x, y]]))
                G.FillRectangle(brush, x * stepX, y * stepY, stepX, stepY);
        }

    // and display the result
    pictureBox1.Image = bmp;
}

这是链接中的函数:

List<Color> interpolateColors(List<Color> stopColors, int count)
{
    SortedDictionary<float, Color> gradient = new SortedDictionary<float, Color>();
    for (int i = 0; i < stopColors.Count; i++)
        gradient.Add(1f * i / (stopColors.Count - 1), stopColors[i]);
    List<Color> ColorList = new List<Color>();

    using (Bitmap bmp = new Bitmap(count, 1))
    using (Graphics G = Graphics.FromImage(bmp))
    {
        Rectangle bmpCRect = new Rectangle(Point.Empty, bmp.Size);
        LinearGradientBrush br = new LinearGradientBrush
                                (bmpCRect, Color.Empty, Color.Empty, 0, false);
        ColorBlend cb = new ColorBlend();
        cb.Positions = new float[gradient.Count];
        for (int i = 0; i < gradient.Count; i++)
            cb.Positions[i] = gradient.ElementAt(i).Key;
        cb.Colors = gradient.Values.ToArray();
        br.InterpolationColors = cb;
        G.FillRectangle(br, bmpCRect);
        for (int i = 0; i < count; i++) ColorList.Add(bmp.GetPixel(i, 0));
        br.Dispose();
    }
    return ColorList;
}

你可能想要绘制带有标签等轴线。你可以使用 Graphics.DrawStringTextRenderer.DrawText 来完成。只需在绘图区域周围留出足够的空间即可!我将数据值转换为 int,直接用作颜色表中的指针。
根据你的数据,你需要将它们缩小或甚至使用对数转换。你的第一张图片显示了从 100 到 20k 的 对数 刻度,而第二张图片则显示了从 0 到 100 的线性刻度。
如果你向我们展示数据结构,我们可以给你进一步的提示如何调整代码来使用它。

0

你可以按照其他答案创建位图。通常使用颜色查找表将FFT对数幅度转换为每个像素或小矩形要使用的颜色也很常见。


我不理解我正在使用的“log”部分,因为有些幅度小于1,这会导致负“log”,并且在绘图时会出现问题,您能否请更清楚地解释一下? - Mohamed Hosnie
通常,人们会对对数幅度进行偏移和缩放,以使其范围适合于您的着色方案查找表(0-255种颜色等)。但是,在取对数之前,确实需要忽略零的幅度。相反,将它们设置为索引您查找表底部的值即可。韦伯-费希纳人类感知定律是使用对数幅度的原因之一。 - hotpaw2
请问您能提供一个代码示例吗?或许您可以使用我在问题描述中添加的代码样本来澄清它。 - Mohamed Hosnie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接