Skip to content

Commit

Permalink
[WIP] Almost got ribbons working
Browse files Browse the repository at this point in the history
Ribbons are rendering properly with ImageSharp ( 😀 ). They just need an axis and the midnight changing option is not working.
  • Loading branch information
atruskie committed Apr 28, 2019
1 parent 51f27bd commit 2005bc8
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,21 @@ public static TimeSpan Divide(this TimeSpan dividend, double divisor)
}

/// <summary>
/// Divides a timespan by an scalar value.
/// Divides a timespan by an timespan and returns a scalar factor.
/// </summary>
public static double Divide(this TimeSpan dividend, TimeSpan divisor)
{
return dividend.Ticks / (double)divisor.Ticks;
}

/// <summary>
/// Divides a timespan by an timespan and the remainder.
/// </summary>
public static TimeSpan Modulo(this TimeSpan dividend, TimeSpan divisor)
{
return TimeSpan.FromTicks(dividend.Ticks % divisor.Ticks);
}

// https://github.com/exceptionless/Exceptionless.DateTimeExtensions/blob/master/src/Exceptionless.DateTimeExtensions/DateTimeOffsetExtensions.cs#L222
public static DateTimeOffset Floor(this DateTimeOffset date, TimeSpan interval)
{
Expand Down
4 changes: 2 additions & 2 deletions src/Acoustics.Shared/FileDateHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public static SortedDictionary<DateTimeOffset, FileInfo> FilterFilesForDates(IEn
if (datesAndFiles.ContainsKey(parsedDate))
{
string message =
$"There was a duplicate date. File {file} with date {parsedDate,'r'} conflicts with existing file {datesAndFiles[parsedDate]}";
$"There was a duplicate date. File {file} with date {parsedDate:O} conflicts with existing file {datesAndFiles[parsedDate]}";
throw new InvalidDataSetException(message);
}

Expand Down Expand Up @@ -132,7 +132,7 @@ public static SortedDictionary<DateTimeOffset, T> FilterObjectsForDates<T>(IEnum
}

string message =
$"There was a duplicate date. File {file} with date {date,'r'} conflicts with existing file {datesAndFiles[date.Value]}";
$"There was a duplicate date. File {file} with date {date:O} conflicts with existing file {datesAndFiles[date.Value]}";
throw new InvalidDataSetException(message);
}

Expand Down
19 changes: 10 additions & 9 deletions src/Acoustics.Shared/Logging/LogExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ namespace log4net
{
using System;
using log4net;
using log4net.Core;

public static class LogExtensions
{
// equivalent to NOTICE
public static readonly Core.Level SuccessLevel = new Core.Level(50_000, "SUCCESS");
public static readonly Level SuccessLevel = new Level(50_000, "SUCCESS");

// Higher than all other levels, but lower than OFF.
// In interactive scenarios we need to be sure that the user sees the message.
public static readonly Core.Level PromptLevel = new Core.Level(150_000, "PROMPT");
public static readonly Level PromptLevel = new Level(150_000, "PROMPT");

/// <summary>
/// Log a message object with the <see cref="F:LogExtensions.PromptLevel"/> level.
Expand Down Expand Up @@ -80,7 +81,7 @@ public static void Success(this ILog log, string format, params object[] args)
/// </remarks>
public static void Verbose(this ILog log, object message)
{
log.Logger.Log(null, log4net.Core.Level.Verbose, message, null);
log.Logger.Log(null, Level.Verbose, message, null);
}

/// <summary>
Expand All @@ -95,7 +96,7 @@ public static void Verbose(this ILog log, object message)
public static void Verbose(this ILog log, string format, params object[] args)
{
var message = args.Length > 0 ? string.Format(format, args) : format;
log.Logger.Log(null, log4net.Core.Level.Verbose, message, null);
log.Logger.Log(null, Level.Verbose, message, null);
}

/// <summary>
Expand All @@ -114,7 +115,7 @@ public static void Verbose(this ILog log, string format, params object[] args)
/// </remarks>
public static void Verbose(this ILog log, object message, Exception exception)
{
log.Logger.Log(null, log4net.Core.Level.Verbose, message, exception);
log.Logger.Log(null, Level.Verbose, message, exception);
}

/// <summary>
Expand All @@ -124,7 +125,7 @@ public static void Verbose(this ILog log, object message, Exception exception)
/// <returns>True if the Verbose logging level is enabled</returns>
public static bool IsVerboseEnabled(this ILog log)
{
return log.Logger.IsEnabledFor(log4net.Core.Level.Verbose);
return log.Logger.IsEnabledFor(Level.Verbose);
}

/// <summary>
Expand All @@ -142,7 +143,7 @@ public static bool IsVerboseEnabled(this ILog log)
/// </remarks>
public static void Trace(this ILog log, object message)
{
log.Logger.Log(null, log4net.Core.Level.Trace, message, null);
log.Logger.Log(null, Level.Trace, message, null);
}

/// <summary>
Expand All @@ -157,7 +158,7 @@ public static void Trace(this ILog log, object message)
public static void Trace(this ILog log, string format, params object[] args)
{
var message = args.Length > 0 ? string.Format(format, args) : format;
log.Logger.Log(null, log4net.Core.Level.Trace, message, null);
log.Logger.Log(null, Level.Trace, message, null);
}

/// <summary>
Expand All @@ -176,7 +177,7 @@ public static void Trace(this ILog log, string format, params object[] args)
/// <seealso cref="M:Trace(object)"/><seealso cref="P:log4net.ILog.IsTraceEnabled"/>
public static void Trace(this ILog log, object message, Exception exception)
{
log.Logger.Log(null, log4net.Core.Level.Trace, message, exception);
log.Logger.Log(null, Level.Trace, message, exception);
}
}
}
1 change: 1 addition & 0 deletions src/AnalysisPrograms/Production/Arguments/MainArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace AnalysisPrograms.Production.Arguments
[Subcommand(ConcatenateIndexFiles.CommandName, typeof(ConcatenateIndexFiles.Arguments))]
[Subcommand(DrawLongDurationSpectrograms.CommandName, typeof(DrawLongDurationSpectrograms.Arguments))]
[Subcommand(DrawZoomingSpectrograms.CommandName, typeof(DrawZoomingSpectrograms.Arguments))]
[Subcommand(RibbonPlots.RibbonPlot.CommandName, typeof(RibbonPlots.RibbonPlot.Arguments))]
[Subcommand(DrawEasyImage.CommandName, typeof(DrawEasyImage.Arguments))]
[Subcommand(Audio2InputForConvCnn.CommandName, typeof(Audio2InputForConvCnn.Arguments))]
[Subcommand(DifferenceSpectrogram.CommandName, typeof(DifferenceSpectrogram.Arguments))]
Expand Down
144 changes: 117 additions & 27 deletions src/AnalysisPrograms/RibbonPlots/RibbonPlot.Entry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ namespace AnalysisPrograms.RibbonPlots
using Acoustics.Shared;
using AnalysisPrograms.Production;
using AudioAnalysisTools.Indices;
using AudioAnalysisTools.LongDurationSpectrograms;
using log4net;
using log4net.Util;
using SixLabors.Fonts;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
Expand All @@ -28,6 +30,7 @@ namespace AnalysisPrograms.RibbonPlots
/// </summary>
public partial class RibbonPlot
{
private static readonly TimeSpan RibbonPlotDomain = TimeSpan.FromHours(24);
private static readonly ILog Log = LogManager.GetLogger(nameof(RibbonPlot));

public static async Task<int> Execute(Arguments arguments)
Expand All @@ -52,15 +55,16 @@ public static async Task<int> Execute(Arguments arguments)
$"{nameof(arguments.OutputDirectory)} was not provided and was automatically set to source directory {arguments.OutputDirectory}");
}

if (arguments.Midnight == null || arguments.Midnight == TimeSpan.FromHours(24))
if (arguments.Midnight == null || arguments.Midnight == TimeSpan.Zero)
{
arguments.Midnight = TimeSpan.Zero;
// we need this to be width of day and not zero for rounding functions later on
arguments.Midnight = RibbonPlotDomain;
Log.Debug($"{nameof(arguments.Midnight)} was reset to {arguments.Midnight}");
}

if (arguments.Midnight < TimeSpan.Zero || arguments.Midnight > TimeSpan.FromHours(24))
if (arguments.Midnight < TimeSpan.Zero || arguments.Midnight > RibbonPlotDomain)
{
throw new InvalidStartOrEndException($"{nameof(arguments.Midnight)} cannot be less than `00:00` or greater than `24:00`");
throw new InvalidStartOrEndException($"{nameof(arguments.Midnight)} cannot be less than `00:00` or greater than `{RibbonPlotDomain}`");
}

LoggedConsole.Write("Begin scanning directories");
Expand Down Expand Up @@ -101,10 +105,16 @@ void Add(string colorMap)
}

// try to find the associated ribbon
var ribbonFile = indexData.Source?.Directory?.EnumerateFiles("*" + colorMap + "*").FirstOrDefault();
var searchPattern = "*" + colorMap + LDSpectrogramRGB.SpectralRibbonTag + "*";
if (Log.IsVerboseEnabled())
{
Log.Verbose($"Searching `{indexData.Source?.Directory}` with pattern `{searchPattern}`.");
}

var ribbonFile = indexData.Source?.Directory?.EnumerateFiles(searchPattern).FirstOrDefault();
if (ribbonFile == null)
{
Log.Warn($"Did not find expected ribbon file for color map {colorMap} in directory {indexData.Source?.Directory}."
Log.Warn($"Did not find expected ribbon file for color map {colorMap} in directory `{indexData.Source?.Directory}`."
+ "This can happen if the ribbon is missing or if more than one file matches the color map.");
}

Expand All @@ -115,61 +125,98 @@ void Add(string colorMap)
// get the min and max dates and other things
var stats = new RibbonPlotStats(datedIndices, arguments.Midnight.Value);

Log.Debug($"Files found between {stats.Min:R} and {stats.Max:R}, rendering between {stats.Start:R} and {stats.End:R}, in {stats.Buckets} buckets");
Log.Debug($"Files found between {stats.Min:O} and {stats.Max:O}, rendering between {stats.Start:O} and {stats.End:O}, in {stats.Buckets} buckets");

bool success = false;
foreach (var (colorMap, ribbons) in datesMappedToColorMaps)
{
CreateRibbonPlot(datedIndices, ribbons, stats);
Log.Info($"Rendering ribbon plot for color map {colorMap}");
if (ribbons.Count(x => x.Value.NotNull()) == 0)
{
Log.Error($"There are no ribbon files found for color map {colorMap} - skipping this color map");
continue;
}

var image = CreateRibbonPlot(datedIndices, ribbons, stats);

var midnight = arguments.Midnight == RibbonPlotDomain
? string.Empty
: "Midnight=" + arguments.Midnight.Value.ToString("hhmm");
var path = FilenameHelpers.AnalysisResultPath(arguments.OutputDirectory, arguments.OutputDirectory.Name, "RibbonPlot", "png", colorMap, midnight);
using (var file = File.Create(path))
{
image.SaveAsPng(file);
}

image.Dispose();

success = true;
}

if (success == false)
{
throw new MissingDataException("Could not find any ribbon files for any of the color maps. No ribbon plots were produced.");
}

return ExceptionLookup.Ok;
}


private static Image<Rgb24> CreateRibbonPlot(SortedDictionary<DateTimeOffset, IndexGenerationData> data,
Dictionary<DateTimeOffset, FileInfo> ribbons, RibbonPlotStats stats)
private static Image<Rgb24> CreateRibbonPlot(
SortedDictionary<DateTimeOffset, IndexGenerationData> data,
Dictionary<DateTimeOffset, FileInfo> ribbons,
RibbonPlotStats stats)
{
const int Padding = 5;
const int HorizontalPadding = 10;
const int LabelWidth = 100;

// read random ribbon in to get height - assumes all ribbons are same height
int ribbonHeight;
var someRibbon = ribbons.First();
int estimatedWidth = (int)Math.Round(TimeSpan.FromHours(24).Divide(data[someRibbon.Key].IndexCalculationDuration), MidpointRounding.ToEven);

var someRibbon = ribbons.First(x => x.Value.NotNull());
int estimatedWidth = (int)Math.Round(RibbonPlotDomain.Divide(data[someRibbon.Key].IndexCalculationDuration), MidpointRounding.ToEven);
Log.DebugFormat("Reading random ribbon file to get dimensions `{0}`", someRibbon.Value.FullName);
using (var testImage = Image.Load(someRibbon.Value.FullName))
{
ribbonHeight = testImage.Height;
}

var finalHeight = (Padding + ribbonHeight) * stats.Buckets;
var ribbonLeft = HorizontalPadding + LabelWidth + HorizontalPadding;
// get width of text
var scaledFont = new Font(SystemFonts.Find("Arial"), ribbonHeight);
int labelWidth = (int)Math.Ceiling(TextMeasurer.Measure(someRibbon.Key.ToString(AppConfigHelper.RenderedDateFormatShort), new RendererOptions(scaledFont)).Width);

var finalHeight = Padding + ((Padding + ribbonHeight) * stats.Buckets);
var ribbonLeft = HorizontalPadding + labelWidth + HorizontalPadding;
var finalWidth = ribbonLeft + estimatedWidth + HorizontalPadding;

// create a new image!
var image = new Image<Rgb24>(Configuration.Default, finalWidth, finalHeight, NamedColors<Rgb24>.White);

// draw labels and voids
int y = Padding;
Log.Debug("Rendering labels and backgrounds");
var day = stats.Start;
var scaledFont = new Font(SystemFonts.Find("Arial"), ribbonHeight);
var textGraphics = new TextGraphicsOptions(true)
{ HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Top };
var textColor = NamedColors<Rgb24>.Black;
var voidColor = NamedColors<Rgb24>.Gray;
var firstOffset = stats.Start.Offset;
for (var b = 0; b < stats.Buckets; b++)
{

if (Log.IsVerboseEnabled())
{
Log.Verbose($"Rendering bucket {day:O} label and void");
}

// get label
var dateLabel = day.ToOffset(firstOffset).ToString(AppConfigHelper.RenderedDateFormatShort);

image.Mutate(Operation);

void Operation(IImageProcessingContext<Rgb24> context)
{
var y = Padding + ((Padding + ribbonHeight) * b);

// draw label
context.DrawText(textGraphics, dateLabel, scaledFont, textColor, new PointF(HorizontalPadding, y));
context.DrawText(textGraphics, dateLabel, scaledFont, textColor, new Point(HorizontalPadding, y));

// draw void
var @void = new RectangularPolygon(ribbonLeft, y, estimatedWidth, ribbonHeight);
Expand All @@ -178,29 +225,70 @@ void Operation(IImageProcessingContext<Rgb24> context)
}

// copy images in
Log.Debug("Pasting ribbons onto plot");
foreach (var (date, ribbon) in ribbons)
{
if (ribbon == null)
{
if (Log.IsVerboseEnabled())
{
Log.Verbose($"Skipped {date:O} while rendering image because ribbon file was null");
}

continue;
}

if (Log.IsVerboseEnabled())
{
Log.Verbose($"Rendering {date:O} spectral ribbon");
}

var datum = data[date];

// determine ribbon start + end
var ribbonStart =
var delta = date - stats.Start;
var ribbonStartBucket = (int)delta.Divide(RibbonPlotDomain).Floor();
var ribbonHorizontalOffset = (int)(delta.Modulo(RibbonPlotDomain).TotalSeconds / datum.IndexCalculationDuration.TotalSeconds).Round();

var ribbonWidth = (int)(datum.RecordingDuration.TotalSeconds / datum.IndexCalculationDuration.TotalSeconds).Round();

var top = _
var left = ribbonLeft + _;
var top = Padding + ((Padding + ribbonHeight) * ribbonStartBucket);
var left = ribbonLeft + ribbonHorizontalOffset;
using (var source = Image.Load<Rgb24>(ribbon.FullName))
{
image.Mutate(x => x.DrawImage());
// the image is longer than the ribbon, need to wrap to next day
if (ribbonHorizontalOffset + ribbonWidth > estimatedWidth)
{
if (Log.IsVerboseEnabled())
{
Log.Verbose($"Rendering {date:O} in two parts, wrapped to next day");
}

var split = estimatedWidth - (ribbonHorizontalOffset + ribbonWidth);
var crop = source.Clone((context) => context.Crop(new Rectangle(0, 0, split, source.Height)));
image.Mutate(x => x.DrawImage(crop, new Point(left, top), GraphicsOptions.Default));

// now draw the wrap around - starting from the left, which is start of new day
top += Padding + ribbonHeight;
left = ribbonLeft;
var rest = source.Clone(context => context.Crop(new Rectangle(split, 0, source.Width, source.Height)));
image.Mutate(x => x.DrawImage(rest, new Point(left, top), GraphicsOptions.Default));
}
else
{
image.Mutate(x => x.DrawImage(source, new Point(left, top), GraphicsOptions.Default));
}
}
}


return image;


}

private class RibbonPlotStats
{
public RibbonPlotStats(SortedDictionary<DateTimeOffset, IndexGenerationData> datedIndices, TimeSpan midnight)
{
this.Midnight = midnight;
this.Min = datedIndices.Keys.First();
this.First = datedIndices[this.Min];
this.Max = datedIndices.Keys.Last();
Expand All @@ -210,6 +298,8 @@ public RibbonPlotStats(SortedDictionary<DateTimeOffset, IndexGenerationData> dat
this.Buckets = (int)Math.Ceiling((this.End - this.Start).Divide(TimeSpan.FromHours(24)));
}

public TimeSpan Midnight { get; }

public DateTimeOffset Min { get; }

public IndexGenerationData First { get; }
Expand Down
Loading

0 comments on commit 2005bc8

Please sign in to comment.