Skip to content

Commit

Permalink
Add longitude animation for geostationary projection
Browse files Browse the repository at this point in the history
  • Loading branch information
nullpainter committed Oct 9, 2020
1 parent d79eb4d commit 631f129
Show file tree
Hide file tree
Showing 30 changed files with 460 additions and 87 deletions.
2 changes: 1 addition & 1 deletion Sanchez.Processing/Extensions/AngleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public static class AngleExtensions
/// </summary>
/// <param name="angle">angle in radians</param>
public static double NormaliseLongitude(this double angle) => angle.Limit(-Math.PI, Math.PI);

/// <summary>
/// Scales an angle to a fractional pixel width, based on an equirectangular projection.
/// </summary>
Expand Down
17 changes: 12 additions & 5 deletions Sanchez.Processing/Models/GeostationaryRenderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,27 @@
{
public class GeostationaryRenderOptions
{
public GeostationaryRenderOptions(double? longitude, double? endLongitude, float hazeAmount)
public GeostationaryRenderOptions(Angle? longitude, Angle? endLongitude, bool inverseRotation, float hazeAmount)
{
Longitude = longitude;
EndLongitude = endLongitude;
InverseRotation = inverseRotation;
Longitude = longitude?.Radians;
EndLongitude = endLongitude?.Radians;
HazeAmount = hazeAmount;
}

/// <summary>
/// Target longitude for geostationary satellite projection.
/// Whether Earth rotation should be performed in a counter-clockwise manner when rotating from <see cref="Longitude"/>
/// to <see cref="EndLongitude"/>.
/// </summary>
public bool InverseRotation { get; set; }

/// <summary>
/// Target longitude in radians for geostationary satellite projection.
/// </summary>
public double? Longitude { get; }

/// <summary>
/// End longitude for timelapse geostationary satellite projection.
/// End longitude in radians for timelapse geostationary satellite projection.
/// </summary>
public double? EndLongitude { get; }

Expand Down
1 change: 1 addition & 0 deletions Sanchez.Workflow/Builders/GeostationaryStepBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal static class GeostationaryStepBuilder
internal static IServiceCollection AddGeostationarySteps(this IServiceCollection services)
{
return services
.AddTransient<SetTargetLongitude>()
.AddTransient<ToGeostationary>()
.AddTransient<RenderUnderlay>()
.AddTransient<ApplyHaze>();
Expand Down
5 changes: 5 additions & 0 deletions Sanchez.Workflow/Models/Data/TimelapseWorkflowData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@

namespace Sanchez.Workflow.Models.Data
{
public class GeostationaryTimelapseWorkflowData : TimelapseWorkflowData
{
public double? Longitude { get; set; }
}

/// <summary>
/// Data backing workflows which perform timelapse animations.
/// </summary>
Expand Down
4 changes: 3 additions & 1 deletion Sanchez.Workflow/Services/WorkflowService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using FluentValidation;
Expand All @@ -12,6 +13,7 @@
using WorkflowCore.Models;
using WorkflowCore.Models.LifeCycleEvents;

[assembly: InternalsVisibleTo("Sanchez.Workflow.Test")]
namespace Sanchez.Workflow.Services
{
/// <summary>
Expand Down Expand Up @@ -62,7 +64,7 @@ private void RegisterWorkflows()
_host.RegisterWorkflow<EquirectangularStitchWorkflow, StitchWorkflowData>();
_host.RegisterWorkflow<EquirectangularTimelapseWorkflow, TimelapseWorkflowData>();
_host.RegisterWorkflow<EquirectangularWorkflow, EquirectangularWorkflowData>();
_host.RegisterWorkflow<GeostationaryReprojectedTimelapseWorkflow, TimelapseWorkflowData>();
_host.RegisterWorkflow<GeostationaryReprojectedTimelapseWorkflow, GeostationaryTimelapseWorkflowData>();
}

/// <summary>
Expand Down
56 changes: 42 additions & 14 deletions Sanchez.Workflow/Steps/Equirectangular/Stitch/ShouldWrite.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using Ardalis.GuardClauses;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -39,6 +40,7 @@ public ShouldWrite(
public Activity? Activity { get; set; }
public DateTime? Timestamp { get; set; }
public int AlreadyRenderedCount { get; [UsedImplicitly] set; }
public string? Identifier { get; [UsedImplicitly] set; }

public override ExecutionResult Run(IStepExecutionContext context)
{
Expand All @@ -50,66 +52,92 @@ public override ExecutionResult Run(IStepExecutionContext context)

if (!Activity.Registrations.Any())
{
_logger.LogInformation("No images found; skipping", Activity.OutputPath);
ProgressBar.Tick($"Scanning {Timestamp:s}");
_logger.LogInformation("No images found; skipping", Activity.OutputPath);

ProgressBar.Tick($"Scanning {Timestamp:s}{Identifier}");
return ExecutionResult.Outcome(false);
}

// Verify minimum number of satellites
if (_options.MinSatellites != null && Activity.Registrations.Count < _options.MinSatellites)
{
_logger.LogInformation("fewer than {minSatellites} for {timestamp}; skipping", _options.MinSatellites, Timestamp);

ProgressBar.Tick($"Skipping {Timestamp:s}");
return ExecutionResult.Outcome(false);
}

// Verify that the output file can be written
if (_fileService.ShouldWrite(Activity.OutputPath))
{
ProgressBar.Tick($"Processing {Timestamp:s}");
ProgressBar.Tick($"Processing {Timestamp:s}{Identifier}");
return ExecutionResult.Outcome(true);
}

_logger.LogInformation("Output file {outputFilename} exists; not overwriting", Activity.OutputPath);
AlreadyRenderedCount++;

ProgressBar.Tick($"Skipping {Timestamp:s}");
ProgressBar.Tick($"Skipping {Timestamp:s}{Identifier}");

return ExecutionResult.Outcome(false);
}
}

internal static class ShouldWriteExtensions
{
internal static IStepBuilder<TData, ShouldWrite> ShouldWrite<TData>(this IWorkflowBuilder<TData> builder, DateTime? timestamp)
internal static IStepBuilder<TData, ShouldWrite> ShouldWrite<TData>(
this IWorkflowBuilder<TData> builder,
DateTime? timestamp,
Expression<Func<TData, string?>>? identifier = null)
where TData : WorkflowData
=> builder
{
var result = builder
.StartWith<ShouldWrite, TData>()
.WithActivity()
.WithProgressBar()
.Input(step => step.Timestamp, data => timestamp)
.Output(data => data.AlreadyRenderedCount, step => step.AlreadyRenderedCount);

internal static IStepBuilder<TData, ShouldWrite> ShouldWrite<TStep, TData>(this IStepBuilder<TData, TStep> builder, DateTime? timestamp)

if (identifier != null) result.Input(step => step.Identifier, identifier);

return result;
}

internal static IStepBuilder<TData, ShouldWrite> ShouldWrite<TStep, TData>(
this IStepBuilder<TData, TStep> builder,
DateTime? timestamp,
Expression<Func<TData, string?>>? identifier = null)
where TStep : IStepBody
where TData : WorkflowData
=> builder
{
var result = builder
.Then<TStep, ShouldWrite, TData>()
.WithActivity()
.WithProgressBar()
.Input(step => step.Timestamp, data => timestamp)
.Output(data => data.AlreadyRenderedCount, step => step.AlreadyRenderedCount);

internal static IStepBuilder<TData, ShouldWrite> ShouldWrite<TStep, TData>(this IStepBuilder<TData, TStep> builder)
if (identifier != null) result.Input(step => step.Identifier, identifier);

return result;
}

internal static IStepBuilder<TData, ShouldWrite> ShouldWrite<TStep, TData>(
this IStepBuilder<TData, TStep> builder,
Expression<Func<TData, string?>>? identifier = null)
where TStep : IStepBody
where TData : TimelapseWorkflowData
=> builder
{
var result = builder
.Then<TStep, ShouldWrite, TData>()
.WithActivity()
.WithProgressBar()
.Input(step => step.Timestamp, data => data.TargetTimestamp)
.Output(data => data.AlreadyRenderedCount, step => step.AlreadyRenderedCount);

if (identifier != null) result.Input(step => step.Identifier, identifier);

return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ public override ExecutionResult Run(IStepExecutionContext context)
Guard.Against.Null(SourceRegistrations, nameof(SourceRegistrations));

// Either derive the start timestamp from the command-line or from the earliest registered file timestamp
var startTimestamp = _options.Timestamp ??= SourceRegistrations.Where(r => r.Timestamp != null).Min(r => r.Timestamp)!.Value;
var endTimestamp = _options.EndTimestamp ??= DateTime.Now;
var registrations = SourceRegistrations.Where(r => r.Timestamp != null).ToList();

var startTimestamp = _options.Timestamp ??= registrations.Min(r => r.Timestamp)!.Value;
var endTimestamp = _options.EndTimestamp ??= registrations.Max(r => r.Timestamp)!.Value;

for (var timestamp = startTimestamp; timestamp < endTimestamp!; timestamp += _options.Interval!.Value)
{
Expand Down
6 changes: 1 addition & 5 deletions Sanchez.Workflow/Steps/Geostationary/RenderUnderlay.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Microsoft.Extensions.Logging;
using Sanchez.Processing.ImageProcessing.Tint;
Expand Down Expand Up @@ -41,9 +40,6 @@ public override async Task<ExecutionResult> RunAsync(IStepExecutionContext conte
Guard.Against.Null(_options.GeostationaryRender, nameof(_options.GeostationaryRender));
Guard.Against.Null(Registration?.Image, nameof(Registration.Image));

var targetLongitude = _options.GeostationaryRender.Longitude;
if (targetLongitude != null) throw new InvalidOperationException("Equirectangular composition should be used used when target longitude is provided");

// Get or generate projected underlay
var underlayOptions = new UnderlayProjectionOptions(
ProjectionType.Geostationary,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using Ardalis.GuardClauses;
using Sanchez.Processing.Extensions;
using Sanchez.Processing.Models;
using Sanchez.Workflow.Extensions;
using Sanchez.Workflow.Models.Data;
using WorkflowCore.Interface;
using WorkflowCore.Models;
using Range = Sanchez.Processing.Models.Angles.Range;

namespace Sanchez.Workflow.Steps.Geostationary.Reprojected
{
internal class SetTargetLongitude : StepBody
{
private readonly RenderOptions _options;

public SetTargetLongitude(RenderOptions options) => _options = options;
public double? Longitude { get; set; }

/// <summary>
/// Timestamp of currently-processed step.
/// </summary>
public DateTime? TargetTimestamp { get; set; }

public List<DateTime>? TimeIntervals { get; set; }

public override ExecutionResult Run(IStepExecutionContext context)
{
Guard.Against.Null(TargetTimestamp, nameof(TargetTimestamp));

var geostationaryOptions = _options.GeostationaryRender!;

Longitude = geostationaryOptions.EndLongitude == null
? geostationaryOptions.Longitude
: GetTimelapseLongitude(geostationaryOptions);

return ExecutionResult.Next();
}

private double GetTimelapseLongitude(GeostationaryRenderOptions geostationaryOptions)
{
Guard.Against.Null(TimeIntervals, nameof(TimeIntervals));
Guard.Against.Zero(TimeIntervals.Count, nameof(TimeIntervals));

var currentIndex = TimeIntervals.IndexOf(TargetTimestamp!.Value);
if (currentIndex < 0) throw new InvalidOperationException($"Unable to find timestamp {TargetTimestamp} in timelapse");

var start = geostationaryOptions.Longitude!.Value;
var end = geostationaryOptions.EndLongitude!.Value;

var range = new Range(start, end).UnwrapLongitude();
var offset = (range.End - range.Start) * (currentIndex / ((double) TimeIntervals.Count - 1));

return (start + offset).NormaliseLongitude();
}
}

public static class SetTargetLongitudeExtensions
{
internal static IStepBuilder<TData, SetTargetLongitude> SetTargetLongitude<TStep, TData>(this IStepBuilder<TData, TStep> builder)
where TStep : IStepBody
where TData : GeostationaryTimelapseWorkflowData
=> builder
.Then<TStep, SetTargetLongitude, TData>()
.Input(step => step.TargetTimestamp, data => data.TargetTimestamp)
.Input(step => step.TimeIntervals, data => data.TimeIntervals)
.Output(data => data.Longitude, step => step.Longitude);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq.Expressions;
using Ardalis.GuardClauses;
using Microsoft.Extensions.Logging;
using Sanchez.Processing.ImageProcessing.ShadeEdges;
Expand Down Expand Up @@ -34,29 +35,32 @@ public ToGeostationary(
_options = options;
}

/// <summary>
/// Target longitude, in radians.
/// </summary>
internal double? Longitude { get; set; }

public override ExecutionResult Run(IStepExecutionContext context)
{
Guard.Against.Null(_options.GeostationaryRender!.Longitude, nameof(GeostationaryRenderOptions.Longitude));
Guard.Against.Null(Longitude, nameof(Longitude));
Guard.Against.Null(Activity, nameof(Activity));
Guard.Against.Null(TargetImage, nameof(TargetImage));

var longitudeRange = Activity.GetVisibleLongitudeRange();

// Determine visible range of all satellite imagery
var longitudeDegrees = _options.GeostationaryRender!.Longitude!.Value;

_logger.LogInformation("Reprojecting to geostationary with longitude {longitudeDegrees} degrees", longitudeDegrees);
_logger.LogInformation("Reprojecting to geostationary with longitude {longitude} degrees", Angle.FromRadians(Longitude!.Value).Degrees);

// Adjust longitude based on the underlay wrapping for visible satellites
var adjustedLongitude = -Math.PI - longitudeRange.Start + Angle.FromDegrees(longitudeDegrees).Radians;
var adjustedLongitude = -Math.PI - longitudeRange.Start + Longitude!.Value;

// Render geostationary image
using (var sourceImage = TargetImage.Clone())
{
TargetImage = sourceImage.ToGeostationaryProjection(adjustedLongitude, Constants.Satellite.DefaultHeight, _options);

// Apply haze if required
var hazeAmount = _options.GeostationaryRender.HazeAmount;
var hazeAmount = _options.GeostationaryRender!.HazeAmount;
if (hazeAmount > 0 && !_options.NoUnderlay)
{
TargetImage.ApplyHaze(_options.Tint, hazeAmount);
Expand All @@ -72,12 +76,13 @@ public override ExecutionResult Run(IStepExecutionContext context)

public static class ToGeostationaryExtensions
{
internal static IStepBuilder<TData, ToGeostationary> ToGeostationary<TStep, TData>(this IStepBuilder<TData, TStep> builder)
internal static IStepBuilder<TData, ToGeostationary> ToGeostationary<TStep, TData>(this IStepBuilder<TData, TStep> builder, Expression<Func<TData, double?>> longitude)
where TStep : IStepBody
where TData : StitchWorkflowData
where TData : WorkflowData
=> builder
.Then<TStep, ToGeostationary, TData>("Reprojecting to geostationary")
.WithActivity()
.Input(step => step.Longitude, longitude)
.Input(step => step.TargetImage, data => data.TargetImage)
.Output(data => data.TargetImage, step => step.TargetImage);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public void Build(IWorkflowBuilder<TimelapseWorkflowData> builder)
.If(data => data.TimeIntervals.Any())
.Do(branch => branch
.ForEach(data => data.TimeIntervals, options => false)
.Do(timeStep => timeStep
.Do(step => step
.SetTargetTimestamp()
.CreateActivities()
.ShouldWrite()
.Branch(true, timeStep
.Branch(true, step
.CreateBranch()
.InitialiseImageProgressBar(data => data.Activity!.Registrations.Count + 1)
.CalculateVisibleRange()
Expand Down
Loading

0 comments on commit 631f129

Please sign in to comment.