Skip to content

Commit

Permalink
Started set up of the Cisticola recognizer
Browse files Browse the repository at this point in the history
Issue #321 The yml file for this recognizer has not yet been edited for parameter values. I.e. only set up the architecture.
  • Loading branch information
towsey authored and atruskie committed Jun 12, 2020
1 parent 0e718d6 commit 86e471b
Show file tree
Hide file tree
Showing 3 changed files with 362 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---

# Golden-headed cisticola = Cisticola exilis
# Resample rate must be 2 X the desired Nyquist
ResampleRate: 8000
# SegmentDuration: units=seconds;
SegmentDuration: 60
# SegmentOverlap: units=seconds;
SegmentOverlap: 0

# Each of these profiles will be analyzed
# This profile is required for the species-specific recogniser and must have the current name.
Profiles:
CisticolaSyllable: !UpwardTrackParameters
ComponentName: Whip
SpeciesName: CisticolaExilis
FrameSize: 512
FrameStep: 256
WindowFunction: HANNING
#BgNoiseThreshold: 0.0
# min and max of the freq band to search
MinHertz: 100
MaxHertz: 200
MinDuration: 0.3
MaxDuration: 1.0
DecibelThreshold: 6.0

#################### POST-PROCESSING of EVENTS ###################

# A: First post-processing steps are to combine overlapping/proximal/sequential events
# 1: Combine overlapping events
#CombineOverlappingEvents: false

# 2: Combine each pair of Boobook syllables as one event
# Can also use this to "mop up" events in neighbourhood - these can be removed later.
CombinePossibleSyllableSequence: true
SyllableStartDifference: 3.0
SyllableHertzGap: 35

# B: Filter the events for excess activity in their upper and lower buffer zones
LowerHertzBuffer: 150
UpperHertzBuffer: 400

# C: Options to save results files
# 4: Available options for saving data files (case-sensitive): [False/Never | True/Always | WhenEventsDetected]
SaveIntermediateWavFiles: Never
SaveIntermediateCsvFiles: false
# Available options (case-sensitive): [False/Never | True/Always | WhenEventsDetected]
# "True" is useful when debugging but "WhenEventsDetected" is required for operational use.

# 5: Available options for saving
#SaveSonogramImages: True
SaveSonogramImages: WhenEventsDetected
# DisplayCsvImage is obsolete - ensure it remains set to: false
DisplayCsvImage: false
## End section for AnalyzeLongRecording

# Other config files to reference
HighResolutionIndicesConfig: "../Towsey.Acoustic.HiResIndicesForRecognisers.yml"

################################################################################
# Common settings
#Standard: &STANDARD
#EventThreshold: 0.2
#BgNoiseThreshold: 3.0

# This notation means the a profile has all of the settings that the Standard profile has,
# however, the DctDuration parameter has been overridden.
# <<: *STANDARD
# DctDuration: 0.3
...
219 changes: 219 additions & 0 deletions src/AnalysisPrograms/Recognizers/Birds/CisticolaExilis.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// <copyright file="CisticolaExilis.cs" company="QutEcoacoustics">
// 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).
// </copyright>

namespace AnalysisPrograms.Recognizers
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using Acoustics.Shared.ConfigFile;
using AnalysisBase;
using AnalysisPrograms.Recognizers.Base;
using AudioAnalysisTools;
using AudioAnalysisTools.Events;
using AudioAnalysisTools.Events.Types;
using AudioAnalysisTools.Indices;
using AudioAnalysisTools.WavTools;
using log4net;
using SixLabors.ImageSharp;
using TowseyLibrary;
using static AnalysisPrograms.Recognizers.GenericRecognizer;
using Path = System.IO.Path;

/// <summary>
/// A recognizer for calls of the golden-headed cisticola (Cisticola exilis), https://en.wikipedia.org/wiki/Golden-headed_cisticola .
/// It is a species of warbler in the family Cisticolidae, found in Australia and 13 Asian countries.
/// Grows to 9–11.5 centimetres (3.5–4.5 in) long, it is usually brown and cream in colour.
/// It produces a variety of calls distinct from other birds, which, according to the Sunshine Coast Council, range from a "teewip" to a "wheezz, whit-whit".
/// It has a very large range and population, which is thought to be increasing. It is not a threatened species.
/// </summary>
internal class CisticolaExilis : RecognizerBase
{
private static readonly ILog CisticolaLog = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

public override string Author => "Towsey";

public override string SpeciesName => "CisticolaExilis";

public override string Description => "[ALPHA] Detects acoustic events for the golden-headed cisticola.";

public override AnalyzerConfig ParseConfig(FileInfo file)
{
RuntimeHelpers.RunClassConstructor(typeof(CisticolaExilisConfig).TypeHandle);
var config = ConfigFile.Deserialize<CisticolaExilisConfig>(file);

// validation of configs can be done here
GenericRecognizer.ValidateProfileTagsMatchAlgorithms(config.Profiles, file);

// This call sets a restriction so that only one generic algorithm is used.
// CHANGE this to accept multiple generic algorithms as required.
//if (result.Profiles.SingleOrDefault() is ForwardTrackParameters)
if (config.Profiles?.Count == 1 && config.Profiles.First().Value is ForwardTrackParameters)
{
return config;
}

throw new ConfigFileException("CisticolaExilis expects one and only one ForwardTrack algorithm.", file);
}

/// <summary>
/// This method is called once per segment (typically one-minute segments).
/// </summary>
/// <param name="audioRecording">one minute of audio recording.</param>
/// <param name="config">config file that contains parameters used by all profiles.</param>
/// <param name="segmentStartOffset">when recording starts.</param>
/// <param name="getSpectralIndexes">not sure what this is.</param>
/// <param name="outputDirectory">where the recognizer results can be found.</param>
/// <param name="imageWidth"> assuming ????.</param>
/// <returns>recognizer results.</returns>
public override RecognizerResults Recognize(
AudioRecording audioRecording,
Config config,
TimeSpan segmentStartOffset,
Lazy<IndexCalculateResult[]> getSpectralIndexes,
DirectoryInfo outputDirectory,
int? imageWidth)
{
//class CisticolaExilisConfig is defined at bottom of this file.
var genericConfig = (CisticolaExilisConfig)config;
var recognizer = new GenericRecognizer();

RecognizerResults combinedResults = recognizer.Recognize(
audioRecording,
genericConfig,
segmentStartOffset,
getSpectralIndexes,
outputDirectory,
imageWidth);

// ################### POST-PROCESSING of EVENTS ###################
// Following two commented lines are different ways of casting lists.
//var newEvents = spectralEvents.Cast<EventCommon>().ToList();
//var spectralEvents = events.Select(x => (SpectralEvent)x).ToList();

// 1: Pull out the chirp events and calculate their frequency profiles.
var (chirpEvents, others) = combinedResults.NewEvents.FilterForEventType<ChirpEvent, EventCommon>();

if (combinedResults.NewEvents.Count == 0)
{
CisticolaLog.Debug($"Return zero events.");
return combinedResults;
}

// 2: Combine overlapping events. If the dB threshold is set low, may get lots of little events.
combinedResults.NewEvents = CompositeEvent.CombineOverlappingEvents(chirpEvents.Cast<EventCommon>().ToList());
CisticolaLog.Debug($"Event count after combining overlaps = {combinedResults.NewEvents.Count}");

// 3: Combine proximal events. If the dB threshold is set low, may get lots of little events.
if (genericConfig.CombinePossibleSyllableSequence)
{
// Convert events to spectral events for combining of possible sequences.
// Can also use this parameter to combine events that are in the upper or lower neighbourhood.
// Such combinations will increase bandwidth of the event and this property can be used later to weed out unlikely events.
var spectralEvents1 = combinedResults.NewEvents.Cast<SpectralEvent>().ToList();
var startDiff = genericConfig.SyllableStartDifference;
var hertzDiff = genericConfig.SyllableHertzGap;
combinedResults.NewEvents = CompositeEvent.CombineProximalEvents(spectralEvents1, TimeSpan.FromSeconds(startDiff), (int)hertzDiff);
CisticolaLog.Debug($"Event count after combining proximals = {combinedResults.NewEvents.Count}");
}

// Get the CisticolaSyllable config.
const string profileName = "CisticolaSyllable";
var configuration = (CisticolaExilisConfig)genericConfig;
var chirpConfig = (ForwardTrackParameters)configuration.Profiles[profileName];

// 4: Filter events on the amount of acoustic activity in their upper and lower neighbourhoods - 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.
// The bandwidth of these two neighbourhoods is determined by the following parameters.
// ########## These parameters could be specified by user in config.yml file.
var upperHertzBuffer = 400;
var lowerHertzBuffer = 150;

// The decibel threshold is currently set 5/6ths of the user specified threshold.
// THIS IS TO BE WATCHED. IT MAY PROVE TO BE INAPPROPRIATE TO HARD-CODE.
// Want the activity in buffer zones to be "somewhat" less than the user-defined threshold.
var neighbourhoodDbThreshold = chirpConfig.DecibelThreshold.Value * 0.8333;

if (upperHertzBuffer > 0 || lowerHertzBuffer > 0)
{
var spectralEvents2 = combinedResults.NewEvents.Cast<SpectralEvent>().ToList();
combinedResults.NewEvents = EventExtentions.FilterEventsOnNeighbourhood(
spectralEvents2,
combinedResults.Sonogram,
lowerHertzBuffer,
upperHertzBuffer,
segmentStartOffset,
neighbourhoodDbThreshold);

CisticolaLog.Debug($"Event count after filtering on neighbourhood = {combinedResults.NewEvents.Count}");
}

if (combinedResults.NewEvents.Count == 0)
{
CisticolaLog.Debug($"Return zero events.");
return combinedResults;
}

// 5: Filter on COMPONENT COUNT in Composite events.
int maxComponentCount = 2;
combinedResults.NewEvents = EventExtentions.FilterEventsOnCompositeContent(combinedResults.NewEvents, maxComponentCount);
CisticolaLog.Debug($"Event count after filtering on component count = {combinedResults.NewEvents.Count}");

// 6: Filter the events for duration in seconds
var minimumEventDuration = chirpConfig.MinDuration;
var maximumEventDuration = chirpConfig.MaxDuration;
if (genericConfig.CombinePossibleSyllableSequence)
{
minimumEventDuration *= 2.0;
maximumEventDuration *= 1.5;
}

combinedResults.NewEvents = EventExtentions.FilterOnDuration(combinedResults.NewEvents, minimumEventDuration.Value, maximumEventDuration.Value);
CisticolaLog.Debug($"Event count after filtering on duration = {combinedResults.NewEvents.Count}");

// 7: Filter the events for bandwidth in Hertz
double average = 280;
double sd = 40;
double sigmaThreshold = 3.0;
combinedResults.NewEvents = EventExtentions.FilterOnBandwidth(combinedResults.NewEvents, average, sd, sigmaThreshold);
CisticolaLog.Debug($"Event count after filtering on bandwidth = {combinedResults.NewEvents.Count}");

//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;
}

/*
/// <summary>
/// Summarize your results. This method is invoked exactly once per original file.
/// </summary>
public override void SummariseResults(
AnalysisSettings settings,
FileSegment inputFileSegment,
EventBase[] events,
SummaryIndexBase[] indices,
SpectralIndexBase[] spectralIndices,
AnalysisResult2[] results)
{
// No operation - do nothing. Feel free to add your own logic.
base.SummariseResults(settings, inputFileSegment, events, indices, spectralIndices, results);
}
*/

/// <inheritdoc cref="CisticolaExilisConfig"/> />
public class CisticolaExilisConfig : GenericRecognizerConfig, INamedProfiles<object>
{
public bool CombinePossibleSyllableSequence { get; set; } = false;

public double SyllableStartDifference { get; set; } = 0.5;

public double SyllableHertzGap { get; set; } = 200;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// <copyright file="CisticolaTests.cs" company="QutEcoacoustics">
// 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).
// </copyright>

namespace Acoustics.Test.AnalysisPrograms.Recognizers
{
using System;
using System.Collections.Generic;
using System.IO;
using Acoustics.Test.TestHelpers;
using Acoustics.Tools.Wav;
using global::AnalysisPrograms.Recognizers;
using global::AnalysisPrograms.SourcePreparers;
using global::AudioAnalysisTools.Events;
using global::AudioAnalysisTools.Events.Types;
using global::AudioAnalysisTools.WavTools;
using global::TowseyLibrary;
using Microsoft.VisualStudio.TestTools.UnitTesting;

/// <summary>
/// Species name = Golden-headed cisticola = Cisticola exilis.
/// </summary>
[TestClass]
public class CisticolaTests : OutputDirectoryTest
{
/// <summary>
/// The canonical recording used for this recognizer is a 31 second recording .
/// </summary>
private static readonly FileInfo TestAsset = PathHelper.ResolveAsset("Recordings", "gympie_np_1192_331618_20150818_054959_31_0.wav");
private static readonly FileInfo ConfigFile = PathHelper.ResolveConfigFile("RecognizerConfigFiles", "Towsey.CisticolaExilis.yml");
private static readonly AudioRecording Recording = new AudioRecording(TestAsset);
private static readonly CisticolaExilis Recognizer = new CisticolaExilis();

[TestMethod]
public void TestRecognizer()
{
var config = Recognizer.ParseConfig(ConfigFile);

var results = Recognizer.Recognize(
audioRecording: Recording,
config: config,
segmentStartOffset: TimeSpan.Zero,
getSpectralIndexes: null,
outputDirectory: this.TestOutputDirectory,
imageWidth: null);

var events = results.NewEvents;
var scoreTrack = results.ScoreTrack;
var plots = results.Plots;
var sonogram = results.Sonogram;

this.SaveTestOutput(
outputDirectory => GenericRecognizer.SaveDebugSpectrogram(results, null, outputDirectory, Recognizer.SpeciesName));

Assert.AreEqual(8, events.Count);
Assert.IsNull(scoreTrack);
Assert.AreEqual(1, plots.Count);
Assert.AreEqual(2667, sonogram.FrameCount);

Assert.IsInstanceOfType(events[1], typeof(CompositeEvent));

var secondEvent = (CompositeEvent)events[1];

Assert.AreEqual(5.375419501133787, secondEvent.EventStartSeconds);
Assert.AreEqual(6.0720181405895692, secondEvent.EventEndSeconds);
Assert.AreEqual(483, secondEvent.LowFrequencyHertz);
Assert.AreEqual(735, secondEvent.HighFrequencyHertz);
Assert.AreEqual(20.901882476071698, secondEvent.Score, TestHelper.AllowedDelta);
Assert.AreEqual(0.20786700431266195, secondEvent.ScoreNormalized, TestHelper.AllowedDelta);
}
}
}

0 comments on commit 86e471b

Please sign in to comment.