diff --git a/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs b/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs index a18ad1209..6ef6a49eb 100644 --- a/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs +++ b/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs @@ -103,14 +103,14 @@ public override RecognizerResults Recognize( // 1: Filter the events for duration in seconds var minimumEventDuration = 0.1; var maximumEventDuration = 0.4; - combinedResults.NewEvents = EventExtentions.FilterOnDuration(combinedResults.NewEvents, minimumEventDuration, maximumEventDuration); + combinedResults.NewEvents = EventFilters.FilterOnDuration(combinedResults.NewEvents, minimumEventDuration, maximumEventDuration); PipitLog.Debug($"Event count after filtering on duration = {combinedResults.NewEvents.Count}"); // 2: Filter the events for bandwidth in Hertz double average = 3500; double sd = 600; double sigmaThreshold = 3.0; - combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); + combinedResults.NewEvents = EventFilters.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); PipitLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}"); combinedResults.NewEvents = FilterEventsOnFrequencyProfile(combinedResults.NewEvents); diff --git a/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs b/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs index 34f20b9ba..c85f422a0 100644 --- a/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs +++ b/src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs @@ -90,14 +90,14 @@ public override RecognizerResults Recognize( // 1: Filter the events for duration in seconds var minimumEventDuration = 0.1; var maximumEventDuration = 0.25; - combinedResults.NewEvents = EventExtentions.FilterOnDuration(combinedResults.NewEvents, minimumEventDuration, maximumEventDuration); + combinedResults.NewEvents = EventFilters.FilterOnDuration(combinedResults.NewEvents, minimumEventDuration, maximumEventDuration); CisticolaLog.Debug($"Event count after filtering on duration = {combinedResults.NewEvents.Count}"); // 2: Filter the events for bandwidth in Hertz double average = 600; double sd = 150; double sigmaThreshold = 3.0; - combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); + combinedResults.NewEvents = EventFilters.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); CisticolaLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}"); // 3: Filter on COMPONENT COUNT in Composite events. diff --git a/src/AudioAnalysisTools/Events/EventExtentions.cs b/src/AudioAnalysisTools/Events/EventExtentions.cs index ece057b96..6545f2a8b 100644 --- a/src/AudioAnalysisTools/Events/EventExtentions.cs +++ b/src/AudioAnalysisTools/Events/EventExtentions.cs @@ -7,101 +7,12 @@ namespace AudioAnalysisTools.Events using System; using System.Collections.Generic; using System.Linq; - using AudioAnalysisTools.Events.Interfaces; using AudioAnalysisTools.Events.Tracks; - using AudioAnalysisTools.Events.Types; - using AudioAnalysisTools.StandardSpectrograms; using MoreLinq; using TowseyLibrary; public static class EventExtentions { - //NOTES on SYNTAX: - //Select is a transform - fails if it encounters anything that is not of type SpectralEvent. - //var spectralEvents = events.Select(x => (SpectralEvent)x).ToList(); - - //Where is a FILTER - only returns spectral events. - //var spectralEvents = events.Where(x => x is SpectralEvent).Cast().ToList(); - //var spectralEvents = events.Where(x => x is ChirpEvent).ToList(); - //var chirpEvents = events.Cast().ToList(); - - public static (List TargetEvents, List OtherEvents) FilterForEventType(this List events) - where U : EventCommon - where T : EventCommon - { - var target = new List(events.Count); - var other = new List(events.Count); - - foreach (var @event in events) - { - if (@event is T t) - { - target.Add(t); - } - else - { - other.Add(@event); - } - } - - return (target, other); - } - - /// - /// Filters lists of spectral events based on their bandwidth. - /// Note: The typical sigma threshold would be 2 to 3 sds. - /// - /// The list of events. - /// The expected value of the bandwidth. - /// The standard deviation of the bandwidth. - /// THe sigma value which determines the max and min thresholds. - /// The filtered list of events. - public static List FilterOnBandwidth(List events, double average, double sd, double sigmaThreshold) - { - var minBandwidth = average - (sd * sigmaThreshold); - if (minBandwidth < 0.0) - { - throw new Exception("Invalid bandwidth passed to method EventExtentions.FilterOnBandwidth()."); - } - - var maxBandwidth = average + (sd * sigmaThreshold); - var outputEvents = events.Where(ev => ((SpectralEvent)ev).BandWidthHertz > minBandwidth && ((SpectralEvent)ev).BandWidthHertz < maxBandwidth).ToList(); - return outputEvents; - } - - public static List FilterOnBandwidth(List events, double minBandwidth, double maxBandwidth) - { - var outputEvents = events.Where(ev => ((SpectralEvent)ev).BandWidthHertz > minBandwidth && ((SpectralEvent)ev).BandWidthHertz < maxBandwidth).ToList(); - return outputEvents; - } - - /// - /// Removes short events from a list of events. - /// - public static List FilterShortEvents(List events, double minimumDurationSeconds) - { - var outputEvents = events.Where(ev => ev.EventDurationSeconds > minimumDurationSeconds).ToList(); - return outputEvents; - } - - /// - /// Removes long events from a list of events. - /// - public static List FilterLongEvents(List events, double maximumDurationSeconds) - { - var outputEvents = events.Where(ev => ev.EventDurationSeconds < maximumDurationSeconds).ToList(); - return outputEvents; - } - - /// - /// Remove events from a list of events whose time duration is either too short or too long. - /// - public static List FilterOnDuration(List events, double minimumDurationSeconds, double maximumDurationSeconds) - { - var outputEvents = events.Where(ev => ((SpectralEvent)ev).EventDurationSeconds > minimumDurationSeconds && ((SpectralEvent)ev).EventDurationSeconds < maximumDurationSeconds).ToList(); - return outputEvents; - } - /// /// Returns the average of the maximum decibel value in each frame of an event. /// @@ -151,286 +62,6 @@ public static double GetAverageDecibelsInEvent(SpectralEvent ev, double[,] spect return avDecibels; } - /// - /// Returns the matrix of neighbourhood values below an event. - /// - /// The event. - /// The spectrogram data as matrix with origin top/left. - /// THe bandwidth of the buffer zone in Hertz. - /// A converter to convert seconds/Hertz to frames/bins. - /// The neighbourhood as a matrix. - public static double[,] GetLowerNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int gap, UnitConverters converter) - { - var bufferBins = (int)Math.Round(bufferHertz / converter.HertzPerFreqBin); - var topBufferBin = converter.GetFreqBinFromHertz(ev.LowFrequencyHertz) - gap; - var bottomBufferBin = topBufferBin - bufferBins + 1; - bottomBufferBin = Math.Max(0, bottomBufferBin); - var frameStart = converter.FrameFromStartTime(ev.EventStartSeconds); - var frameEnd = converter.FrameFromStartTime(ev.EventEndSeconds); - int dataLength = spectrogramData.GetLength(0); - frameEnd = Math.Min(dataLength - 1, frameEnd); - var subMatrix = MatrixTools.Submatrix(spectrogramData, frameStart, bottomBufferBin, frameEnd, topBufferBin); - return subMatrix; - } - - /// - /// Returns the matrix of neighbourhood values above an event. - /// - /// The event. - /// The spectrogram data as matrix with origin top/left. - /// The bandwidth of the buffer zone in Hertz. - /// A converter to convert seconds/Hertz to frames/bins. - /// The neighbourhood as a matrix. - public static double[,] GetUpperNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int gap, UnitConverters converter) - { - var bufferBins = (int)Math.Round(bufferHertz / converter.HertzPerFreqBin); - var bottomBufferBin = converter.GetFreqBinFromHertz(ev.HighFrequencyHertz) + gap; - var topBufferBin = bottomBufferBin + bufferBins - 1; - var frameStart = converter.FrameFromStartTime(ev.EventStartSeconds); - var frameEnd = converter.FrameFromStartTime(ev.EventEndSeconds); - int dataLength = spectrogramData.GetLength(0); - frameEnd = Math.Min(dataLength - 1, frameEnd); - var subMatrix = MatrixTools.Submatrix(spectrogramData, frameStart, bottomBufferBin, frameEnd, topBufferBin); - return subMatrix; - } - - /// - /// Gets the upper and lower buffer zones (above and below an event). - /// Returns them as one combined matrix. - /// This makes it easier to determine the presense of acoustic events (especially wind) in the buffer zones. - /// - /// The event. - /// The spectrogram data as matrix with origin top/left. - /// The bandwidth of the lower buffer zone in Hertz. - /// Number of freq bins left as gap below event. - /// The bandwidth of the upper buffer zone in Hertz. - /// Number of freq bins left as gap above event. - /// A converter to convert seconds/Hertz to frames/bins. - /// A single matrix. - public static double[,] GetNeighbourhoodAsOneMatrix( - SpectralEvent ev, - double[,] spectrogramData, - double lowerHertzBuffer, - int lowerBinGap, - double upperHertzBuffer, - int upperBinGap, - UnitConverters converter) - { - double[,] subMatrix1 = null; - if (upperHertzBuffer > 0) - { - subMatrix1 = GetUpperNeighbourhood(ev, spectrogramData, upperHertzBuffer, upperBinGap, converter); - } - - double[,] subMatrix2 = null; - if (lowerHertzBuffer > 0) - { - subMatrix2 = GetLowerNeighbourhood(ev, spectrogramData, lowerHertzBuffer, lowerBinGap, converter); - } - - if (subMatrix1 == null && subMatrix2 == null) - { - return null; - } - - if (subMatrix1 == null) - { - return subMatrix2; - } - - if (subMatrix2 == null) - { - return subMatrix1; - } - - var matrix = MatrixTools.ConcatenateTwoMatrices(subMatrix1, subMatrix2); - return matrix; - } - - /// - /// Calculates the average amplitude in the frequency bins just above the event. - /// If it contains above threshold acoustic content, this is unlikely to be a discrete event. - /// NOTE: This method takes a simple average of log values. This is good enough for the purpose, although not mathematically correct. - /// Logs are computationally expensive. - /// - /// The event. - /// The spectrogram data as matrix with origin top/left. - /// THe bandwidth of the buffer zone in Hertz. - /// Number of freq bins as gap between event and buffer zone. - /// A converter to convert seconds/Hertz to frames/bins. - /// Unweighted average of the spectrogram amplitude in buffer band above the event. - public static double GetAverageAmplitudeInUpperNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int binGap, UnitConverters converter) - { - var subMatrix = GetUpperNeighbourhood(ev, spectrogramData, bufferHertz, binGap, converter); - var averageRowDecibels = MatrixTools.GetRowAverages(subMatrix); - var av = averageRowDecibels.Average(); - return av; - } - - /// - /// Calculates the average amplitude in the frequency bins just below the event. - /// If it contains above threshold acoustic content, this is unlikely to be a discrete event. - /// NOTE: This method takes a simple average of log values. This is good enough for the purpose, although not mathematically correct. - /// Logs are computationally expensive. - /// - /// The event. - /// The spectrogram data as matrix with origin top/left. - /// The bandwidth of the buffer zone in bins. - /// Number of freq bins as gap between event and buffer zone. - /// A converter to convert seconds/Hertz to frames/bins. - /// Unweighted average of the spectrogram amplitude in buffer band below the event. - public static double GetAverageAmplitudeInLowerNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int binGap, UnitConverters converter) - { - var subMatrix = GetLowerNeighbourhood(ev, spectrogramData, bufferHertz, binGap, converter); - var averageRowDecibels = MatrixTools.GetRowAverages(subMatrix); - var av = averageRowDecibels.Average(); - return av; - } - - /// - /// Removes events from a list of events that contain excessive noise in the upper neighbourhood. - /// Excess noise can indicate that this is not a legitimate event. - /// This method measures noise as the average decibel value in the buffer zones above and below the events. - /// - /// A list of spectral events. - /// A matrix of the spectrogram in which event occurs. - /// The band width of the required lower buffer. 100-200Hz is often appropriate. - /// The band width of the required upper buffer. 300-500Hz is often appropriate. - /// Converts sec/Hz to frame/bin. - /// Threshold noise level - assumed to be in decibels. - /// A list of filtered events. - public static List FilterEventsOnNeighbourhoodAverage( - List events, - double[,] spectrogramData, - double lowerHertzBuffer, - double upperHertzBuffer, - UnitConverters converter, - double decibelThreshold) - { - // allow bin gaps above and below the event. - int upperBinGap = 4; - int lowerBinGap = 2; - - var filteredEvents = new List(); - foreach (var ev in events) - { - var avLowerNhAmplitude = EventExtentions.GetAverageAmplitudeInLowerNeighbourhood((SpectralEvent)ev, spectrogramData, lowerHertzBuffer, lowerBinGap, converter); - var avUpperNhAmplitude = EventExtentions.GetAverageAmplitudeInUpperNeighbourhood((SpectralEvent)ev, spectrogramData, upperHertzBuffer, upperBinGap, converter); - - // Require that both the lower and upper buffer zones contain less acoustic activity than the threshold. - if (avLowerNhAmplitude < decibelThreshold && avUpperNhAmplitude < decibelThreshold) - { - // There is little acoustic activity in the designated buffer zones. It is likely to be a discrete event. - filteredEvents.Add(ev); - } - } - - return filteredEvents; - } - - /// - /// Removes events from a list of events that contain excessive noise in the event side bands; i.e. thhe upper and/or lower neighbouring frequency bands. - /// Excess noise can indicate that this is not a legitimate event. - /// This method counts the bins and frames containing above threshold activity (decibel value) in the buffer zones above and below the events. - /// - /// A list of spectral events. - /// The decibel spectrogram in which the events occurs. - /// The band width of the required lower buffer. 100-200Hz is often appropriate. - /// The band width of the required upper buffer. 300-500Hz is often appropriate. - /// Minimum required decibel difference between event activity and neighbourhood activity. - /// A list of filtered events. - public static List FilterEventsOnNeighbourhood( - List events, - BaseSonogram spectrogram, - int lowerHertzBuffer, - int upperHertzBuffer, - TimeSpan segmentStartOffset, - double decibelBuffer) - { - // allow bin gaps above and below the event. - int upperBinGap = 4; - int lowerBinGap = 2; - - var converter = new UnitConverters( - segmentStartOffset: segmentStartOffset.TotalSeconds, - sampleRate: spectrogram.SampleRate, - frameSize: spectrogram.Configuration.WindowSize, - frameOverlap: spectrogram.Configuration.WindowOverlap); - - var filteredEvents = new List(); - foreach (var ev in events) - { - var eventDecibels = GetAverageDecibelsInEvent(ev, spectrogram.Data, converter); - var sidebandMatrix = GetNeighbourhoodAsOneMatrix(ev, spectrogram.Data, lowerHertzBuffer, lowerBinGap, upperHertzBuffer, upperBinGap, converter); - var averageRowDecibels = MatrixTools.GetRowAverages(sidebandMatrix); - var averageColDecibels = MatrixTools.GetColumnAverages(sidebandMatrix); - int noisyRowCount = averageRowDecibels.Count(x => x > (eventDecibels - decibelBuffer)); - int noisyColCount = averageColDecibels.Count(x => x > (eventDecibels - decibelBuffer)); - - // Require that there be at most one buffer bin and one buffer frame containing excessive acoustic activity. - if (noisyRowCount <= 1 && noisyColCount <= 1) - { - // There is reduced acoustic activity in the upper and lower buffer zones. It is likely to be a discrete event. - filteredEvents.Add(ev); - } - } - - return filteredEvents; - } - - /// - /// Removes composite events from a list of EventCommon that contain more than the specfied number of SpectralEvent components. - /// - public static List FilterEventsOnCompositeContent( - List events, - int maxComponentCount) - { - var filteredEvents = new List(); - - foreach (var ev in events) - { - if (ev is CompositeEvent && ((CompositeEvent)ev).ComponentCount > maxComponentCount) - { - // ignore composite events which contain more than the specified component events. - continue; - } - - filteredEvents.Add(ev); - } - - return filteredEvents; - } - - /// - /// Removes composite events from a list of EventCommon where the component syllables do not have the correct periodicity. - /// - public static List FilterEventsOnSyllablePeriodicity( - List events, - double expectedPeriod, - double periodSd) - { - var filteredEvents = new List(); - - foreach (var ev in events) - { - if (ev is CompositeEvent) - { - var actualPeriodicity = ((CompositeEvent)ev).CalculatePeriodicity(); - var minAllowedPeriodicity = expectedPeriod - (3 * periodSd); - var maxAllowedPeriodicity = expectedPeriod + (3 * periodSd); - if (actualPeriodicity < minAllowedPeriodicity || actualPeriodicity > maxAllowedPeriodicity) - { - // ignore composite events which do not have the correct periodicity - continue; - } - } - - filteredEvents.Add(ev); - } - - return filteredEvents; - } - /// /// Combines all the tracks in all the events in the passed list into a single track. /// Each frame in the composite event is assigned the spectral point having maximum amplitude. diff --git a/src/AudioAnalysisTools/Events/EventFilters.cs b/src/AudioAnalysisTools/Events/EventFilters.cs new file mode 100644 index 000000000..9aecf0a7d --- /dev/null +++ b/src/AudioAnalysisTools/Events/EventFilters.cs @@ -0,0 +1,382 @@ +// +// All code in this file and all associated files are the copyright and property of the QUT Ecoacoustics Research Group (formerly MQUTeR, and formerly QUT Bioacoustics Research Group). +// + +namespace AudioAnalysisTools.Events +{ + using System; + using System.Collections.Generic; + using System.Linq; + using AudioAnalysisTools.Events.Types; + using AudioAnalysisTools.StandardSpectrograms; + using TowseyLibrary; + + public static class EventFilters + { + //NOTES on SYNTAX: + //"Select" is a transform - fails if it encounters anything that is not of type SpectralEvent. + //var spectralEvents = events.Select(x => (SpectralEvent)x).ToList(); + + //"Where" is a FILTER - only returns spectral events. + //var spectralEvents = events.Where(x => x is SpectralEvent).Cast().ToList(); + //var spectralEvents = events.Where(x => x is ChirpEvent).ToList(); + //var chirpEvents = events.Cast().ToList(); + + public static (List TargetEvents, List OtherEvents) FilterForEventType(this List events) + where U : EventCommon + where T : EventCommon + { + var target = new List(events.Count); + var other = new List(events.Count); + + foreach (var @event in events) + { + if (@event is T t) + { + target.Add(t); + } + else + { + other.Add(@event); + } + } + + return (target, other); + } + + /// + /// Filters lists of spectral events based on their bandwidth. + /// Note: The typical sigma threshold would be 2 to 3 sds. + /// + /// The list of events. + /// The expected value of the bandwidth. + /// The standard deviation of the bandwidth. + /// THe sigma value which determines the max and min thresholds. + /// The filtered list of events. + public static List FilterOnBandwidth(List events, double average, double sd, double sigmaThreshold) + { + var minBandwidth = average - (sd * sigmaThreshold); + if (minBandwidth < 0.0) + { + throw new Exception("Invalid bandwidth passed to method EventExtentions.FilterOnBandwidth()."); + } + + var maxBandwidth = average + (sd * sigmaThreshold); + var outputEvents = events.Where(ev => ((SpectralEvent)ev).BandWidthHertz > minBandwidth && ((SpectralEvent)ev).BandWidthHertz < maxBandwidth).ToList(); + return outputEvents; + } + + public static List FilterOnBandwidth(List events, double minBandwidth, double maxBandwidth) + { + var outputEvents = events.Where(ev => ((SpectralEvent)ev).BandWidthHertz > minBandwidth && ((SpectralEvent)ev).BandWidthHertz < maxBandwidth).ToList(); + return outputEvents; + } + + /// + /// Removes short events from a list of events. + /// + public static List FilterShortEvents(List events, double minimumDurationSeconds) + { + var outputEvents = events.Where(ev => ev.EventDurationSeconds > minimumDurationSeconds).ToList(); + return outputEvents; + } + + /// + /// Removes long events from a list of events. + /// + public static List FilterLongEvents(List events, double maximumDurationSeconds) + { + var outputEvents = events.Where(ev => ev.EventDurationSeconds < maximumDurationSeconds).ToList(); + return outputEvents; + } + + /// + /// Remove events from a list of events whose time duration is either too short or too long. + /// + public static List FilterOnDuration(List events, double minimumDurationSeconds, double maximumDurationSeconds) + { + var outputEvents = events.Where(ev => ((SpectralEvent)ev).EventDurationSeconds > minimumDurationSeconds && ((SpectralEvent)ev).EventDurationSeconds < maximumDurationSeconds).ToList(); + return outputEvents; + } + + /// + /// Removes composite events from a list of EventCommon that contain more than the specfied number of SpectralEvent components. + /// + public static List FilterEventsOnCompositeContent( + List events, + int maxComponentCount) + { + var filteredEvents = new List(); + + foreach (var ev in events) + { + if (ev is CompositeEvent && ((CompositeEvent)ev).ComponentCount > maxComponentCount) + { + // ignore composite events which contain more than the specified component events. + continue; + } + + filteredEvents.Add(ev); + } + + return filteredEvents; + } + + /// + /// Removes composite events from a list of EventCommon where the component syllables do not have the correct periodicity. + /// + public static List FilterEventsOnSyllablePeriodicity( + List events, + double expectedPeriod, + double periodSd) + { + var filteredEvents = new List(); + + foreach (var ev in events) + { + if (ev is CompositeEvent) + { + var actualPeriodicity = ((CompositeEvent)ev).CalculatePeriodicity(); + var minAllowedPeriodicity = expectedPeriod - (3 * periodSd); + var maxAllowedPeriodicity = expectedPeriod + (3 * periodSd); + if (actualPeriodicity < minAllowedPeriodicity || actualPeriodicity > maxAllowedPeriodicity) + { + // ignore composite events which do not have the correct periodicity + continue; + } + } + + filteredEvents.Add(ev); + } + + return filteredEvents; + } + + /// + /// Removes events from a list of events that contain excessive noise in the upper neighbourhood. + /// Excess noise can indicate that this is not a legitimate event. + /// This method measures noise as the average decibel value in the buffer zones above and below the events. + /// + /// A list of spectral events. + /// A matrix of the spectrogram in which event occurs. + /// The band width of the required lower buffer. 100-200Hz is often appropriate. + /// The band width of the required upper buffer. 300-500Hz is often appropriate. + /// Converts sec/Hz to frame/bin. + /// Threshold noise level - assumed to be in decibels. + /// A list of filtered events. + public static List FilterEventsOnNeighbourhoodAverage( + List events, + double[,] spectrogramData, + double lowerHertzBuffer, + double upperHertzBuffer, + UnitConverters converter, + double decibelThreshold) + { + // allow bin gaps above and below the event. + int upperBinGap = 4; + int lowerBinGap = 2; + + var filteredEvents = new List(); + foreach (var ev in events) + { + var avLowerNhAmplitude = GetAverageAmplitudeInLowerNeighbourhood((SpectralEvent)ev, spectrogramData, lowerHertzBuffer, lowerBinGap, converter); + var avUpperNhAmplitude = GetAverageAmplitudeInUpperNeighbourhood((SpectralEvent)ev, spectrogramData, upperHertzBuffer, upperBinGap, converter); + + // Require that both the lower and upper buffer zones contain less acoustic activity than the threshold. + if (avLowerNhAmplitude < decibelThreshold && avUpperNhAmplitude < decibelThreshold) + { + // There is little acoustic activity in the designated buffer zones. It is likely to be a discrete event. + filteredEvents.Add(ev); + } + } + + return filteredEvents; + } + + /// + /// Removes events from a list of events that contain excessive noise in the event side bands; i.e. thhe upper and/or lower neighbouring frequency bands. + /// Excess noise can indicate that this is not a legitimate event. + /// This method counts the bins and frames containing above threshold activity (decibel value) in the buffer zones above and below the events. + /// + /// A list of spectral events. + /// The decibel spectrogram in which the events occurs. + /// The band width of the required lower buffer. 100-200Hz is often appropriate. + /// The band width of the required upper buffer. 300-500Hz is often appropriate. + /// Minimum required decibel difference between event activity and neighbourhood activity. + /// A list of filtered events. + public static List FilterEventsOnNeighbourhood( + List events, + BaseSonogram spectrogram, + int lowerHertzBuffer, + int upperHertzBuffer, + TimeSpan segmentStartOffset, + double decibelBuffer) + { + // allow bin gaps above and below the event. + int upperBinGap = 4; + int lowerBinGap = 2; + + var converter = new UnitConverters( + segmentStartOffset: segmentStartOffset.TotalSeconds, + sampleRate: spectrogram.SampleRate, + frameSize: spectrogram.Configuration.WindowSize, + frameOverlap: spectrogram.Configuration.WindowOverlap); + + var filteredEvents = new List(); + foreach (var ev in events) + { + var eventDecibels = EventExtentions.GetAverageDecibelsInEvent(ev, spectrogram.Data, converter); + var sidebandMatrix = GetNeighbourhoodAsOneMatrix(ev, spectrogram.Data, lowerHertzBuffer, lowerBinGap, upperHertzBuffer, upperBinGap, converter); + var averageRowDecibels = MatrixTools.GetRowAverages(sidebandMatrix); + var averageColDecibels = MatrixTools.GetColumnAverages(sidebandMatrix); + int noisyRowCount = averageRowDecibels.Count(x => x > (eventDecibels - decibelBuffer)); + int noisyColCount = averageColDecibels.Count(x => x > (eventDecibels - decibelBuffer)); + + // Require that there be at most one buffer bin and one buffer frame containing excessive acoustic activity. + if (noisyRowCount <= 1 && noisyColCount <= 1) + { + // There is reduced acoustic activity in the upper and lower buffer zones. It is likely to be a discrete event. + filteredEvents.Add(ev); + } + } + + return filteredEvents; + } + + /// + /// Returns the matrix of neighbourhood values below an event. + /// + /// The event. + /// The spectrogram data as matrix with origin top/left. + /// THe bandwidth of the buffer zone in Hertz. + /// A converter to convert seconds/Hertz to frames/bins. + /// The neighbourhood as a matrix. + public static double[,] GetLowerNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int gap, UnitConverters converter) + { + var bufferBins = (int)Math.Round(bufferHertz / converter.HertzPerFreqBin); + var topBufferBin = converter.GetFreqBinFromHertz(ev.LowFrequencyHertz) - gap; + var bottomBufferBin = topBufferBin - bufferBins + 1; + bottomBufferBin = Math.Max(0, bottomBufferBin); + var frameStart = converter.FrameFromStartTime(ev.EventStartSeconds); + var frameEnd = converter.FrameFromStartTime(ev.EventEndSeconds); + int dataLength = spectrogramData.GetLength(0); + frameEnd = Math.Min(dataLength - 1, frameEnd); + var subMatrix = MatrixTools.Submatrix(spectrogramData, frameStart, bottomBufferBin, frameEnd, topBufferBin); + return subMatrix; + } + + /// + /// Returns the matrix of neighbourhood values above an event. + /// + /// The event. + /// The spectrogram data as matrix with origin top/left. + /// The bandwidth of the buffer zone in Hertz. + /// A converter to convert seconds/Hertz to frames/bins. + /// The neighbourhood as a matrix. + public static double[,] GetUpperNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int gap, UnitConverters converter) + { + var bufferBins = (int)Math.Round(bufferHertz / converter.HertzPerFreqBin); + var bottomBufferBin = converter.GetFreqBinFromHertz(ev.HighFrequencyHertz) + gap; + var topBufferBin = bottomBufferBin + bufferBins - 1; + var frameStart = converter.FrameFromStartTime(ev.EventStartSeconds); + var frameEnd = converter.FrameFromStartTime(ev.EventEndSeconds); + int dataLength = spectrogramData.GetLength(0); + frameEnd = Math.Min(dataLength - 1, frameEnd); + var subMatrix = MatrixTools.Submatrix(spectrogramData, frameStart, bottomBufferBin, frameEnd, topBufferBin); + return subMatrix; + } + + /// + /// Gets the upper and lower buffer zones (above and below an event). + /// Returns them as one combined matrix. + /// This makes it easier to determine the presense of acoustic events (especially wind) in the buffer zones. + /// + /// The event. + /// The spectrogram data as matrix with origin top/left. + /// The bandwidth of the lower buffer zone in Hertz. + /// Number of freq bins left as gap below event. + /// The bandwidth of the upper buffer zone in Hertz. + /// Number of freq bins left as gap above event. + /// A converter to convert seconds/Hertz to frames/bins. + /// A single matrix. + public static double[,] GetNeighbourhoodAsOneMatrix( + SpectralEvent ev, + double[,] spectrogramData, + double lowerHertzBuffer, + int lowerBinGap, + double upperHertzBuffer, + int upperBinGap, + UnitConverters converter) + { + double[,] subMatrix1 = null; + if (upperHertzBuffer > 0) + { + subMatrix1 = GetUpperNeighbourhood(ev, spectrogramData, upperHertzBuffer, upperBinGap, converter); + } + + double[,] subMatrix2 = null; + if (lowerHertzBuffer > 0) + { + subMatrix2 = GetLowerNeighbourhood(ev, spectrogramData, lowerHertzBuffer, lowerBinGap, converter); + } + + if (subMatrix1 == null && subMatrix2 == null) + { + return null; + } + + if (subMatrix1 == null) + { + return subMatrix2; + } + + if (subMatrix2 == null) + { + return subMatrix1; + } + + var matrix = MatrixTools.ConcatenateTwoMatrices(subMatrix1, subMatrix2); + return matrix; + } + + /// + /// Calculates the average amplitude in the frequency bins just above the event. + /// If it contains above threshold acoustic content, this is unlikely to be a discrete event. + /// NOTE: This method takes a simple average of log values. This is good enough for the purpose, although not mathematically correct. + /// Logs are computationally expensive. + /// + /// The event. + /// The spectrogram data as matrix with origin top/left. + /// THe bandwidth of the buffer zone in Hertz. + /// Number of freq bins as gap between event and buffer zone. + /// A converter to convert seconds/Hertz to frames/bins. + /// Unweighted average of the spectrogram amplitude in buffer band above the event. + public static double GetAverageAmplitudeInUpperNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int binGap, UnitConverters converter) + { + var subMatrix = GetUpperNeighbourhood(ev, spectrogramData, bufferHertz, binGap, converter); + var averageRowDecibels = MatrixTools.GetRowAverages(subMatrix); + var av = averageRowDecibels.Average(); + return av; + } + + /// + /// Calculates the average amplitude in the frequency bins just below the event. + /// If it contains above threshold acoustic content, this is unlikely to be a discrete event. + /// NOTE: This method takes a simple average of log values. This is good enough for the purpose, although not mathematically correct. + /// Logs are computationally expensive. + /// + /// The event. + /// The spectrogram data as matrix with origin top/left. + /// The bandwidth of the buffer zone in bins. + /// Number of freq bins as gap between event and buffer zone. + /// A converter to convert seconds/Hertz to frames/bins. + /// Unweighted average of the spectrogram amplitude in buffer band below the event. + public static double GetAverageAmplitudeInLowerNeighbourhood(SpectralEvent ev, double[,] spectrogramData, double bufferHertz, int binGap, UnitConverters converter) + { + var subMatrix = GetLowerNeighbourhood(ev, spectrogramData, bufferHertz, binGap, converter); + var averageRowDecibels = MatrixTools.GetRowAverages(subMatrix); + var av = averageRowDecibels.Average(); + return av; + } + } +}