From 8a1539a371b05510ef3483cfabc45c7bfdf6a1f1 Mon Sep 17 00:00:00 2001 From: towsey Date: Mon, 13 Apr 2020 12:28:23 +1000 Subject: [PATCH] More work on Track class Issue #297 --- .../Events/Tracks/PointData.cs | 43 +++++ src/AudioAnalysisTools/Events/Tracks/Track.cs | 163 +++++++++++++++++- .../Scales/LinearSecondsScale.cs | 25 +++ .../Scales/LinearTemporalScale.cs | 37 ---- src/AudioAnalysisTools/UnitConverters.cs | 61 +++++-- 5 files changed, 277 insertions(+), 52 deletions(-) create mode 100644 src/AudioAnalysisTools/Events/Tracks/PointData.cs create mode 100644 src/AudioAnalysisTools/Scales/LinearSecondsScale.cs delete mode 100644 src/AudioAnalysisTools/Scales/LinearTemporalScale.cs diff --git a/src/AudioAnalysisTools/Events/Tracks/PointData.cs b/src/AudioAnalysisTools/Events/Tracks/PointData.cs new file mode 100644 index 000000000..8ab54a03d --- /dev/null +++ b/src/AudioAnalysisTools/Events/Tracks/PointData.cs @@ -0,0 +1,43 @@ +// +// 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 +{ + using System.Collections.Generic; + using System.Linq; + using AudioAnalysisTools.Events.Drawing; + using SixLabors.ImageSharp; + using SixLabors.ImageSharp.Processing; + + public class PointData + { + public ISet Points { get; } + + public void DrawPointsAsFill(IImageProcessingContext graphics, EventRenderingOptions options) + { + // overlay point data on image with 50% opacity + // TODO: a much more efficient implementation exists if we derive from Region and convert + // our set to a region. + foreach (var point in this.Points) + { + var area = options.Converters.GetPixelRectangle(point); + graphics.Fill(options.Fill, area); + } + } + + public void DrawPointsAsPath(IImageProcessingContext graphics, EventRenderingOptions options) + { + // visits each point once + // assumes each point describes a line + // assumes a SortedSet is used (and that iteration order is signficant, unlike with HashSet) + var path = this.Points.Select(x => options.Converters.GetPoint(x)).ToArray(); + + // note not using AA here + // note could base pen thickness off ISpectralPoint thickness for a more accurate representation + graphics.DrawLines( + options.Border, + path); + } + } +} \ No newline at end of file diff --git a/src/AudioAnalysisTools/Events/Tracks/Track.cs b/src/AudioAnalysisTools/Events/Tracks/Track.cs index 688e54d8e..83ed50694 100644 --- a/src/AudioAnalysisTools/Events/Tracks/Track.cs +++ b/src/AudioAnalysisTools/Events/Tracks/Track.cs @@ -4,22 +4,181 @@ namespace AudioAnalysisTools.Events.Interfaces { + using System; using System.Collections.Generic; + using System.Linq; + using Acoustics.Shared; using AudioAnalysisTools.Events.Drawing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; public class Track : ITrack { + private readonly UnitConverters converter; + /// /// Initializes a new instance of the class. /// Constructor. /// - public Track() + public Track(UnitConverters converter) + { + this.converter = converter; + this.Points = new HashSet(); + } + + //public ISet Points => throw new System.NotImplementedException(); + + public ISet Points { get; } + + /// + /// Adds a new point to track given the fram, freq bin and amplitude. + /// + /// The frame number. + /// The freq bin number. + /// The amplitude at given point. + public void SetPoint(int frame, int bin, double amplitude) + { + var sStart = this.converter.GetSecondsDurationFromFrameCount(frame); + var seconds = new Interval(sStart, sStart + this.converter.SecondsScale.SecondsPerFrame); + var hertzLow = this.converter.PixelsToHertz(bin); + var hertz = new Interval(hertzLow, hertzLow + this.converter.HertzPerFreqBin); + var point = new SpectralPoint(seconds, hertz, amplitude); + this.Points.Add(point); + } + + public int PointCount() { + return this.Points.Count; } - public ISet Points => throw new System.NotImplementedException(); + //public int GetStartFrame() + //{ + // return this.Points x => x.Seconds.Min(); + //} + + public double GetStartTimeSeconds() + { + return this.Points.Min(x => x.Seconds.Minimum); + } + + //public int GetEndFrame() + //{ + // return this.frameIds.Max(); + //} + + public double GetEndTimeSeconds(double frameStepSeconds) + { + return this.Points.Max(x => x.Seconds.Maximum); + } + + //public int GetTrackFrameCount() + //{ + // return this.frameIds.Max() - this.frameIds.Min() + 1; + //} + + //public double GetTrackDurationSeconds(double frameStepSeconds) + //{ + // return this.GetTrackFrameCount() * frameStepSeconds; + //} + + //public int GetBottomFreqBin() + //{ + // return this.freqBinIds.Min(); + //} + + public int GetBottomFreqHertz(double hertzPerBin) + { + return (int)Math.Round(this.Points.Min(x => x.Hertz.Minimum)); + } + + //public int GetTopFreqBin() + //{ + // return this.freqBinIds.Max(); + //} + + public int GetTopFreqHertz(double hertzPerBin) + { + return (int)Math.Round(this.Points.Max(x => x.Hertz.Maximum)); + } + + //public int GetTrackFreqBinCount() + //{ + // return this.freqBinIds.Max() - this.freqBinIds.Min() + 1; + //} + + public int GetTrackBandWidthHertz(double hertzPerBin) + { + var minHertz = this.Points.Min(x => x.Hertz.Minimum); + var maxHertz = this.Points.Max(x => x.Hertz.Maximum); + return (int)Math.Round(maxHertz - minHertz); + } + + /// + /// returns the track as a matrix of seconds, Hertz and amplitude values. + /// + /// the time scale. + /// The frequqwency scale. + /// The track matrix. + //public double[,] GetTrackAsMatrix(double frameStepSeconds, double hertzPerBin) + //{ + // var trackMatrix = new double[this.PointCount(), 3]; + // for (int i = 0; i < this.PointCount(); i++) + // { + // trackMatrix[i, 0] = this.frameIds[i] * frameStepSeconds; + // trackMatrix[i, 1] = this.freqBinIds[i] * hertzPerBin; + // trackMatrix[i, 2] = this.amplitudeSequence[i]; + // } + + // return trackMatrix; + //} + + /// + /// Returns an array that has the same number of time frames as the track. + /// Each element contains the highest frequency (Hertz) for that time frame. + /// NOTE: For tracks that include extreme frequency modulation (e.g. clicks and vertical tracks), + /// this method returns the highest frequency value in each time frame. + /// + /// the frequency scale. + /// An array of Hertz values. + //public int[] GetTrackAsSequenceOfHertzValues(double hertzPerBin) + //{ + // int pointCount = this.frameIds.Count; + // var hertzTrack = new int[this.GetTrackFrameCount()]; + // for (int i = 0; i < pointCount; i++) + // { + // int frameId = this.frameIds[i]; + // int frequency = (int)Math.Round(this.freqBinIds[i] * hertzPerBin); + // if (hertzTrack[frameId] < frequency) + // { + // hertzTrack[frameId] = frequency; + // } + // } + + // return hertzTrack; + //} + + /// + /// Returns the maximum amplitude in each time frame. + /// + /// an array of amplitude values. + //public double[] GetAmplitudeOverTimeFrames() + //{ + // var frameCount = this.GetTrackFrameCount(); + // int startFrame = this.GetStartFrame(); + // var amplitudeArray = new double[frameCount]; + + // // add in amplitude values + // for (int i = 0; i < this.amplitudeSequence.Count; i++) + // { + // int elapsedFrames = this.frameIds[i] - startFrame; + // if (amplitudeArray[elapsedFrames] < this.amplitudeSequence[i]) + // { + // amplitudeArray[elapsedFrames] = this.amplitudeSequence[i]; + // } + // } + + // return amplitudeArray; + //} public void Draw(IImageProcessingContext graphics, EventRenderingOptions options) where T : struct, IPixel diff --git a/src/AudioAnalysisTools/Scales/LinearSecondsScale.cs b/src/AudioAnalysisTools/Scales/LinearSecondsScale.cs new file mode 100644 index 000000000..76b727790 --- /dev/null +++ b/src/AudioAnalysisTools/Scales/LinearSecondsScale.cs @@ -0,0 +1,25 @@ +// +// 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.Scales +{ + using System; + + /// + /// This class converts between frames and time duration in seconds. + /// A complication arises when the frames of a spectrogram are overlapped. + /// + public class LinearSecondsScale + { + public LinearSecondsScale(int sampleRate, int frameSize, int stepSize) + { + this.SecondsPerFrame = frameSize / (double)sampleRate; + this.SecondsPerFrameStep = stepSize / (double)sampleRate; + } + + public double SecondsPerFrame { get; } + + public double SecondsPerFrameStep { get; } + } +} diff --git a/src/AudioAnalysisTools/Scales/LinearTemporalScale.cs b/src/AudioAnalysisTools/Scales/LinearTemporalScale.cs deleted file mode 100644 index 6096a6081..000000000 --- a/src/AudioAnalysisTools/Scales/LinearTemporalScale.cs +++ /dev/null @@ -1,37 +0,0 @@ -// -// 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.Scales -{ - using System; - - /// - /// This class converts between frames and time duration in seconds. - /// A complication arises when the frames of a spectrogram are overlapped. - /// - public class LinearTemporalScale - { - public LinearTemporalScale(int sampleRate, int frameSize, int stepSize) - { - this.FrameDurationSeconds = frameSize / (double)sampleRate; - this.FrameStepSeconds = stepSize / (double)sampleRate; - } - - public double FrameDurationSeconds { get; } - - public double FrameStepSeconds { get; } - - public double GetSecondsDurationFromFrameCount(int frameCount) - { - return frameCount * this.FrameStepSeconds; - } - - //public double FromDelta(double yDelta) - //{ - // var normalDelta = yDelta / this.rd; - // var d = normalDelta * this.dd; - // return this.clamp ? d.Clamp(this.d1, this.d2) : d; - //} - } -} diff --git a/src/AudioAnalysisTools/UnitConverters.cs b/src/AudioAnalysisTools/UnitConverters.cs index 096aad9e7..3c90aaae0 100644 --- a/src/AudioAnalysisTools/UnitConverters.cs +++ b/src/AudioAnalysisTools/UnitConverters.cs @@ -4,11 +4,11 @@ namespace AudioAnalysisTools { + using System; using Acoustics.Shared; using AudioAnalysisTools.Events.Interfaces; using AudioAnalysisTools.Scales; using SixLabors.ImageSharp; - using System; public class UnitConverters { @@ -41,9 +41,9 @@ public UnitConverters(double segmentStartOffset, int sampleRate, int frameSize) this.FrameOverlap = 0.0; this.NyquistFrequency = sampleRate / 2; int totalBinCount = frameSize / 2; - - this.TimeScale = new LinearTemporalScale(sampleRate, frameSize, frameSize); - this.SpectralScale = new LinearScale((0.0, this.NyquistFrequency), (totalBinCount, 0.0)); // invert y-axis + this.HertzPerFreqBin = this.NyquistFrequency / totalBinCount; + this.SecondsPerFrame = frameSize / (double)sampleRate; + this.SecondsPerFrameStep = frameSize / (double)sampleRate; } /// @@ -60,8 +60,8 @@ public UnitConverters(double segmentStartOffset, int sampleRate, int frameSize, { this.StepSize = stepSize; this.FrameOverlap = 1 - (stepSize / (double)frameSize); - int totalBinCount = frameSize / 2; - this.TimeScale = new LinearTemporalScale(sampleRate, frameSize, stepSize); + this.SecondsPerFrame = frameSize / (double)sampleRate; + this.SecondsPerFrameStep = stepSize / (double)sampleRate; } /// @@ -79,8 +79,8 @@ public UnitConverters(double segmentStartOffset, int sampleRate, int frameSize, { this.FrameOverlap = frameOverlap; this.StepSize = (int)Math.Round(frameSize * (1 - frameOverlap)); - int totalBinCount = frameSize / 2; - this.TimeScale = new LinearTemporalScale(sampleRate, frameSize, this.StepSize); + this.SecondsPerFrame = frameSize / (double)sampleRate; + this.SecondsPerFrameStep = this.StepSize / (double)sampleRate; } public double SegmentStartOffset { get; } @@ -95,6 +95,12 @@ public UnitConverters(double segmentStartOffset, int sampleRate, int frameSize, public int NyquistFrequency { get; } + public double SecondsPerFrame { get; } + + public double SecondsPerFrameStep { get; } + + public double HertzPerFreqBin { get; } + /// /// Gets the temporal scale. /// @@ -104,12 +110,12 @@ public UnitConverters(double segmentStartOffset, int sampleRate, int frameSize, public LinearScale TemporalScale { get; } /// - /// Gets the temporal scale. + /// Gets the temporal scale in second units. /// /// - /// Measured in seconds per pixel. + /// Measured in seconds per spectrogram frame. /// - public LinearTemporalScale TimeScale { get; } + public LinearSecondsScale SecondsScale { get; } /// /// Gets the spectral scale. @@ -170,9 +176,38 @@ public SizeF GetSize(ISpectralEvent @event) public double RecordingRelativeToSegmentRelative(double seconds) => seconds + this.SegmentStartOffset; - public double LinearScale_SecondsDurationFromFrameCount(int frameCount) + //public double SecondsDurationFromFrameCount(int frameCount) + //{ + // return this.SecondsScale.GetSecondsDurationFromFrameCount(frameCount); + //} + + //public int FrameCountFromSecondsDuration(double secondsDuration) + //{ + // return this.SecondsScale.GetFrameCountFromSecondsDuration(secondsDuration); + //} + + /// + /// Returns the duration in seconds of the passed number of frames. + /// NOTE: In the case where frames are overlapped, the last frame in any sequence is longer than the frame step. + /// This correction becomes sgnificant when the frameCount is small. + /// + /// The number of frames. + /// Duration inseconds. + public double GetSecondsDurationFromFrameCount(int frameCount) + { + return ((frameCount - 1) * this.SecondsPerFrameStep) + this.SecondsPerFrame; + } + + /// + /// Returns the number of frames for the passed duration in seconds. + /// TODO: Yet to be determined whether the exact frame count should be round, floor or celing. + /// + /// The elapsed time. + /// The number of frames. + public int GetFrameCountFromSecondsDuration(double seconds) { - return this.TimeScale.GetSecondsDurationFromFrameCount(frameCount); + int stepsMinusOne = (int)Math.Round((seconds - this.SecondsPerFrame) / this.SecondsPerFrameStep); + return 1 + stepsMinusOne; } } }