From a51495e1ad8cc4ef73f8059e887fa2e94a9270d5 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Mon, 14 Aug 2023 00:09:14 +0900 Subject: [PATCH 01/10] =?UTF-8?q?Brightness,=20Gamma,=20Invert=E3=82=A8?= =?UTF-8?q?=E3=83=95=E3=82=A7=E3=82=AF=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Engine/Graphics/ColorMatrix.cs | 14 ++++++ .../Graphics/FilterEffects/Brightness.cs | 33 ++++++++++++ .../FilterEffects/FilterEffectContext.cs | 28 ++++++++++- .../Graphics/FilterEffects/Gamma.cs | 50 +++++++++++++++++++ .../Graphics/FilterEffects/Invert.cs | 35 +++++++++++++ .../Graphics/FilterEffects/Lighting.cs | 3 +- src/Beutl.Engine/Graphics/LookupTable.cs | 12 +++++ .../Configure/Effects/EffectsOperator.cs | 17 +++++++ .../Configure/Effects/FilterEffectOperator.cs | 2 +- src/Beutl.Operators/LibraryRegistrar.cs | 15 ++++++ 10 files changed, 206 insertions(+), 3 deletions(-) create mode 100644 src/Beutl.Engine/Graphics/FilterEffects/Brightness.cs create mode 100644 src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs create mode 100644 src/Beutl.Engine/Graphics/FilterEffects/Invert.cs diff --git a/src/Beutl.Engine/Graphics/ColorMatrix.cs b/src/Beutl.Engine/Graphics/ColorMatrix.cs index 8c496debf..b20299cf8 100644 --- a/src/Beutl.Engine/Graphics/ColorMatrix.cs +++ b/src/Beutl.Engine/Graphics/ColorMatrix.cs @@ -134,6 +134,14 @@ public static ColorMatrix CreateLuminanceToAlpha() return CreateFromSpan(span); } + + public static ColorMatrix CreateBrightness(float amount) + { + Span span = stackalloc float[20]; + CreateBrightness(amount, span); + + return CreateFromSpan(span); + } public float[] ToArray() { @@ -300,6 +308,12 @@ internal static void CreateLuminanceToAlphaMatrix(Span span) span[17] = 0.0721F; } + internal static void CreateBrightness(float amount, Span span) + { + span[0] = span[6] = span[12] = amount; + span[18] = 1; + } + internal static void ToSkiaColorMatrix(Span array) { array[4] *= 255; diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Brightness.cs b/src/Beutl.Engine/Graphics/FilterEffects/Brightness.cs new file mode 100644 index 000000000..b8bd94bb7 --- /dev/null +++ b/src/Beutl.Engine/Graphics/FilterEffects/Brightness.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; + +namespace Beutl.Graphics.Effects; + +public sealed class Brightness : FilterEffect +{ + public static readonly CoreProperty AmountProperty; + private float _amount = 100; + + static Brightness() + { + AmountProperty = ConfigureProperty(nameof(Amount)) + .Accessor(o => o.Amount, (o, v) => o.Amount = v) + .DefaultValue(100) + .Register(); + + AffectsRender(AmountProperty); + } + + [Range(0, float.MaxValue)] + public float Amount + { + get => _amount; + set => SetAndRaise(AmountProperty, ref _amount, value); + } + + public override void ApplyTo(FilterEffectContext context) + { + float amount = _amount / 100f; + + context.Brightness(amount); + } +} diff --git a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs index 5d9d3bdce..5bf6e1bfc 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs @@ -232,6 +232,14 @@ public void ColorMatrix(in ColorMatrix matrix) AppendSKColorFilter(matrix, (m, _) => SKColorFilter.CreateColorMatrix(m.ToArrayForSkia())); } + public void ColorMatrix(T data, Func factory) + where T : IEquatable + { + AppendSKColorFilter( + (data, factory), + (t, _) => SKColorFilter.CreateColorMatrix((t.factory.Invoke(t.data)).ToArrayForSkia())); + } + public void Saturate(float amount) { AppendSKColorFilter(amount, (s, _) => @@ -271,6 +279,19 @@ public void LuminanceToAlpha() }); } + public void Brightness(float amount) + { + AppendSKColorFilter(amount, (s, _) => + { + float[] array = new float[20]; + Graphics.ColorMatrix.CreateBrightness(amount, array); + //M15,M25,M35,M45がゼロなので意味がない + //Graphics.ColorMatrix.ToSkiaColorMatrix(array); + + return SKColorFilter.CreateColorMatrix(array); + }); + } + public void HighContrast(bool grayscale, HighContrastInvertStyle invertStyle, float contrast) { AppendSKColorFilter( @@ -321,7 +342,12 @@ public void LookupTable(T data, Func factory, float strength { if (table.Dimension == LookupTableDimension.OneDimension) { - return SKColorFilter.CreateTable(table.ToByteArray(data.strength, 0)); + byte[] array = table.ToByteArray(data.strength, 0); + return SKColorFilter.CreateTable( + Graphics.LookupTable.s_linear, + array, + array, + array); } else { diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs b/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs new file mode 100644 index 000000000..1b0710020 --- /dev/null +++ b/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; + +namespace Beutl.Graphics.Effects; + +public sealed class Gamma : FilterEffect +{ + public static readonly CoreProperty AmountProperty; + public static readonly CoreProperty StrengthProperty; + private float _amount = 100; + private float _strength = 100; + + static Gamma() + { + AmountProperty = ConfigureProperty(nameof(Amount)) + .Accessor(o => o.Amount, (o, v) => o.Amount = v) + .DefaultValue(100) + .Register(); + + StrengthProperty = ConfigureProperty(nameof(Strength)) + .Accessor(o => o.Strength, (o, v) => o.Strength = v) + .DefaultValue(100) + .Register(); + + AffectsRender(AmountProperty, StrengthProperty); + } + + [Range(1, 300)] + public float Amount + { + get => _amount; + set => SetAndRaise(AmountProperty, ref _amount, value); + } + + [Range(0, 100)] + public float Strength + { + get => _strength; + set => SetAndRaise(StrengthProperty, ref _strength, value); + } + + public override void ApplyTo(FilterEffectContext context) + { + float amount = _amount / 100f; + + context.LookupTable( + amount, + LookupTable.Gamma, + _strength / 100); + } +} diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs b/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs new file mode 100644 index 000000000..b88b1cbfe --- /dev/null +++ b/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs @@ -0,0 +1,35 @@ +using System.ComponentModel.DataAnnotations; +using System.Reactive; + +namespace Beutl.Graphics.Effects; + +public sealed class Invert : FilterEffect +{ + public static readonly CoreProperty AmountProperty; + private float _amount = 100; + + static Invert() + { + AmountProperty = ConfigureProperty(nameof(Amount)) + .Accessor(o => o.Amount, (o, v) => o.Amount = v) + .DefaultValue(100) + .Register(); + + AffectsRender(AmountProperty); + } + + [Range(0, 100)] + public float Amount + { + get => _amount; + set => SetAndRaise(AmountProperty, ref _amount, value); + } + + public override void ApplyTo(FilterEffectContext context) + { + context.LookupTable( + Unit.Default, + _ => LookupTable.Invert(), + _amount / 100); + } +} diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Lighting.cs b/src/Beutl.Engine/Graphics/FilterEffects/Lighting.cs index 28d940513..e03372644 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/Lighting.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/Lighting.cs @@ -6,13 +6,14 @@ public sealed class Lighting : FilterEffect { public static readonly CoreProperty MultiplyProperty; public static readonly CoreProperty AddProperty; - private Color _multiply; + private Color _multiply = Colors.White; private Color _add; static Lighting() { MultiplyProperty = ConfigureProperty(nameof(Multiply)) .Accessor(o => o.Multiply, (o, v) => o.Multiply = v) + .DefaultValue(Colors.White) .Register(); AddProperty = ConfigureProperty(nameof(Add)) diff --git a/src/Beutl.Engine/Graphics/LookupTable.cs b/src/Beutl.Engine/Graphics/LookupTable.cs index 5ffc5ccc4..3a36724ca 100644 --- a/src/Beutl.Engine/Graphics/LookupTable.cs +++ b/src/Beutl.Engine/Graphics/LookupTable.cs @@ -127,6 +127,18 @@ public static LookupTable Gamma(float gamma) return table; } + public static LookupTable Invert() + { + var table = new LookupTable(); + + Parallel.For(0, 256, pos => + { + table.AsSpan()[pos] = 1f - (pos / 255f); + }); + + return table; + } + public static LookupTable FromStream(Stream stream) { using var reader = new StreamReader(stream); diff --git a/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs b/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs index 60ddc9c99..b42689d43 100644 --- a/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs +++ b/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs @@ -66,3 +66,20 @@ public sealed class TransformEffectOperator : FilterEffectOperator Transform { get; set; } = new(TransformEffect.TransformProperty, null); } + +public sealed class BrightnessOperator : FilterEffectOperator +{ + public Setter Amount { get; set; } = new(Brightness.AmountProperty, 100); +} + +public sealed class GammaOperator : FilterEffectOperator +{ + public Setter Amount { get; set; } = new(Gamma.AmountProperty, 100); + + public Setter Strength { get; set; } = new(Gamma.StrengthProperty, 100); +} + +public sealed class InvertOperator : FilterEffectOperator +{ + public Setter Amount { get; set; } = new(Invert.AmountProperty, 100); +} diff --git a/src/Beutl.Operators/Configure/Effects/FilterEffectOperator.cs b/src/Beutl.Operators/Configure/Effects/FilterEffectOperator.cs index 64cb94ba5..a3b685f46 100644 --- a/src/Beutl.Operators/Configure/Effects/FilterEffectOperator.cs +++ b/src/Beutl.Operators/Configure/Effects/FilterEffectOperator.cs @@ -6,7 +6,7 @@ namespace Beutl.Operators.Configure.Effects; -public abstract class FilterEffectOperator : ConfigureOperator, ISourceTransformer +public abstract class FilterEffectOperator : ConfigureOperator where T : FilterEffect, new() { private readonly ConditionalWeakTable _table = new(); diff --git a/src/Beutl.Operators/LibraryRegistrar.cs b/src/Beutl.Operators/LibraryRegistrar.cs index a3fef70d8..96d46ca90 100644 --- a/src/Beutl.Operators/LibraryRegistrar.cs +++ b/src/Beutl.Operators/LibraryRegistrar.cs @@ -231,6 +231,21 @@ public static void RegisterAll() .BindFilterEffect() ) + .AddMultiple("Brightness", m => m + .BindSourceOperator() + .BindFilterEffect() + ) + + .AddMultiple("Gamma", m => m + .BindSourceOperator() + .BindFilterEffect() + ) + + .AddMultiple("Invert", m => m + .BindSourceOperator() + .BindFilterEffect() + ) + .AddMultiple(Strings.Transform, m => m .BindSourceOperator() .BindFilterEffect() From adf7c48f9772db1a46d6782367fe7bdbecdf12ee Mon Sep 17 00:00:00 2001 From: indigo-san Date: Mon, 14 Aug 2023 00:11:42 +0900 Subject: [PATCH 02/10] =?UTF-8?q?Hsv,=20Cmyk=E6=A7=8B=E9=80=A0=E4=BD=93?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Engine/Beutl.Engine.csproj | 3 - src/Beutl.Engine/Media/Cmyk.cs | 65 ++++++++--------- src/Beutl.Engine/Media/Color.cs | 73 +++++++++++++++++++ src/Beutl.Engine/Media/Hsv.cs | 72 +++++++++--------- src/Beutl.Engine/Media/YCbCr.cs | 85 ---------------------- tests/Beutl.Engine.UnitTests/ColorTests.cs | 30 ++++++++ 6 files changed, 168 insertions(+), 160 deletions(-) delete mode 100644 src/Beutl.Engine/Media/YCbCr.cs diff --git a/src/Beutl.Engine/Beutl.Engine.csproj b/src/Beutl.Engine/Beutl.Engine.csproj index f650eccba..88ff67e26 100644 --- a/src/Beutl.Engine/Beutl.Engine.csproj +++ b/src/Beutl.Engine/Beutl.Engine.csproj @@ -4,9 +4,6 @@ - - - diff --git a/src/Beutl.Engine/Media/Cmyk.cs b/src/Beutl.Engine/Media/Cmyk.cs index 14f64da71..0a4d6fd52 100644 --- a/src/Beutl.Engine/Media/Cmyk.cs +++ b/src/Beutl.Engine/Media/Cmyk.cs @@ -1,13 +1,15 @@ -namespace BeUtl.Media; +namespace Beutl.Media; -public struct Cmyk : IEquatable +public readonly struct Cmyk : IEquatable { - public Cmyk(double c, double m, double y, double k) + // 0-1 + public Cmyk(float c, float m, float y, float k, float a) { C = c; M = m; Y = y; K = k; + A = a; } public Cmyk(Color rgb) @@ -20,18 +22,15 @@ public Cmyk(Hsv hsv) this = hsv.ToCmyk(); } - public Cmyk(YCbCr yc) - { - this = yc.ToCmyk(); - } - - public double C { readonly get; set; } + public float C { get; } - public double M { readonly get; set; } + public float M { get; } - public double Y { readonly get; set; } + public float Y { get; } - public double K { readonly get; set; } + public float K { get; } + + public float A { get; } public static bool operator ==(Cmyk left, Cmyk right) { @@ -43,33 +42,30 @@ public Cmyk(YCbCr yc) return !(left == right); } - public readonly Color ToColor() + public Color ToColor() { - var cc = C / 100.0; - var mm = M / 100.0; - var yy = Y / 100.0; - var kk = K / 100.0; - - var r = (1.0 - cc) * (1.0 - kk); - var g = (1.0 - mm) * (1.0 - kk); - var b = (1.0 - yy) * (1.0 - kk); - r = Math.Round(r * 255.0); - g = Math.Round(g * 255.0); - b = Math.Round(b * 255.0); - - return Color.FromArgb(255, (byte)r, (byte)g, (byte)b); + float c = C; + float m = M; + float y = Y; + float k = K; + + float r = (1.0F - c) * (1.0F - k); + float g = (1.0F - m) * (1.0F - k); + float b = (1.0F - y) * (1.0F - k); + float a = A; + r = MathF.Round(r * 255.0F); + g = MathF.Round(g * 255.0F); + b = MathF.Round(b * 255.0F); + a = MathF.Round(a * 255.0F); + + return Color.FromArgb((byte)a, (byte)r, (byte)g, (byte)b); } - public readonly Hsv ToHsv() + public Hsv ToHsv() { return ToColor().ToHsv(); } - public readonly YCbCr ToYCbCr() - { - return ToColor().ToYCbCr(); - } - public override bool Equals(object? obj) { return obj is Cmyk cmyk && Equals(cmyk); @@ -80,11 +76,12 @@ public bool Equals(Cmyk other) return C == other.C && M == other.M && Y == other.Y && - K == other.K; + K == other.K && + A == other.A; } public override int GetHashCode() { - return HashCode.Combine(C, M, Y, K); + return HashCode.Combine(C, M, Y, K, A); } } diff --git a/src/Beutl.Engine/Media/Color.cs b/src/Beutl.Engine/Media/Color.cs index 91a7c3282..0b439af2c 100644 --- a/src/Beutl.Engine/Media/Color.cs +++ b/src/Beutl.Engine/Media/Color.cs @@ -287,6 +287,79 @@ public int ToInt32() return A << 24 | R << 16 | G << 8 | B; } + /// + /// Converts this 32Bit color to HSV. + /// + /// Returns the HSV. + public Hsv ToHsv() + { + float h = default; + float s; + float v; + float a = A / 255F; + float min = Math.Min(Math.Min(R, G), B); + float max = Math.Max(Math.Max(R, G), B); + + float delta = max - min; + + v = 100.0F * max / 255.0F; + + if (max == 0.0F) + { + s = 0; + } + else + { + s = 100.0F * delta / max; + } + + if (s == 0) + { + h = 0; + } + else + { + if (R == max) + { + h = 60.0F * (G - B) / delta; + } + else if (G == max) + { + h = 120.0F + (60.0F * (B - R) / delta); + } + else if (B == max) + { + h = 240.0F + (60.0F * (R - G) / delta); + } + + if (h < 0.0) + { + h += 360.0F; + } + } + + return new Hsv(h, s, v, a); + } + + /// + /// Converts this 32Bit color to CMYK. + /// + /// Returns the CMYK. + public Cmyk ToCmyk() + { + float rr = R / 255.0F; + float gg = G / 255.0F; + float bb = B / 255.0F; + float aa = A / 255.0F; + + float k = 1.0F - Math.Max(Math.Max(rr, gg), bb); + float c = (1.0F - rr - k) / (1.0F - k); + float m = (1.0F - gg - k) / (1.0F - k); + float y = (1.0F - bb - k) / (1.0F - k); + + return new Cmyk(c, m, y, k, aa); + } + /// /// Check if two colors are equal. /// diff --git a/src/Beutl.Engine/Media/Hsv.cs b/src/Beutl.Engine/Media/Hsv.cs index 327ddf123..2e7a2bd12 100644 --- a/src/Beutl.Engine/Media/Hsv.cs +++ b/src/Beutl.Engine/Media/Hsv.cs @@ -1,19 +1,20 @@ using System.Runtime.InteropServices; -namespace BeUtl.Media; +namespace Beutl.Media; [StructLayout(LayoutKind.Sequential)] -public struct Hsv : IEquatable +public readonly struct Hsv : IEquatable { // Hue 0 - 360 // Saturation 0-100% // Value 0-100% - public Hsv(double h, double s, double v) + public Hsv(float h, float s, float v, float a) { H = h; S = s; V = v; + A = a; } public Hsv(Color rgb) @@ -26,16 +27,13 @@ public Hsv(Cmyk cmyk) this = cmyk.ToHsv(); } - public Hsv(YCbCr yc) - { - this = yc.ToHsv(); - } + public float H { get; } - public double H { readonly get; set; } + public float S { get; } - public double S { readonly get; set; } + public float V { get; } - public double V { readonly get; set; } + public float A { get; } public static bool operator ==(Hsv left, Hsv right) { @@ -47,29 +45,30 @@ public Hsv(YCbCr yc) return !(left == right); } - public readonly Color ToColor() + public Color ToColor() { - double r; - double g; - double b; + float r; + float g; + float b; + float a=A; if (S == 0) { - r = g = b = Math.Round(V * 2.55); + r = g = b = MathF.Round(V * 2.55F); return Color.FromArgb(255, (byte)r, (byte)g, (byte)b); } - var hh = H; - var ss = S / 100.0; - var vv = V / 100.0; - if (hh >= 360.0) - hh = 0.0; - hh /= 60.0; + float hh = H; + float ss = S / 100.0F; + float vv = V / 100.0F; + if (hh >= 360.0F) + hh = 0.0F; + hh /= 60.0F; - var i = (long)hh; - var ff = hh - i; - var p = vv * (1.0 - ss); - var q = vv * (1.0 - ss * ff); - var t = vv * (1.0 - ss * (1.0 - ff)); + float i = (long)hh; + float ff = hh - i; + float p = vv * (1.0F - ss); + float q = vv * (1.0F - ss * ff); + float t = vv * (1.0F - ss * (1.0F - ff)); switch ((int)i) { @@ -105,23 +104,19 @@ public readonly Color ToColor() break; } - r = Math.Round(r * 255.0); - g = Math.Round(g * 255.0); - b = Math.Round(b * 255.0); + r = MathF.Round(r * 255.0F); + g = MathF.Round(g * 255.0F); + b = MathF.Round(b * 255.0F); + a = MathF.Round(a * 255.0F); - return Color.FromArgb(255, (byte)r, (byte)g, (byte)b); + return Color.FromArgb((byte)a, (byte)r, (byte)g, (byte)b); } - public readonly Cmyk ToCmyk() + public Cmyk ToCmyk() { return ToColor().ToCmyk(); } - public readonly YCbCr ToYCbCr() - { - return ToColor().ToYCbCr(); - } - public override bool Equals(object? obj) { return obj is Hsv hsv && Equals(hsv); @@ -131,11 +126,12 @@ public bool Equals(Hsv other) { return H == other.H && S == other.S && - V == other.V; + V == other.V && + A == other.A; } public override int GetHashCode() { - return HashCode.Combine(H, S, V); + return HashCode.Combine(H, S, V, A); } } diff --git a/src/Beutl.Engine/Media/YCbCr.cs b/src/Beutl.Engine/Media/YCbCr.cs deleted file mode 100644 index 43c5936d3..000000000 --- a/src/Beutl.Engine/Media/YCbCr.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Runtime.InteropServices; - -namespace BeUtl.Media; - -[StructLayout(LayoutKind.Sequential)] -public struct YCbCr : IEquatable -{ - public YCbCr(float y, float cb, float cr) - { - Y = y; - Cb = cb; - Cr = cr; - } - - public YCbCr(Color rgb) - { - this = rgb.ToYCbCr(); - } - - public YCbCr(Hsv hsv) - { - this = hsv.ToYCbCr(); - } - - public YCbCr(Cmyk cmyk) - { - this = cmyk.ToYCbCr(); - } - - public float Y { readonly get; set; } - - public float Cb { readonly get; set; } - - public float Cr { readonly get; set; } - - public static bool operator ==(YCbCr left, YCbCr right) - { - return left.Equals(right); - } - - public static bool operator !=(YCbCr left, YCbCr right) - { - return !(left == right); - } - - public readonly Color ToColor() - { - var y = Y; - var cb = Cb - 128F; - var cr = Cr - 128F; - - var r = MathF.Round(y + 1.402F * cr, MidpointRounding.AwayFromZero); - var g = MathF.Round(y - 0.344136F * cb - 0.714136F * cr, MidpointRounding.AwayFromZero); - var b = MathF.Round(y + 1.772F * cb, MidpointRounding.AwayFromZero); - - return Color.FromArgb(255, (byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); - } - - public readonly Cmyk ToCmyk() - { - return ToColor().ToCmyk(); - } - - public readonly Hsv ToHsv() - { - return ToColor().ToHsv(); - } - - public override bool Equals(object? obj) - { - return obj is YCbCr cr && Equals(cr); - } - - public bool Equals(YCbCr other) - { - return Y == other.Y && - Cb == other.Cb && - Cr == other.Cr; - } - - public override int GetHashCode() - { - return HashCode.Combine(Y, Cb, Cr); - } -} diff --git a/tests/Beutl.Engine.UnitTests/ColorTests.cs b/tests/Beutl.Engine.UnitTests/ColorTests.cs index 29754dc83..a06c7b81e 100644 --- a/tests/Beutl.Engine.UnitTests/ColorTests.cs +++ b/tests/Beutl.Engine.UnitTests/ColorTests.cs @@ -133,4 +133,34 @@ public void TryParseInvalidNumber() { Assert.False(Color.TryParse("#ff808g80", out _)); } + + // テストケースは適当です。 + // I chose test case randomly. + [Test] + [TestCase("Aqua")] + [TestCase("DeepPink")] + [TestCase("Blue")] + [TestCase("Gold")] + public void ToHsv(string color) + { + var expected = Color.Parse(color); + var hsv = expected.ToHsv(); + var actual = hsv.ToColor(); + + Assert.AreEqual(expected, actual); + } + + [Test] + [TestCase("Aqua")] + [TestCase("DeepPink")] + [TestCase("Blue")] + [TestCase("Gold")] + public void ToCmyk(string color) + { + var expected = Color.Parse(color); + var cmyk = expected.ToCmyk(); + var actual = cmyk.ToColor(); + + Assert.AreEqual(expected, actual); + } } From 02e43a71e14ae4479adc7c575de3a8979a91c2cc Mon Sep 17 00:00:00 2001 From: indigo-san Date: Mon, 14 Aug 2023 00:13:48 +0900 Subject: [PATCH 03/10] =?UTF-8?q?TransformOperator=E3=82=92FilterEffect?= =?UTF-8?q?=E3=83=99=E3=83=BC=E3=82=B9=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Configure/Transform/TransformOperator.cs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Beutl.Operators/Configure/Transform/TransformOperator.cs b/src/Beutl.Operators/Configure/Transform/TransformOperator.cs index 0c453d65e..4a3652fa2 100644 --- a/src/Beutl.Operators/Configure/Transform/TransformOperator.cs +++ b/src/Beutl.Operators/Configure/Transform/TransformOperator.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using Beutl.Graphics; -using Beutl.Graphics.Transformation; +using Beutl.Graphics.Effects; namespace Beutl.Operators.Configure.Transform; @@ -11,7 +11,7 @@ namespace Beutl.Operators.Configure.Transform; public abstract class TransformOperator : ConfigureOperator where T : Transform, new() { - private readonly ConditionalWeakTable _table = new(); + private readonly ConditionalWeakTable _table = new(); protected override void PreProcess(Drawable target, T value) { @@ -20,12 +20,20 @@ protected override void PreProcess(Drawable target, T value) protected override void Process(Drawable target, T value) { - MultiTransform multi = _table.GetValue(target, _ => new MultiTransform()); - if (target.Transform != multi) + CombinedFilterEffect composed = _table.GetValue(target, _ => new CombinedFilterEffect()); + if (target.FilterEffect != composed) { - multi.Left = value; - multi.Right = target.Transform; - target.Transform = multi; + if (composed.Second is not TransformEffect transformEffect) + { + transformEffect = new TransformEffect(); + composed.Second = transformEffect; + } + + transformEffect.Transform = value; + + composed.Second = transformEffect; + composed.First = target.FilterEffect; + target.FilterEffect = composed; } } From 02efa972357ab941150f91e2ae69fc995213a267 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Mon, 14 Aug 2023 01:10:20 +0900 Subject: [PATCH 04/10] =?UTF-8?q?Fill=E3=83=97=E3=83=AD=E3=83=91=E3=83=86?= =?UTF-8?q?=E3=82=A3=E3=81=AE=E5=88=9D=E6=9C=9F=E5=80=A4=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Engine/Graphics/Drawable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Beutl.Engine/Graphics/Drawable.cs b/src/Beutl.Engine/Graphics/Drawable.cs index 1ae5f6837..dd6c3f989 100644 --- a/src/Beutl.Engine/Graphics/Drawable.cs +++ b/src/Beutl.Engine/Graphics/Drawable.cs @@ -24,7 +24,7 @@ public abstract class Drawable : Renderable, IHierarchical private AlignmentX _alignX = AlignmentX.Center; private AlignmentY _alignY = AlignmentY.Center; private RelativePoint _transformOrigin = RelativePoint.Center; - private IBrush? _fill = new SolidColorBrush(Colors.White); + private IBrush? _fill = null; private IBrush? _opacityMask; private BlendMode _blendMode = BlendMode.SrcOver; From c811927349cfd9d72258658d66faa576a0f0bb06 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Mon, 14 Aug 2023 01:10:52 +0900 Subject: [PATCH 05/10] =?UTF-8?q?Threshold=E3=82=A8=E3=83=95=E3=82=A7?= =?UTF-8?q?=E3=82=AF=E3=83=88=E3=81=8CAlpha=E3=83=81=E3=83=A3=E3=83=B3?= =?UTF-8?q?=E3=83=8D=E3=83=AB=E3=82=92=E7=84=A1=E8=A6=96=E3=81=99=E3=82=8B?= =?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs b/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs index a3ac559c5..b2dffdc36 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; +using SkiaSharp; + namespace Beutl.Graphics.Effects; public sealed class Threshold : FilterEffect @@ -43,17 +45,19 @@ public override void ApplyTo(FilterEffectContext context) int threshold = Math.Clamp((int)(_value / 100f * 255), 0, 255); context.HighContrast(true, HighContrastInvertStyle.NoInvert, 0); - context.LookupTable(threshold, (threshold) => + context.AppendSKColorFilter((threshold, strength: (_strength / 100)), (data, _) => { var lut = new LookupTable(); Span span = lut.AsSpan(); - for (int i = threshold; i < 256; i++) + for (int i = data.threshold; i < 256; i++) { span[i] = 1; } - return lut; - }, _strength / 100); + byte[] array = lut.ToByteArray(data.strength, 0); + + return SKColorFilter.CreateTable(array); + }); } } From 6ea0cce24893f81e135cd3d6d9690dc841046d7a Mon Sep 17 00:00:00 2001 From: indigo-san Date: Mon, 14 Aug 2023 01:11:13 +0900 Subject: [PATCH 06/10] =?UTF-8?q?=E8=89=B2=E3=83=97=E3=83=AD=E3=83=91?= =?UTF-8?q?=E3=83=86=E3=82=A3=E3=81=AE=E3=83=A9=E3=82=A4=E3=83=96=E3=83=97?= =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E3=82=92=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PropertyEditors/ColorEditor.cs | 44 ++++++++++++++++--- .../Styling/PropertyEditors/ColorEditor.axaml | 4 ++ .../Editors/ColorEditorViewModel.cs | 14 +++++- .../Editors/IConfigureLivePreview.cs | 8 ++++ .../Views/Editors/PropertyEditorMenu.axaml | 24 ++++++---- .../Views/Editors/PropertyEditorMenu.axaml.cs | 8 +++- 6 files changed, 86 insertions(+), 16 deletions(-) create mode 100644 src/Beutl/ViewModels/Editors/IConfigureLivePreview.cs diff --git a/src/Beutl.Controls/PropertyEditors/ColorEditor.cs b/src/Beutl.Controls/PropertyEditors/ColorEditor.cs index 6fa1fe4fd..b1138744a 100644 --- a/src/Beutl.Controls/PropertyEditors/ColorEditor.cs +++ b/src/Beutl.Controls/PropertyEditors/ColorEditor.cs @@ -17,6 +17,10 @@ public class ColorEditor : PropertyEditor (o, v) => o.Value = v, defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty IsLivePreviewEnabledProperty = + AvaloniaProperty.Register(nameof(IsLivePreviewEnabled)); + + private Color _oldValue; private Color _value; public Color Value @@ -25,24 +29,54 @@ public Color Value set => SetAndRaise(ValueProperty, ref _value, value); } + public bool IsLivePreviewEnabled + { + get => GetValue(IsLivePreviewEnabledProperty); + set => SetValue(IsLivePreviewEnabledProperty, value); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); ColorPickerButton button = e.NameScope.Get("PART_ColorPickerButton"); button.ColorChanged += OnColorChanged; + button.FlyoutOpened += OnFlyoutOpened; + button.FlyoutClosed += OnFlyoutClosed; button.FlyoutConfirmed += OnFlyoutConfirmed; } + private void OnFlyoutOpened(ColorPickerButton sender, EventArgs args) + { + if (IsLivePreviewEnabled) + _oldValue = _value; + } + + private void OnFlyoutClosed(ColorPickerButton sender, EventArgs args) + { + if (IsLivePreviewEnabled) + { + RaiseEvent(new PropertyEditorValueChangedEventArgs( + Value, _oldValue, ValueChangedEvent)); + } + } + private void OnFlyoutConfirmed(ColorPickerButton sender, ColorButtonColorChangedEventArgs args) { - Value = args.NewColor.GetValueOrDefault(); - RaiseEvent(new PropertyEditorValueChangedEventArgs( - Value, args.OldColor.GetValueOrDefault(), ValueChangedEvent)); + if (!IsLivePreviewEnabled) + { + Value = args.NewColor.GetValueOrDefault(); + RaiseEvent(new PropertyEditorValueChangedEventArgs( + Value, args.OldColor.GetValueOrDefault(), ValueChangedEvent)); + } } private void OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args) { - RaiseEvent(new PropertyEditorValueChangedEventArgs( - args.NewColor.GetValueOrDefault(), args.OldColor.GetValueOrDefault(), ValueChangingEvent)); + if (IsLivePreviewEnabled) + { + Value = args.NewColor.GetValueOrDefault(); + RaiseEvent(new PropertyEditorValueChangedEventArgs( + args.NewColor.GetValueOrDefault(), args.OldColor.GetValueOrDefault(), ValueChangingEvent)); + } } } diff --git a/src/Beutl.Controls/Styling/PropertyEditors/ColorEditor.axaml b/src/Beutl.Controls/Styling/PropertyEditors/ColorEditor.axaml index 8840609b5..9866a97e4 100644 --- a/src/Beutl.Controls/Styling/PropertyEditors/ColorEditor.axaml +++ b/src/Beutl.Controls/Styling/PropertyEditors/ColorEditor.axaml @@ -33,7 +33,9 @@ Grid.Column="1" Margin="4,2" HorizontalAlignment="Right" + CustomPaletteColors="{DynamicResource PaletteColors}" IsEnabled="{TemplateBinding IsReadOnly, Converter={x:Static BoolConverters.Not}}" + ShowAcceptDismissButtons="{TemplateBinding IsLivePreviewEnabled, Converter={x:Static BoolConverters.Not}}" UseColorPalette="True" Color="{Binding Value, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" /> @@ -73,7 +75,9 @@ Grid.Column="1" Margin="4,2" HorizontalAlignment="Right" + CustomPaletteColors="{DynamicResource PaletteColors}" IsEnabled="{TemplateBinding IsReadOnly, Converter={x:Static BoolConverters.Not}}" + ShowAcceptDismissButtons="{TemplateBinding IsLivePreviewEnabled, Converter={x:Static BoolConverters.Not}}" UseColorPalette="True" Color="{Binding Value, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" /> diff --git a/src/Beutl/ViewModels/Editors/ColorEditorViewModel.cs b/src/Beutl/ViewModels/Editors/ColorEditorViewModel.cs index aca5b6974..7ba750bb3 100644 --- a/src/Beutl/ViewModels/Editors/ColorEditorViewModel.cs +++ b/src/Beutl/ViewModels/Editors/ColorEditorViewModel.cs @@ -10,7 +10,7 @@ namespace Beutl.ViewModels.Editors; -public sealed class ColorEditorViewModel : ValueEditorViewModel +public sealed class ColorEditorViewModel : ValueEditorViewModel, IConfigureLivePreview { public ColorEditorViewModel(IAbstractProperty property) : base(property) @@ -23,13 +23,25 @@ public ColorEditorViewModel(IAbstractProperty property) public ReadOnlyReactivePropertySlim Value2 { get; } + public ReactivePropertySlim IsLivePreviewEnabled { get; } = new(true); + public override void Accept(IPropertyEditorContextVisitor visitor) { base.Accept(visitor); if (visitor is ColorEditor editor) { editor[!ColorEditor.ValueProperty] = Value2.ToBinding(); + editor[!ColorEditor.IsLivePreviewEnabledProperty] = IsLivePreviewEnabled.ToBinding(); editor.ValueChanged += OnValueChanged; + editor.ValueChanging += OnValueChanging; + } + } + + private void OnValueChanging(object? sender, PropertyEditorValueChangedEventArgs e) + { + if (sender is ColorEditor editor) + { + editor.Value = SetCurrentValueAndGetCoerced(editor.Value.ToMedia()).ToAvalonia(); } } diff --git a/src/Beutl/ViewModels/Editors/IConfigureLivePreview.cs b/src/Beutl/ViewModels/Editors/IConfigureLivePreview.cs new file mode 100644 index 000000000..cbdaece0e --- /dev/null +++ b/src/Beutl/ViewModels/Editors/IConfigureLivePreview.cs @@ -0,0 +1,8 @@ +using Reactive.Bindings; + +namespace Beutl.ViewModels.Editors; + +public interface IConfigureLivePreview +{ + ReactivePropertySlim IsLivePreviewEnabled { get; } +} diff --git a/src/Beutl/Views/Editors/PropertyEditorMenu.axaml b/src/Beutl/Views/Editors/PropertyEditorMenu.axaml index 8a6ccd86a..008a16b91 100644 --- a/src/Beutl/Views/Editors/PropertyEditorMenu.axaml +++ b/src/Beutl/Views/Editors/PropertyEditorMenu.axaml @@ -5,6 +5,7 @@ xmlns:icons="using:FluentIcons.FluentAvalonia" xmlns:lang="using:Beutl.Language" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:vm="using:Beutl.ViewModels.Editors" Margin="0,0,4,0" VerticalAlignment="Center" @@ -20,15 +21,20 @@ Background="Transparent" BorderThickness="0" Click="Button_Click"> - - - - - - - + + + + + + + + diff --git a/src/Beutl/Views/Editors/PropertyEditorMenu.axaml.cs b/src/Beutl/Views/Editors/PropertyEditorMenu.axaml.cs index c3b5f32c3..e08f8233a 100644 --- a/src/Beutl/Views/Editors/PropertyEditorMenu.axaml.cs +++ b/src/Beutl/Views/Editors/PropertyEditorMenu.axaml.cs @@ -19,13 +19,19 @@ public PropertyEditorMenu() InitializeComponent(); } + protected override void OnDataContextChanged(EventArgs e) + { + base.OnDataContextChanged(e); + toggleLivePreview.IsVisible = DataContext is IConfigureLivePreview; + } + private void Button_Click(object? sender, RoutedEventArgs e) { if (DataContext is BaseEditorViewModel viewModel) { if (!viewModel.HasAnimation.Value && sender is Button button) { - button.ContextMenu?.Open(); + button.ContextFlyout?.ShowAt(button); } else if (viewModel.GetService() is { } scene) { From a827b8a5974cfa5027ad17e7ef2a42dd77a7ae10 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Tue, 15 Aug 2023 21:38:41 +0900 Subject: [PATCH 07/10] =?UTF-8?q?LookupTable=E3=82=92static=20class?= =?UTF-8?q?=E3=81=AB=E3=81=97=E3=81=A6=E9=85=8D=E5=88=97=E3=81=ABLUT?= =?UTF-8?q?=E3=82=92=E7=94=9F=E6=88=90=E3=81=99=E3=82=8B=E3=83=A1=E3=82=BD?= =?UTF-8?q?=E3=83=83=E3=83=89=E3=81=A8CubeFile=E3=82=92=E5=88=86=E3=81=91?= =?UTF-8?q?=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + Directory.Packages.props | 1 + src/Beutl.Engine/Beutl.Engine.csproj | 1 + src/Beutl.Engine/Graphics/ColorMatrix.cs | 37 ++- src/Beutl.Engine/Graphics/CubeFile.cs | 195 ++++++++++++ .../Graphics/CubeFileDimension.cs | 8 + .../FilterEffects/FilterEffectContext.cs | 196 ++++++++---- .../Graphics/FilterEffects/Gamma.cs | 4 +- .../Graphics/FilterEffects/Invert.cs | 4 +- .../Graphics/FilterEffects/Threshold.cs | 21 +- src/Beutl.Engine/Graphics/LookupTable.cs | 295 +++++------------- 11 files changed, 461 insertions(+), 304 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/Beutl.Engine/Graphics/CubeFile.cs create mode 100644 src/Beutl.Engine/Graphics/CubeFileDimension.cs diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..aff9295ac --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.defaultSolution": "Beutl.sln" +} \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props index 40741c730..5e5dec3cb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,6 +29,7 @@ + diff --git a/src/Beutl.Engine/Beutl.Engine.csproj b/src/Beutl.Engine/Beutl.Engine.csproj index 88ff67e26..b65bf78c7 100644 --- a/src/Beutl.Engine/Beutl.Engine.csproj +++ b/src/Beutl.Engine/Beutl.Engine.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Beutl.Engine/Graphics/ColorMatrix.cs b/src/Beutl.Engine/Graphics/ColorMatrix.cs index b20299cf8..d090b1201 100644 --- a/src/Beutl.Engine/Graphics/ColorMatrix.cs +++ b/src/Beutl.Engine/Graphics/ColorMatrix.cs @@ -134,7 +134,7 @@ public static ColorMatrix CreateLuminanceToAlpha() return CreateFromSpan(span); } - + public static ColorMatrix CreateBrightness(float amount) { Span span = stackalloc float[20]; @@ -154,15 +154,34 @@ public float[] ToArray() }; } - internal float[] ToArrayForSkia() + internal void ToArrayForSkia(float[] array) { - return new float[] - { - M11, M12, M13, M14, M15 * 255, - M21, M22, M23, M24, M25 * 255, - M31, M32, M33, M34, M35 * 255, - M41, M42, M43, M44, M45 * 255, - }; + if (array.Length != 20) + throw new ArgumentException("配列の長さが無効です。"); + + array[0] = M11; + array[1] = M12; + array[2] = M13; + array[3] = M14; + array[4] = M15 * 255; + + array[5] = M21; + array[6] = M22; + array[7] = M23; + array[8] = M24; + array[9] = M25 * 255; + + array[10] = M31; + array[11] = M32; + array[12] = M33; + array[13] = M34; + array[14] = M35 * 255; + + array[15] = M41; + array[16] = M42; + array[17] = M43; + array[18] = M44; + array[19] = M45 * 255; } public static bool operator ==(in ColorMatrix value1, in ColorMatrix value2) => value1.Equals(value2); diff --git a/src/Beutl.Engine/Graphics/CubeFile.cs b/src/Beutl.Engine/Graphics/CubeFile.cs new file mode 100644 index 000000000..7c3ebbdca --- /dev/null +++ b/src/Beutl.Engine/Graphics/CubeFile.cs @@ -0,0 +1,195 @@ +using System.Numerics; +using System.Text.RegularExpressions; + +namespace Beutl.Graphics; + +public partial class CubeFile +{ + private static readonly Regex s_lutSizeReg = LUTSizeRegex(); + private static readonly Regex s_titleReg = TitleRegex(); + private static readonly Regex s_domainMinReg = DomainMinRegex(); + private static readonly Regex s_domainMaxReg = DomainMaxRegex(); + + public required string Title { get; init; } + + public required CubeFileDimension Dimention { get; init; } + + public required int Size { get; init; } + + public required Vector3 Min { get; init; } + + public required Vector3 Max { get; init; } + + public required Vector3[] Data { get; init; } + + private int Near(float x) + { + return Math.Min((int)x, Size - 1); + } + + private Vector3 LinearInterplate(float p) + { + Vector3 near = Data[Near(p)]; + Vector3 neighbor = Data[Near(p + 1)]; + if (near == neighbor) + { + return near; + } + else + { + float progress = p - Near(p); + + return ((neighbor - near) * progress) + near; + } + } + + public void ToLUT(float strength, byte[] r, byte[] g, byte[] b) + { + if (Dimention != CubeFileDimension.OneDimension) + throw new InvalidOperationException("ToLUTメソッドは1D LUTでのみ使えます。"); + + if (r.Length != 256 || g.Length != 256 || b.Length != 256) + throw new ArgumentException("配列の長さが無効です。"); + + int size = Size; + for (int i = 0; i < 256; i++) + { + float p = i * size / 256f; + Vector3 v = LinearInterplate(p); + + float add = i * (1 - strength); + r[i] = (byte)((((v.X * 255) + 0.5) * strength) + add); + g[i] = (byte)((((v.Y * 255) + 0.5) * strength) + add); + b[i] = (byte)((((v.Z * 255) + 0.5) * strength) + add); + } + } + + public static CubeFile FromStream(Stream stream) + { + using var reader = new StreamReader(stream); + int i = 0; + ReadInfo( + reader, + out string? title, + out CubeFileDimension dim, + out int size, + out Vector3 min, + out Vector3 max); + + int length = (int)Math.Pow(size, (int)dim); + Vector3[] data = new Vector3[length]; + + while (i < length) + { + string? line = reader.ReadLine(); + if (line is not null && !line.StartsWith('#')) + { + string[] values = line.Split(' '); + if (values.Length != 3) continue; + + if (float.TryParse(values[0], out float r) && + float.TryParse(values[1], out float g) && + float.TryParse(values[2], out float b)) + { + data[i] = new(r, g, b); + i++; + } + } + } + + return new CubeFile + { + Title = title, + Dimention = dim, + Size = size, + Min = min, + Max = max, + Data = data + }; + } + + private static void ReadInfo(StreamReader reader, out string title, out CubeFileDimension dim, out int size, out Vector3 min, out Vector3 max) + { + title = string.Empty; + dim = CubeFileDimension.ThreeDimension; + size = 33; + min = new(0, 0, 0); + max = new(1, 1, 1); + bool titleFound = false; + bool lutSizeFound = false; + bool minFound = false; + bool maxFound = false; + + while (!reader.EndOfStream) + { + if (titleFound && lutSizeFound && minFound && maxFound) break; + string? line = reader.ReadLine(); + if (line is not null) + { + if (s_lutSizeReg.IsMatch(line)) + { + if (lutSizeFound) + throw new Exception(); + lutSizeFound = true; + Match match = s_lutSizeReg.Match(line); + size = int.Parse(match.Groups["size"].Value); + switch (match.Groups["dim"].Value) + { + case "3D": + if (size < 2 || 256 < size) + throw new Exception(); + dim = CubeFileDimension.ThreeDimension; + break; + + case "1D": + if (size < 2 || 65536 < size) + throw new Exception(); + dim = CubeFileDimension.OneDimension; + break; + + default: + throw new Exception(); + } + } + else if (s_titleReg.IsMatch(line)) + { + titleFound = true; + Match match = s_lutSizeReg.Match(line); + title = match.Groups["text"].Value; + } + else if (s_domainMaxReg.IsMatch(line)) + { + maxFound = true; + Match match = s_domainMaxReg.Match(line); + float r = float.Parse(match.Groups["red"].Value); + float g = float.Parse(match.Groups["green"].Value); + float b = float.Parse(match.Groups["blue"].Value); + max = new(r, g, b); + } + else if (s_domainMinReg.IsMatch(line)) + { + minFound = true; + Match match = s_domainMinReg.Match(line); + float r = float.Parse(match.Groups["red"].Value); + float g = float.Parse(match.Groups["green"].Value); + float b = float.Parse(match.Groups["blue"].Value); + min = new(r, g, b); + } + } + } + + reader.BaseStream.Position = 0; + } + + [GeneratedRegex("^LUT_(?.*?)_SIZE (?.*?)$")] + private static partial Regex LUTSizeRegex(); + + [GeneratedRegex("^TITLE \"(?.*?)\"$")] + private static partial Regex TitleRegex(); + + [GeneratedRegex("^DOMAIN_MIN (?.*?) (?.*?) (?.*?)$")] + private static partial Regex DomainMinRegex(); + + [GeneratedRegex("^DOMAIN_MAX (?.*?) (?.*?) (?.*?)$")] + private static partial Regex DomainMaxRegex(); +} diff --git a/src/Beutl.Engine/Graphics/CubeFileDimension.cs b/src/Beutl.Engine/Graphics/CubeFileDimension.cs new file mode 100644 index 000000000..735f52788 --- /dev/null +++ b/src/Beutl.Engine/Graphics/CubeFileDimension.cs @@ -0,0 +1,8 @@ +namespace Beutl.Graphics; + +public enum CubeFileDimension +{ + OneDimension = 1, + + ThreeDimension = 3, +} diff --git a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs index 5bf6e1bfc..44eef18e2 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/FilterEffectContext.cs @@ -4,16 +4,46 @@ using Beutl.Collections.Pooled; using Beutl.Media; - +using Microsoft.Extensions.ObjectPool; using SkiaSharp; namespace Beutl.Graphics.Effects; +internal sealed class ArrayPooledObjectPolicy : IPooledObjectPolicy +{ + private readonly int _length; + + public ArrayPooledObjectPolicy(int length) + { + _length = length; + } + + public T[] Create() + { + return new T[_length]; + } + + public bool Return(T[] obj) + { + Array.Clear(obj); + return true; + } +} + public sealed class FilterEffectContext : IDisposable, IEquatable { private readonly PooledList<(FilterEffect FE, int Version)> _versions; internal readonly PooledList _items; + internal static readonly ObjectPool s_lutPool; + internal static readonly ObjectPool s_colorMatPool; + + static FilterEffectContext() + { + s_lutPool = new DefaultObjectPool(new ArrayPooledObjectPolicy(256)); + s_colorMatPool = new DefaultObjectPool(new ArrayPooledObjectPolicy(20)); + } + public FilterEffectContext(Rect bounds) { Bounds = OriginalBounds = bounds; @@ -229,7 +259,19 @@ public void Dilate(float radiusX, float radiusY) public void ColorMatrix(in ColorMatrix matrix) { - AppendSKColorFilter(matrix, (m, _) => SKColorFilter.CreateColorMatrix(m.ToArrayForSkia())); + AppendSKColorFilter(matrix, (m, _) => + { + float[] array = s_colorMatPool.Get(); + try + { + m.ToArrayForSkia(array); + return SKColorFilter.CreateColorMatrix(array); + } + finally + { + s_colorMatPool.Return(array); + } + }); } public void ColorMatrix(T data, Func factory) @@ -237,19 +279,38 @@ public void ColorMatrix(T data, Func factory) { AppendSKColorFilter( (data, factory), - (t, _) => SKColorFilter.CreateColorMatrix((t.factory.Invoke(t.data)).ToArrayForSkia())); + (t, _) => + { + float[] array = s_colorMatPool.Get(); + try + { + t.factory.Invoke(t.data).ToArrayForSkia(array); + return SKColorFilter.CreateColorMatrix(array); + } + finally + { + s_colorMatPool.Return(array); + } + }); } public void Saturate(float amount) { AppendSKColorFilter(amount, (s, _) => { - float[] array = new float[20]; - Graphics.ColorMatrix.CreateSaturateMatrix(s, array); - //M15,M25,M35,M45がゼロなので意味がない - //Graphics.ColorMatrix.ToSkiaColorMatrix(array); + float[] array = s_colorMatPool.Get(); + try + { + Graphics.ColorMatrix.CreateSaturateMatrix(s, array); + //M15,M25,M35,M45がゼロなので意味がない + //Graphics.ColorMatrix.ToSkiaColorMatrix(array); - return SKColorFilter.CreateColorMatrix(array); + return SKColorFilter.CreateColorMatrix(array); + } + finally + { + s_colorMatPool.Return(array); + } }); } @@ -257,12 +318,19 @@ public void HueRotate(float degrees) { AppendSKColorFilter(degrees, (s, _) => { - float[] array = new float[20]; - Graphics.ColorMatrix.CreateHueRotateMatrix(degrees, array); - //M15,M25,M35,M45がゼロなので意味がない - //Graphics.ColorMatrix.ToSkiaColorMatrix(array); + float[] array = s_colorMatPool.Get(); + try + { + Graphics.ColorMatrix.CreateHueRotateMatrix(degrees, array); + //M15,M25,M35,M45がゼロなので意味がない + //Graphics.ColorMatrix.ToSkiaColorMatrix(array); - return SKColorFilter.CreateColorMatrix(array); + return SKColorFilter.CreateColorMatrix(array); + } + finally + { + s_colorMatPool.Return(array); + } }); } @@ -270,12 +338,19 @@ public void LuminanceToAlpha() { AppendSKColorFilter(Unit.Default, (_, _) => { - float[] array = new float[20]; - Graphics.ColorMatrix.CreateLuminanceToAlphaMatrix(array); - //M15,M25,M35,M45がゼロなので意味がない - //Graphics.ColorMatrix.ToSkiaColorMatrix(array); + float[] array = s_colorMatPool.Get(); + try + { + Graphics.ColorMatrix.CreateLuminanceToAlphaMatrix(array); + //M15,M25,M35,M45がゼロなので意味がない + //Graphics.ColorMatrix.ToSkiaColorMatrix(array); - return SKColorFilter.CreateColorMatrix(array); + return SKColorFilter.CreateColorMatrix(array); + } + finally + { + s_colorMatPool.Return(array); + } }); } @@ -283,12 +358,19 @@ public void Brightness(float amount) { AppendSKColorFilter(amount, (s, _) => { - float[] array = new float[20]; - Graphics.ColorMatrix.CreateBrightness(amount, array); - //M15,M25,M35,M45がゼロなので意味がない - //Graphics.ColorMatrix.ToSkiaColorMatrix(array); + float[] array = s_colorMatPool.Get(); + try + { + Graphics.ColorMatrix.CreateBrightness(amount, array); + //M15,M25,M35,M45がゼロなので意味がない + //Graphics.ColorMatrix.ToSkiaColorMatrix(array); - return SKColorFilter.CreateColorMatrix(array); + return SKColorFilter.CreateColorMatrix(array); + } + finally + { + s_colorMatPool.Return(array); + } }); } @@ -311,52 +393,56 @@ public void LumaColor() AppendSKColorFilter(Unit.Default, (_, _) => SKColorFilter.CreateLumaColor()); } - public void LookupTable(LookupTable table, float strength = 1, int versionCounter = -1) + public void LookupTable( + T data, + float strength, + Action factory) + where T : IEquatable { - AppendSKColorFilter((table, strength, versionCounter), (data, _) => + AppendSKColorFilter((data, strength, factory), (data, _) => { - if (data.table.IsDisposed) - return null; + byte[] a = s_lutPool.Get(); + byte[] r = s_lutPool.Get(); + byte[] g = s_lutPool.Get(); + byte[] b = s_lutPool.Get(); - if (data.table.Dimension == LookupTableDimension.OneDimension) + try { - return SKColorFilter.CreateTable(data.table.ToByteArray(data.strength, 0)); + data.factory(data.data, (a, r, g, b)); + + Graphics.LookupTable.SetStrength(data.strength, (a, r, g, b)); + return SKColorFilter.CreateTable(a, r, g, b); } - else + finally { - return SKColorFilter.CreateTable( - Graphics.LookupTable.s_linear, - data.table.ToByteArray(data.strength, 0), - data.table.ToByteArray(data.strength, 1), - data.table.ToByteArray(data.strength, 2)); + s_lutPool.Return(a); + s_lutPool.Return(r); + s_lutPool.Return(g); + s_lutPool.Return(b); } }); } - public void LookupTable(T data, Func factory, float strength = 1) + public void LookupTable( + T data, + float strength, + Action factory) where T : IEquatable { - AppendSKColorFilter((data, factory, strength), (data, _) => + AppendSKColorFilter((data, strength, factory), (data, _) => { - using (LookupTable table = data.factory.Invoke(data.data)) + byte[] array = s_lutPool.Get(); + + try { - if (table.Dimension == LookupTableDimension.OneDimension) - { - byte[] array = table.ToByteArray(data.strength, 0); - return SKColorFilter.CreateTable( - Graphics.LookupTable.s_linear, - array, - array, - array); - } - else - { - return SKColorFilter.CreateTable( - Graphics.LookupTable.s_linear, - table.ToByteArray(data.strength, 0), - table.ToByteArray(data.strength, 1), - table.ToByteArray(data.strength, 2)); - } + data.factory(data.data, array); + + Graphics.LookupTable.SetStrength(data.strength, array); + return SKColorFilter.CreateTable(array); + } + finally + { + s_lutPool.Return(array); } }); } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs b/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs index 1b0710020..44b0f4b52 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/Gamma.cs @@ -44,7 +44,7 @@ public override void ApplyTo(FilterEffectContext context) context.LookupTable( amount, - LookupTable.Gamma, - _strength / 100); + _strength / 100, + (float data, byte[] array) => LookupTable.Gamma(array, data)); } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs b/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs index b88b1cbfe..fd6d181bd 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/Invert.cs @@ -29,7 +29,7 @@ public override void ApplyTo(FilterEffectContext context) { context.LookupTable( Unit.Default, - _ => LookupTable.Invert(), - _amount / 100); + _amount / 100, + (Unit _, byte[] array) => LookupTable.Invert(array)); } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs b/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs index b2dffdc36..ded31c4b4 100644 --- a/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs +++ b/src/Beutl.Engine/Graphics/FilterEffects/Threshold.cs @@ -45,19 +45,16 @@ public override void ApplyTo(FilterEffectContext context) int threshold = Math.Clamp((int)(_value / 100f * 255), 0, 255); context.HighContrast(true, HighContrastInvertStyle.NoInvert, 0); - context.AppendSKColorFilter((threshold, strength: (_strength / 100)), (data, _) => - { - var lut = new LookupTable(); - Span span = lut.AsSpan(); - for (int i = data.threshold; i < 256; i++) + context.LookupTable( + threshold, + _strength / 100, + (int data, byte[] array) => { - span[i] = 1; - } - - byte[] array = lut.ToByteArray(data.strength, 0); - - return SKColorFilter.CreateTable(array); - }); + for (int i = data; i < array.Length; i++) + { + array[i] = 255; + } + }); } } diff --git a/src/Beutl.Engine/Graphics/LookupTable.cs b/src/Beutl.Engine/Graphics/LookupTable.cs index 3a36724ca..8a513fb68 100644 --- a/src/Beutl.Engine/Graphics/LookupTable.cs +++ b/src/Beutl.Engine/Graphics/LookupTable.cs @@ -1,40 +1,9 @@ -using System.Buffers; -using System.Numerics; -using System.Text.RegularExpressions; +namespace Beutl.Graphics; -namespace Beutl.Graphics; - -public enum LookupTableDimension -{ - OneDimension = 1, - - ThreeDimension = 3, -} - -public sealed unsafe partial class LookupTable : IDisposable +public static partial class LookupTable { internal static readonly byte[] s_linear; - - private static readonly Regex s_lutSizeReg = LUTSizeRegex(); - private static readonly Regex s_titleReg = TitleRegex(); - private static readonly Regex s_domainMinReg = DomainMinRegex(); - private static readonly Regex s_domainMaxReg = DomainMaxRegex(); - private readonly float[][] _arrays; - - public LookupTable(int length = 256, int lutsize = 256, LookupTableDimension dim = LookupTableDimension.OneDimension) - { - _arrays = new float[(int)dim][]; - - for (int i = 0; i < _arrays.Length; i++) - { - _arrays[i] = ArrayPool.Shared.Rent(length); - Array.Clear(_arrays[i]); - } - - Size = lutsize; - Length = length; - Dimension = dim; - } + internal static readonly byte[] s_invert; static LookupTable() { @@ -43,247 +12,125 @@ static LookupTable() { s_linear[i] = (byte)i; } - } - - ~LookupTable() - { - Dispose(); - } - - public int Length { get; } - - public int Size { get; } - - public LookupTableDimension Dimension { get; } - public bool IsDisposed { get; private set; } - - public static LookupTable Solarisation(int cycle = 2) - { - var table = new LookupTable(); - Span data = table.AsSpan(); + s_invert = new byte[256]; for (int i = 0; i < 256; i++) { - data[i] = (float)((Math.Sin(i * cycle * Math.PI / 255) + 1) / 2); + s_invert[i] = (byte)(255 - i); } - - return table; } - public static LookupTable Negaposi(byte value = 255) + public static void Linear(byte[] data) { - var table = new LookupTable(); - - Parallel.For(0, 256, pos => - { - table.AsSpan()[pos] = (value - pos) / 256f; - }); + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - return table; + s_linear.CopyTo(data, 0); } - public static LookupTable Negaposi(byte red = 255, byte green = 255, byte blue = 255) + public static void Invert(byte[] data) { - if (red == green && green == blue) return Negaposi(red); - - var table = new LookupTable(256, 256, LookupTableDimension.ThreeDimension); - Parallel.For(0, 256, pos => - { - Span rData = table.AsSpan(0); - Span gData = table.AsSpan(1); - Span bData = table.AsSpan(2); - - rData[pos] = (red - pos) / 256f; - gData[pos] = (green - pos) / 256f; - bData[pos] = (blue - pos) / 256f; - }); + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - return table; + s_invert.CopyTo(data, 0); } - public static LookupTable Contrast(short contrast) + public static void SetStrength(float strength, byte[] data) { - contrast = Math.Clamp(contrast, (short)-255, (short)255); - var table = new LookupTable(); + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - Parallel.For(0, 256, pos => + for (int i = 0; i < data.Length; i++) { - table.AsSpan()[pos] = Helper.Set255Round((1f + contrast / 255f) * (pos - 128f) + 128f) / 255f; - }); - - return table; + float add = i * (1 - strength); + data[i] = (byte)((data[i] * strength) + add); + } } - public static LookupTable Gamma(float gamma) + public static void SetStrength(float strength, (byte[] A, byte[] R, byte[] G, byte[] B) data) { - gamma = Math.Clamp(gamma, 0.01f, 3f); - var table = new LookupTable(); + if (data.A.Length != 256 + || data.R.Length != 256 + || data.G.Length != 256 + || data.B.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - Parallel.For(0, 256, pos => + for (int i = 0; i < 256; i++) { - table.AsSpan()[pos] = Helper.Set255Round(MathF.Pow(pos / 255f, 1f / gamma)); - }); - - return table; + float add = i * (1 - strength); + data.A[i] = (byte)((data.A[i] * strength) + add); + data.R[i] = (byte)((data.R[i] * strength) + add); + data.G[i] = (byte)((data.G[i] * strength) + add); + data.B[i] = (byte)((data.B[i] * strength) + add); + } } - public static LookupTable Invert() + public static void Solarisation(byte[] data, int cycle = 2) { - var table = new LookupTable(); + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - Parallel.For(0, 256, pos => + for (int i = 0; i < data.Length; i++) { - table.AsSpan()[pos] = 1f - (pos / 255f); - }); - - return table; + data[i] = (byte)((Math.Sin(i * cycle * Math.PI / 255) + 1) / 2 * 255); + } } - public static LookupTable FromStream(Stream stream) + public static void Negaposi(byte[] data, byte value = 255) { - using var reader = new StreamReader(stream); - int i = 0; - ReadInfo(reader, out _, out LookupTableDimension dim, out int size, out _, out _); - - int length = (int)Math.Pow(size, (int)dim); - var table = new LookupTable(length, size, dim); - Span rData = table.AsSpan(0); - Span gData = table.AsSpan(1); - Span bData = table.AsSpan(2); + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - while (i < length) + for (int i = 0; i < data.Length; i++) { - string? line = reader.ReadLine(); - if (line is not null) - { - string[] values = line.Split(' '); - if (values.Length != 3) continue; - - if (float.TryParse(values[0], out float r) && - float.TryParse(values[1], out float g) && - float.TryParse(values[2], out float b)) - { - rData[i] = r; - gData[i] = g; - bData[i] = b; - i++; - } - } + data[i] = (byte)(value - i); } - - return table; } - public static LookupTable FromCube(string file) + public static void Negaposi((byte[] R, byte[] G, byte[] B) data, byte red = 255, byte green = 255, byte blue = 255) { - using var stream = new FileStream(file, FileMode.Open); + if (data.R.Length != 256 || data.G.Length != 256 || data.B.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - return FromStream(stream); - } - - public Span AsSpan(int dimension = 0) - { - return _arrays[dimension].AsSpan(); - } - - private int Near(float x) - { - return Math.Min((int)(x + 0.5), Size - 1); - } + if (red == green && green == blue) + { + Negaposi(data.R, red); + data.R.CopyTo(data.G, 0); + data.R.CopyTo(data.B, 0); + } - public byte[] ToByteArray(float strength, int dimension = 0) - { - float[] src = _arrays[dimension]; - byte[] dst = new byte[256]; for (int i = 0; i < 256; i++) { - float r = i * Size / 256f; - float vec = src[Near(r)]; - - dst[i] = (byte)((((vec * 255) + 0.5) * strength) + (i * (1 - strength))); + data.R[i] = (byte)(red - i); + data.G[i] = (byte)(green - i); + data.B[i] = (byte)(blue - i); } - - return dst; } - public void Dispose() + public static void Contrast(byte[] data, short contrast) { - if (!IsDisposed) - { - foreach (float[] item in _arrays) - { - ArrayPool.Shared.Return(item); - } + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); + + contrast = Math.Clamp(contrast, (short)-255, (short)255); - GC.SuppressFinalize(this); - IsDisposed = true; + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)Helper.Set255Round((1f + contrast / 255f) * (i - 128f) + 128f); } } - private static void ReadInfo(StreamReader reader, out string title, out LookupTableDimension dim, out int size, out Vector3 min, out Vector3 max) + public static void Gamma(byte[] data, float gamma) { - title = string.Empty; - dim = LookupTableDimension.ThreeDimension; - size = 33; - min = new(0, 0, 0); - max = new(1, 1, 1); - bool titleFound = false; - bool lutSizeFound = false; - bool minFound = false; - bool maxFound = false; + if (data.Length != 256) + throw new ArgumentException("配列の長さが無効です", nameof(data)); - while (!reader.EndOfStream) + gamma = Math.Clamp(gamma, 0.01f, 3f); + + for (int i = 0; i < data.Length; i++) { - if (titleFound && lutSizeFound && minFound && maxFound) break; - string? line = reader.ReadLine(); - if (line is not null) - { - if (s_lutSizeReg.IsMatch(line)) - { - lutSizeFound = true; - Match match = s_lutSizeReg.Match(line); - size = int.Parse(match.Groups["size"].Value); - dim = match.Groups["dim"].Value is "3D" ? LookupTableDimension.ThreeDimension : LookupTableDimension.OneDimension; - } - else if (s_titleReg.IsMatch(line)) - { - titleFound = true; - Match match = s_lutSizeReg.Match(line); - title = match.Groups["text"].Value; - } - else if (s_domainMaxReg.IsMatch(line)) - { - maxFound = true; - Match match = s_domainMaxReg.Match(line); - float r = float.Parse(match.Groups["red"].Value); - float g = float.Parse(match.Groups["green"].Value); - float b = float.Parse(match.Groups["blue"].Value); - max = new(r, g, b); - } - else if (s_domainMinReg.IsMatch(line)) - { - minFound = true; - Match match = s_domainMinReg.Match(line); - float r = float.Parse(match.Groups["red"].Value); - float g = float.Parse(match.Groups["green"].Value); - float b = float.Parse(match.Groups["blue"].Value); - min = new(r, g, b); - } - } + data[i] = (byte)Helper.Set255Round(MathF.Pow(i / 255f, 1f / gamma) * 255f); } - - reader.BaseStream.Position = 0; } - - [GeneratedRegex("^LUT_(?.*?)_SIZE (?.*?)$")] - private static partial Regex LUTSizeRegex(); - - [GeneratedRegex("^TITLE \"(?.*?)\"$")] - private static partial Regex TitleRegex(); - - [GeneratedRegex("^DOMAIN_MIN (?.*?) (?.*?) (?.*?)$")] - private static partial Regex DomainMinRegex(); - - [GeneratedRegex("^DOMAIN_MAX (?.*?) (?.*?) (?.*?)$")] - private static partial Regex DomainMaxRegex(); } From bd4ad8e2fad308e33838f860866213d3ce566a56 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Wed, 16 Aug 2023 00:08:31 +0900 Subject: [PATCH 08/10] =?UTF-8?q?FilterEffectEditor=E3=81=AE=E5=90=8D?= =?UTF-8?q?=E5=89=8D=E8=A1=A8=E7=A4=BA=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editors/FilterEffectEditorViewModel.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Beutl/ViewModels/Editors/FilterEffectEditorViewModel.cs b/src/Beutl/ViewModels/Editors/FilterEffectEditorViewModel.cs index 2d9772860..b71bce484 100644 --- a/src/Beutl/ViewModels/Editors/FilterEffectEditorViewModel.cs +++ b/src/Beutl/ViewModels/Editors/FilterEffectEditorViewModel.cs @@ -18,7 +18,18 @@ public sealed class FilterEffectEditorViewModel : ValueEditorViewModel property) : base(property) { - FilterName = Value.Select(v => (v != null ? LibraryService.Current.FindItem(v.GetType()) : null)?.DisplayName ?? "Null") + FilterName = Value.Select(v => + { + if (v != null) + { + Type type = v.GetType(); + return LibraryService.Current.FindItem(type)?.DisplayName ?? type.Name; + } + else + { + return "Null"; + } + }) .ToReadOnlyReactivePropertySlim() .DisposeWith(Disposables); From 5ff34b05f5db05fce3d6e73b79d6552b96570a28 Mon Sep 17 00:00:00 2001 From: indigo-san Date: Wed, 16 Aug 2023 00:08:43 +0900 Subject: [PATCH 09/10] =?UTF-8?q?StorageFileEditor=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Styling/PropertyEditors/StorageFileEditor.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Beutl.Controls/Styling/PropertyEditors/StorageFileEditor.axaml b/src/Beutl.Controls/Styling/PropertyEditors/StorageFileEditor.axaml index d2ff52216..111368258 100644 --- a/src/Beutl.Controls/Styling/PropertyEditors/StorageFileEditor.axaml +++ b/src/Beutl.Controls/Styling/PropertyEditors/StorageFileEditor.axaml @@ -37,7 +37,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="{StaticResource TextBoxIconFontSize}" - Foreground="{StaticResource TextControlButtonForeground}" + Foreground="{DynamicResource TextControlButtonForeground}" Symbol="Open" /> From b62fbded178d9198dd437a0dcb49f1f7668078df Mon Sep 17 00:00:00 2001 From: indigo-san Date: Wed, 16 Aug 2023 00:12:33 +0900 Subject: [PATCH 10/10] =?UTF-8?q?Cube=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=82=92=E4=BD=BF=E3=81=A3=E3=81=A6LUT=E3=82=92=E9=81=A9?= =?UTF-8?q?=E7=94=A8=E3=81=99=E3=82=8B=E3=82=A8=E3=83=95=E3=82=A7=E3=82=AF?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Packages.props | 1 + src/Beutl.Engine/Beutl.Engine.csproj | 1 + src/Beutl.Engine/Graphics/CubeFile.cs | 85 ++++++- .../Graphics/FilterEffects/LutEffect.cs | 228 ++++++++++++++++++ src/Beutl.Engine/Graphics/LookupTable.cs | 4 + .../Rendering/SharedGPUContext.cs | 54 +++++ .../Configure/Effects/EffectsOperator.cs | 7 + src/Beutl.Operators/LibraryRegistrar.cs | 5 + src/Beutl/App.axaml.cs | 3 +- src/Beutl/Program.cs | 1 + 10 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 src/Beutl.Engine/Graphics/FilterEffects/LutEffect.cs create mode 100644 src/Beutl.Engine/Rendering/SharedGPUContext.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 5e5dec3cb..2d1ed8fe0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,6 +21,7 @@ + diff --git a/src/Beutl.Engine/Beutl.Engine.csproj b/src/Beutl.Engine/Beutl.Engine.csproj index b65bf78c7..095a69bcb 100644 --- a/src/Beutl.Engine/Beutl.Engine.csproj +++ b/src/Beutl.Engine/Beutl.Engine.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Beutl.Engine/Graphics/CubeFile.cs b/src/Beutl.Engine/Graphics/CubeFile.cs index 7c3ebbdca..6099a4c59 100644 --- a/src/Beutl.Engine/Graphics/CubeFile.cs +++ b/src/Beutl.Engine/Graphics/CubeFile.cs @@ -1,9 +1,11 @@ using System.Numerics; using System.Text.RegularExpressions; +using OpenCvSharp; + namespace Beutl.Graphics; -public partial class CubeFile +public partial class CubeFile : IEquatable { private static readonly Regex s_lutSizeReg = LUTSizeRegex(); private static readonly Regex s_titleReg = TitleRegex(); @@ -27,6 +29,72 @@ private int Near(float x) return Math.Min((int)x, Size - 1); } + // https://shizenkarasuzon.hatenablog.com/entry/2020/08/13/185223#1DLUT%E3%81%A83DLUT + public Vector3 TrilinearInterplate(Vec3b color) + { + Span pos = stackalloc byte[3]; // 0~33 + Span delta = stackalloc float[3]; // + int lut_size = Size; + int lut_size_2 = lut_size * lut_size; + + pos[0] = (byte)(color[0] * lut_size / 256); + pos[1] = (byte)(color[1] * lut_size / 256); + pos[2] = (byte)(color[2] * lut_size / 256); + + delta[0] = color[0] * lut_size / 256.0f - pos[0]; + delta[1] = color[1] * lut_size / 256.0f - pos[1]; + delta[2] = color[2] * lut_size / 256.0f - pos[2]; + + Span vertex_color = stackalloc Vector3[8]; + Span surf_color = stackalloc Vector3[4]; + Span line_color = stackalloc Vector3[2]; + Vector3 out_color; + + int index = pos[0] + pos[1] * lut_size + pos[2] * lut_size_2; + + Span next_index = stackalloc int[3] + { + 1, + lut_size, + lut_size_2 + }; + + if (index % lut_size == lut_size - 1) + { + next_index[0] = 0; + } + if (index / lut_size % lut_size == lut_size - 1) + { + next_index[1] = 0; + } + if (index / lut_size_2 % lut_size == lut_size - 1) + { + next_index[2] = 0; + } + + // https://en.wikipedia.org/wiki/Trilinear_interpolation + vertex_color[0] = Data[index]; + vertex_color[1] = Data[index + next_index[0]]; + vertex_color[2] = Data[index + next_index[0] + next_index[1]]; + vertex_color[3] = Data[index + next_index[1]]; + vertex_color[4] = Data[index + next_index[2]]; + vertex_color[5] = Data[index + next_index[0] + next_index[2]]; + vertex_color[6] = Data[index + next_index[0] + next_index[1] + next_index[2]]; + vertex_color[7] = Data[index + next_index[1] + next_index[2]]; + + surf_color[0] = vertex_color[0] * (1.0f - delta[2]) + vertex_color[4] * delta[2]; + surf_color[1] = vertex_color[1] * (1.0f - delta[2]) + vertex_color[5] * delta[2]; + surf_color[2] = vertex_color[2] * (1.0f - delta[2]) + vertex_color[6] * delta[2]; + surf_color[3] = vertex_color[3] * (1.0f - delta[2]) + vertex_color[7] * delta[2]; + + line_color[0] = surf_color[0] * (1.0f - delta[0]) + surf_color[1] * delta[0]; + line_color[1] = surf_color[2] * (1.0f - delta[0]) + surf_color[3] * delta[0]; + + out_color = line_color[0] * (1.0f - delta[1]) + line_color[1] * delta[1]; + + return out_color; + } + private Vector3 LinearInterplate(float p) { Vector3 near = Data[Near(p)]; @@ -192,4 +260,19 @@ private static void ReadInfo(StreamReader reader, out string title, out CubeFile [GeneratedRegex("^DOMAIN_MAX (?.*?) (?.*?) (?.*?)$")] private static partial Regex DomainMaxRegex(); + + public bool Equals(CubeFile? other) + { + return ReferenceEquals(this, other); + } + + public override bool Equals(object? obj) + { + return Equals(obj as CubeFile); + } + + public override int GetHashCode() + { + return HashCode.Combine(Title, Dimention, Size, Min, Max, Data); + } } diff --git a/src/Beutl.Engine/Graphics/FilterEffects/LutEffect.cs b/src/Beutl.Engine/Graphics/FilterEffects/LutEffect.cs new file mode 100644 index 000000000..d6cb94228 --- /dev/null +++ b/src/Beutl.Engine/Graphics/FilterEffects/LutEffect.cs @@ -0,0 +1,228 @@ +using System.ComponentModel.DataAnnotations; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Beutl.Media; +using Beutl.Rendering; + +using ILGPU; +using ILGPU.Runtime; + +using Microsoft.Extensions.Logging; + +using OpenCvSharp; + +using SkiaSharp; + +namespace Beutl.Graphics.Effects; + +public sealed class LutEffect : FilterEffect +{ + public static readonly CoreProperty SourceProperty; + public static readonly CoreProperty StrengthProperty; + private static readonly ILogger s_logger=BeutlApplication.Current.LoggerFactory.CreateLogger(); + private FileInfo? _source; + private float _strength = 100; + private CubeFile? _cube; + + static LutEffect() + { + SourceProperty = ConfigureProperty(nameof(Source)) + .Accessor(o => o.Source, (o, v) => o.Source = v) + .Register(); + + StrengthProperty = ConfigureProperty(nameof(Strength)) + .Accessor(o => o.Strength, (o, v) => o.Strength = v) + .DefaultValue(100) + .Register(); + + AffectsRender(SourceProperty, StrengthProperty); + } + + public FileInfo? Source + { + get => _source; + set + { + if (SetAndRaise(SourceProperty, ref _source, value)) + { + OnSourceChanged(value); + } + } + } + + private void OnSourceChanged(FileInfo? value) + { + _cube = null; + if (value != null) + { + using FileStream stream = value.OpenRead(); + try + { + _cube = CubeFile.FromStream(stream); + } + catch (Exception ex) + { + s_logger.LogError(ex, "Cubeファイルの解析に失敗しました。{FileName}", value.FullName); + } + } + } + + [Range(0, 100)] + public float Strength + { + get => _strength; + set => SetAndRaise(StrengthProperty, ref _strength, value); + } + + public override void ApplyTo(FilterEffectContext context) + { + if (_cube != null) + { + if (_cube.Dimention == CubeFileDimension.OneDimension) + { + context.LookupTable( + _cube, + _strength / 100, + (CubeFile cube, (byte[] A, byte[] R, byte[] G, byte[] B) data) => + { + LookupTable.Linear(data.A); + cube.ToLUT(1, data.R, data.G, data.B); + }); + } + else + { + context.Custom((_cube, _strength / 100), OnApply3DLUT_GPU, (_, r) => r); + } + } + } + + private unsafe void OnApply3DLUT_GPU((CubeFile, float) data, FilterEffectCustomOperationContext context) + { + if (context.Target.Surface?.Value is { } surface) + { + Accelerator accelerator = SharedGPUContext.Accelerator; + var kernel = accelerator.LoadAutoGroupedStreamKernel< + Index1D, ArrayView, ArrayView, int, float>(Apply3DLUTKernel); + + var size = PixelSize.FromSize(context.Target.Size, 1); + var imgInfo = new SKImageInfo(size.Width, size.Height, SKColorType.Bgra8888); + + using var source = accelerator.Allocate1D(size.Width * size.Height); + using var lut = accelerator.Allocate1D(data.Item1.Data); + + CopyFromCPU(source, surface, imgInfo); + + kernel((int)source.Length, source.View, lut.View, data.Item1.Size, data.Item2); + + SKCanvas canvas = surface.Canvas; + canvas.Clear(); + + using var skBmp = new SKBitmap(imgInfo); + + CopyToCPU(source, skBmp); + + canvas.DrawBitmap(skBmp, 0, 0); + } + } + + private static unsafe void CopyFromCPU(MemoryBuffer1D source, SKSurface surface, SKImageInfo imageInfo) + { + void* tmp = NativeMemory.Alloc((nuint)source.LengthInBytes); + try + { + bool result = surface.ReadPixels(imageInfo, (nint)tmp, imageInfo.Width * 4, 0, 0); + + source.View.CopyFromCPU(ref Unsafe.AsRef(tmp), source.Length); + } + finally + { + NativeMemory.Free(tmp); + } + } + + private static unsafe void CopyToCPU(MemoryBuffer1D source, SKBitmap bitmap) + { + source.View.CopyToCPU(ref Unsafe.AsRef((void*)bitmap.GetPixels()), source.Length); + } + + private static Vector3 TrilinearInterplate(Vec4b color, int lut_size, ArrayView lut) + { + Vec3b pos = default; // 0~33 + Vec3f delta = default; // + int lut_size_2 = lut_size * lut_size; + + pos.Item0 = (byte)(color.Item0 * lut_size / 256); + pos.Item1 = (byte)(color.Item1 * lut_size / 256); + pos.Item2 = (byte)(color.Item2 * lut_size / 256); + + delta.Item0 = color.Item0 * lut_size / 256.0f - pos.Item0; + delta.Item1 = color.Item1 * lut_size / 256.0f - pos.Item1; + delta.Item2 = color.Item2 * lut_size / 256.0f - pos.Item2; + + Vector3 vertex_color_0, vertex_color_1, vertex_color_2, vertex_color_3, vertex_color_4, vertex_color_5, vertex_color_6, vertex_color_7; + Vector3 surf_color_0, surf_color_1, surf_color_2, surf_color_3; + Vector3 line_color_0, line_color_1; + Vector3 out_color; + + int index = pos.Item0 + pos.Item1 * lut_size + pos.Item2 * lut_size_2; + + int next_index_0 = 1, next_index_1 = lut_size, next_index_2 = lut_size_2; + + if (index % lut_size == lut_size - 1) + { + next_index_0 = 0; + } + if (index / lut_size % lut_size == lut_size - 1) + { + next_index_1 = 0; + } + if (index / lut_size_2 % lut_size == lut_size - 1) + { + next_index_2 = 0; + } + + // https://en.wikipedia.org/wiki/Trilinear_interpolation + vertex_color_0 = lut[index]; + vertex_color_1 = lut[index + next_index_0]; + vertex_color_2 = lut[index + next_index_0 + next_index_1]; + vertex_color_3 = lut[index + next_index_1]; + vertex_color_4 = lut[index + next_index_2]; + vertex_color_5 = lut[index + next_index_0 + next_index_2]; + vertex_color_6 = lut[index + next_index_0 + next_index_1 + next_index_2]; + vertex_color_7 = lut[index + next_index_1 + next_index_2]; + + surf_color_0 = vertex_color_0 * (1.0f - delta.Item2) + vertex_color_4 * delta.Item2; + surf_color_1 = vertex_color_1 * (1.0f - delta.Item2) + vertex_color_5 * delta.Item2; + surf_color_2 = vertex_color_2 * (1.0f - delta.Item2) + vertex_color_6 * delta.Item2; + surf_color_3 = vertex_color_3 * (1.0f - delta.Item2) + vertex_color_7 * delta.Item2; + + line_color_0 = surf_color_0 * (1.0f - delta.Item0) + surf_color_1 * delta.Item0; + line_color_1 = surf_color_2 * (1.0f - delta.Item0) + surf_color_3 * delta.Item0; + + out_color = line_color_0 * (1.0f - delta.Item1) + line_color_1 * delta.Item1; + + return out_color; + } + + private static void Apply3DLUTKernel(Index1D index, ArrayView src, ArrayView lut, int lutSize, float strength) + { + Vec4b pixel = src[index]; + + Vector3 newColor = TrilinearInterplate(pixel, lutSize, lut); + + src[index] = SetStrength(strength, newColor, pixel); + } + + private static Vec4b SetStrength(float strength, Vector3 color, Vec4b original) + { + var newColor = new Vec4b((byte)(color.X * 255), (byte)(color.Y * 255), (byte)(color.Z * 255), original.Item3); + + newColor.Item0 = (byte)((newColor.Item0 * strength) + (original.Item0 * (1 - strength))); + newColor.Item1 = (byte)((newColor.Item1 * strength) + (original.Item1 * (1 - strength))); + newColor.Item2 = (byte)((newColor.Item2 * strength) + (original.Item2 * (1 - strength))); + + return newColor; + } +} diff --git a/src/Beutl.Engine/Graphics/LookupTable.cs b/src/Beutl.Engine/Graphics/LookupTable.cs index 8a513fb68..7b4efb1f9 100644 --- a/src/Beutl.Engine/Graphics/LookupTable.cs +++ b/src/Beutl.Engine/Graphics/LookupTable.cs @@ -38,6 +38,8 @@ public static void Invert(byte[] data) public static void SetStrength(float strength, byte[] data) { + if (strength == 1) return; + if (data.Length != 256) throw new ArgumentException("配列の長さが無効です", nameof(data)); @@ -50,6 +52,8 @@ public static void SetStrength(float strength, byte[] data) public static void SetStrength(float strength, (byte[] A, byte[] R, byte[] G, byte[] B) data) { + if (strength == 1) return; + if (data.A.Length != 256 || data.R.Length != 256 || data.G.Length != 256 diff --git a/src/Beutl.Engine/Rendering/SharedGPUContext.cs b/src/Beutl.Engine/Rendering/SharedGPUContext.cs new file mode 100644 index 000000000..c2de64079 --- /dev/null +++ b/src/Beutl.Engine/Rendering/SharedGPUContext.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using ILGPU; +using ILGPU.Runtime; + +using Microsoft.Extensions.Logging; + +namespace Beutl.Rendering; + +public class SharedGPUContext +{ + private static readonly ILogger s_logger = BeutlApplication.Current.LoggerFactory.CreateLogger(); + + private static Context? s_context; + private static Device? s_device; + private static Accelerator? s_accelerator; + + public static Context Context => s_context!; + + public static Device Device => s_device!; + + public static Accelerator Accelerator => s_accelerator!; + + public static void Create() + { + RenderThread.Dispatcher.VerifyAccess(); + + s_context = Context.CreateDefault(); + + s_device = s_context.GetPreferredDevice(false); + + using var sw = new StringWriter(); + s_device.PrintInformation(sw); + s_logger.LogInformation("ILGPU.Runtime.Device.PrintInformation: {Info}", sw.ToString()); + + s_accelerator = s_device.CreateAccelerator(s_context); + } + + public static void Shutdown() + { + RenderThread.Dispatcher.Invoke(() => + { + s_accelerator?.Dispose(); + s_accelerator = null; + s_device = null; + s_context?.Dispose(); + s_context = null; + }); + } +} diff --git a/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs b/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs index b42689d43..b3b3e4283 100644 --- a/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs +++ b/src/Beutl.Operators/Configure/Effects/EffectsOperator.cs @@ -83,3 +83,10 @@ public sealed class InvertOperator : FilterEffectOperator { public Setter Amount { get; set; } = new(Invert.AmountProperty, 100); } + +public sealed class LutEffectOperator : FilterEffectOperator +{ + public Setter Source { get; set; } = new(LutEffect.SourceProperty, null); + + public Setter Strength { get; set; } = new(LutEffect.StrengthProperty, 100); +} diff --git a/src/Beutl.Operators/LibraryRegistrar.cs b/src/Beutl.Operators/LibraryRegistrar.cs index 96d46ca90..20b40cd1a 100644 --- a/src/Beutl.Operators/LibraryRegistrar.cs +++ b/src/Beutl.Operators/LibraryRegistrar.cs @@ -246,6 +246,11 @@ public static void RegisterAll() .BindFilterEffect() ) + .AddMultiple("LUT (Cube File)", m => m + .BindSourceOperator() + .BindFilterEffect() + ) + .AddMultiple(Strings.Transform, m => m .BindSourceOperator() .BindFilterEffect() diff --git a/src/Beutl/App.axaml.cs b/src/Beutl/App.axaml.cs index 34c292943..a16775b1a 100644 --- a/src/Beutl/App.axaml.cs +++ b/src/Beutl/App.axaml.cs @@ -117,7 +117,8 @@ private static void SetupLogger() public override void OnFrameworkInitializationCompleted() { - _ = RenderThread.Dispatcher; + RenderThread.Dispatcher.Invoke(SharedGPUContext.Create); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainWindow diff --git a/src/Beutl/Program.cs b/src/Beutl/Program.cs index 7340213c9..19f8d3e9f 100644 --- a/src/Beutl/Program.cs +++ b/src/Beutl/Program.cs @@ -49,6 +49,7 @@ public static void Main(string[] args) } finally { + SharedGPUContext.Shutdown(); SharedGRContext.Shutdown(); RenderThread.Dispatcher.Stop(); }