diff --git a/docs/guides/generic_recognizers.md b/docs/guides/generic_recognizers.md index 1c3c6cce8..174acbd64 100644 --- a/docs/guides/generic_recognizers.md +++ b/docs/guides/generic_recognizers.md @@ -383,21 +383,23 @@ Some of these algorithms have extra parameters, some do not, but all do have the | Oscillation | [!OscillationParameters](xref:AnalysisPrograms.Recognizers.Base.OscillationParameters) | | Harmonic | [!HarmonicParameters](xref:AnalysisPrograms.Recognizers.Base.HarmonicParameters) | -### [PostProcessing](xref:AudioAnalysisTools.Events.Types.EventPostProcessing.PostProcessingConfig) -The post processing stage is run after event detection (the `Profiles`). -Note that these post-processing steps are performed on all acoustic events collectively, i.e. all those "discovered" -by all the *profiles* in the list of profiles. -Add a post processing section to you config file by adding the `PostProcessing` parameter and indenting the sub-parameters. +### [PostProcessing](xref:AudioAnalysisTools.Events.Types.EventPostProcessing.PostProcessingConfig) + +Post-processing of events is performed after event detection. However it is important to understand that post-processing is performed once for each of the DecibelThresholds. As an example: suppose you have three decibel thresholds (6, 9 and 12 dB is a typical set of values) in each of two profiles. All the events detected at threshold 6 dB (by both profiles) will be collected together and subjected to the post processing steps. Typically some or all of the events may fail to be accepted as "true" events based on your post-processing parameters. Then all the events detected at 9 dB will be collected and independently subjected to post-processing. Then, likewise, all events detected at the 12 dB threshold will be post-processed. In other words, one round of post-processing is performed for each decibel threshold. This sequence of multiple post-processing steps gives rise to one or more temporally nested events. Think of them as Russion doll events! The final post-processing step is to remove all but the longest duration event in any nested set of events. [!code-yaml[post_processing](./Ecosounds.NinoxBoobook.yml#L34-L34 "Post Processing")] -Post processing is optional. You may just want to combine or filter component events using code you have written yourself. +Post processing is optional - you may decide to combine or filter the "raw" events using code you have written yourself. To add a post-processing section to your config file, insert the `PostProcessing` parameter and indent the sub-parameters. There are five post-processing possibilities each of which you may choose to use or not. Note that the post-processing steps are performed in this order which cannot be changed by the user: + - Combine events having temporal _and_ spectral overlap. + - Combine possible sequences of events that constitute a "call". + - Remove (filter) events whose duration is outside an acceptable range. + - Remove (filter) events whose bandwidth is outside an acceptable range. + - Remove (filter) events having excessive acoustic activity in their sidebands. -#### Combining overlapping syllables into calls -Combining syllables is the first of two *post-processing* steps. +### Combine events having temporal _and_ spectral overlap [!code-yaml[post_processing_combining](./Ecosounds.NinoxBoobook.yml#L34-L42 "Post Processing: Combining")] @@ -407,7 +409,8 @@ set this true for two reasons: - the target call is composed of two or more overlapping syllables that you want to join as one event. - whistle events often require this step to unite whistle fragments detections into one event. -#### Combining syllables into calls + +### Combine possible sequences of events that constitute a "call" Unlike overlapping events, if you want to combine a group of events (like syllables) that are near each other but not overlapping, then make use of the `SyllableSequence` parameter. @@ -430,7 +433,7 @@ constraints defined by `SyllableMaxCount` and `ExpectedPeriod`. See the document for more information. -### Remove events whose duration or bandwidth lies outside an expected range. +### Remove events whose duration is outside an acceptable range [!code-yaml[post_processing_filtering](./Ecosounds.NinoxBoobook.yml?start=34&end=62&highlight=20- "Post Processing: filtering")] @@ -442,6 +445,7 @@ This filter removes events whose duration lies outside three standard deviations - `DurationStandardDeviation` defines _one_ SD of the assumed distribution. Assuming the duration is normally distributed, three SDs sets hard upper and lower duration bounds that includes 99.7% of instances. The filtering algorithm calculates these hard bounds and removes acoustic events that fall outside the bounds. +### Remove events whose bandwidth is outside an acceptable range Use the parameter `Bandwidth` to filter out events whose bandwidth is too small or large. This filter removes events whose bandwidth lies outside three standard deviations (SDs) of an expected value. diff --git a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs index c6c7b92e4..584980be7 100644 --- a/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs +++ b/src/AnalysisPrograms/Recognizers/GenericRecognizer.cs @@ -135,12 +135,55 @@ public override RecognizerResults Recognize( var results = RunProfiles(audioRecording, configuration, segmentStartOffset); // ############################### POST-PROCESSING OF GENERIC EVENTS ############################### + var postprocessingConfig = configuration.PostProcessing; - results.NewEvents = EventPostProcessing.PostProcessingOfSpectralEvents( - results.NewEvents, - postprocessingConfig, - results.Sonogram, - segmentStartOffset); + var postEvents = new List(); + + // count number of events detected at each decibel threshold. + for (int i = 1; i <= 24; i++) + { + var dbEvents = EventFilters.FilterOnDecibelDetectionThreshold(results.NewEvents, (double)i); + + if (dbEvents.Count > 0) + { + //Log.Debug($"Profiles detected {dbEvents.Count} events at threshold {i} dB."); + var ppEvents = EventPostProcessing.PostProcessingOfSpectralEvents( + dbEvents, + postprocessingConfig, + (double)i, + results.Sonogram, + segmentStartOffset); + + postEvents.AddRange(ppEvents); + } + } + + // Running profiles with multiple dB thresholds produces nested (Russian doll) events. + // Remove all but the outermost event. + // Add a spacer for easier reading of the debug output. + Log.Debug($" "); + Log.Debug($"Event count BEFORE removing enclosed events = {postEvents.Count}."); + results.NewEvents = CompositeEvent.RemoveEnclosedEvents(postEvents); + Log.Debug($"Event count AFTER removing enclosed events = {postEvents.Count}."); + + // Write out the events to log. + //Log.Debug($"FINAL event count = {postEvents.Count}."); + if (postEvents.Count > 0) + { + int counter = 0; + foreach (var ev in postEvents) + { + counter++; + var spEvent = (SpectralEvent)ev; + Log.Debug($" Event[{counter}]: Start={spEvent.EventStartSeconds:f1}; Duration={spEvent.EventDurationSeconds:f2}; Bandwidth={spEvent.BandWidthHertz} Hz"); + } + } + + //results.NewEvents = EventPostProcessing.PostProcessingOfSpectralEvents( + // results.NewEvents, + // postprocessingConfig, + // results.Sonogram, + // segmentStartOffset); return results; } @@ -186,10 +229,6 @@ public static RecognizerResults RunProfiles( profileResults.Sonogram = thresholdResults.Sonogram; } - // Running profiles with multiple dB thresholds produces nested (Russian doll) events. - // Remove all but the outermost event. - profileResults.NewEvents = CompositeEvent.RemoveEnclosedEvents(profileResults.NewEvents); - // Add additional info to the remaining acoustic events profileResults.NewEvents.ForEach(ae => { @@ -357,6 +396,13 @@ public static RecognizerResults RunOneProfile( Sonogram = null, }; + //add info about decibel threshold into the event. + //This info is used later during post-processing of events. + foreach (var ev in spectralEvents) + { + ev.DecibelDetectionThreshold = decibelThreshold.Value; + } + allResults.NewEvents.AddRange(spectralEvents); allResults.Plots.AddRange(plots); allResults.Sonogram = spectrogram; diff --git a/src/AudioAnalysisTools/Events/EventCommon.cs b/src/AudioAnalysisTools/Events/EventCommon.cs index 2696dbffb..8b2620d13 100644 --- a/src/AudioAnalysisTools/Events/EventCommon.cs +++ b/src/AudioAnalysisTools/Events/EventCommon.cs @@ -27,6 +27,12 @@ public abstract class EventCommon : EventBase, IDrawableEvent /// public string Profile { get; set; } + /// + /// Gets or sets the Decibel threshold at which the event was detected. + /// This is used during post-processing to group events according to the threshold of their detection.. + /// + public double DecibelDetectionThreshold { get; set; } + /// /// Gets the component name for this event. /// The component name should indicate what type of event this. diff --git a/src/AudioAnalysisTools/Events/EventFilters.cs b/src/AudioAnalysisTools/Events/EventFilters.cs index 69b27235f..d38b43ddc 100644 --- a/src/AudioAnalysisTools/Events/EventFilters.cs +++ b/src/AudioAnalysisTools/Events/EventFilters.cs @@ -77,17 +77,19 @@ public static List FilterOnBandwidth(List events, doub var filteredEvents = new List(); + var count = 0; foreach (var ev in events) { + count++; var bandwidth = ((SpectralEvent)ev).BandWidthHertz; if ((bandwidth > minBandwidth) && (bandwidth < maxBandwidth)) { - Log.Debug($" Event accepted: Actual bandwidth = {bandwidth}"); + Log.Debug($" Event{count} accepted: Actual bandwidth = {bandwidth}"); filteredEvents.Add(ev); } else { - Log.Debug($" Event rejected: Actual bandwidth = {bandwidth}"); + Log.Debug($" Event{count} rejected: Actual bandwidth = {bandwidth}"); continue; } } @@ -113,6 +115,24 @@ public static List FilterLongEvents(List events, d return outputEvents; } + /// + /// Filters lists of spectral events based on their DecibelDetectionThreshold. + /// + /// The list of events. + /// The Decibel Detection Threshold. + /// The filtered list of events. + public static List FilterOnDecibelDetectionThreshold(List events, double threshold) + { + if (threshold <= 0.0) + { + throw new Exception("Invalid Decibel Detection Threshold passed to method EventExtentions.FilterOnDecibelDetectionThreshold(). Minimum threshold <= 0 seconds"); + } + + // The following line does it all BUT it does not allow for feedback to the user. + var outputEvents = events.Where(ev => (ev.DecibelDetectionThreshold == threshold)).ToList(); + return outputEvents; + } + /// /// Filters lists of spectral events based on their duration. /// Note: The typical sigma threshold would be 2 to 3 sds. @@ -145,17 +165,19 @@ public static List FilterOnDuration(List events, doubl var filteredEvents = new List(); + var count = 0; foreach (var ev in events) { + count++; var duration = ((SpectralEvent)ev).EventDurationSeconds; if ((duration > minimumDurationSeconds) && (duration < maximumDurationSeconds)) { - Log.Debug($" Event accepted: Actual duration = {duration:F3}s"); + Log.Debug($" Event{count} accepted: Actual duration = {duration:F3}s"); filteredEvents.Add(ev); } else { - Log.Debug($" Event rejected: Actual duration = {duration:F3}s"); + Log.Debug($" Event{count} rejected: Actual duration = {duration:F3}s"); continue; } } @@ -197,12 +219,16 @@ public static List FilterEventsOnSyllableCountAndPeriodicity(List(); + int count = 0; foreach (var ev in events) { + count++; + // ignore non-composite events if (ev is CompositeEvent == false) { filteredEvents.Add(ev); + Log.Debug($" Event{count} accepted one syllable."); continue; } @@ -225,12 +251,12 @@ public static List FilterEventsOnSyllableCountAndPeriodicity(List maxSyllableCount) { - Log.Debug($" EventRejected: Actual syllable count > max: {syllableCount} > {maxSyllableCount}"); + Log.Debug($" Event{count} rejected: Actual syllable count > max: {syllableCount} > {maxSyllableCount}"); continue; } @@ -240,6 +266,7 @@ public static List FilterEventsOnSyllableCountAndPeriodicity(List FilterEventsOnSyllableCountAndPeriodicity(List= minExpectedPeriod && actualAvPeriod <= maxExpectedPeriod) { - Log.Debug($" EventAccepted: Actual average syllable interval = {actualAvPeriod:F3}"); + Log.Debug($" Event{count} accepted: Actual average syllable interval = {actualAvPeriod:F3}"); filteredEvents.Add(ev); } else { - Log.Debug($" EventRejected: Actual average syllable interval = {actualAvPeriod:F3}"); + Log.Debug($" Event{count} rejected: Actual average syllable interval = {actualAvPeriod:F3}"); } } } diff --git a/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs b/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs index fdfb61e73..812153eb2 100644 --- a/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs +++ b/src/AudioAnalysisTools/Events/Types/EventPostProcessing.cs @@ -17,25 +17,37 @@ public static class EventPostProcessing private const float SigmaThreshold = 3.0F; private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); + /// + /// This method post-processes a set of acoustic events that have been detected by all profiles at the passed decibel threshold. + /// + /// A list of events before post-processing. + /// The config file to be used for post-processing. + /// Decibel threshold used to detect the passed events. + /// A spectrogram of the events. + /// Time in seconds since beginning of the recording. + /// A list of events after post-processing. public static List PostProcessingOfSpectralEvents( List newEvents, PostProcessingConfig postprocessingConfig, + double decibelThreshold, BaseSonogram spectrogram, TimeSpan segmentStartOffset) { // The following generic post-processing steps are determined by config settings. - // Step 1: Combine overlapping events - events derived from all profiles. + // Step 1: Combine overlapping events - events derived from profiles. // Step 2: Combine possible syllable sequences and filter on excess syllable count. - // Step 3: Remove events whose bandwidth is too small or large. - // Step 4: Remove events that have excessive noise in their side-bands. + // Step 3: Remove events whose duration is too small or large. + // Step 4: Remove events whose bandwidth is too small or large. + // Step 5: Remove events that have excessive noise in their side-bands. - Log.Debug($"\nTotal EVENT COUNT BEFORE post-processing = {newEvents.Count}"); + Log.Debug($"\nBEFORE post-processing."); + Log.Debug($"TOTAL EVENTS detected by profiles at {decibelThreshold:F0} dB threshold = {newEvents.Count}"); // 1: Combine overlapping events. // This will be necessary where many small events have been found - possibly because the dB threshold is set low. - if (postprocessingConfig.CombineOverlappingEvents) + if (postprocessingConfig.CombineOverlappingEvents && (newEvents.Count > 0)) { - Log.Debug($"COMBINE EVENTS HAVING TEMPORAl *AND* SPECTRAL OVERLAP"); + Log.Debug($"COMBINE EVENTS HAVING TEMPORAL *AND* SPECTRAL OVERLAP"); newEvents = CompositeEvent.CombineOverlappingEvents(newEvents.Cast().ToList()); Log.Debug($" Event count after combining overlapped events = {newEvents.Count}"); } @@ -45,7 +57,7 @@ public static List PostProcessingOfSpectralEvents( // Such combinations will increase bandwidth of the event and this property can be used later to weed out unlikely events. var sequenceConfig = postprocessingConfig.SyllableSequence; - if (sequenceConfig.NotNull() && sequenceConfig.CombinePossibleSyllableSequence) + if (sequenceConfig.NotNull() && sequenceConfig.CombinePossibleSyllableSequence && (newEvents.Count > 0)) { Log.Debug($"COMBINE PROXIMAL EVENTS"); @@ -66,8 +78,7 @@ public static List PostProcessingOfSpectralEvents( 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"); + Log.Debug($" Expected Syllable Sequence: max={maxComponentCount}, 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}"); @@ -82,7 +93,7 @@ public static List PostProcessingOfSpectralEvents( var sdEventDuration = postprocessingConfig.Duration.DurationStandardDeviation; var minDuration = expectedEventDuration - (SigmaThreshold * sdEventDuration); var maxDuration = expectedEventDuration + (SigmaThreshold * sdEventDuration); - Log.Debug($" Duration: expected={expectedEventDuration}s, sd={sdEventDuration} min={minDuration}s, max={maxDuration}s"); + Log.Debug($" Duration: expected={expectedEventDuration}s, sd={sdEventDuration} min={minDuration:F3}s, max={maxDuration:F3}s"); newEvents = EventFilters.FilterOnDuration(newEvents, expectedEventDuration, sdEventDuration, SigmaThreshold); Log.Debug($" Event count after filtering on duration = {newEvents.Count}"); } @@ -130,19 +141,6 @@ public static List PostProcessingOfSpectralEvents( } } - // Write out the events to log. - Log.Debug($"FINAL event count = {newEvents.Count}."); - if (newEvents.Count > 0) - { - int counter = 0; - foreach (var ev in newEvents) - { - counter++; - var spEvent = (SpectralEvent)ev; - Log.Debug($" Event[{counter}]: Start={spEvent.EventStartSeconds:f1}; Duration={spEvent.EventDurationSeconds:f2}; Bandwidth={spEvent.BandWidthHertz} Hz"); - } - } - return newEvents; }