diff --git a/src/AudioAnalysisTools/Events/EventFilters.cs b/src/AudioAnalysisTools/Events/EventFilters.cs index f9eb14b5d..9625a972d 100644 --- a/src/AudioAnalysisTools/Events/EventFilters.cs +++ b/src/AudioAnalysisTools/Events/EventFilters.cs @@ -62,18 +62,37 @@ public static List FilterOnBandwidth(List events, doub var minBandwidth = average - (sd * sigmaThreshold); if (minBandwidth < 0.0) { - throw new Exception("Invalid bandwidth passed to method EventExtentions.FilterOnBandwidth()."); + throw new Exception("Invalid bandwidth passed to method EventExtentions.FilterOnBandwidth(). Min bandwidth < 0 Hertz."); } var maxBandwidth = average + (sd * sigmaThreshold); - var outputEvents = events.Where(ev => ((SpectralEvent)ev).BandWidthHertz >= minBandwidth && ((SpectralEvent)ev).BandWidthHertz <= maxBandwidth).ToList(); + var outputEvents = FilterOnBandwidth(events, minBandwidth, maxBandwidth); 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; + // The following line does it all BUT it does not allow for feedback to the user. + //var outputEvents = events.Where(ev => ((SpectralEvent)ev).BandWidthHertz >= minBandwidth && ((SpectralEvent)ev).BandWidthHertz <= maxBandwidth).ToList(); + + var filteredEvents = new List(); + + foreach (var ev in events) + { + var bandwidth = ((SpectralEvent)ev).BandWidthHertz; + if ((bandwidth > minBandwidth) && (bandwidth < maxBandwidth)) + { + Log.Debug($" Event accepted: Actual bandwidth = {bandwidth}"); + filteredEvents.Add(ev); + } + else + { + Log.Debug($" Event rejected: Actual bandwidth = {bandwidth}"); + continue; + } + } + + return filteredEvents; } /// @@ -94,15 +113,6 @@ public static List FilterLongEvents(List events, d 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; - } - /// /// Filters lists of spectral events based on their duration. /// Note: The typical sigma threshold would be 2 to 3 sds. @@ -114,17 +124,45 @@ public static List FilterOnDuration(List events, doubl /// The filtered list of events. public static List FilterOnDuration(List events, double average, double sd, double sigmaThreshold) { - var minDuration = average - (sd * sigmaThreshold); - if (minDuration < 0.0) + var minimumDurationSeconds = average - (sd * sigmaThreshold); + if (minimumDurationSeconds < 0.0) { - throw new Exception("Invalid seconds duration passed to method EventExtentions.FilterOnDuration()."); + throw new Exception("Invalid seconds duration passed to method EventExtentions.FilterOnDuration(). Minimum event duration < 0 seconds"); } - var maxDuration = average + (sd * sigmaThreshold); - var outputEvents = events.Where(ev => ((SpectralEvent)ev).EventDurationSeconds >= minDuration && ((SpectralEvent)ev).EventDurationSeconds <= maxDuration).ToList(); + var maximumDurationSeconds = average + (sd * sigmaThreshold); + var outputEvents = FilterOnDuration(events, minimumDurationSeconds, maximumDurationSeconds); 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) + { + // The following line does it all BUT it does not allow for feedback to the user. + //var filteredEvents = events.Where(ev => ((SpectralEvent)ev).EventDurationSeconds >= minimumDurationSeconds && ((SpectralEvent)ev).EventDurationSeconds <= maximumDurationSeconds).ToList(); + + var filteredEvents = new List(); + + foreach (var ev in events) + { + var duration = ((SpectralEvent)ev).EventDurationSeconds; + if ((duration > minimumDurationSeconds) && (duration < maximumDurationSeconds)) + { + Log.Debug($" Event accepted: Actual duration = {duration:F3}s"); + filteredEvents.Add(ev); + } + else + { + Log.Debug($" Event rejected: Actual duration = {duration:F3}s"); + continue; + } + } + + return filteredEvents; + } + /// /// Removes composite events from a list of EventCommon that contain more than the specfied number of SpectralEvent components. /// @@ -153,8 +191,9 @@ public static List FilterEventsOnComponentCount( /// public static List FilterEventsOnSyllableCountAndPeriodicity(List events, int maxSyllableCount, double expectedPeriod, double expectedSd) { - var minExpectedPeriod = expectedPeriod - (3 * expectedSd); - var maxExpectedPeriod = expectedPeriod + (3 * expectedSd); + var SigmaThreshold = 3.0; + var minExpectedPeriod = expectedPeriod - (SigmaThreshold * expectedSd); + var maxExpectedPeriod = expectedPeriod + (SigmaThreshold * expectedSd); var filteredEvents = new List(); @@ -216,12 +255,12 @@ public static List FilterEventsOnSyllableCountAndPeriodicity(List= minExpectedPeriod && actualAvPeriod <= maxExpectedPeriod) { - Log.Debug($" EventAccepted: Actual average syllable interval = {actualAvPeriod}"); + Log.Debug($" EventAccepted: Actual average syllable interval = {actualAvPeriod:F3}"); filteredEvents.Add(ev); } else { - Log.Debug($" EventRejected: Actual average syllable interval = {actualAvPeriod}"); + Log.Debug($" EventRejected: Actual average syllable interval = {actualAvPeriod:F3}"); } } } @@ -233,7 +272,7 @@ public static (bool[] TemporalFootprint, double TimeScale) GetTemporalFootprint( { if (compositeEvent is CompositeEvent == false) { - throw new Exception("Invalid event type. Event passed to GetTemporalFotprint() must be of type CompositeEvent."); + throw new Exception("Invalid event type. Event passed to GetTemporalFootprint() must be of type CompositeEvent."); } // get the composite events. @@ -297,6 +336,7 @@ public static (int Count, double AveragePeriod, double SdPeriod) GetPeriodicity( /// 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. /// The max allowed value for the average decibels value (over all spectrogram cells) in a sideband of an event. + /// The max allowed value for the decibels value in a sideband timeframe or freq bin. /// Start time of the current recording segment. /// A list of filtered events. public static List FilterEventsOnSidebandActivity( @@ -305,6 +345,7 @@ public static List FilterEventsOnSidebandActivity( int lowerHertzBuffer, int upperHertzBuffer, double thresholdForAverageDecibelsInSidebands, + double thresholdForMaxSidebandActivity, TimeSpan segmentStartOffset) { // allow bin gaps below the event. @@ -313,7 +354,7 @@ public static List FilterEventsOnSidebandActivity( //The decibel value of any other event in the sidebands of a focal event // cannot come within 3 dB of the dB value of the focal event. - var decibelBuffer = 3.0; + //var decibelBuffer = 3.0; var converter = new UnitConverters( segmentStartOffset: segmentStartOffset.TotalSeconds, @@ -326,33 +367,44 @@ public static List FilterEventsOnSidebandActivity( var filteredEvents = new List(); foreach (var ev in events) { - var avEventDecibels = EventExtentions.GetAverageDecibelsInEvent(ev, spectrogramData, converter); - var maxSidebandEventDecibels = Math.Max(0.0, avEventDecibels - decibelBuffer); + //var avEventDecibels = EventExtentions.GetAverageDecibelsInEvent(ev, spectrogramData, converter); + //var maxSidebandEventDecibels = Math.Max(0.0, avEventDecibels - decibelBuffer); - var retainEvent1 = true; - var retainEvent2 = true; + var upperSidebandGood = true; + var lowerSidebandGood = true; if (lowerHertzBuffer > 0) { var lowerSidebandMatrix = GetLowerEventSideband(ev, spectrogramData, lowerHertzBuffer, lowerBinGap, converter); - retainEvent1 = IsSidebandActivityBelowThreshold( + lowerSidebandGood = IsSidebandActivityBelowThreshold( lowerSidebandMatrix, - maxSidebandEventDecibels, + thresholdForMaxSidebandActivity, thresholdForAverageDecibelsInSidebands); + + if (!lowerSidebandGood) + { + Log.Debug($" EventRejected: Lower sideband has acoustic activity above {thresholdForAverageDecibelsInSidebands} dB"); + } } if (upperHertzBuffer > 0) { var upperSidebandMatrix = GetUpperEventSideband(ev, spectrogramData, upperHertzBuffer, upperBinGap, converter); - retainEvent2 = IsSidebandActivityBelowThreshold( + upperSidebandGood = IsSidebandActivityBelowThreshold( upperSidebandMatrix, - maxSidebandEventDecibels, + thresholdForMaxSidebandActivity, thresholdForAverageDecibelsInSidebands); + + if (!upperSidebandGood) + { + Log.Debug($" EventRejected: Upper sideband has acoustic activity above {thresholdForAverageDecibelsInSidebands} dB"); + } } - if (retainEvent1 && retainEvent2) + if (upperSidebandGood && lowerSidebandGood) { // The acoustic activity in event sidebands is below the threshold. It is likely to be a discrete event. + Log.Debug($" EventAccepted: Both sidebands have acoustic activity below {thresholdForAverageDecibelsInSidebands} dB."); filteredEvents.Add(ev); } } diff --git a/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs b/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs index 4efd16482..fa7b2591c 100644 --- a/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs +++ b/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs @@ -14,6 +14,7 @@ namespace AudioAnalysisTools.Events.Types public static class EventPostProcessing { + private const float SigmaThreshold = 3.0F; private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); public static List PostProcessingOfSpectralEvents( @@ -45,12 +46,14 @@ public static List PostProcessingOfSpectralEvents( if (sequenceConfig.NotNull() && sequenceConfig.CombinePossibleSyllableSequence) { + Log.Debug($"COMBINE PROXIMAL EVENTS"); + // Must first convert events to spectral events. var spectralEvents1 = newEvents.Cast().ToList(); var startDiff = sequenceConfig.SyllableStartDifference; var hertzDiff = sequenceConfig.SyllableHertzGap; newEvents = CompositeEvent.CombineProximalEvents(spectralEvents1, TimeSpan.FromSeconds(startDiff), (int)hertzDiff); - Log.Debug($"Event count after combining proximal events = {newEvents.Count}"); + Log.Debug($" Event count after combining proximal events = {newEvents.Count}"); // Now filter on properties of the sequences which are treated as Composite events. if (sequenceConfig.FilterSyllableSequence) @@ -59,54 +62,66 @@ public static List PostProcessingOfSpectralEvents( var maxComponentCount = sequenceConfig.SyllableMaxCount; var periodAv = sequenceConfig.ExpectedPeriod; var periodSd = sequenceConfig.PeriodStandardDeviation; - var minPeriod = periodAv - (3 * periodSd); - var maxPeriod = periodAv + (3 * periodSd); - Log.Debug($"FILTER SYLLABLE SEQUENCE"); + var minPeriod = periodAv - (SigmaThreshold * periodSd); + var maxPeriod = periodAv + (SigmaThreshold * periodSd); + Log.Debug($"FILTER ON SYLLABLE SEQUENCE"); Log.Debug($" Syllables: max={maxComponentCount}"); Log.Debug($" Period: av={periodAv}s, sd={periodSd:F3} min={minPeriod:F3}s, max={maxPeriod:F3}s"); newEvents = EventFilters.FilterEventsOnSyllableCountAndPeriodicity(newEvents, maxComponentCount, periodAv, periodSd); - Log.Debug($"Event count after filtering on periodicity = {newEvents.Count}"); + Log.Debug($" Event count after filtering on periodicity = {newEvents.Count}"); } } // 3: Filter the events for time duration (seconds) - if (postprocessingConfig.Duration != null) + if ((postprocessingConfig.Duration != null) && (newEvents.Count > 0)) { + Log.Debug($"FILTER ON EVENT DURATION"); var expectedEventDuration = postprocessingConfig.Duration.ExpectedDuration; var sdEventDuration = postprocessingConfig.Duration.DurationStandardDeviation; - newEvents = EventFilters.FilterOnDuration(newEvents, expectedEventDuration, sdEventDuration, sigmaThreshold: 3.0); - Log.Debug($"Event count after filtering on duration = {newEvents.Count}"); + var minDuration = expectedEventDuration - (SigmaThreshold * sdEventDuration); + var maxDuration = expectedEventDuration + (SigmaThreshold * sdEventDuration); + Log.Debug($" Duration: expected={expectedEventDuration}s, sd={sdEventDuration} min={minDuration}s, max={maxDuration}s"); + newEvents = EventFilters.FilterOnDuration(newEvents, expectedEventDuration, sdEventDuration, SigmaThreshold); + Log.Debug($" Event count after filtering on duration = {newEvents.Count}"); } // 4: Filter the events for bandwidth in Hertz - if (postprocessingConfig.Bandwidth != null) + if ((postprocessingConfig.Bandwidth != null) && (newEvents.Count > 0)) { + Log.Debug($"FILTER ON EVENT BANDWIDTH"); var expectedEventBandwidth = postprocessingConfig.Bandwidth.ExpectedBandwidth; var sdBandwidth = postprocessingConfig.Bandwidth.BandwidthStandardDeviation; - newEvents = EventFilters.FilterOnBandwidth(newEvents, expectedEventBandwidth, sdBandwidth, sigmaThreshold: 3.0); - Log.Debug($"Event count after filtering on bandwidth = {newEvents.Count}"); + var minBandwidth = expectedEventBandwidth - (SigmaThreshold * sdBandwidth); + var maxBandwidth = expectedEventBandwidth + (SigmaThreshold * sdBandwidth); + Log.Debug($" Bandwidth: expected={expectedEventBandwidth}Hz, sd={sdBandwidth} min={minBandwidth}Hz, max={maxBandwidth}Hz"); + newEvents = EventFilters.FilterOnBandwidth(newEvents, expectedEventBandwidth, sdBandwidth, SigmaThreshold); + Log.Debug($" Event count after filtering on bandwidth = {newEvents.Count}"); } // 5: Filter events on the amount of acoustic activity in their upper and lower sidebands - their buffer zone. // The idea is that an unambiguous event should have some acoustic space above and below. // The filter requires that the average acoustic activity in each frame and bin of the upper and lower buffer zones should not exceed the user specified decibel threshold. var sidebandActivity = postprocessingConfig.SidebandActivity; - if (sidebandActivity != null) + if ((sidebandActivity != null) && (newEvents.Count > 0)) { + Log.Debug($"FILTER ON SIDEBAND ACTIVITY"); + Log.Debug($" Max permitted sideband background = {sidebandActivity.MaxAverageDecibels:F0} dB"); + Log.Debug($" Max permitted sideband activity = {sidebandActivity.MaxActivityDecibels:F0} dB"); var spectralEvents2 = newEvents.Cast().ToList(); newEvents = EventFilters.FilterEventsOnSidebandActivity( spectralEvents2, spectrogram, sidebandActivity.LowerHertzBuffer, sidebandActivity.UpperHertzBuffer, - sidebandActivity.MaxAverageSidebandDecibels, + sidebandActivity.MaxAverageDecibels, + sidebandActivity.MaxActivityDecibels, segmentStartOffset); - Log.Debug($"Event count after filtering on acoustic activity in sidebands = {newEvents.Count}"); + Log.Debug($" Event count after filtering on sideband activity = {newEvents.Count}"); } // Write out the events to log. - Log.Debug($"Final event count = {newEvents.Count}."); + Log.Debug($"FINAL event count = {newEvents.Count}."); if (newEvents.Count > 0) { int counter = 0; @@ -202,11 +217,18 @@ public class SidebandConfig public int LowerHertzBuffer { get; set; } /// - /// Gets or sets a value indicating the maximum value of the average decibels of acoustic activity + /// Gets or sets a value indicating the maximum value of the average decibels of background acoustic activity /// in the upper and lower sidebands of an event. The average is over all spectrogram cells in each sideband. /// This value is used only if LowerHertzBuffer > 0 OR UpperHertzBuffer > 0. /// - public double MaxAverageSidebandDecibels { get; set; } + public double MaxAverageDecibels { get; set; } + + /// + /// Gets or sets a value indicating the maximum decibel value in a sideband frequency bin or timeframe. + /// The decibel value is an average over all spectrogram cells in any frame or bin. + /// This value is used only if LowerHertzBuffer > 0 OR UpperHertzBuffer > 0. + /// + public double MaxActivityDecibels { get; set; } } /// diff --git a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs index 6021c98ab..868d5dcd5 100644 --- a/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs +++ b/tests/Acoustics.Test/AnalysisPrograms/Recognizers/GenericRecognizerTests.cs @@ -126,7 +126,7 @@ public void TestBlobAlgorithm() { UpperHertzBuffer = 0, LowerHertzBuffer = 0, - MaxAverageSidebandDecibels = 0, + MaxAverageDecibels = 0, }, }, }; @@ -190,7 +190,7 @@ public void TestOscillationAlgorithm() { UpperHertzBuffer = 0, LowerHertzBuffer = 0, - MaxAverageSidebandDecibels = 0, + MaxAverageDecibels = 0, }, }, }; @@ -252,7 +252,7 @@ public void TestWhistleAlgorithm() { UpperHertzBuffer = 0, LowerHertzBuffer = 0, - MaxAverageSidebandDecibels = 0, + MaxAverageDecibels = 0, }, }, }; @@ -998,7 +998,7 @@ public void TestAedAlgorithm() { UpperHertzBuffer = 0, LowerHertzBuffer = 0, - MaxAverageSidebandDecibels = 0, + MaxAverageDecibels = 0, }, }, }; @@ -1088,7 +1088,7 @@ public void TestMultipleAlgorithms() { UpperHertzBuffer = 0, LowerHertzBuffer = 0, - MaxAverageSidebandDecibels = 0, + MaxAverageDecibels = 0, }, }, };