diff --git a/src/Aardvark.Base.Tensors.CSharp/PixImage/PixImage.cs b/src/Aardvark.Base.Tensors.CSharp/PixImage/PixImage.cs index 9349fd49..4a06fcd8 100644 --- a/src/Aardvark.Base.Tensors.CSharp/PixImage/PixImage.cs +++ b/src/Aardvark.Base.Tensors.CSharp/PixImage/PixImage.cs @@ -328,6 +328,140 @@ internal static void Ignore(T _) { } #endregion + #region Processors + + private static readonly Dictionary s_processors = new() + { + { PixProcessor.Instance, 1 } + }; + + /// + /// Sets the priority of a PixImage processor. + /// The priority determines the order in which proocessors are invoked to scale, rotate, or remap an image. + /// Processors with higher priority are invoked first. + /// If the processor does not exist, it is added with the given priority. + /// + /// The processor to modify. + /// The priority to set. + public static void SetProcessor(IPixProcessor processor, int priority) + { + lock (s_processors) + { + s_processors[processor] = priority; + } + } + + /// + /// Adds a PixImage processor. + /// Assigns a priority that is greater than the highest priority among existing processors, resulting in a LIFO order. + /// If the processor already exists, the priority is modified. + /// + /// The processor to add. + public static void AddProcessor(IPixProcessor processor) + { + lock (s_processors) + { + var maxPriority = s_processors.Values.Max(-1); + s_processors[processor] = maxPriority + 1; + } + } + + /// + /// Removes a PixImage processor. + /// + /// The processor to remove. + public static void RemoveProcessor(IPixProcessor processor) + { + lock (s_processors) { s_processors.Remove(processor); } + } + + /// + /// Gets a dictionary of registered processors with their associated priority. + /// Only returns processors that have at least the given minimum capabilities. + /// + /// The minimum capabilities for a processor to be considered. + /// A dictionary of registered processors. + public static Dictionary GetProcessorsWithPriority(PixProcessorCaps minCapabilities = PixProcessorCaps.None) + { + lock (s_processors) + { + var result = new Dictionary(); + + foreach (var p in s_processors) + { + if (p.Key.Capabilities.HasFlag(minCapabilities)) + result[p.Key] = p.Value; + } + + return result; + } + } + + /// + /// Gets a list of registered processors sorted by priority in descending order. + /// Only returns processors that have at least the given minimum capabilities. + /// + /// The minimum capabilities for a processor to be considered. + /// A list of registered processors. + public static List GetProcessors(PixProcessorCaps minCapabilities = PixProcessorCaps.None) + { + lock (s_processors) + { + var list = new List>(); + + foreach (var p in s_processors) + { + if (p.Key.Capabilities.HasFlag(minCapabilities)) + list.Add(p); + } + + list.Sort((x, y) => y.Value - x.Value); + return list.Map(x => x.Key); + } + } + + internal static PixImage InvokeProcessors( + Func> invoke, + PixProcessorCaps minCapabilities, + string operationDescription) + { + PixImage result; + + foreach (var p in GetProcessors(minCapabilities)) + { + try + { + result = invoke(p); + if (result != null) return result; + } + catch (Exception e) + { + Report.Warn($"Failed to {operationDescription} with {p.Name} image processor: {e.Message}"); + } + } + + var processors = GetProcessors(PixProcessorCaps.None); + var errorMessage = $"Cannot {operationDescription}"; + + if (processors.Count == 0) + { + errorMessage += ", no image processors available!"; + } + else + { + errorMessage += ", available image processors:" + Environment.NewLine; + + foreach (var p in processors) + { + errorMessage += $" - {p.Name}: {p.Capabilities}" + Environment.NewLine; + } + } + + throw new NotSupportedException(errorMessage); + } + + #endregion + #region Constructors static PixImage() @@ -1333,29 +1467,45 @@ public IEnumerable> Channels #region Image Manipulation + #region Transformed + public override PixImage TransformedPixImage(ImageTrafo trafo) => Transformed(trafo); public PixImage Transformed(ImageTrafo trafo) => new PixImage(Format, Volume.Transformed(trafo)); + #endregion + + #region Remapped + public override PixImage RemappedPixImage(Matrix xMap, Matrix yMap, ImageInterpolation ip = ImageInterpolation.Cubic) => Remapped(xMap, xMap, ip); public PixImage Remapped(Matrix xMap, Matrix yMap, ImageInterpolation ip = ImageInterpolation.Cubic) { - if (s_remappedFun == null) - { - throw new NotSupportedException($"No remapping function has been installed via PixImage<{(typeof(T).Name)}>.SetRemappedFun"); - } + return InvokeProcessors( + (p) => p.Remap(this, xMap, yMap, ip, default), + PixProcessorCaps.Remap, "remap image" + ); + } - return new PixImage(Format, s_remappedFun(Volume, xMap, yMap, ip)); + [Obsolete("Use the PixImage processor API instead.")] + public static void SetRemappedFun(Func, Matrix, Matrix, ImageInterpolation, Volume> remappedFun) + { + LegacyPixProcessor.Instance.SetRemapFun( + (remappedFun == null) ? null : (pi, xMap, yMap, ip) => new (pi.Format, remappedFun(pi.Volume, xMap, yMap, ip)) + ); + + if (LegacyPixProcessor.Instance.Capabilities != PixProcessorCaps.None) + SetProcessor(LegacyPixProcessor.Instance, 0); + else + RemoveProcessor(LegacyPixProcessor.Instance); } - private static Func, Matrix, Matrix, ImageInterpolation, Volume> s_remappedFun = null; + #endregion - public static void SetRemappedFun(Func, Matrix, Matrix, ImageInterpolation, Volume> remappedFun) - => s_remappedFun = remappedFun; + #region Resized public override PixImage ResizedPixImage(V2i newSize, ImageInterpolation ip = ImageInterpolation.Cubic) => Scaled((V2d)newSize / (V2d)Size, ip); @@ -1366,47 +1516,68 @@ public PixImage Resized(V2i newSize, ImageInterpolation ip = ImageInterpolati public PixImage Resized(int xSize, int ySize, ImageInterpolation ip = ImageInterpolation.Cubic) => Scaled(new V2d(xSize, ySize) / (V2d)Size, ip); + #endregion + + #region Rotated + public override PixImage RotatedPixImage(double angleInRadiansCCW, bool resize = true, ImageInterpolation ip = ImageInterpolation.Cubic) => Rotated(angleInRadiansCCW, resize, ip); public PixImage Rotated(double angleInRadiansCCW, bool resize = true, ImageInterpolation ip = ImageInterpolation.Cubic) { - if (s_rotatedFun == null) - { - throw new NotSupportedException($"No rotating function has been installed via PixImage<{(typeof(T).Name)}>.SetRotatedFun"); - } + return InvokeProcessors( + (p) => p.Rotate(this, angleInRadiansCCW, resize, ip, default), + PixProcessorCaps.Rotate, "rotate image" + ); + } - return new PixImage(Format, s_rotatedFun(Volume, angleInRadiansCCW, resize, ip)); + [Obsolete("Use the PixImage processor API instead.")] + public static void SetRotatedFun(Func, double, bool, ImageInterpolation, Volume> rotatedFun) + { + LegacyPixProcessor.Instance.SetRotateFun( + (rotatedFun == null) ? null : (pi, angle, resize, ip) => new (pi.Format, rotatedFun(pi.Volume, angle, resize, ip)) + ); + + if (LegacyPixProcessor.Instance.Capabilities != PixProcessorCaps.None) + SetProcessor(LegacyPixProcessor.Instance, 0); + else + RemoveProcessor(LegacyPixProcessor.Instance); } - private static Func, double, bool, ImageInterpolation, Volume> s_rotatedFun = null; + #endregion - public static void SetRotatedFun(Func, double, bool, ImageInterpolation, Volume> rotatedFun) - => s_rotatedFun = rotatedFun; + #region Scaled public override PixImage ScaledPixImage(V2d scaleFactor, ImageInterpolation ip = ImageInterpolation.Cubic) => Scaled(scaleFactor, ip); public PixImage Scaled(V2d scaleFactor, ImageInterpolation ip = ImageInterpolation.Cubic) { - if (s_scaledFun == null) - { - throw new NotSupportedException($"No scaling function has been installed via PixImage<{(typeof(T).Name)}>.SetScaledFun"); - } - - if (!(scaleFactor.X > 0.0 && scaleFactor.Y > 0.0)) throw new ArgumentOutOfRangeException(nameof(scaleFactor)); + if (scaleFactor.AnySmallerOrEqual(0)) + throw new ArgumentOutOfRangeException($"Scale factor must be positive ({scaleFactor})."); // SuperSample is only available for scale factors < 1; fall back to Cubic - if ((scaleFactor.X >= 1.0 || scaleFactor.Y >= 1.0) && ip == ImageInterpolation.SuperSample) + if (scaleFactor.AnyGreater(1.0) && ip == ImageInterpolation.SuperSample) ip = ImageInterpolation.Cubic; - return new PixImage(Format, s_scaledFun(Volume, scaleFactor, ip)); + return InvokeProcessors( + (p) => p.Scale(this, scaleFactor, ip), + PixProcessorCaps.Scale, "scale image" + ); } - private static Func, V2d, ImageInterpolation, Volume> s_scaledFun = TensorExtensions.Scaled; - + [Obsolete("Use the PixImage processor API instead.")] public static void SetScaledFun(Func, V2d, ImageInterpolation, Volume> scaledFun) - => s_scaledFun = scaledFun; + { + LegacyPixProcessor.Instance.SetScaleFun( + (scaledFun == null) ? null : (pi, scaleFactor, ip) => new (pi.Format, scaledFun(pi.Volume, scaleFactor, ip)) + ); + + if (LegacyPixProcessor.Instance.Capabilities != PixProcessorCaps.None) + SetProcessor(LegacyPixProcessor.Instance, 0); + else + RemoveProcessor(LegacyPixProcessor.Instance); + } public PixImage Scaled(double scaleFactor, ImageInterpolation ip = ImageInterpolation.Cubic) => Scaled(new V2d(scaleFactor, scaleFactor), ip); @@ -1416,6 +1587,8 @@ public PixImage Scaled(double xScaleFactor, double yScaleFactor, ImageInterpo #endregion + #endregion + #region SubImages /// diff --git a/src/Aardvark.Base.Tensors.CSharp/PixImage/PixProcessor.cs b/src/Aardvark.Base.Tensors.CSharp/PixImage/PixProcessor.cs new file mode 100644 index 00000000..6d0a41b6 --- /dev/null +++ b/src/Aardvark.Base.Tensors.CSharp/PixImage/PixProcessor.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; + +namespace Aardvark.Base +{ + [Flags] + public enum PixProcessorCaps + { + None = 0, + Scale = 1, + Rotate = 2, + Remap = 4, + All = Scale | Rotate | Remap, + } + + /// + /// Interface for plugins providing various image processing methods. + /// + public interface IPixProcessor + { + /// + /// Name of the processor. + /// + string Name { get; } + + /// + /// Capabilities of the processor. + /// + PixProcessorCaps Capabilities { get; } + + /// + /// Scales the given image. + /// Returns null if the operation is not supported. + /// + /// The image to scale. + /// The scale factor to apply in each dimension. + /// The interpolation method to use. + PixImage Scale(PixImage image, V2d scaleFactor, ImageInterpolation interpolation); + + /// + /// Rotates an image counter-clockwise by the given angle in radians. + /// Returns null if the operation is not supported. + /// + /// The image to rotate. + /// The angle in radians by which to rotate the image. + /// True if the resulting image is resized accordingly, false otherwise. + /// The interpolation method to use. + /// The value to use for border pixels. + PixImage Rotate(PixImage image, double angleInRadians, bool resize, ImageInterpolation interpolation, T border = default); + + /// + /// Applies a generic geometric transformation to the given image. + /// Computes the values of the resulting image as f(x, y) = [(x, y), (x, y)]. + /// Returns null if the operation is not supported. + /// + /// The image to transform. + /// The mapping for X coordinates. + /// The mapping for Y coordinates. + /// The interpolation method to use. + /// The value to use for border pixels. + PixImage Remap(PixImage image, Matrix mapX, Matrix mapY, ImageInterpolation interpolation, T border = default); + } + + /// + /// Basic image processor using built-in Aardvark algorithms and methods. + /// + public sealed class PixProcessor : IPixProcessor + { + public string Name => "Aardvark"; + + public PixProcessorCaps Capabilities => PixProcessorCaps.Scale; + + public PixImage Scale(PixImage image, V2d scaleFactor, ImageInterpolation interpolation) + => new (image.Format, TensorExtensions.Scaled(image.Volume, scaleFactor, interpolation)); + + public PixImage Rotate(PixImage image, double angleInRadians, bool resize,ImageInterpolation interpolation, T border = default) + => null; + + public PixImage Remap(PixImage image, Matrix mapX, Matrix mapY, ImageInterpolation interpolation, T border = default) + => null; + + private PixProcessor() { } + + public static PixProcessor Instance { get; } = new(); + } + + /// + /// Processor for compatibility with legacy API. + /// + public sealed class LegacyPixProcessor : IPixProcessor + { + private readonly Dictionary> scaleFuns = new(); + private readonly Dictionary> rotateFuns = new(); + private readonly Dictionary, Matrix, ImageInterpolation, PixImage>> remapFuns = new(); + + public string Name => "Aardvark (Legacy)"; + + public PixProcessorCaps Capabilities + { + get + { + var result = PixProcessorCaps.None; + if (scaleFuns.Count > 0) result |= PixProcessorCaps.Scale; + if (rotateFuns.Count > 0) result |= PixProcessorCaps.Rotate; + if (remapFuns.Count > 0) result |= PixProcessorCaps.Remap; + return result; + } + } + + public void SetScaleFun(Func, V2d, ImageInterpolation, PixImage> scaleFun) + { + if (scaleFun != null) + scaleFuns[typeof(T)] = (p, s, i) => scaleFun((PixImage)p, s, i); + else + scaleFuns.Remove(typeof(T)); + } + + public PixImage Scale(PixImage image, V2d scaleFactor, ImageInterpolation interpolation) + { + if (scaleFuns.TryGetValue(typeof(T), out var fun)) + return (PixImage)fun(image, scaleFactor, interpolation); + else + return null; + } + + public void SetRotateFun(Func, double, bool, ImageInterpolation, PixImage> rotateFun) + { + if (rotateFun != null) + rotateFuns[typeof(T)] = (p, a, r, i) => rotateFun((PixImage)p, a, r, i); + else + rotateFuns.Remove(typeof(T)); + } + + public PixImage Rotate(PixImage image, double angleInRadians, bool resize, ImageInterpolation interpolation, T border = default) + { + if (rotateFuns.TryGetValue(typeof(T), out var fun)) + return (PixImage)fun(image, angleInRadians, resize, interpolation); + else + return null; + } + + public void SetRemapFun(Func, Matrix, Matrix, ImageInterpolation, PixImage> remapFun) + { + if (remapFun != null) + remapFuns[typeof(T)] = (p, x, y, i) => remapFun((PixImage)p, x, y, i); + else + remapFuns.Remove(typeof(T)); + } + + public PixImage Remap(PixImage image, Matrix mapX, Matrix mapY, ImageInterpolation interpolation, T border = default) + { + if (remapFuns.TryGetValue(typeof(T), out var fun)) + return (PixImage)fun(image, mapX, mapY, interpolation); + else + return null; + } + + private LegacyPixProcessor() { } + + public static LegacyPixProcessor Instance { get; } = new(); + } +} \ No newline at end of file