/// <summary> /// /// </summary> /// <param name="ae">an acoustic event</param> /// <param name="dbArray">The sequence of frame dB over the event</param> /// <returns></returns> public static System.Tuple <double, double> KiwiPeakAnalysis(AcousticEvent ae, double[] dbArray) { //dbArray = DataTools.filterMovingAverage(dbArray, 3); bool[] peaks = DataTools.GetPeaks(dbArray); //locate the peaks double[] peakValues = new double[dbArray.Length]; for (int i = 0; i < dbArray.Length; i++) { if (peaks[i]) { peakValues[i] = dbArray[i]; } } //take the top N peaks int N = 5; double[] topNValues = new double[N]; for (int p = 0; p < N; p++) { int maxID = DataTools.GetMaxIndex(peakValues); topNValues[p] = peakValues[maxID]; peakValues[maxID] = 0.0; } //PROCESS PEAK DECIBELS double avPeakDB, sdPeakDB; NormalDist.AverageAndSD(topNValues, out avPeakDB, out sdPeakDB); return(System.Tuple.Create(avPeakDB, sdPeakDB)); }
public void TestFreqScaleOnArtificialSignal1() { int sampleRate = 22050; double duration = 20; // signal duration in seconds int[] harmonics = { 500, 1000, 2000, 4000, 8000 }; int windowSize = 512; var freqScale = new FrequencyScale(sampleRate / 2, windowSize, 1000); var outputImagePath = Path.Combine(this.outputDirectory.FullName, "Signal1_LinearFreqScale.png"); var recording = DspFilters.GenerateTestRecording(sampleRate, duration, harmonics, WaveType.Cosine); var sonoConfig = new SonogramConfig { WindowSize = freqScale.WindowSize, WindowOverlap = 0.0, SourceFName = "Signal1", NoiseReductionType = NoiseReductionType.Standard, NoiseReductionParameter = 0.12, }; var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); // pick a row, any row var oneSpectrum = MatrixTools.GetRow(sonogram.Data, 40); oneSpectrum = DataTools.filterMovingAverage(oneSpectrum, 5); var peaks = DataTools.GetPeaks(oneSpectrum); for (int i = 5; i < peaks.Length - 5; i++) { if (peaks[i]) { LoggedConsole.WriteLine($"bin ={freqScale.BinBounds[i, 0]}, Herz={freqScale.BinBounds[i, 1]}-{freqScale.BinBounds[i + 1, 1]} "); } } foreach (int h in harmonics) { LoggedConsole.WriteLine($"Harmonic {h}Herz should be in bin {freqScale.GetBinIdForHerzValue(h)}"); } // spectrogram without framing, annotation etc var image = sonogram.GetImage(); string title = $"Spectrogram of Harmonics: {DataTools.Array2String(harmonics)} SR={sampleRate} Window={windowSize}"; image = sonogram.GetImageFullyAnnotated(image, title, freqScale.GridLineLocations); image.Save(outputImagePath); // Check that image dimensions are correct Assert.AreEqual(861, image.Width); Assert.AreEqual(310, image.Height); Assert.IsTrue(peaks[11]); Assert.IsTrue(peaks[22]); Assert.IsTrue(peaks[45]); Assert.IsTrue(peaks[92]); Assert.IsTrue(peaks[185]); }
public static void TestMethod_GenerateSignal1() { int sampleRate = 22050; double duration = 20; // signal duration in seconds int[] harmonics = { 500, 1000, 2000, 4000, 8000 }; int windowSize = 512; var freqScale = new FrequencyScale(sampleRate / 2, windowSize, 1000); string path = @"C:\SensorNetworks\Output\Sonograms\UnitTestSonograms\SineSignal1.png"; var recording = GenerateTestRecording(sampleRate, duration, harmonics, WaveType.Cosine); var sonoConfig = new SonogramConfig { WindowSize = freqScale.WindowSize, WindowOverlap = 0.0, SourceFName = "Signal1", NoiseReductionType = NoiseReductionType.Standard, NoiseReductionParameter = 0.12, }; var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); // pick a row, any row var oneSpectrum = MatrixTools.GetRow(sonogram.Data, 40); oneSpectrum = DataTools.normalise(oneSpectrum); var peaks = DataTools.GetPeaks(oneSpectrum, 0.5); for (int i = 2; i < peaks.Length - 2; i++) { if (peaks[i]) { LoggedConsole.WriteLine($"bin ={freqScale.BinBounds[i, 0]}, Herz={freqScale.BinBounds[i, 1]}-{freqScale.BinBounds[i + 1, 1]} "); } } if (peaks[11] && peaks[22] && peaks[45] && peaks[92] && peaks[185]) { LoggedConsole.WriteSuccessLine("Spectral Peaks found at correct places"); } else { LoggedConsole.WriteErrorLine("Spectral Peaks found at INCORRECT places"); } foreach (int h in harmonics) { LoggedConsole.WriteLine($"Harmonic {h}Herz should be in bin {freqScale.GetBinIdForHerzValue(h)}"); } // spectrogram without framing, annotation etc var image = sonogram.GetImage(); string title = $"Spectrogram of Harmonics: {DataTools.Array2String(harmonics)} SR={sampleRate} Window={windowSize}"; image = sonogram.GetImageFullyAnnotated(image, title, freqScale.GridLineLocations); image.Save(path); }
public static double CalculateSpikeIndex(double[] envelope, double spikeThreshold) { int length = envelope.Length; // int isolatedSpikeCount = 0; double peakIntenisty = 0.0; double spikeIntensity = 0.0; var peaks = DataTools.GetPeaks(envelope); int peakCount = 0; for (int i = 1; i < length - 1; i++) { if (!peaks[i]) { continue; //count spikes } peakCount++; double diffMinus1 = Math.Abs(envelope[i] - envelope[i - 1]); double diffPlus1 = Math.Abs(envelope[i] - envelope[i + 1]); double avDifference = (diffMinus1 + diffPlus1) / 2; peakIntenisty += avDifference; if (avDifference > spikeThreshold) { //isolatedSpikeCount++; // count isolated spikes spikeIntensity += avDifference; } } if (peakCount == 0) { return(0.0); } return(spikeIntensity / peakIntenisty); } //CalculateSpikeIndex()
/// <summary> /// THE KEY ANALYSIS METHOD /// </summary> /// <param name="recording"> /// The segment Of Source File. /// </param> /// <param name="configDict"> /// The config Dict. /// </param> /// <param name="value"></param> /// <returns> /// The <see cref="LimnodynastesConvexResults"/>. /// </returns> internal static LimnodynastesConvexResults Analysis( Dictionary <string, double[, ]> dictionaryOfHiResSpectralIndices, AudioRecording recording, Dictionary <string, string> configDict, AnalysisSettings analysisSettings, SegmentSettingsBase segmentSettings) { // for Limnodynastes convex, in the D.Stewart CD, there are peaks close to: //1. 1950 Hz //2. 1460 hz //3. 970 hz These are 490 Hz apart. // for Limnodynastes convex, in the JCU recording, there are peaks close to: //1. 1780 Hz //2. 1330 hz //3. 880 hz These are 450 Hz apart. // So strategy is to look for three peaks separated by same amount and in the vicinity of the above, // starting with highest power (the top peak) and working down to lowest power (bottom peak). var outputDir = segmentSettings.SegmentOutputDirectory; TimeSpan segmentStartOffset = segmentSettings.SegmentStartOffset; //KeyValuePair<string, double[,]> kvp = dictionaryOfHiResSpectralIndices.First(); var spg = dictionaryOfHiResSpectralIndices["RHZ"]; int rhzRowCount = spg.GetLength(0); int rhzColCount = spg.GetLength(1); int sampleRate = recording.SampleRate; double herzPerBin = sampleRate / 2 / (double)rhzRowCount; double scoreThreshold = (double?)double.Parse(configDict["EventThreshold"]) ?? 3.0; int minimumFrequency = (int?)int.Parse(configDict["MinHz"]) ?? 850; int dominantFrequency = (int?)int.Parse(configDict["DominantFrequency"]) ?? 1850; // # The Limnodynastes call has three major peaks. The dominant peak is at 1850 or as set above. // # The second and third peak are at equal gaps below. DominantFreq-gap and DominantFreq-(2*gap); // # Set the gap in the Config file. Should typically be in range 880 to 970 int peakGapInHerz = (int?)int.Parse(configDict["PeakGap"]) ?? 470; int F1AndF2Gap = (int)Math.Round(peakGapInHerz / herzPerBin); //int F1AndF2Gap = 10; // 10 = number of freq bins int F1AndF3Gap = 2 * F1AndF2Gap; //int F1AndF3Gap = 20; int hzBuffer = 250; int bottomBin = 5; int dominantBin = (int)Math.Round(dominantFrequency / herzPerBin); int binBuffer = (int)Math.Round(hzBuffer / herzPerBin);; int dominantBinMin = dominantBin - binBuffer; int dominantBinMax = dominantBin + binBuffer; // freqBin + rowID = binCount - 1; // therefore: rowID = binCount - freqBin - 1; int minRowID = rhzRowCount - dominantBinMax - 1; int maxRowID = rhzRowCount - dominantBinMin - 1; int bottomRow = rhzRowCount - bottomBin - 1; var list = new List <Point>(); // loop through all spectra/columns of the hi-res spectrogram. for (int c = 1; c < rhzColCount - 1; c++) { double maxAmplitude = -double.MaxValue; int idOfRowWithMaxAmplitude = 0; for (int r = minRowID; r <= bottomRow; r++) { if (spg[r, c] > maxAmplitude) { maxAmplitude = spg[r, c]; idOfRowWithMaxAmplitude = r; } } if (idOfRowWithMaxAmplitude < minRowID) { continue; } if (idOfRowWithMaxAmplitude > maxRowID) { continue; } // want a spectral peak. if (spg[idOfRowWithMaxAmplitude, c] < spg[idOfRowWithMaxAmplitude, c - 1]) { continue; } if (spg[idOfRowWithMaxAmplitude, c] < spg[idOfRowWithMaxAmplitude, c + 1]) { continue; } // peak should exceed thresold amplitude if (spg[idOfRowWithMaxAmplitude, c] < 3.0) { continue; } // convert row ID to freq bin ID int freqBinID = rhzRowCount - idOfRowWithMaxAmplitude - 1; list.Add(new Point(c, freqBinID)); // we now have a list of potential hits for LimCon. This needs to be filtered. // Console.WriteLine("Col {0}, Bin {1} ", c, freqBinID); } // DEBUG ONLY // ################################ TEMPORARY ################################ // superimpose point on RHZ HiRes spectrogram for debug purposes bool drawOnHiResSpectrogram = true; //string filePath = @"G:\SensorNetworks\Output\Frogs\TestOfHiResIndices-2016July\Test\Towsey.HiResIndices\SpectrogramImages\3mile_creek_dam_-_Herveys_Range_1076_248366_20130305_001700_30_0min.CombinedGreyScale.png"; var fileName = Path.GetFileNameWithoutExtension(segmentSettings.SegmentAudioFile.Name); string filePath = outputDir.FullName + @"\SpectrogramImages\" + fileName + ".CombinedGreyScale.png"; var debugImage = new FileInfo(filePath); if (!debugImage.Exists) { drawOnHiResSpectrogram = false; } if (drawOnHiResSpectrogram) { // put red dot where max is Bitmap bmp = new Bitmap(filePath); foreach (Point point in list) { bmp.SetPixel(point.X + 70, 1911 - point.Y, Color.Red); } // mark off every tenth frequency bin for (int r = 0; r < 26; r++) { bmp.SetPixel(68, 1911 - (r * 10), Color.Blue); bmp.SetPixel(69, 1911 - (r * 10), Color.Blue); } // mark off upper bound and lower frequency bound bmp.SetPixel(69, 1911 - dominantBinMin, Color.Lime); bmp.SetPixel(69, 1911 - dominantBinMax, Color.Lime); //bmp.SetPixel(69, 1911 - maxRowID, Color.Lime); string opFilePath = outputDir.FullName + @"\SpectrogramImages\" + fileName + ".CombinedGreyScaleAnnotated.png"; bmp.Save(opFilePath); } // END DEBUG ################################ TEMPORARY ################################ // now construct the standard decibel spectrogram WITHOUT noise removal, and look for LimConvex // get frame parameters for the analysis double epsilon = Math.Pow(0.5, recording.BitsPerSample - 1); int frameSize = rhzRowCount * 2; int frameStep = frameSize; // this default = zero overlap double frameDurationInSeconds = frameSize / (double)sampleRate; double frameStepInSeconds = frameStep / (double)sampleRate; double framesPerSec = 1 / frameStepInSeconds; //var dspOutput = DSP_Frames.ExtractEnvelopeAndFFTs(recording, frameSize, frameStep); //// Generate deciBel spectrogram //double[,] deciBelSpectrogram = MFCCStuff.DecibelSpectra(dspOutput.amplitudeSpectrogram, dspOutput.WindowPower, sampleRate, epsilon); // i: Init SONOGRAM config var sonoConfig = new SonogramConfig { SourceFName = recording.BaseName, WindowSize = frameSize, WindowOverlap = 0.0, NoiseReductionType = NoiseReductionType.None, }; // init sonogram BaseSonogram sonogram = new SpectrogramStandard(sonoConfig, recording.WavReader); // remove the DC row of the spectrogram sonogram.Data = MatrixTools.Submatrix(sonogram.Data, 0, 1, sonogram.Data.GetLength(0) - 1, sonogram.Data.GetLength(1) - 1); //scores.Add(new Plot("Decibels", DataTools.NormaliseMatrixValues(dBArray), ActivityAndCover.DefaultActivityThresholdDb)); //scores.Add(new Plot("Active Frames", DataTools.Bool2Binary(activity.activeFrames), 0.0)); // convert spectral peaks to frequency //var tuple_DecibelPeaks = SpectrogramTools.HistogramOfSpectralPeaks(deciBelSpectrogram); //int[] peaksBins = tuple_DecibelPeaks.Item2; //double[] freqPeaks = new double[peaksBins.Length]; //int binCount = sonogram.Data.GetLength(1); //for (int i = 1; i < peaksBins.Length; i++) freqPeaks[i] = (lowerBinBound + peaksBins[i]) / (double)nyquistBin; //scores.Add(new Plot("Max Frequency", freqPeaks, 0.0)); // location of peaks for spectral images // create new list of LimCon hits in the standard spectrogram. double timeSpanOfFrameInSeconds = frameSize / (double)sampleRate; var newList = new List <int[]>(); int lastFrameID = sonogram.Data.GetLength(0) - 1; int lastBinID = sonogram.Data.GetLength(1) - 1; foreach (Point point in list) { double secondsFromStartOfSegment = (point.X * 0.1) + 0.05; // convert point.Y to center of time-block. int framesFromStartOfSegment = (int)Math.Round(secondsFromStartOfSegment / timeSpanOfFrameInSeconds); // location of max point is uncertain, so search in neighbourhood. // NOTE: sonogram.data matrix is time*freqBin double maxValue = -double.MaxValue; int idOfTMax = framesFromStartOfSegment; int idOfFMax = point.Y; for (int deltaT = -4; deltaT <= 4; deltaT++) { for (int deltaF = -1; deltaF <= 1; deltaF++) { int newT = framesFromStartOfSegment + deltaT; if (newT < 0) { newT = 0; } else if (newT > lastFrameID) { newT = lastFrameID; } double value = sonogram.Data[newT, point.Y + deltaF]; if (value > maxValue) { maxValue = value; idOfTMax = framesFromStartOfSegment + deltaT; idOfFMax = point.Y + deltaF; } } } // newList.Add(new Point(frameSpan, point.Y)); int[] array = new int[2]; array[0] = idOfTMax; array[1] = idOfFMax; newList.Add(array); } // Now obtain more of spectrogram to see if have peaks at two other places characteristic of Limnodynastes convex. // In the D.Stewart CD, there are peaks close to: //1. 1950 Hz //2. 1460 hz //3. 970 hz These are 490 Hz apart. // For Limnodynastes convex, in the JCU recording, there are peaks close to: //1. 1780 Hz //2. 1330 hz //3. 880 hz These are 450 Hz apart. // So strategy is to look for three peaks separated by same amount and in the vicinity of the above, // starting with highest power (the top peak) and working down to lowest power (bottom peak). //We have found top/highest peak - now find the other two. int secondDominantFrequency = 1380; int secondDominantBin = (int)Math.Round(secondDominantFrequency / herzPerBin); int thirdDominantFrequency = 900; int thirdDominantBin = (int)Math.Round(thirdDominantFrequency / herzPerBin); var acousticEvents = new List <AcousticEvent>(); int Tbuffer = 2; // First extract a sub-matrix. foreach (int[] array in newList) { // NOTE: sonogram.data matrix is time*freqBin int Tframe = array[0]; int F1bin = array[1]; double[,] subMatrix = MatrixTools.Submatrix(sonogram.Data, Tframe - Tbuffer, 0, Tframe + Tbuffer, F1bin); double F1power = subMatrix[Tbuffer, F1bin]; // convert to vector var spectrum = MatrixTools.GetColumnAverages(subMatrix); // use the following code to get estimate of background noise double[,] powerMatrix = MatrixTools.Submatrix(sonogram.Data, Tframe - 3, 10, Tframe + 3, F1bin); double averagePower = (MatrixTools.GetRowAverages(powerMatrix)).Average(); double score = F1power - averagePower; // debug - checking what the spectrum looks like. //for (int i = 0; i < 18; i++) // spectrum[i] = -100.0; //DataTools.writeBarGraph(spectrum); // locate the peaks in lower frequency bands, F2 and F3 bool[] peaks = DataTools.GetPeaks(spectrum); int F2bin = 0; double F2power = -200.0; // dB for (int i = -3; i <= 2; i++) { int bin = F1bin - F1AndF2Gap + i; if ((peaks[bin]) && (F2power < subMatrix[1, bin])) { F2bin = bin; F2power = subMatrix[1, bin]; } } if (F2bin == 0) { continue; } if (F2power == -200.0) { continue; } score += (F2power - averagePower); int F3bin = 0; double F3power = -200.0; for (int i = -5; i <= 2; i++) { int bin = F1bin - F1AndF3Gap + i; if ((peaks[bin]) && (F3power < subMatrix[1, bin])) { F3bin = bin; F3power = subMatrix[1, bin]; } } if (F3bin == 0) { continue; } if (F3power == -200.0) { continue; } score += (F3power - averagePower); score /= 3; // ignore events where SNR < decibel threshold if (score < scoreThreshold) { continue; } // ignore events with wrong power distribution. A good LimnoConvex call has strongest F1 power if ((F3power > F1power) || (F2power > F1power)) { continue; } //freq Bin ID must be converted back to Matrix row ID // freqBin + rowID = binCount - 1; // therefore: rowID = binCount - freqBin - 1; minRowID = rhzRowCount - F1bin - 2; maxRowID = rhzRowCount - F3bin - 1; int F1RowID = rhzRowCount - F1bin - 1; int F2RowID = rhzRowCount - F2bin - 1; int F3RowID = rhzRowCount - F3bin - 1; int maxfreq = dominantFrequency + hzBuffer; int topBin = (int)Math.Round(maxfreq / herzPerBin); int frameCount = 4; double duration = frameCount * frameStepInSeconds; double startTimeWrtSegment = (Tframe - 2) * frameStepInSeconds; // Got to here so start initialising an acoustic event var ae = new AcousticEvent(segmentStartOffset, startTimeWrtSegment, duration, minimumFrequency, maxfreq); ae.SetTimeAndFreqScales(framesPerSec, herzPerBin); //var ae = new AcousticEvent(oblong, recording.Nyquist, binCount, frameDurationInSeconds, frameStepInSeconds, frameCount); //ae.StartOffset = TimeSpan.FromSeconds(Tframe * frameStepInSeconds); var pointF1 = new Point(2, topBin - F1bin); var pointF2 = new Point(2, topBin - F2bin); var pointF3 = new Point(2, topBin - F3bin); ae.Points = new List <Point>(); ae.Points.Add(pointF1); ae.Points.Add(pointF2); ae.Points.Add(pointF3); //tried using HitElements but did not do what I wanted later on. //ae.HitElements = new HashSet<Point>(); //ae.HitElements = new SortedSet<Point>(); //ae.HitElements.Add(pointF1); //ae.HitElements.Add(pointF2); //ae.HitElements.Add(pointF3); ae.Score = score; //ae.MinFreq = Math.Round((topBin - F3bin - 5) * herzPerBin); //ae.MaxFreq = Math.Round(topBin * herzPerBin); acousticEvents.Add(ae); } // now add in extra common info to the acoustic events acousticEvents.ForEach(ae => { ae.SpeciesName = configDict[AnalysisKeys.SpeciesName]; ae.SegmentStartSeconds = segmentStartOffset.TotalSeconds; ae.SegmentDurationSeconds = recording.Duration.TotalSeconds; ae.Name = abbreviatedName; ae.BorderColour = Color.Red; ae.FileName = recording.BaseName; }); double[] scores = new double[rhzColCount]; // predefinition of score array double nomalisationConstant = scoreThreshold * 4; // four times the score threshold double compressionFactor = rhzColCount / (double)sonogram.Data.GetLength(0); foreach (AcousticEvent ae in acousticEvents) { ae.ScoreNormalised = ae.Score / nomalisationConstant; if (ae.ScoreNormalised > 1.0) { ae.ScoreNormalised = 1.0; } int frameID = (int)Math.Round(ae.EventStartSeconds / frameDurationInSeconds); int hiresFrameID = (int)Math.Floor(frameID * compressionFactor); scores[hiresFrameID] = ae.ScoreNormalised; } var plot = new Plot(AnalysisName, scores, scoreThreshold); // DEBUG ONLY ################################ TEMPORARY ################################ // Draw a standard spectrogram and mark of hites etc. bool createStandardDebugSpectrogram = true; var imageDir = new DirectoryInfo(outputDir.FullName + @"\SpectrogramImages"); if (!imageDir.Exists) { imageDir.Create(); } if (createStandardDebugSpectrogram) { var fileName2 = Path.GetFileNameWithoutExtension(segmentSettings.SegmentAudioFile.Name); string filePath2 = Path.Combine(imageDir.FullName, fileName + ".Spectrogram.png"); Bitmap sonoBmp = (Bitmap)sonogram.GetImage(); int height = sonoBmp.Height; foreach (AcousticEvent ae in acousticEvents) { ae.DrawEvent(sonoBmp); //g.DrawRectangle(pen, ob.ColumnLeft, ob.RowTop, ob.ColWidth-1, ob.RowWidth); //ae.DrawPoint(sonoBmp, ae.HitElements.[0], Color.OrangeRed); //ae.DrawPoint(sonoBmp, ae.HitElements[1], Color.Yellow); //ae.DrawPoint(sonoBmp, ae.HitElements[2], Color.Green); ae.DrawPoint(sonoBmp, ae.Points[0], Color.OrangeRed); ae.DrawPoint(sonoBmp, ae.Points[1], Color.Yellow); ae.DrawPoint(sonoBmp, ae.Points[2], Color.LimeGreen); } // draw the original hits on the standard sonogram foreach (int[] array in newList) { sonoBmp.SetPixel(array[0], height - array[1], Color.Cyan); } // mark off every tenth frequency bin on the standard sonogram for (int r = 0; r < 20; r++) { sonoBmp.SetPixel(0, height - (r * 10) - 1, Color.Blue); sonoBmp.SetPixel(1, height - (r * 10) - 1, Color.Blue); } // mark off upper bound and lower frequency bound sonoBmp.SetPixel(0, height - dominantBinMin, Color.Lime); sonoBmp.SetPixel(0, height - dominantBinMax, Color.Lime); sonoBmp.Save(filePath2); } // END DEBUG ################################ TEMPORARY ################################ return(new LimnodynastesConvexResults { Sonogram = sonogram, Hits = null, Plot = plot, Events = acousticEvents, RecordingDuration = recording.Duration, }); } // Analysis()
public void TestFreqScaleOnArtificialSignal2() { int sampleRate = 64000; double duration = 30; // signal duration in seconds int[] harmonics = { 500, 1000, 2000, 4000, 8000 }; var freqScale = new FrequencyScale(FreqScaleType.Linear125Octaves7Tones28Nyquist32000); var outputImagePath = Path.Combine(this.outputDirectory.FullName, "Signal2_OctaveFreqScale.png"); var recording = DspFilters.GenerateTestRecording(sampleRate, duration, harmonics, WaveType.Cosine); // init the default sonogram config var sonoConfig = new SonogramConfig { WindowSize = freqScale.WindowSize, WindowOverlap = 0.2, SourceFName = "Signal2", NoiseReductionType = NoiseReductionType.None, NoiseReductionParameter = 0.0, }; var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); sonogram.Data = OctaveFreqScale.ConvertAmplitudeSpectrogramToDecibelOctaveScale(sonogram.Data, freqScale); // pick a row, any row var oneSpectrum = MatrixTools.GetRow(sonogram.Data, 40); oneSpectrum = DataTools.filterMovingAverage(oneSpectrum, 5); var peaks = DataTools.GetPeaks(oneSpectrum); var peakIds = new List <int>(); for (int i = 5; i < peaks.Length - 5; i++) { if (peaks[i]) { int peakId = freqScale.BinBounds[i, 0]; peakIds.Add(peakId); LoggedConsole.WriteLine($"Spectral peak located in bin {peakId}, Herz={freqScale.BinBounds[i, 1]}"); } } foreach (int h in harmonics) { LoggedConsole.WriteLine($"Harmonic {h}Herz should be in bin {freqScale.GetBinIdForHerzValue(h)}"); } Assert.AreEqual(5, peakIds.Count); Assert.AreEqual(129, peakIds[0]); Assert.AreEqual(257, peakIds[1]); Assert.AreEqual(513, peakIds[2]); Assert.AreEqual(1025, peakIds[3]); Assert.AreEqual(2049, peakIds[4]); var image = sonogram.GetImage(); string title = $"Spectrogram of Harmonics: {DataTools.Array2String(harmonics)} SR={sampleRate} Window={freqScale.WindowSize}"; image = sonogram.GetImageFullyAnnotated(image, title, freqScale.GridLineLocations); image.Save(outputImagePath); // Check that image dimensions are correct Assert.AreEqual(146, image.Width); Assert.AreEqual(310, image.Height); }
/// <summary> /// New and alternative version of Lconvex recogniser because discovered that the call is more variable than I first realised. /// </summary> internal RecognizerResults Gruntwork2(AudioRecording audioRecording, Config configuration, DirectoryInfo outputDirectory, TimeSpan segmentStartOffset) { // make a spectrogram double noiseReductionParameter = configuration.GetDoubleOrNull(AnalysisKeys.NoiseBgThreshold) ?? 0.1; int frameStep = 512; int sampleRate = audioRecording.SampleRate; double frameStepInSeconds = frameStep / (double)sampleRate; double framesPerSec = 1 / frameStepInSeconds; var config = new SonogramConfig { WindowSize = frameStep, // this default = zero overlap WindowOverlap = 0.0, NoiseReductionType = NoiseReductionType.Standard, NoiseReductionParameter = noiseReductionParameter, }; // now construct the standard decibel spectrogram WITH noise removal, and look for LimConvex // get frame parameters for the analysis var sonogram = (BaseSonogram) new SpectrogramStandard(config, audioRecording.WavReader); // remove the DC column // var spg = MatrixTools.Submatrix(sonogram.Data, 0, 1, sonogram.Data.GetLength(0) - 1, sonogram.Data.GetLength(1) - 1); // sonogram.Data = spg; var spg = sonogram.Data; int rowCount = spg.GetLength(0); int colCount = spg.GetLength(1); double herzPerBin = sampleRate / 2.0 / colCount; string abbreviatedSpeciesName = configuration[AnalysisKeys.AbbreviatedSpeciesName] ?? "<no.sp>"; // ## TWO THRESHOLDS // The threshold dB amplitude in the dominant freq bin required to yield an event double eventThresholdDb = configuration.GetDoubleOrNull("PeakThresholdDecibels") ?? 3.0; // minimum score for an acceptable event - that is when processing the score array. double similarityThreshold = configuration.GetDoubleOrNull(AnalysisKeys.EventThreshold) ?? 0.5; // IMPORTANT: The following frame durations assume a sampling rate = 22050 and window size of 512. int callFrameWidth = 5; int callHalfWidth = callFrameWidth / 2; // minimum number of bins covering frequency bandwidth of L.convex call // call has binWidth=25 but we want zero buffer of four bins either side. int callBinWidth = 25; int binSilenceBuffer = 4; int topFrequency = configuration.GetInt("TopFrequency"); // # The Limnodynastes call has a duration of 3-5 frames given the above settings. // # But we will assume 5-7 because sometimes the three harmonics are not exactly alligned!! // # The call has three major peaks. The top peak, typically the dominant peak, is at approx 1850, a value which is set in the convig. // # The second and third peak are at equal gaps below. TopFreq-gap and TopFreq-(2*gap); // # The gap could be set in the Config file, but this is not implemented yet. // Instead the algorithm uses three pre-fixed templates that determine the different kinds ogap. Gap is typically close to 500Hz // In the D.Stewart CD, there are peaks close to: //1. 1950 Hz //2. 1460 hz //3. 970 hz These are 490 Hz apart. // In the Kiyomi's JCU recording, there are peaks close to: //1. 1780 Hz //2. 1330 hz //3. 880 hz These are 450 Hz apart. // So the strategy is to look for three peaks separated by same amount and in the vicinity of the above, // To this end we produce three templates each of length 36, but having 2nd and 3rd peaks at different intervals. var templates = GetLconvexTemplates(callBinWidth, binSilenceBuffer); int templateHeight = templates[0].Length; // NOTE: could give user control over other call features // Such as frequency gap between peaks. But not in this first iteration of the recognizer. //int peakGapInHerz = (int)configuration["PeakGap"]; int searchBand = 8; int topBin = (int)Math.Round(topFrequency / herzPerBin); int bottomBin = topBin - templateHeight - searchBand + 1; if (bottomBin < 0) { Log.Fatal("Template bandwidth exceeds availble bandwidth given your value for top frequency."); } spg = MatrixTools.Submatrix(spg, 0, bottomBin, sonogram.Data.GetLength(0) - 1, topBin); double[,] frames = MatrixTools.Submatrix(spg, 0, 0, callFrameWidth - 1, spg.GetLength(1) - 1); double[] spectrum = MatrixTools.GetColumnSums(frames); // set up arrays for monitoring important event parameters double[] decibels = new double[rowCount]; int[] bottomBins = new int[rowCount]; double[] scores = new double[rowCount]; // predefinition of score array int[] templateIds = new int[rowCount]; double[,] hits = new double[rowCount, colCount]; // loop through all spectra/rows of the spectrogram - NB: spg is rotated to vertical. for (int s = callFrameWidth; s < rowCount; s++) { double[] rowToRemove = MatrixTools.GetRow(spg, s - callFrameWidth); double[] rowToAdd = MatrixTools.GetRow(spg, s); // shift frame block to the right. for (int b = 0; b < spectrum.Length; b++) { spectrum[b] = spectrum[b] - rowToRemove[b] + rowToAdd[b]; } // now check if frame block matches a template. ScanEventScores(spectrum, templates, out double eventScore, out int eventBottomBin, out int templateId); //hits[rowCount, colCount]; decibels[s - callHalfWidth - 1] = spectrum.Max() / callFrameWidth; bottomBins[s - callHalfWidth - 1] = eventBottomBin + bottomBin; scores[s - callHalfWidth - 1] = eventScore; templateIds[s - callHalfWidth - 1] = templateId; } // loop through all spectra // we now have a score array and decibel array and bottom bin array for the entire spectrogram. // smooth them to find events scores = DataTools.filterMovingAverageOdd(scores, 5); decibels = DataTools.filterMovingAverageOdd(decibels, 3); var peaks = DataTools.GetPeaks(scores); // loop through the score array and find potential events var potentialEvents = new List <AcousticEvent>(); for (int s = callHalfWidth; s < scores.Length - callHalfWidth - 1; s++) { if (!peaks[s]) { continue; } if (scores[s] < similarityThreshold) { continue; } if (decibels[s] < eventThresholdDb) { continue; } // put hits into hits matrix // put cosine score into the score array //for (int s = point.X; s <= point.Y; s++) //{ // hits[s, topBins[s]] = 10; //} int bottomBinForEvent = bottomBins[s]; int topBinForEvent = bottomBinForEvent + templateHeight; int topFreqForEvent = (int)Math.Round(topBinForEvent * herzPerBin); int bottomFreqForEvent = (int)Math.Round(bottomBinForEvent * herzPerBin); double startTime = (s - callHalfWidth) * frameStepInSeconds; double durationTime = callFrameWidth * frameStepInSeconds; var newEvent = new AcousticEvent(segmentStartOffset, startTime, durationTime, bottomFreqForEvent, topFreqForEvent) { //Name = string.Empty, // remove name because it hides spectral content of the event. Name = "Lc" + templateIds[s], Score = scores[s], }; newEvent.SetTimeAndFreqScales(framesPerSec, herzPerBin); potentialEvents.Add(newEvent); } // display the original score array scores = DataTools.normalise(scores); var scorePlot = new Plot(this.DisplayName + " scores", scores, similarityThreshold); DataTools.Normalise(decibels, eventThresholdDb, out double[] normalisedDb, out double normalisedThreshold); var decibelPlot = new Plot("Decibels", normalisedDb, normalisedThreshold); var debugPlots = new List <Plot> { scorePlot, decibelPlot }; if (this.displayDebugImage) { var debugImage = DisplayDebugImage(sonogram, potentialEvents, debugPlots, hits); var debugPath = outputDirectory.Combine(FilenameHelpers.AnalysisResultName(Path.GetFileNameWithoutExtension(audioRecording.BaseName), this.Identifier, "png", "DebugSpectrogram")); debugImage.Save(debugPath.FullName); } // display the cosine similarity scores var plot = new Plot(this.DisplayName, scores, similarityThreshold); var plots = new List <Plot> { plot }; // add names into the returned events string speciesName = configuration[AnalysisKeys.SpeciesName] ?? this.SpeciesName; foreach (var ae in potentialEvents) { ae.Name = abbreviatedSpeciesName; ae.SpeciesName = speciesName; } return(new RecognizerResults() { Events = potentialEvents, Hits = hits, Plots = plots, Sonogram = sonogram, }); }
/// <summary> /// Counts the number of spectral tracks or harmonics in the passed ferquency band. /// Also calculates the average amplitude of the peaks to each succeeding trough. /// </summary> /// <param name="values">Spectral values in the frequency band.</param> /// <param name="row">This argument is NOT used. Is included only for debugging purposes.</param> public static Tuple <double, int, bool[]> CountHarmonicTracks(double[] values, int expectedHarmonicCount) { int L = values.Length; int expectedPeriod = L / expectedHarmonicCount; int midPeriod = expectedPeriod / 2; //double[] smooth = DataTools.filterMovingAverage(values, 3); double[] smooth = values; bool[] peaks = DataTools.GetPeaks(smooth); int peakCount = DataTools.CountTrues(peaks); //return if too far outside limits int lowerLimit = expectedHarmonicCount / 2; int upperLimit = expectedHarmonicCount * 2; if (peakCount <= lowerLimit) { return(Tuple.Create(0.0, 0, peaks)); } else if (peakCount >= upperLimit) { return(Tuple.Create(0.0, peakCount, peaks)); } // Store peak locations. var peakLocations = new List <int>(); for (int i = 0; i < values.Length; i++) { if (peaks[i]) { peakLocations.Add(i); } } //// If have too many peaks (local maxima), remove the lowest of them //if (peakCount > (expectedHarmonicCount + 1)) //{ // var peakValues = new double[peakCount]; // for (int i = 0; i < peakCount; i++) peakValues[i] = values[peakLocations[i]]; // IEnumerable<double> ordered = peakValues.OrderByDescending(d => d); // double avValue = ordered.Take(expectedHarmonicCount).Average(); // double min = ordered.Last(); // double threshold = min + ((avValue - min) / 2); // // apply threshold to remove low peaks // for (int i = 0; i < L; i++) // { // if ((peaks[i]) && (values[i] < threshold)) peaks[i] = false; // } // // recalculate the number of peaks // peakCount = -1; // for (int i = 0; i < L; i++) // { // if (peaks[i]) // { // peakCount++; // peakLocations[peakCount] = i; // } // } //} //if (peakCount <= 1) return Tuple.Create(0.0, 0, peaks); double amplitude = 0.0; for (int i = 0; i < peakLocations.Count; i++) { int troughIndex = peakLocations[i] + midPeriod; if (troughIndex >= L) { troughIndex = peakLocations[i] - midPeriod; } double delta = smooth[peakLocations[i]] - smooth[troughIndex]; if (delta > 1.0) { amplitude += delta; // dB threshold - required a minimum perceptible difference } } double avAmplitude = amplitude / peakCount; return(Tuple.Create(avAmplitude, peakCount, peaks)); }
} //DetectBarsInTheRowsOfaMatrix() /// A METHOD TO DETECT HARMONICS IN THE ROWS of the passed portion of a sonogram. /// This method assume the matrix is derived from a spectrogram rotated so that the matrix rows are spectral columns of sonogram. /// Was first developed for crow calls. /// First looks for a decibel profile that matches the passed call duration and decibel loudness /// Then samples the centre portion for the correct harmonic period. /// </summary> /// <param name="m"></param> /// <param name="amplitudeThreshold"></param> /// <returns></returns> public static Tuple <double[], double[], double[]> DetectHarmonicsInSonogramMatrix(double[,] m, double dBThreshold, int callSpan) { int zeroBinCount = 3; //to remove low freq content which dominates the spectrum int halfspan = callSpan / 2; double[] dBArray = MatrixTools.GetRowAverages(m); dBArray = DataTools.filterMovingAverage(dBArray, 3); bool doNoiseRemoval = true; if (doNoiseRemoval) { double StandardDeviationCount = 0.1; // number of noise SDs to calculate noise threshold - determines severity of noise reduction SNR.BackgroundNoise bgn = SNR.SubtractBackgroundNoiseFromSignal(dBArray, StandardDeviationCount); dBArray = bgn.NoiseReducedSignal; } bool[] peaks = DataTools.GetPeaks(dBArray); int rowCount = m.GetLength(0); int colCount = m.GetLength(1); var intensity = new double[rowCount]; //an array of period intensity var periodicity = new double[rowCount]; //an array of the periodicity values for (int r = halfspan; r < rowCount - halfspan; r++) { //APPLY A FILTER: must satisfy the following conditions for a call. if (!peaks[r]) { continue; } if (dBArray[r] < dBThreshold) { continue; } double lowerDiff = dBArray[r] - dBArray[r - halfspan]; double upperDiff = dBArray[r] - dBArray[r + halfspan]; if (lowerDiff < dBThreshold || upperDiff < dBThreshold) { continue; } double[] prevRow = DataTools.DiffFromMean(MatrixTools.GetRow(m, r - 1)); double[] thisRow = DataTools.DiffFromMean(MatrixTools.GetRow(m, r)); var spectrum = AutoAndCrossCorrelation.CrossCorr(prevRow, thisRow); for (int s = 0; s < zeroBinCount; s++) { spectrum[s] = 0.0; //in real data these bins are dominant and hide other frequency content } spectrum = DataTools.NormaliseArea(spectrum); int maxId = DataTools.GetMaxIndex(spectrum); double intensityValue = spectrum[maxId]; intensity[r] = intensityValue; double period = 0.0; if (maxId != 0) { period = 2 * colCount / (double)maxId; } periodicity[r] = period; prevRow = thisRow; } // rows return(Tuple.Create(dBArray, intensity, periodicity)); } //DetectHarmonicsInSonogramMatrix()
public static void TestMethod_GenerateSignal2() { int sampleRate = 64000; double duration = 30; // signal duration in seconds int[] harmonics = { 500, 1000, 2000, 4000, 8000 }; var freqScale = new FrequencyScale(FreqScaleType.Linear125Octaves7Tones28Nyquist32000); string path = @"C:\SensorNetworks\Output\Sonograms\UnitTestSonograms\SineSignal2.png"; var recording = GenerateTestRecording(sampleRate, duration, harmonics, WaveType.Cosine); // init the default sonogram config var sonoConfig = new SonogramConfig { WindowSize = freqScale.WindowSize, WindowOverlap = 0.2, SourceFName = "Signal2", NoiseReductionType = NoiseReductionType.None, NoiseReductionParameter = 0.0, }; var sonogram = new AmplitudeSonogram(sonoConfig, recording.WavReader); sonogram.Data = OctaveFreqScale.ConvertAmplitudeSpectrogramToDecibelOctaveScale(sonogram.Data, freqScale); // pick a row, any row var oneSpectrum = MatrixTools.GetRow(sonogram.Data, 40); oneSpectrum = DataTools.normalise(oneSpectrum); var peaks = DataTools.GetPeaks(oneSpectrum, 0.5); var peakIds = new List <int>(); for (int i = 5; i < peaks.Length - 5; i++) { if (peaks[i]) { int peakId = freqScale.BinBounds[i, 0]; peakIds.Add(peakId); LoggedConsole.WriteLine($"Spectral peak located in bin {peakId}, Herz={freqScale.BinBounds[i, 1]}"); } } //if (peaks[129] && peaks[257] && peaks[513] && peaks[1025] && peaks[2049]) if (peakIds[0] == 129 && peakIds[1] == 257 && peakIds[2] == 513 && peakIds[3] == 1025 && peakIds[4] == 2049) { LoggedConsole.WriteSuccessLine("Spectral Peaks found at correct places"); } else { LoggedConsole.WriteErrorLine("Spectral Peaks found at INCORRECT places"); } foreach (int h in harmonics) { LoggedConsole.WriteLine($"Harmonic {h}Hertz should be in bin {freqScale.GetBinIdForHerzValue(h)}"); } // spectrogram without framing, annotation etc var image = sonogram.GetImage(); string title = $"Spectrogram of Harmonics: {DataTools.Array2String(harmonics)} SR={sampleRate} Window={freqScale.WindowSize}"; image = sonogram.GetImageFullyAnnotated(image, title, freqScale.GridLineLocations); image.Save(path); }
} // end method ConvertODScores2Events() /* * public static double PeakEntropy(double[] array) * { * bool[] peaks = DataTools.GetPeaks(array); * int peakCount = DataTools.CountTrues(peaks); * //set up histogram of peak energies * double[] histogram = new double[peakCount]; * int count = 0; * for (int k = 0; k < array.Length; k++) * { * if (peaks[k]) * { * histogram[count] = array[k]; * count++; * } * } * histogram = DataTools.NormaliseMatrixValues(histogram); * histogram = DataTools.Normalise2Probabilites(histogram); * double normFactor = Math.Log(histogram.Length) / DataTools.ln2; //normalize for length of the array * double entropy = DataTools.Entropy(histogram) / normFactor; * return entropy; * } * */ /// <summary> /// returns the periodicity in an array of values. /// </summary> public static double[] PeriodicityAnalysis(double[] array) { //DataTools.writeBarGraph(array); var A = AutoAndCrossCorrelation.MyCrossCorrelation(array, array); // do 2/3rds of maximum possible lag int dctLength = A.Length; A = DataTools.SubtractMean(A); //DataTools.writeBarGraph(A); double[,] cosines = MFCCStuff.Cosines(dctLength, dctLength); //set up the cosine coefficients double[] dct = MFCCStuff.DCT(A, cosines); for (int i = 0; i < dctLength; i++) { dct[i] = Math.Abs(dct[i]); //convert to absolute values } //DataTools.writeBarGraph(dct); for (int i = 0; i < 3; i++) { dct[i] = 0.0; //remove low freq oscillations from consideration } dct = DataTools.normalise2UnitLength(dct); var peaks = DataTools.GetPeaks(dct); // remove non-peak values and low values for (int i = 0; i < dctLength; i++) { if (!peaks[i] || dct[i] < 0.2) { dct[i] = 0.0; } } DataTools.writeBarGraph(dct); //get periodicity of highest three values int peakCount = 3; var period = new double[peakCount]; var maxIndex = new double[peakCount]; for (int i = 0; i < peakCount; i++) { int indexOfMaxValue = DataTools.GetMaxIndex(dct); maxIndex[i] = indexOfMaxValue; //double oscilFreq = indexOfMaxValue / dctDuration * 0.5; //Times 0.5 because index = Pi and not 2Pi if ((double)indexOfMaxValue == 0) { period[i] = 0.0; } else { period[i] = dctLength / (double)indexOfMaxValue * 2; } dct[indexOfMaxValue] = 0.0; // remove value for next iteration } LoggedConsole.WriteLine("Max indices = {0:f0}, {1:f0}, {2:f0}.", maxIndex[0], maxIndex[1], maxIndex[2]); return(period); }