Drawing a matrix with a gradient of colors “Spectr

2019-05-13 17:36发布

问题:

After using STFT(Short-time Fourier transform) the output is a matrix that represents a 3d plot as though (A[X, Y] = M) A is the output matrix, X is the time , Y is the frequency, and the third dimension M is the amplitude illustrated by the intensity of the pixel color as in the following pictures:

Spectrogram 2

How do I draw the output matrix A with a gradient of colors like in the pictures in C#?
Is there a library that contains a spectrogram control for C#?



Update:
After some modifications on the given algorithm I could draw the spectrogram, I didn't change the color palette except the first color changed to black but I don't know why it's very faded!

This one represents a sound saying

Bye Bye

Bye Bye Spectrogram

And this one of a pure sine wave so it's almost the same frequency all the time

Pure sine wave Spectrogram

The output is accepted it represents the frequencies of the input signal as expected, but i think there is a way to make the spectrogram as well illustrated as the ones in the examples, could you please take a look at my code and suggest modifications?


This is the event handler:

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);
}


And this one is the drawing function after my modifications:

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;
}

I don't really understand the log thing that you are talking about in your answers, I'm sorry for my little knowledge.



Update:
This is the output after adding taking log10 to the magnitudes (negative values neglected):

  1. This one of "Bye bye" from before:

  1. A Shotgun Blast:

  1. A Music Box:

I Think this output is acceptable, it is different from the examples I brought in the beginning but I think it's better.

回答1:

No, there is no out of the box control I know of. There may well be outside libraries you can buy, of course, but shhh, you can't ask on SO about them..

In theory you could use, or I guess I should rather say abuse a Chart control for this. But since DataPoints are rather expensive objects, or at least more expensive than they look, this seems not advisable.

Instead you can simply draw the graph into a Bitmap yourself.

  • Step one is to decide on a gradient of colors. See the interpolateColors function here for an example of this!

  • Then you would simply do a double loop over the data using floats for the step and pixel sizes and do a Graphics.FillRectangle there.

Here is a simple example using GDI+ to create a Bitmap and a Winforms PictureBox for display. It doesn't add any axes to the graphic and fills it completely.

It first creates a few sample data and a gradient wih 1000 colors. Then it draws into a Bitmap and displays the result:

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;
}

Here is the function from the link:

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;
}

You would probably want to draw axes with labels etc. You can use Graphics.DrawString or TextRenderer.DrawText to do so. Just leave enough space around the drawing area!

I used the data values cast to int as direct pointers into the color table.

Depending on your data you will need to scale them down or even use a log conversion. The first of your images show a logarithmic scale going from 100 to 20k, the second looks linear going from 0 to 100.

If you show us your data structure we can give you further hints how to adapt the code to use it..



回答2:

You can create a bitmap as per the other answer. It's also common to use a color lookup table to convert FFT log magnitude to the color to use for each pixel or small rectangle.