|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Drawing; |
| 4 | +using System.Drawing.Imaging; |
| 5 | +using System.Runtime.InteropServices; |
| 6 | +using System.Text; |
| 7 | +using System.Threading.Tasks; |
| 8 | + |
| 9 | +namespace Spectrogram |
| 10 | +{ |
| 11 | + /// <summary> |
| 12 | + /// This class converts a collection of FFTs to a colormapped spectrogram image |
| 13 | + /// </summary> |
| 14 | + public class ImageMaker |
| 15 | + { |
| 16 | + /// <summary> |
| 17 | + /// Colormap used to translate intensity to pixel color |
| 18 | + /// </summary> |
| 19 | + public Colormap Colormap; |
| 20 | + |
| 21 | + /// <summary> |
| 22 | + /// Intensity is multiplied by this number before converting it to the pixel color according to the colormap |
| 23 | + /// </summary> |
| 24 | + public double Intensity = 1; |
| 25 | + |
| 26 | + /// <summary> |
| 27 | + /// If True, intensity will be log-scaled to represent Decibels |
| 28 | + /// </summary> |
| 29 | + public bool IsDecibel = false; |
| 30 | + |
| 31 | + /// <summary> |
| 32 | + /// If <see cref="IsDecibel"/> is enabled, intensity will be scaled by this value prior to log transformation |
| 33 | + /// </summary> |
| 34 | + public double DecibelScaleFactor = 1; |
| 35 | + |
| 36 | + /// <summary> |
| 37 | + /// If False, the spectrogram will proceed in time from left to right across the whole image. |
| 38 | + /// If True, the image will be broken and the newest FFTs will appear on the left and oldest on the right. |
| 39 | + /// </summary> |
| 40 | + public bool IsRoll = false; |
| 41 | + |
| 42 | + /// <summary> |
| 43 | + /// If <see cref="IsRoll"/> is enabled, this value indicates the pixel position of the break point. |
| 44 | + /// </summary> |
| 45 | + public int RollOffset = 0; |
| 46 | + |
| 47 | + /// <summary> |
| 48 | + /// If True, the spectrogram will flow top-down (oldest to newest) rather than left-right. |
| 49 | + /// </summary> |
| 50 | + public bool IsRotated = false; |
| 51 | + |
| 52 | + public ImageMaker() |
| 53 | + { |
| 54 | + |
| 55 | + } |
| 56 | + |
| 57 | + public Bitmap GetBitmap(List<double[]> ffts) |
| 58 | + { |
| 59 | + if (ffts.Count == 0) |
| 60 | + throw new ArgumentException("Not enough data in FFTs to generate an image yet."); |
| 61 | + |
| 62 | + int Width = IsRotated ? ffts[0].Length : ffts.Count; |
| 63 | + int Height = IsRotated ? ffts.Count : ffts[0].Length; |
| 64 | + |
| 65 | + Bitmap bmp = new(Width, Height, PixelFormat.Format8bppIndexed); |
| 66 | + Colormap.Apply(bmp); |
| 67 | + |
| 68 | + Rectangle lockRect = new(0, 0, Width, Height); |
| 69 | + BitmapData bitmapData = bmp.LockBits(lockRect, ImageLockMode.ReadOnly, bmp.PixelFormat); |
| 70 | + int stride = bitmapData.Stride; |
| 71 | + |
| 72 | + byte[] bytes = new byte[bitmapData.Stride * bmp.Height]; |
| 73 | + Parallel.For(0, Width, col => |
| 74 | + { |
| 75 | + int sourceCol = col; |
| 76 | + if (IsRoll) |
| 77 | + { |
| 78 | + sourceCol += Width - RollOffset % Width; |
| 79 | + if (sourceCol >= Width) |
| 80 | + sourceCol -= Width; |
| 81 | + } |
| 82 | + |
| 83 | + for (int row = 0; row < Height; row++) |
| 84 | + { |
| 85 | + double value = IsRotated ? ffts[row][sourceCol] : ffts[sourceCol][row]; |
| 86 | + if (IsDecibel) |
| 87 | + value = 20 * Math.Log10(value * DecibelScaleFactor + 1); |
| 88 | + value *= Intensity; |
| 89 | + value = Math.Min(value, 255); |
| 90 | + int bytePosition = (Height - 1 - row) * stride + col; |
| 91 | + bytes[bytePosition] = (byte)value; |
| 92 | + } |
| 93 | + }); |
| 94 | + |
| 95 | + Marshal.Copy(bytes, 0, bitmapData.Scan0, bytes.Length); |
| 96 | + bmp.UnlockBits(bitmapData); |
| 97 | + |
| 98 | + return bmp; |
| 99 | + } |
| 100 | + } |
| 101 | +} |
0 commit comments