diff --git a/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs b/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs index c95e87d34..77fd1fcba 100644 --- a/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs +++ b/src/AnalysisPrograms/Recognizers/Birds/AnthusNovaeseelandiae.cs @@ -129,12 +129,135 @@ public override RecognizerResults Recognize( combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); PipitLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}"); + combinedResults.NewEvents = FilterEventsOnFrequencyProfile(combinedResults.NewEvents); + + //foreach (var ev in whistleEvents) + //{ + // // Calculate frequency profile score for event + // SetFrequencyProfileScore((WhistleEvent)ev); + //} + //UNCOMMENT following line if you want special debug spectrogram, i.e. with special plots. // NOTE: Standard spectrograms are produced by setting SaveSonogramImages: "True" or "WhenEventsDetected" in UserName.SpeciesName.yml config file. //GenericRecognizer.SaveDebugSpectrogram(territorialResults, genericConfig, outputDirectory, audioRecording.BaseName); return combinedResults; } + /// + /// This method assumes that the only events of interest are composite events. + /// + /// THe current list of events. + /// A list of composite events. + public static List FilterEventsOnFrequencyProfile(List events) + { + // select only the composite events. + //var compositeEvents = events.Select(x => (CompositeEvent)x).ToList(); + var (compositeEvents, others) = events.FilterForEventType(); + + if (compositeEvents == null) + { + return null; + } + + // get the composite track for each composite event. + var returnEvents = new List(); + foreach (var ev in compositeEvents) + { + var componentEvents = ev.ComponentEvents; + var points = EventExtentions.GetCompositeTrack(componentEvents).ToArray(); + var length = points.Length - 1; + + //WriteFrequencyProfile(points); + + // Only select events having strong downward slope in spectrogram. + var avFirstTwoEvents = (points[0].Hertz.Minimum + points[0].Hertz.Minimum) / 2; + var avLastTwoEvents = (points[length - 1].Hertz.Minimum + points[length - 2].Hertz.Minimum) / 2; + if (avFirstTwoEvents - avLastTwoEvents > 500) + { + returnEvents.Add(ev); + } + } + + return returnEvents; + } + + /// + /// The Boobook call syllable is shaped like an inverted "U". Its total duration is close to 0.15 seconds. + /// The rising portion lasts for 0.06s, followed by a turning portion, 0.03s, followed by the decending portion of 0.06s. + /// The constants for this method were obtained from the calls in a Gympie recording obtained by Yvonne Phillips. + /// + /// An event containing at least one forward track i.e. a chirp. + public static void SetFrequencyProfileScore(ChirpEvent ev) + { + const double risingDuration = 0.06; + const double gapDuration = 0.03; + const double fallingDuration = 0.06; + + var track = ev.Tracks.First(); + var profile = track.GetTrackFrequencyProfile().ToArray(); + + // get the first point + var firstPoint = track.Points.First(); + var frameDuration = firstPoint.Seconds.Maximum - firstPoint.Seconds.Minimum; + var risingFrameCount = (int)Math.Floor(risingDuration / frameDuration); + var gapFrameCount = (int)Math.Floor(gapDuration / frameDuration); + var fallingFrameCount = (int)Math.Floor(fallingDuration / frameDuration); + + var startSum = 0.0; + if (profile.Length >= risingFrameCount) + { + for (var i = 0; i <= risingFrameCount; i++) + { + startSum += profile[i]; + } + } + + int startFrame = risingFrameCount + gapFrameCount; + int endFrame = startFrame + fallingFrameCount; + var endSum = 0.0; + if (profile.Length >= endFrame) + { + for (var i = startFrame; i <= endFrame; i++) + { + endSum += profile[i]; + } + } + + // set score to 1.0 if the profile has inverted U shape. + double score = 0.0; + if (startSum > 0.0 && endSum < 0.0) + { + score = 1.0; + } + + ev.FrequencyProfileScore = score; + } + + /// + /// . + /// + /// List of spectral points. + public static void WriteFrequencyProfile(ISpectralPoint[] points) + { + /* Here are the frequency profiles of some events. + * Note that the first five frames (0.057 seconds) have positive slope and subsequent frames have negative slope. + * The final frames are likely to be echo and to be avoided. + * Therefore take the first 0.6s to calculate the positive slope, leave a gap of 0.025 seconds and then get negative slope from the next 0.6 seconds. + */ + + if (points != null) + { + var str = $"Track({points[0].Seconds.Minimum:F2}):"; + + foreach (var point in points) + { + str += $" {point.Hertz.Minimum},"; + } + + Console.WriteLine(str); + } + } + /* /// /// Summarize your results. This method is invoked exactly once per original file. diff --git a/src/AnalysisPrograms/Recognizers/Birds/BotaurusPoiciloptilus.cs b/src/AnalysisPrograms/Recognizers/Birds/BotaurusPoiciloptilus.cs index c96e8342f..1fb4d96d3 100644 --- a/src/AnalysisPrograms/Recognizers/Birds/BotaurusPoiciloptilus.cs +++ b/src/AnalysisPrograms/Recognizers/Birds/BotaurusPoiciloptilus.cs @@ -93,15 +93,6 @@ public override RecognizerResults Recognize( //var newEvents = spectralEvents.Cast().ToList(); //var spectralEvents = events.Select(x => (SpectralEvent)x).ToList(); - // Uncomment the next line when want to obtain the event frequency profiles. - // WriteFrequencyProfiles(chirpEvents); - - //foreach (var ev in whistleEvents) - //{ - // // Calculate frequency profile score for event - // SetFrequencyProfileScore((WhistleEvent)ev); - //} - if (combinedResults.NewEvents.Count == 0) { BitternLog.Debug($"Return zero events."); @@ -185,6 +176,15 @@ public override RecognizerResults Recognize( combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold); BitternLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}"); + // Uncomment the next line when want to obtain the event frequency profiles. + // WriteFrequencyProfiles(chirpEvents); + + //foreach (var ev in whistleEvents) + //{ + // // Calculate frequency profile score for event + // SetFrequencyProfileScore((WhistleEvent)ev); + //} + //UNCOMMENT following line if you want special debug spectrogram, i.e. with special plots. // NOTE: Standard spectrograms are produced by setting SaveSonogramImages: "True" or "WhenEventsDetected" in UserName.SpeciesName.yml config file. //GenericRecognizer.SaveDebugSpectrogram(territorialResults, genericConfig, outputDirectory, audioRecording.BaseName); diff --git a/src/AudioAnalysisTools/Events/EventExtentions.cs b/src/AudioAnalysisTools/Events/EventExtentions.cs index 3ac65c5f2..5d6f63a25 100644 --- a/src/AudioAnalysisTools/Events/EventExtentions.cs +++ b/src/AudioAnalysisTools/Events/EventExtentions.cs @@ -9,6 +9,7 @@ namespace AudioAnalysisTools.Events using System.Linq; using AudioAnalysisTools.Events.Types; using AudioAnalysisTools.StandardSpectrograms; + using MoreLinq; using TowseyLibrary; public static class EventExtentions @@ -360,5 +361,26 @@ public static List FilterEventsOnCompositeContent( 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. + /// The points in the returned array are in temporal order. + /// + /// List of spectral events. + public static IEnumerable GetCompositeTrack(List events) + { + var spectralEvents = events.Select(x => (WhipEvent)x); + var points = spectralEvents.SelectMany(x => x.Tracks.SelectMany(t => t.Points)); + + // group all the points by their start time. + var groupStarts = points.GroupBy(p => p.Seconds); + + // for each group, for each point in group, choose the point having maximum (amplitude) value. + // Since there maybe multiple points having maximum amplitude, we pick the first one. + var maxAmplitudePoints = groupStarts.Select(g => g.MaxBy(p => p.Value).First()); + + return maxAmplitudePoints.OrderBy(p => p); + } } }