diff --git a/src/Acoustics.Shared/ImageSharp/Drawing.cs b/src/Acoustics.Shared/ImageSharp/Drawing.cs index 5db984cbb..05d78c5cd 100644 --- a/src/Acoustics.Shared/ImageSharp/Drawing.cs +++ b/src/Acoustics.Shared/ImageSharp/Drawing.cs @@ -6,6 +6,7 @@ namespace Acoustics.Shared.ImageSharp { using System; using System.IO; + using System.Linq; using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -140,7 +141,7 @@ public static Image NewImage(int width, int height, Color fill) /// /// A specialized class the deals with drawing graphics without anti-aliasing. - /// It deal with two issues: + /// It deals with two issues: /// - Lines in ImageSharp are drawn on the centre pixel. Without AA they're drawn a pixel /// off. This class draws all lines with +0.0,+0.5 coordinates. /// See https://github.com/SixLabors/ImageSharp.Drawing/issues/28 @@ -159,41 +160,36 @@ public NoAA(IImageProcessingContext context) public void DrawLine(IPen pen, int x1, int y1, int x2, int y2) { - var a = new PointF(x1, y1) + Bug28Offset; - var b = new PointF(x2, y2) + Bug28Offset; - - this.context.DrawLines( - Drawing.NoAntiAlias, - pen, - a, - b); + this.DrawLines(pen, new Point(x1, y1), new Point(x2, y2)); } - public void DrawLine(IPen pen, params PointF[] points) + public void DrawLines(IPen pen, params PointF[] points) { - for (int i = 0; i < points.Length; i++) + // i've no idea why, but repeating the first point and last point + // and adding random offsets in reduces visual errors in line drawing! + var slope = points[0].Y.CompareTo(points[^1].Y) switch { - points[i].Offset(Bug28Offset); - } + -1 => 0.0f, + 0 => 0, + 1 => 0.5f, + _ => throw new NotImplementedException(), + }; + var offset = new PointF(slope, Bug28Offset.Y); + var modifiedPoints = points + .Select(p => p + offset) + .Prepend(points[0] + Bug28Offset) + .Append(points[^1] + Bug28Offset) + .ToArray(); this.context.DrawLines( NoAntiAlias, pen, - points); + modifiedPoints); } - public void DrawLine(Color color, float thickness, params PointF[] points) + public void DrawLines(Color color, float thickness, params PointF[] points) { - for (int i = 0; i < points.Length; i++) - { - points[i].Offset(Bug28Offset); - } - - this.context.DrawLines( - NoAntiAlias, - color, - thickness, - points); + this.DrawLines(new Pen(color, thickness), points); } public void DrawRectangle(Pen pen, int x1, int y1, int x2, int y2) diff --git a/src/Acoustics.Shared/ImageSharp/ImageSharpExtensions.cs b/src/Acoustics.Shared/ImageSharp/ImageSharpExtensions.cs index 3e4bd0a08..513f5e45a 100644 --- a/src/Acoustics.Shared/ImageSharp/ImageSharpExtensions.cs +++ b/src/Acoustics.Shared/ImageSharp/ImageSharpExtensions.cs @@ -235,6 +235,7 @@ public static void Clear(this IImageProcessingContext context, Color color) /// /// Apparently blending pixels with transparency is not supported for Rgb24 images. /// See the FillDoesNotBlendByDefault.Test smoke test. + /// BUG: Blending does not occur with fill https://github.com/SixLabors/ImageSharp.Drawing/issues/38. /// /// The drawing context. /// The brush to fill with. diff --git a/src/Acoustics.Shared/Interval.cs b/src/Acoustics.Shared/Interval.cs index ecb812410..3ea86442c 100644 --- a/src/Acoustics.Shared/Interval.cs +++ b/src/Acoustics.Shared/Interval.cs @@ -252,10 +252,26 @@ public override int GetHashCode() /// String representation. /// public override string ToString() + { + return this.ToString(false); + } + + /// + /// Gets string representation of the Interval. + /// technially incorrectly representing this value. + /// + /// + /// If true only prints interval data and not type name. + /// + /// + /// String representation. + /// + public string ToString(bool suppressName) { var left = this.IsMinimumInclusive ? "[" : "("; var right = this.IsMaximumInclusive ? "]" : ")"; - return $"{nameof(Interval)}: {left}{this.Minimum}, {this.Maximum}{right}"; + var name = suppressName ? string.Empty : nameof(Interval) + ": "; + return $"{name}{left}{this.Minimum}, {this.Maximum}{right}"; } public int CompareTo(Interval other) diff --git a/src/Acoustics.Tools/Audio/CustomSpectrogramUtility.cs b/src/Acoustics.Tools/Audio/CustomSpectrogramUtility.cs index deefd0007..3ad5f78cf 100644 --- a/src/Acoustics.Tools/Audio/CustomSpectrogramUtility.cs +++ b/src/Acoustics.Tools/Audio/CustomSpectrogramUtility.cs @@ -589,7 +589,7 @@ private static double[] InvokeDotNetFft(double[] data, int windowSize, int coeff // calculate power of the DC value - first column of matrix // foreach time step or frame - for (int i = 0; i < frameCount; i++) + for (int i = 0; i < frameCount; i++) { if (amplitudeM[i, 0] < epsilon) { diff --git a/src/AudioAnalysisTools/Events/InstantEvent.cs b/src/AudioAnalysisTools/Events/InstantEvent.cs index 5c4577866..05ebe0116 100644 --- a/src/AudioAnalysisTools/Events/InstantEvent.cs +++ b/src/AudioAnalysisTools/Events/InstantEvent.cs @@ -15,7 +15,7 @@ public override void Draw(IImageProcessingContext graphics, EventRenderingOption { // simply draw a full-height line var startPixel = options.Converters.SecondsToPixels(this.EventStartSeconds); - graphics.NoAA().DrawLine( + graphics.NoAA().DrawLines( options.Border, new PointF(startPixel, 0), new PointF(startPixel, graphics.GetCurrentSize().Height)); diff --git a/src/AudioAnalysisTools/Events/Interfaces/IPointData.cs b/src/AudioAnalysisTools/Events/Interfaces/IPointData.cs index d4415cf7c..ab0b70dd8 100644 --- a/src/AudioAnalysisTools/Events/Interfaces/IPointData.cs +++ b/src/AudioAnalysisTools/Events/Interfaces/IPointData.cs @@ -5,6 +5,7 @@ namespace AudioAnalysisTools { using System.Linq; + using Acoustics.Shared.ImageSharp; using AudioAnalysisTools.Events.Drawing; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; @@ -53,14 +54,19 @@ public void DrawPointsAsFill(IImageProcessingContext graphics, EventRenderingOpt public void DrawPointsAsPath(IImageProcessingContext graphics, EventRenderingOptions options) { // visits each point once - // assumes each point describes a line + // assumes each point pair describes a line // assumes a SortedSet is used (and that iteration order is signficant, unlike with HashSet) // TODO: maybe add an orderby? - var path = this.Points.Select(x => options.Converters.GetPoint(x)).ToArray(); + var path = this + .Points + .OrderBy(x => x) + .Select(options.Converters.GetPoint) + .ToArray(); - // note not using AA here - // note could base pen thickness off ISpectralPoint thickness for a more accurate representation - graphics.DrawLines( + // note: using AA here + // note: could base pen thickness off ISpectralPoint thickness for a more accurate representation + //graphics.Draw() + graphics.NoAA().DrawLines( options.Border, path); } diff --git a/src/AudioAnalysisTools/Events/SpectralPoint.cs b/src/AudioAnalysisTools/Events/SpectralPoint.cs index 4420bc789..5f78f880b 100644 --- a/src/AudioAnalysisTools/Events/SpectralPoint.cs +++ b/src/AudioAnalysisTools/Events/SpectralPoint.cs @@ -63,5 +63,10 @@ public int CompareTo(object obj) return this.Value.CompareTo(otherPoint.Value); } + + public override string ToString() + { + return $"{nameof(SpectralPoint)}: {this.Seconds.ToString(true)} s, {this.Hertz.ToString(true)} Hz, {this.Value} value"; + } } } diff --git a/src/AudioAnalysisTools/Events/TemporalEvent.cs b/src/AudioAnalysisTools/Events/TemporalEvent.cs index 89e31e6ee..08eb210ec 100644 --- a/src/AudioAnalysisTools/Events/TemporalEvent.cs +++ b/src/AudioAnalysisTools/Events/TemporalEvent.cs @@ -20,13 +20,13 @@ public override void Draw(IImageProcessingContext graphics, EventRenderingOption { // simply draw a full-height lines either side of the vent var startPixel = options.Converters.SecondsToPixels(this.EventStartSeconds); - graphics.NoAA().DrawLine( + graphics.NoAA().DrawLines( options.Border, new PointF(startPixel, 0), new PointF(startPixel, graphics.GetCurrentSize().Height)); var endPixel = options.Converters.SecondsToPixels(this.EventEndSeconds); - graphics.NoAA().DrawLine( + graphics.NoAA().DrawLines( options.Border, new PointF(endPixel, 0), new PointF(endPixel, graphics.GetCurrentSize().Height)); diff --git a/src/AudioAnalysisTools/Events/Tracks/Track.cs b/src/AudioAnalysisTools/Events/Tracks/Track.cs index 8a68308b5..cd0a66156 100644 --- a/src/AudioAnalysisTools/Events/Tracks/Track.cs +++ b/src/AudioAnalysisTools/Events/Tracks/Track.cs @@ -27,12 +27,31 @@ public class Track : ITrack /// Initializes a new instance of the class. /// Constructor. /// + /// + /// A reference to unit conversions this track class should use to + /// convert spectrogram data to real units. + /// public Track(UnitConverters converter) { this.converter = converter; this.Points = new SortedSet(); } + /// + /// + /// A set of initial points to add into the point data collection. + /// + public Track( + UnitConverters converter, + params (int Frame, int Bin, double Amplitude)[] initialPoints) + : this(converter) + { + foreach (var point in initialPoints) + { + this.SetPoint(point.Frame, point.Bin, point.Amplitude); + } + } + public int PointCount => this.Points.Count; public double StartTimeSeconds => this.converter.SegmentStartOffset + this.Points.Min(x => x.Seconds.Minimum); @@ -149,6 +168,11 @@ public double[] GetAmplitudeOverTimeFrames() /// /// Draws the track on an image given by its processing context. /// + /// + /// Implementation is fairly simple. It sorts all points by the default IComparable method + /// which sorts points by time (ascending), frequency (ascending) and finally value. + /// The sorted collection is then used as a set of points to connect lines to. + /// public void Draw(IImageProcessingContext graphics, EventRenderingOptions options) { ((IPointData)this).DrawPointsAsPath(graphics, options); diff --git a/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs b/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs index 26ab0dc51..11fc2d9fd 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/BaseSonogram.cs @@ -781,7 +781,7 @@ public static Image DrawTitleBarOfGrayScaleSpectrogram(string title, int if (tag.NotNull()) { - g.NoAA().DrawLine( + g.NoAA().DrawLines( tag.Value, 1f, new PointF(0, 0), diff --git a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs index 05d0429ac..6cc930d50 100644 --- a/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs +++ b/src/AudioAnalysisTools/StandardSpectrograms/SpectrogramTools.cs @@ -82,7 +82,7 @@ public static Image GetSonogramPlusCharts( { spectrogram = Image_MultiTrack.OverlayScoresAsRedTransparency(spectrogram, hits); - // following line needs to be reworked if want to call OverlayRainbowTransparency(hits); + // following line needs to be reworked if want to call OverlayRainbowTransparency(hits); //image.OverlayRainbowTransparency(hits); } diff --git a/src/AudioAnalysisTools/UnitConverters.cs b/src/AudioAnalysisTools/UnitConverters.cs index 9a3ccfc17..45f9469a4 100644 --- a/src/AudioAnalysisTools/UnitConverters.cs +++ b/src/AudioAnalysisTools/UnitConverters.cs @@ -181,6 +181,16 @@ public PointF GetPoint(ISpectralPoint point) (float)this.SpectralScale.To(point.Hertz.Maximum)); } + public PointF GetPointCentroid(ISpectralPoint point) + { + var centerX = point.Seconds.Center(); + var centerY = point.Hertz.Center(); + + return new PointF( + (float)this.TemporalScale.To(centerX), + (float)this.SpectralScale.To(centerY)); + } + /// /// Gets the width and height of an event. /// @@ -223,12 +233,12 @@ public int FrameFromStartTime(double startTime) public double GetStartTimeInSecondsOfFrame(int frameId) { - return frameId * this.SecondsPerFrameStep; + return this.SegmentStartOffset + (frameId * this.SecondsPerFrameStep); } public double GetEndTimeInSecondsOfFrame(int frameId) { - return this.GetStartTimeInSecondsOfFrame(frameId) + this.SecondsPerFrame; + return this.SegmentStartOffset + (this.GetStartTimeInSecondsOfFrame(frameId) + this.SecondsPerFrame); } /// diff --git a/tests/Acoustics.Test/AudioAnalysisTools/Events/SpectralPointTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/Events/SpectralPointTests.cs index 89e0aef85..f6be9ca76 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/Events/SpectralPointTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/Events/SpectralPointTests.cs @@ -7,6 +7,7 @@ namespace Acoustics.Test.AudioAnalysisTools.Events using Acoustics.Shared; using global::AudioAnalysisTools.Events; using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; [TestClass] public class SpectralPointTests @@ -61,5 +62,58 @@ public void TestHashCode() Assert.AreEqual(a.GetHashCode(), b.GetHashCode()); Assert.AreNotEqual(a.GetHashCode(), c.GetHashCode()); } + + [DataTestMethod] + [DataRow(0.1, 0.2, 1000, 2000, 3, -1)] + [DataRow(5.0, 5.2, 1000, 2000, 3, 1)] + [DataRow(1.0, 5.0, 1000, 2000, 3, -1)] + [DataRow(1.0, 5.0, 7000, 9000, 3, 1)] + [DataRow(1.0, 5.0, 5000, 6000, 1, -1)] + [DataRow(1.0, 5.0, 5000, 6000, 4, 1)] + [DataRow(1.0, 5.0, 5000, 6000, 3, 0)] + public void TestComparer(double t1, double t2, double h1, double h2, double v, int expected) + { + var other = new SpectralPoint( + (1, 5), + (5000, 6000), + 3); + + var test = new SpectralPoint( + (t1, t2), + (h1, h2), + v); + + var actual = test.CompareTo(other); + var actualInverse = other.CompareTo(test); + + Assert.AreEqual(expected, actual); + + var inverseExpected = expected switch + { + -1 => 1, + 0 => 0, + 1 => -1, + _ => throw new NotSupportedException(), + }; + Assert.AreEqual(inverseExpected, actualInverse); + } + + [TestMethod] + public void TestToString() + { + Interval seconds = (1, 5); + Interval hertz = (5000, 6000); + + var test = new SpectralPoint( + seconds, + hertz, + 3); + + var actual = test.ToString(); + + Assert.AreEqual( + $"SpectralPoint: [1, 5) s, [5000, 6000) Hz, 3 value", + actual); + } } } diff --git a/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTests.cs b/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTests.cs index 399336099..495d367e3 100644 --- a/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTests.cs +++ b/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTests.cs @@ -4,32 +4,43 @@ namespace Acoustics.Test.AudioAnalysisTools.Events.Tracks { - using System; - using System.Collections.Generic; - using System.Text; using global::AudioAnalysisTools; - using global::AudioAnalysisTools.Events.Interfaces; using global::AudioAnalysisTools.Events.Tracks; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] public class TrackTests { - [TestMethod] - public void TestTrackProperties() - { - var converter = new UnitConverters( + /// + /// Each frame is 100 Hz, each bin is 0.05 seconds. + /// + public static readonly UnitConverters NiceTestConverter = + new UnitConverters( segmentStartOffset: 60, sampleRate: 1000, frameSize: 100, frameOverlap: 0.5); - var track = new Track(converter); - track.SetPoint(5, 5, 1); - track.SetPoint(6, 6, 2); - track.SetPoint(7, 7, 3); - track.SetPoint(8, 8, 4); - track.SetPoint(9, 9, 5); +#pragma warning disable SA1310 // Field names should not contain underscore + /// + /// Get a track that is diagonal, increasing one unit + /// both in time and frequency for each subsequent point. + /// + public static readonly Track TestTrack_TimePositive_FrequencyPositive = + new Track( + NiceTestConverter, + (5, 5, 1), + (6, 6, 2), + (7, 7, 3), + (8, 8, 4), + (9, 9, 5)); + +#pragma warning restore SA1310 // Field names should not contain underscore + + [TestMethod] + public void TestTrackProperties() + { + var track = TestTrack_TimePositive_FrequencyPositive; Assert.AreEqual(5, track.PointCount); @@ -53,14 +64,8 @@ public void TestTrackProperties() [TestMethod] public void TestWhistleProperties() { - var converter = new UnitConverters( - segmentStartOffset: 60, - sampleRate: 1000, - frameSize: 100, - frameOverlap: 0.5); - //create new track with whistle - var track = new Track(converter); + var track = new Track(NiceTestConverter); track.SetPoint(5, 5, 1); track.SetPoint(6, 5, 2); track.SetPoint(7, 5, 3); @@ -84,14 +89,8 @@ public void TestWhistleProperties() [TestMethod] public void TestClickProperties() { - var converter = new UnitConverters( - segmentStartOffset: 60, - sampleRate: 1000, - frameSize: 100, - frameOverlap: 0.5); - //Create new track with click - var track = new Track(converter); + var track = new Track(NiceTestConverter); track.SetPoint(5, 5, 1); track.SetPoint(5, 6, 2); track.SetPoint(5, 7, 3); @@ -115,16 +114,10 @@ public void TestClickProperties() [TestMethod] public void TestTrackAsSequenceOfHertzValues() { - var converter = new UnitConverters( - segmentStartOffset: 60, - sampleRate: 1000, - frameSize: 100, - frameOverlap: 0.5); - - //Create new track with flat and vertical parts + // Create new track with flat and vertical parts // frame duration = 0.1 seconds. // Bin width = 10 Hz. - var track = new Track(converter); + var track = new Track(NiceTestConverter); track.SetPoint(5, 5, 1); track.SetPoint(6, 5, 2); track.SetPoint(7, 6, 3); @@ -138,8 +131,8 @@ public void TestTrackAsSequenceOfHertzValues() double[] expectedArray = { 0, 10, 10, 80 }; CollectionAssert.AreEqual(expectedArray, hertzTrack); - //Create new track that apparently goes backwards in time! - track = new Track(converter); + // Create new track that apparently goes backwards in time! + track = new Track(NiceTestConverter); track.SetPoint(10, 4, 1); track.SetPoint(9, 5, 2); track.SetPoint(8, 6, 3); diff --git a/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTestsDrawing.cs b/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTestsDrawing.cs new file mode 100644 index 000000000..f91f8186a --- /dev/null +++ b/tests/Acoustics.Test/AudioAnalysisTools/Events/Tracks/TrackTestsDrawing.cs @@ -0,0 +1,62 @@ +// +// 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 Acoustics.Test.AudioAnalysisTools.Events.Tracks +{ + using Acoustics.Shared.ImageSharp; + using Acoustics.Test.TestHelpers; + using global::AudioAnalysisTools; + using global::AudioAnalysisTools.Events.Drawing; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; + + [TestClass] + public class TrackTestsDrawing : GeneratedImageTest + { + private const int ImageSize = 15; + /// + /// A set of unit converters that scales up some points so that they're visible + /// and easily debuggable in a diagnostic image. + /// + public static readonly UnitConverters ScaledUnitConverters = + new UnitConverters( + segmentStartOffset: 60, + segmentDuration: ImageSize * 0.05, + nyquistFrequency: ImageSize * 10, + imageWidth: ImageSize, + imageHeight: ImageSize); + + public static readonly EventRenderingOptions Options = new EventRenderingOptions(ScaledUnitConverters); + + [TestMethod("Test draw ↗")] + public void TestDraw() + { + var specification = @" +............... +............... +............... +............... +............... +.........R..... +........R...... +.......R....... +......R........ +.....R......... +............... +............... +............... +............... +............... +"; + this.ExpectedImage = TestImage.Create(ImageSize, ImageSize, Color.Black, specification); + this.ActualImage = Drawing.NewImage(ImageSize, ImageSize, Color.Black); + + this.ActualImage.Mutate(x => TrackTests.TestTrack_TimePositive_FrequencyPositive.Draw(x, Options)); + + this.AssertImagesEqual(); + } + } +} diff --git a/tests/Acoustics.Test/Shared/Drawing/DrawLineTest.cs b/tests/Acoustics.Test/Shared/Drawing/DrawLineTest.cs new file mode 100644 index 000000000..8e2173813 --- /dev/null +++ b/tests/Acoustics.Test/Shared/Drawing/DrawLineTest.cs @@ -0,0 +1,270 @@ +// +// 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 Acoustics.Test.Shared.Drawing +{ + using System; + using System.Linq; + using Acoustics.Test.TestHelpers; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using SixLabors.ImageSharp; + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; + + [TestClass] + public class DrawLineTest : GeneratedImageTest + { + [TestMethod] + [TestCategory("smoketest")] + public void DiagonalLineNotDrawnProperly() + { + var color = Color.Red; + var pen = new Pen(color, 1); + + // a 3-pixel line, bottom left to top right, 1px padding around edge + // . . . . . + // . . . R . + // . . R . . + // . R . . . + // . . . . . + var expected = new Image(5, 5); + expected[1, 3] = color; + expected[2, 2] = color; + expected[3, 1] = color; + + var actual = new Image(5, 5); + actual.Mutate( + context => context.DrawLines( + new GraphicsOptions() { Antialias = false, AntialiasSubpixelDepth = 0 }, + pen, + new PointF(1, 3), + new PointF(3, 1))); + + this.ExpectedImage = expected; + this.ActualImage = actual; + + // this should pass without bug + //this.AssertImagesEqual(); + + /* + Assert.Fail failed. Images are not equal - total delta 0.00019455253 is not less than tolerance 0. + Difference are: + + - at (2,1) in actual the expected color is Rgba32(0, 0, 0, 255) and actual is Rgba32(255, 0, 0, 255) + - at (3,1) in actual the expected color is Rgba32(255, 0, 0, 255) and actual is Rgba32(0, 0, 0, 255) + - at (1,2) in actual the expected color is Rgba32(0, 0, 0, 255) and actual is Rgba32(255, 0, 0, 255) + - at (2,2) in actual the expected color is Rgba32(255, 0, 0, 255) and actual is Rgba32(0, 0, 0, 255) + - at (1,3) in actual the expected color is Rgba32(255, 0, 0, 255) and actual is Rgba32(0, 0, 0, 255) + + (and 0 more..) + */ + + // this should not work, but does because of bug. Bug image looks like: + // . . . . . + // . . R . . + // . R . . . + // . . . . . + // . . . . . + Assert.AreEqual(color.ToPixel(), actual[2, 1]); + Assert.AreEqual(color.ToPixel(), actual[1, 2]); + } + + [TestMethod] + [TestCategory("smoketest")] + public void DiagonalLineNotDrawnProperlyCrossCheckBug28() + { + // the same as last test but checks that https://github.com/SixLabors/ImageSharp.Drawing/issues/28 + // is not the root of the funkyness + + var color = Color.Red; + var pen = new Pen(color, 1); + + // a 3-pixel line, bottom left to top right, 1px padding around edge + // . . . . . + // . . . R . + // . . R . . + // . R . . . + // . . . . . + var expected = new Image(5, 5); + expected[1, 3] = color; + expected[2, 2] = color; + expected[3, 1] = color; + + var actual = new Image(5, 5); + actual.Mutate( + context => context.DrawLines( + new GraphicsOptions() { Antialias = false, AntialiasSubpixelDepth = 0 }, + pen, + new PointF(1, 3) + new PointF(0.0f, 0.5f), + new PointF(3, 1) + new PointF(0.0f, 0.5f))); + + this.ExpectedImage = expected; + this.ActualImage = actual; + + // this should pass without bug + //this.AssertImagesEqual(); + + /* + Assert.Fail failed. Images are not equal - total delta 0.00015564202 is not less than tolerance 0. + Difference are: + + - at (2,1) in actual the expected color is Rgba32(0, 0, 0, 255) and actual is Rgba32(255, 0, 0, 255) + - at (3,1) in actual the expected color is Rgba32(255, 0, 0, 255) and actual is Rgba32(0, 0, 0, 255) + - at (1,2) in actual the expected color is Rgba32(0, 0, 0, 255) and actual is Rgba32(255, 0, 0, 255) + - at (2,2) in actual the expected color is Rgba32(255, 0, 0, 255) and actual is Rgba32(0, 0, 0, 255) + + (and 0 more..) + + */ + + // this should not work, but does because of bug. Bug image looks like: + // . . . . . + // . . R . . + // . R . . . + // . R . . . + // . . . . . + Assert.AreEqual(color.ToPixel(), actual[2, 1]); + Assert.AreEqual(color.ToPixel(), actual[1, 2]); + } + + [TestMethod] + [TestCategory("smoketest")] + public void DiagonalLineNotDrawnProperlyCrossCheckBug28SecondAttempt() + { + // the same as last test but checks that https://github.com/SixLabors/ImageSharp.Drawing/issues/28 + // is not the root of the funkyness + + var color = Color.Red; + var pen = new Pen(color, 1); + + // a 3-pixel line, bottom left to top right, 1px padding around edge + // . . . . . + // . . . R . + // . . R . . + // . R . . . + // . . . . . + var expected = new Image(5, 5); + expected[1, 3] = color; + expected[2, 2] = color; + expected[3, 1] = color; + + var actual = new Image(5, 5); + actual.Mutate( + context => context.DrawLines( + new GraphicsOptions() { Antialias = false, AntialiasSubpixelDepth = 0 }, + pen, + new PointF(1, 3) + new PointF(0.5f, 0.5f), + new PointF(3, 1) + new PointF(0.5f, 0.5f))); + + this.ExpectedImage = expected; + this.ActualImage = actual; + + // this should pass without bug + //this.AssertImagesEqual(); + + /* + Assert.Fail failed. Images are not equal - total delta 7.782101E-05 is not less than tolerance 0. + Difference are: + + - at (1,3) in actual the expected color is Rgba32(255, 0, 0, 255) and actual is Rgba32(0, 0, 0, 255) + - at (2,3) in actual the expected color is Rgba32(0, 0, 0, 255) and actual is Rgba32(255, 0, 0, 255) + + (and 0 more..) + */ + + // this should not work, but does because of bug. Bug image looks like: + // . . . . . + // . . . R . + // . . R . . + // . . R . . + // . . . . . + Assert.AreEqual(color.ToPixel(), actual[2, 3]); + } + + [TestMethod] + public void TestOurWrapperMethodDrawsCorrectLine() + { + var color = Color.Red; + var pen = new Pen(color, 1); + + // a 3-pixel line, bottom left to top right, 1px padding around edge + // . . . . . + // . . . R . + // . . R . . + // . R . . . + // . . . . . + var expected = new Image(5, 5); + expected[1, 3] = color; + expected[2, 2] = color; + expected[3, 1] = color; + + var actual = new Image(5, 5); + + // our wrapper method correct for Bug28 offset and + // repeats the first point in the line, which seems to remove artifacts + actual.Mutate( + context => context.NoAA().DrawLines( + pen, + new PointF(1, 3), + new PointF(3, 1))); + + this.ExpectedImage = expected; + this.ActualImage = actual; + + // this should pass without bug + this.AssertImagesEqual(); + + } + + [TestMethod] + public void TestNoAADrawLineDiagonalMultiplePoints() + { + // red line, 1px per row, diagonal top left to bottom right + // but top left and bottom right pixels are empty + string specification = ".100\n" + Enumerable + .Range(1, 98) + .Select(x => new string('.', x) + "R") + .Join("\n"); + this.ExpectedImage = new TestImage(100, 100, Color.Black) + .FillPattern(specification, Color.Black) + .Finish(); + + var path = Enumerable + .Range(1, 99) + .Select(x => new PointF(x, x)) + .ToArray(); + + this.ActualImage = new Image(Configuration.Default, 100, 100, Color.Black); + this.ActualImage.Mutate(x => x.NoAA().DrawLines(Pens.Solid(Color.Red, 1f), path)); + + this.AssertImagesEqual(); + } + + [TestMethod] + public void TestNoAADrawLineDiagonalFewerPoints() + { + // red line, 1px per row, diagonal top left to bottom right + // but bottom left and top right pixels are empty + string specification = ".100\n" + Enumerable + .Range(1, 98) + .Select(x => new string('.', 100 - x - 1) + "R") + .Join("\n"); + this.ExpectedImage = new TestImage(100, 100, Color.Black) + .FillPattern(specification, Color.Black) + .Finish(); + + var path = new[] + { + new PointF(1, 98), + new PointF(1, 98), + new PointF(98, 1), + }; + + this.ActualImage = new Image(Configuration.Default, 100, 100, Color.Black); + this.ActualImage.Mutate(x => x.NoAA().DrawLines(Pens.Solid(Color.Red, 1f), path)); + + this.AssertImagesEqual(); + } + } +} diff --git a/tests/Acoustics.Test/Shared/Drawing/DrawingTests.cs b/tests/Acoustics.Test/Shared/Drawing/DrawingTests.cs index fde25ade4..a3dc7f9ec 100644 --- a/tests/Acoustics.Test/Shared/Drawing/DrawingTests.cs +++ b/tests/Acoustics.Test/Shared/Drawing/DrawingTests.cs @@ -11,6 +11,9 @@ namespace Acoustics.Test.Shared.Drawing using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; + using System; + using System.Linq; + using System.Text; [TestClass] public class DrawingTests : GeneratedImageTest diff --git a/tests/Acoustics.Test/Shared/Drawing/FillDoesNotBlendByDefault.cs b/tests/Acoustics.Test/Shared/Drawing/FillDoesNotBlendByDefault.cs index e7b91f6b4..2070ab263 100644 --- a/tests/Acoustics.Test/Shared/Drawing/FillDoesNotBlendByDefault.cs +++ b/tests/Acoustics.Test/Shared/Drawing/FillDoesNotBlendByDefault.cs @@ -28,34 +28,11 @@ public void Test() // expected result //Assert.AreEqual(expectedColor, image[50, 50]); - Assert.AreEqual(new Rgb24(255, 0, 0), image[50, 50]); - // fails with: Assert.AreEqual failed. Expected:. Actual:. - // Actual (buggy?) result - // According to @JimBobSquarePants this is the expected bahviour - /* - - > @atruskie You're expecting the wrong thing. - > - > You have a 24bit pixel image. Adjusting the opacity of the color you are blending will not make a difference since - > it will be converted to Rgb24. By default the GraphicsOptions used by filling will have a color blending mode of - > PixelColorBlendingMode.Normal and alpha composition mode of PixelAlphaCompositionMode.SrcOverwhich will simply paint - > the color over the background since there is no alpha component. - > - > To get your expected result the background should have an alpha component. So use Rgba32. - > - > You should also dispose of your images once you have finished with them. - - @JimBobSquarePants, thanks for the response. I really don't need an Rgba32 image. The image itself doesn't need any transparency. - - Given that IImageProcessingContext is meant to be a pixel agnostic drawing interface, my question becomes, how do I fill a region with a 50% opacity colour? So setting BlendPercentage = 0.5f in graphics options does a fill with a blend. I admittedly don't understand the effects of PixelColorBlendingMode and PixelAlphaCompositionMode but I tried various values and it did not seem to have any effect. - - However, may I suggest what I was expecting should work? Here's why: + Assert.AreEqual(new Rgb24(255, 0, 0), image[50, 50]); - System.Drawing and SkiaSharp act how I expect (caveat: I am no expert on anything). Example: https://gist.github.com/atruskie/02f6fb35d6967c57ed543d109e83736e - If a Rgb24 pixel blender encounters an Rgba32 pixel, it blends with the rgba32's opacity as I expect, so why doesn't that symmetry hold for image&fill? Example: https://gist.github.com/atruskie/f12f22830f79ba42943433edee964a53 - */ + // BUG: Blending does not occur with fill https://github.com/SixLabors/ImageSharp.Drawing/issues/38 // thus we need our own method. see below } diff --git a/tests/Acoustics.Test/Shared/Drawing/NegativeTextBug.cs b/tests/Acoustics.Test/Shared/Drawing/NegativeTextBug.cs index e28d8e9a1..c5a5dcd34 100644 --- a/tests/Acoustics.Test/Shared/Drawing/NegativeTextBug.cs +++ b/tests/Acoustics.Test/Shared/Drawing/NegativeTextBug.cs @@ -15,7 +15,6 @@ namespace Acoustics.Test.Shared.Drawing [TestClass] public class NegativeTextBug : GeneratedImageTest { - public NegativeTextBug() { this.ActualImage = new Image(Configuration.Default, 100, 100, Color.Black); @@ -26,7 +25,7 @@ public NegativeTextBug() /// TODO BUG: see https://github.com/SixLabors/ImageSharp.Drawing/issues/30. /// [TestMethod] - [TestCategory("SpecialCase")] + [TestCategory("smoketest")] public void TextFailsToRender() { var text = "2016-12-10"; diff --git a/tests/Acoustics.Test/Shared/Drawing/RectangleCornerBugTest.cs b/tests/Acoustics.Test/Shared/Drawing/RectangleCornerBugTest.cs index de2638df3..0ee2fdd96 100644 --- a/tests/Acoustics.Test/Shared/Drawing/RectangleCornerBugTest.cs +++ b/tests/Acoustics.Test/Shared/Drawing/RectangleCornerBugTest.cs @@ -22,6 +22,7 @@ public class RectangleCornerBugTest : OutputDirectoryTest public const double MissingCornerDelta = 9.727626E-08 + 0.00000001; [TestMethod] + [TestCategory("smoketest")] public void RectangleHasMissingBottomRightCorner() { var testImage = new Image(Configuration.Default, 100, 100, Color.Black); @@ -65,6 +66,7 @@ public void RectangleHasMissingBottomRightCorner() } [TestMethod] + [TestCategory("smoketest")] public void DrawTest() { // arrange diff --git a/tests/Acoustics.Test/TestHelpers/TestImage.cs b/tests/Acoustics.Test/TestHelpers/TestImage.cs index bd2ef60f1..2463a8c35 100644 --- a/tests/Acoustics.Test/TestHelpers/TestImage.cs +++ b/tests/Acoustics.Test/TestHelpers/TestImage.cs @@ -27,11 +27,12 @@ public class TestImage { 'O', Color.Orange }, { 'W', Color.White }, { 'E', Color.Black }, + { '.', Color.Black }, }; private readonly Image image; private readonly List> operations; - private readonly Stack<(int Repeats, int startIndex)> loops = new Stack<(int Repeats, int startIndex)>(); + private readonly Stack<(int Repeats, int StartIndex)> loops = new Stack<(int Repeats, int StartIndex)>(); public TestImage(int width, int height, Rgb24? backgroundColor) { @@ -53,7 +54,7 @@ public static Image Create(int width, int height, Color color, string spe public Point Cursor { get; private set; } - public TestImage FillPattern(string specification) + public TestImage FillPattern(string specification, Color? defaultBackground = null) { Point Action(Point cursor, IImageProcessingContext context) { @@ -96,7 +97,7 @@ Point Action(Point cursor, IImageProcessingContext context) } // now modify pixel buffer - ParseLine(rest, ref buffer, DefaultBackground); + ParseLine(rest, ref buffer, defaultBackground ?? DefaultBackground); // finally repeat each buffer onto image rows for (int r = 0; r < repeats; r++) @@ -193,7 +194,7 @@ internal static void ParseLine(ReadOnlySpan line, ref Span buffer, break; default: - throw new InvalidOperationException("unknown or unexpected code: " + line[current]); + throw new InvalidOperationException("unknown or unexpected code in TestImage specification string: " + line[current]); } }