mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Added Quantize node
This commit is contained in:
parent
c7202bb94c
commit
afd17b2661
@ -16,7 +16,6 @@
|
||||
<PackageReference Include="NoStringEvaluating" Version="2.4.0" />
|
||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||
<PackageReference Include="ScreenCapture.NET" Version="1.2.0" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||
</ItemGroup>
|
||||
|
||||
@ -26,6 +25,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Nodes\Color\Screens\QuantizeNodeCustomView.axaml.cs">
|
||||
<DependentUpon>QuantizeNodeCustomView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Nodes\Easing\Screens\EasingTypeNodeEasingView.axaml.cs">
|
||||
<DependentUpon>EasingTypeNodeEasingView.axaml</DependentUpon>
|
||||
</Compile>
|
||||
|
||||
68
src/Artemis.VisualScripting/Nodes/Color/QuantizeNode.cs
Normal file
68
src/Artemis.VisualScripting/Nodes/Color/QuantizeNode.cs
Normal file
@ -0,0 +1,68 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.VisualScripting.Nodes.Color.Screens;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color;
|
||||
|
||||
public class QuantizeNodeStorage
|
||||
{
|
||||
public int PaletteSize { get; set; } = 32;
|
||||
public bool IgnoreLimits { get; set; }
|
||||
};
|
||||
|
||||
[Node("Quantize", "Quantizes the image into key-colors", "Image", InputType = typeof(SKBitmap), OutputType = typeof(SKColor))]
|
||||
public class QuantizeNode : Node<QuantizeNodeStorage, QuantizeNodeCustomViewModel>
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
public InputPin<SKBitmap> Image { get; set; }
|
||||
|
||||
public OutputPin<SKColor> Vibrant { get; set; }
|
||||
public OutputPin<SKColor> Muted { get; set; }
|
||||
public OutputPin<SKColor> DarkVibrant { get; set; }
|
||||
public OutputPin<SKColor> DarkMuted { get; set; }
|
||||
public OutputPin<SKColor> LightVibrant { get; set; }
|
||||
public OutputPin<SKColor> LightMuted { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public QuantizeNode()
|
||||
: base("Quantize", "Quantizes the image into key-colors")
|
||||
{
|
||||
Image = CreateInputPin<SKBitmap>("Image");
|
||||
|
||||
Vibrant = CreateOutputPin<SKColor>("Vibrant");
|
||||
Muted = CreateOutputPin<SKColor>("Muted");
|
||||
DarkVibrant = CreateOutputPin<SKColor>("DarkVibrant");
|
||||
DarkMuted = CreateOutputPin<SKColor>("DarkMuted");
|
||||
LightVibrant = CreateOutputPin<SKColor>("LightVibrant");
|
||||
LightMuted = CreateOutputPin<SKColor>("LightMuted");
|
||||
|
||||
Storage = new QuantizeNodeStorage();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
public override void Evaluate()
|
||||
{
|
||||
SKBitmap? image = Image.Value;
|
||||
if (image == null) return;
|
||||
|
||||
SKColor[] colorPalette = ColorQuantizer.Quantize(image.Pixels, Storage?.PaletteSize ?? 32);
|
||||
ColorSwatch swatch = ColorQuantizer.FindAllColorVariations(colorPalette, Storage?.IgnoreLimits ?? false);
|
||||
|
||||
Vibrant.Value = swatch.Vibrant;
|
||||
Muted.Value = swatch.Muted;
|
||||
DarkVibrant.Value = swatch.DarkVibrant;
|
||||
DarkMuted.Value = swatch.DarkMuted;
|
||||
LightVibrant.Value = swatch.LightVibrant;
|
||||
LightMuted.Value = swatch.LightMuted;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Color.Screens"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.VisualScripting.Nodes.Color.Screens.QuantizeNodeCustomView"
|
||||
x:DataType="screens:QuantizeNodeCustomViewModel">
|
||||
<StackPanel Orientation="Vertical" VerticalAlignment="Top" Margin="16,4,0,4">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="Palette Size" VerticalAlignment="Center" Margin="0,0,0,4" Width="70" FontSize="11" />
|
||||
<ComboBox Classes="condensed" Width="75" Items="{CompiledBinding PaletteSizes}" SelectedItem="{CompiledBinding PaletteSize}"
|
||||
ToolTip.Tip="The number of colors the image is compressed to before selecting the final colors." />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
|
||||
<TextBlock Text="Ignore Limits" VerticalAlignment="Center" Margin="0,0,0,4" Width="70" FontSize="11" />
|
||||
<CheckBox Classes="condensed" IsChecked="{CompiledBinding IgnoreLimits}"
|
||||
ToolTip.Tip="Ignore hard limits on whether a color is considered for each category. If disabled some of the results may be empty." />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,17 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color.Screens;
|
||||
|
||||
public partial class QuantizeNodeCustomView : ReactiveUserControl<QuantizeNodeCustomViewModel>
|
||||
{
|
||||
public QuantizeNodeCustomView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||
using Artemis.UI.Shared.VisualScripting;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color.Screens;
|
||||
|
||||
public class QuantizeNodeCustomViewModel : CustomNodeViewModel
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly QuantizeNode _node;
|
||||
private readonly INodeEditorService _nodeEditorService;
|
||||
|
||||
public ObservableCollection<int> PaletteSizes { get; } = new() { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 };
|
||||
|
||||
public int PaletteSize
|
||||
{
|
||||
get => _node.Storage?.PaletteSize ?? 32;
|
||||
set
|
||||
{
|
||||
if ((_node.Storage != null) && (_node.Storage.PaletteSize != value))
|
||||
{
|
||||
_node.Storage.PaletteSize = value;
|
||||
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<QuantizeNodeStorage>(_node, _node.Storage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IgnoreLimits
|
||||
{
|
||||
get => _node.Storage?.IgnoreLimits ?? false;
|
||||
set
|
||||
{
|
||||
if ((_node.Storage != null) && (_node.Storage.IgnoreLimits != value))
|
||||
{
|
||||
_node.Storage.IgnoreLimits = value;
|
||||
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<QuantizeNodeStorage>(_node, _node.Storage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public QuantizeNodeCustomViewModel(QuantizeNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script)
|
||||
{
|
||||
this._node = node;
|
||||
this._nodeEditorService = nodeEditorService;
|
||||
|
||||
NodeModified += (_, _) => this.RaisePropertyChanged(nameof(PaletteSize));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.VisualScripting.Nodes.Image.Screens;
|
||||
using ScreenCapture.NET;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Image;
|
||||
|
||||
[Node("Capture Screen", "Captures a region of the screen", "Image", OutputType = typeof(SKBitmap))]
|
||||
public class CaptureScreenNode : Node<object, CaptureScreenNodeCustomViewModel>
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private static readonly Thread _thread;
|
||||
private static readonly IScreenCaptureService _screenCaptureService = new DX11ScreenCaptureService();
|
||||
private static readonly IScreenCapture _screenCapture;
|
||||
|
||||
static CaptureScreenNode()
|
||||
{
|
||||
IEnumerable<GraphicsCard> graphicsCards = _screenCaptureService.GetGraphicsCards();
|
||||
IEnumerable<Display> displays = _screenCaptureService.GetDisplays(graphicsCards.First());
|
||||
_screenCapture = _screenCaptureService.GetScreenCapture(displays.First());
|
||||
|
||||
_thread = new Thread(() =>
|
||||
{
|
||||
while (true)
|
||||
_screenCapture.CaptureScreen();
|
||||
});
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
private CaptureZone _captureZone;
|
||||
|
||||
public OutputPin<SKBitmap> Output { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public CaptureScreenNode()
|
||||
: base("Capture Screen", "Captures a region of the screen")
|
||||
{
|
||||
Output = CreateOutputPin<SKBitmap>("Image");
|
||||
|
||||
_captureZone = _screenCapture.RegisterCaptureZone(20, 20, 256, 256, 1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
public override unsafe void Evaluate()
|
||||
{
|
||||
lock (_captureZone.Buffer)
|
||||
{
|
||||
ReadOnlySpan<byte> capture = _captureZone.Buffer;
|
||||
if (capture.IsEmpty) return;
|
||||
|
||||
fixed (byte* ptr = capture)
|
||||
Output.Value = SKBitmap.FromImage(SKImage.FromPixels(new SKImageInfo(_captureZone.Width, _captureZone.Height, SKColorType.Bgra8888, SKAlphaType.Opaque), new IntPtr(ptr), _captureZone.Stride));
|
||||
|
||||
//TODO DarthAffe 18.08.2022: Dispose Output or better reuse it
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,559 +0,0 @@
|
||||
using System.Buffers;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Image;
|
||||
|
||||
[Node("Quantize", "Quantizes the image into key-colors", "Image", InputType = typeof(SKBitmap), OutputType = typeof(SKColor))]
|
||||
public class QuantizeNode : Node
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
public InputPin<SKBitmap> Image { get; set; }
|
||||
|
||||
public OutputPin<SKColor> Vibrant { get; set; }
|
||||
public OutputPin<SKColor> Muted { get; set; }
|
||||
public OutputPin<SKColor> DarkVibrant { get; set; }
|
||||
public OutputPin<SKColor> DarkMuted { get; set; }
|
||||
public OutputPin<SKColor> LightVibrant { get; set; }
|
||||
public OutputPin<SKColor> LightMuted { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
public QuantizeNode()
|
||||
: base("Quantize", "Quantizes the image into key-colors")
|
||||
{
|
||||
Image = CreateInputPin<SKBitmap>("Image");
|
||||
|
||||
Vibrant = CreateOutputPin<SKColor>("Vibrant");
|
||||
Muted = CreateOutputPin<SKColor>("Muted");
|
||||
DarkVibrant = CreateOutputPin<SKColor>("DarkVibrant");
|
||||
DarkMuted = CreateOutputPin<SKColor>("DarkMuted");
|
||||
LightVibrant = CreateOutputPin<SKColor>("LightVibrant");
|
||||
LightMuted = CreateOutputPin<SKColor>("LightMuted");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
public override void Evaluate()
|
||||
{
|
||||
if (Image.Value == null) return;
|
||||
|
||||
SKColor[] colorPalette = ColorQuantizer.Quantize(Image.Value.Pixels, 32); //TODO DarthAffe 18.08.2022: Palette-Size as input
|
||||
ColorSwatch swatch = ColorQuantizer.FindAllColorVariations(colorPalette, true);
|
||||
|
||||
Vibrant.Value = swatch.Vibrant;
|
||||
Muted.Value = swatch.Muted;
|
||||
DarkVibrant.Value = swatch.DarkVibrant;
|
||||
DarkMuted.Value = swatch.DarkMuted;
|
||||
LightVibrant.Value = swatch.LightVibrant;
|
||||
LightMuted.Value = swatch.LightMuted;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Quantizer //TODO DarthAffe 18.08.2022: external project?
|
||||
|
||||
public static class ColorQuantizer
|
||||
{
|
||||
public static SKColor[] Quantize(in Span<SKColor> colors, int amount)
|
||||
{
|
||||
if ((amount & (amount - 1)) != 0)
|
||||
throw new ArgumentException("Must be power of two", nameof(amount));
|
||||
|
||||
Queue<ColorCube> cubes = new(amount);
|
||||
cubes.Enqueue(new ColorCube(colors, 0, colors.Length, SortTarget.None));
|
||||
|
||||
while (cubes.Count < amount)
|
||||
{
|
||||
ColorCube cube = cubes.Dequeue();
|
||||
|
||||
if (cube.TrySplit(colors, out ColorCube? a, out ColorCube? b))
|
||||
{
|
||||
cubes.Enqueue(a);
|
||||
cubes.Enqueue(b);
|
||||
}
|
||||
}
|
||||
|
||||
SKColor[] result = new SKColor[cubes.Count];
|
||||
int i = 0;
|
||||
foreach (ColorCube colorCube in cubes)
|
||||
result[i++] = colorCube.GetAverageColor(colors);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ColorSwatch FindAllColorVariations(IEnumerable<SKColor> colors, bool ignoreLimits = false)
|
||||
{
|
||||
SKColor bestVibrantColor = SKColor.Empty;
|
||||
SKColor bestLightVibrantColor = SKColor.Empty;
|
||||
SKColor bestDarkVibrantColor = SKColor.Empty;
|
||||
SKColor bestMutedColor = SKColor.Empty;
|
||||
SKColor bestLightMutedColor = SKColor.Empty;
|
||||
SKColor bestDarkMutedColor = SKColor.Empty;
|
||||
float bestVibrantScore = float.MinValue;
|
||||
float bestLightVibrantScore = float.MinValue;
|
||||
float bestDarkVibrantScore = float.MinValue;
|
||||
float bestMutedScore = float.MinValue;
|
||||
float bestLightMutedScore = float.MinValue;
|
||||
float bestDarkMutedScore = float.MinValue;
|
||||
|
||||
//ugly but at least we only loop through the enumerable once ¯\_(ツ)_/¯
|
||||
foreach (SKColor color in colors)
|
||||
{
|
||||
static void SetIfBetterScore(ref float bestScore, ref SKColor bestColor, SKColor newColor, ColorType type, bool ignoreLimits)
|
||||
{
|
||||
float newScore = GetScore(newColor, type, ignoreLimits);
|
||||
if (newScore > bestScore)
|
||||
{
|
||||
bestScore = newScore;
|
||||
bestColor = newColor;
|
||||
}
|
||||
}
|
||||
|
||||
SetIfBetterScore(ref bestVibrantScore, ref bestVibrantColor, color, ColorType.Vibrant, ignoreLimits);
|
||||
SetIfBetterScore(ref bestLightVibrantScore, ref bestLightVibrantColor, color, ColorType.LightVibrant, ignoreLimits);
|
||||
SetIfBetterScore(ref bestDarkVibrantScore, ref bestDarkVibrantColor, color, ColorType.DarkVibrant, ignoreLimits);
|
||||
SetIfBetterScore(ref bestMutedScore, ref bestMutedColor, color, ColorType.Muted, ignoreLimits);
|
||||
SetIfBetterScore(ref bestLightMutedScore, ref bestLightMutedColor, color, ColorType.LightMuted, ignoreLimits);
|
||||
SetIfBetterScore(ref bestDarkMutedScore, ref bestDarkMutedColor, color, ColorType.DarkMuted, ignoreLimits);
|
||||
}
|
||||
|
||||
return new ColorSwatch
|
||||
{
|
||||
Vibrant = bestVibrantColor,
|
||||
LightVibrant = bestLightVibrantColor,
|
||||
DarkVibrant = bestDarkVibrantColor,
|
||||
Muted = bestMutedColor,
|
||||
LightMuted = bestLightMutedColor,
|
||||
DarkMuted = bestDarkMutedColor,
|
||||
};
|
||||
}
|
||||
|
||||
private static float GetScore(SKColor color, ColorType type, bool ignoreLimits = false)
|
||||
{
|
||||
static float InvertDiff(float value, float target)
|
||||
{
|
||||
return 1 - Math.Abs(value - target);
|
||||
}
|
||||
|
||||
color.ToHsl(out float _, out float saturation, out float luma);
|
||||
saturation /= 100f;
|
||||
luma /= 100f;
|
||||
|
||||
if (!ignoreLimits && ((saturation <= GetMinSaturation(type)) || (saturation >= GetMaxSaturation(type)) || (luma <= GetMinLuma(type)) || (luma >= GetMaxLuma(type))))
|
||||
{
|
||||
//if either saturation or luma falls outside the min-max, return the
|
||||
//lowest score possible unless we're ignoring these limits.
|
||||
return float.MinValue;
|
||||
}
|
||||
|
||||
float totalValue = (InvertDiff(saturation, GetTargetSaturation(type)) * WEIGHT_SATURATION) + (InvertDiff(luma, GetTargetLuma(type)) * WEIGHT_LUMA);
|
||||
|
||||
const float TOTAL_WEIGHT = WEIGHT_SATURATION + WEIGHT_LUMA;
|
||||
|
||||
return totalValue / TOTAL_WEIGHT;
|
||||
}
|
||||
|
||||
#region Constants
|
||||
|
||||
private const float TARGET_DARK_LUMA = 0.26f;
|
||||
private const float MAX_DARK_LUMA = 0.45f;
|
||||
private const float MIN_LIGHT_LUMA = 0.55f;
|
||||
private const float TARGET_LIGHT_LUMA = 0.74f;
|
||||
private const float MIN_NORMAL_LUMA = 0.3f;
|
||||
private const float TARGET_NORMAL_LUMA = 0.5f;
|
||||
private const float MAX_NORMAL_LUMA = 0.7f;
|
||||
private const float TARGET_MUTES_SATURATION = 0.3f;
|
||||
private const float MAX_MUTES_SATURATION = 0.3f;
|
||||
private const float TARGET_VIBRANT_SATURATION = 1.0f;
|
||||
private const float MIN_VIBRANT_SATURATION = 0.35f;
|
||||
private const float WEIGHT_SATURATION = 3f;
|
||||
private const float WEIGHT_LUMA = 5f;
|
||||
|
||||
private static float GetTargetLuma(ColorType colorType) => colorType switch
|
||||
{
|
||||
ColorType.Vibrant => TARGET_NORMAL_LUMA,
|
||||
ColorType.LightVibrant => TARGET_LIGHT_LUMA,
|
||||
ColorType.DarkVibrant => TARGET_DARK_LUMA,
|
||||
ColorType.Muted => TARGET_NORMAL_LUMA,
|
||||
ColorType.LightMuted => TARGET_LIGHT_LUMA,
|
||||
ColorType.DarkMuted => TARGET_DARK_LUMA,
|
||||
_ => throw new ArgumentException(nameof(colorType))
|
||||
};
|
||||
|
||||
private static float GetMinLuma(ColorType colorType) => colorType switch
|
||||
{
|
||||
ColorType.Vibrant => MIN_NORMAL_LUMA,
|
||||
ColorType.LightVibrant => MIN_LIGHT_LUMA,
|
||||
ColorType.DarkVibrant => 0f,
|
||||
ColorType.Muted => MIN_NORMAL_LUMA,
|
||||
ColorType.LightMuted => MIN_LIGHT_LUMA,
|
||||
ColorType.DarkMuted => 0,
|
||||
_ => throw new ArgumentException(nameof(colorType))
|
||||
};
|
||||
|
||||
private static float GetMaxLuma(ColorType colorType) => colorType switch
|
||||
{
|
||||
ColorType.Vibrant => MAX_NORMAL_LUMA,
|
||||
ColorType.LightVibrant => 1f,
|
||||
ColorType.DarkVibrant => MAX_DARK_LUMA,
|
||||
ColorType.Muted => MAX_NORMAL_LUMA,
|
||||
ColorType.LightMuted => 1f,
|
||||
ColorType.DarkMuted => MAX_DARK_LUMA,
|
||||
_ => throw new ArgumentException(nameof(colorType))
|
||||
};
|
||||
|
||||
private static float GetTargetSaturation(ColorType colorType) => colorType switch
|
||||
{
|
||||
ColorType.Vibrant => TARGET_VIBRANT_SATURATION,
|
||||
ColorType.LightVibrant => TARGET_VIBRANT_SATURATION,
|
||||
ColorType.DarkVibrant => TARGET_VIBRANT_SATURATION,
|
||||
ColorType.Muted => TARGET_MUTES_SATURATION,
|
||||
ColorType.LightMuted => TARGET_MUTES_SATURATION,
|
||||
ColorType.DarkMuted => TARGET_MUTES_SATURATION,
|
||||
_ => throw new ArgumentException(nameof(colorType))
|
||||
};
|
||||
|
||||
private static float GetMinSaturation(ColorType colorType) => colorType switch
|
||||
{
|
||||
ColorType.Vibrant => MIN_VIBRANT_SATURATION,
|
||||
ColorType.LightVibrant => MIN_VIBRANT_SATURATION,
|
||||
ColorType.DarkVibrant => MIN_VIBRANT_SATURATION,
|
||||
ColorType.Muted => 0,
|
||||
ColorType.LightMuted => 0,
|
||||
ColorType.DarkMuted => 0,
|
||||
_ => throw new ArgumentException(nameof(colorType))
|
||||
};
|
||||
|
||||
private static float GetMaxSaturation(ColorType colorType) => colorType switch
|
||||
{
|
||||
ColorType.Vibrant => 1f,
|
||||
ColorType.LightVibrant => 1f,
|
||||
ColorType.DarkVibrant => 1f,
|
||||
ColorType.Muted => MAX_MUTES_SATURATION,
|
||||
ColorType.LightMuted => MAX_MUTES_SATURATION,
|
||||
ColorType.DarkMuted => MAX_MUTES_SATURATION,
|
||||
_ => throw new ArgumentException(nameof(colorType))
|
||||
};
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal readonly struct ColorRanges
|
||||
{
|
||||
public readonly byte RedRange;
|
||||
public readonly byte GreenRange;
|
||||
public readonly byte BlueRange;
|
||||
|
||||
public ColorRanges(byte redRange, byte greenRange, byte blueRange)
|
||||
{
|
||||
this.RedRange = redRange;
|
||||
this.GreenRange = greenRange;
|
||||
this.BlueRange = blueRange;
|
||||
}
|
||||
}
|
||||
|
||||
internal class ColorCube
|
||||
{
|
||||
private readonly int _from;
|
||||
private readonly int _length;
|
||||
private SortTarget _currentOrder = SortTarget.None;
|
||||
|
||||
public ColorCube(in Span<SKColor> fullColorList, int from, int length, SortTarget preOrdered)
|
||||
{
|
||||
this._from = from;
|
||||
this._length = length;
|
||||
|
||||
OrderColors(fullColorList.Slice(from, length), preOrdered);
|
||||
}
|
||||
|
||||
private void OrderColors(in Span<SKColor> colors, SortTarget preOrdered)
|
||||
{
|
||||
if (colors.Length < 2) return;
|
||||
ColorRanges colorRanges = GetColorRanges(colors);
|
||||
|
||||
if ((colorRanges.RedRange > colorRanges.GreenRange) && (colorRanges.RedRange > colorRanges.BlueRange))
|
||||
{
|
||||
if (preOrdered != SortTarget.Red)
|
||||
RadixLikeSortRed.Sort(colors);
|
||||
|
||||
_currentOrder = SortTarget.Red;
|
||||
}
|
||||
else if (colorRanges.GreenRange > colorRanges.BlueRange)
|
||||
{
|
||||
if (preOrdered != SortTarget.Green)
|
||||
RadixLikeSortGreen.Sort(colors);
|
||||
|
||||
_currentOrder = SortTarget.Green;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (preOrdered != SortTarget.Blue)
|
||||
RadixLikeSortBlue.Sort(colors);
|
||||
|
||||
_currentOrder = SortTarget.Blue;
|
||||
}
|
||||
}
|
||||
|
||||
private ColorRanges GetColorRanges(in Span<SKColor> colors)
|
||||
{
|
||||
if (colors.Length < 512)
|
||||
{
|
||||
byte redMin = byte.MaxValue;
|
||||
byte redMax = byte.MinValue;
|
||||
byte greenMin = byte.MaxValue;
|
||||
byte greenMax = byte.MinValue;
|
||||
byte blueMin = byte.MaxValue;
|
||||
byte blueMax = byte.MinValue;
|
||||
|
||||
for (int i = 0; i < colors.Length; i++)
|
||||
{
|
||||
SKColor color = colors[i];
|
||||
if (color.Red < redMin) redMin = color.Red;
|
||||
if (color.Red > redMax) redMax = color.Red;
|
||||
if (color.Green < greenMin) greenMin = color.Green;
|
||||
if (color.Green > greenMax) greenMax = color.Green;
|
||||
if (color.Blue < blueMin) blueMin = color.Blue;
|
||||
if (color.Blue > blueMax) blueMax = color.Blue;
|
||||
}
|
||||
|
||||
return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin));
|
||||
}
|
||||
else
|
||||
{
|
||||
Span<bool> redBuckets = stackalloc bool[256];
|
||||
Span<bool> greenBuckets = stackalloc bool[256];
|
||||
Span<bool> blueBuckets = stackalloc bool[256];
|
||||
|
||||
for (int i = 0; i < colors.Length; i++)
|
||||
{
|
||||
SKColor color = colors[i];
|
||||
redBuckets[color.Red] = true;
|
||||
greenBuckets[color.Green] = true;
|
||||
blueBuckets[color.Blue] = true;
|
||||
}
|
||||
|
||||
byte redMin = 0;
|
||||
byte redMax = 0;
|
||||
byte greenMin = 0;
|
||||
byte greenMax = 0;
|
||||
byte blueMin = 0;
|
||||
byte blueMax = 0;
|
||||
|
||||
for (byte i = 0; i < redBuckets.Length; i++)
|
||||
if (redBuckets[i])
|
||||
{
|
||||
redMin = i;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = redBuckets.Length - 1; i >= 0; i--)
|
||||
if (redBuckets[i])
|
||||
{
|
||||
redMax = (byte)i;
|
||||
break;
|
||||
}
|
||||
|
||||
for (byte i = 0; i < greenBuckets.Length; i++)
|
||||
if (greenBuckets[i])
|
||||
{
|
||||
greenMin = i;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = greenBuckets.Length - 1; i >= 0; i--)
|
||||
if (greenBuckets[i])
|
||||
{
|
||||
greenMax = (byte)i;
|
||||
break;
|
||||
}
|
||||
|
||||
for (byte i = 0; i < blueBuckets.Length; i++)
|
||||
if (blueBuckets[i])
|
||||
{
|
||||
blueMin = i;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = blueBuckets.Length - 1; i >= 0; i--)
|
||||
if (blueBuckets[i])
|
||||
{
|
||||
blueMax = (byte)i;
|
||||
break;
|
||||
}
|
||||
|
||||
return new ColorRanges((byte)(redMax - redMin), (byte)(greenMax - greenMin), (byte)(blueMax - blueMin));
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TrySplit(in Span<SKColor> fullColorList, [NotNullWhen(returnValue: true)] out ColorCube? a, [NotNullWhen(returnValue: true)] out ColorCube? b)
|
||||
{
|
||||
Span<SKColor> colors = fullColorList.Slice(_from, _length);
|
||||
|
||||
if (colors.Length < 2)
|
||||
{
|
||||
a = null;
|
||||
b = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
int median = colors.Length / 2;
|
||||
|
||||
a = new ColorCube(fullColorList, _from, median, _currentOrder);
|
||||
b = new ColorCube(fullColorList, _from + median, colors.Length - median, _currentOrder);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
internal SKColor GetAverageColor(in Span<SKColor> fullColorList)
|
||||
{
|
||||
Span<SKColor> colors = fullColorList.Slice(_from, _length);
|
||||
|
||||
int r = 0, g = 0, b = 0;
|
||||
for (int i = 0; i < colors.Length; i++)
|
||||
{
|
||||
SKColor color = colors[i];
|
||||
r += color.Red;
|
||||
g += color.Green;
|
||||
b += color.Blue;
|
||||
}
|
||||
|
||||
return new SKColor(
|
||||
(byte)(r / colors.Length),
|
||||
(byte)(g / colors.Length),
|
||||
(byte)(b / colors.Length)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RadixLikeSortRed
|
||||
{
|
||||
#region Methods
|
||||
|
||||
public static void Sort(in Span<SKColor> span)
|
||||
{
|
||||
Span<int> counts = stackalloc int[256];
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
counts[span[i].Red]++;
|
||||
|
||||
Span<SKColor[]> buckets = ArrayPool<SKColor[]>.Shared.Rent(256).AsSpan(0, 256);
|
||||
for (int i = 0; i < counts.Length; i++)
|
||||
buckets[i] = ArrayPool<SKColor>.Shared.Rent(counts[i]);
|
||||
|
||||
Span<int> currentBucketIndex = stackalloc int[256];
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
SKColor color = span[i];
|
||||
int index = color.Red;
|
||||
SKColor[] bucket = buckets[index];
|
||||
int bucketIndex = currentBucketIndex[index];
|
||||
currentBucketIndex[index]++;
|
||||
bucket[bucketIndex] = color;
|
||||
}
|
||||
|
||||
int newIndex = 0;
|
||||
for (int i = 0; i < buckets.Length; i++)
|
||||
{
|
||||
Span<SKColor> bucket = buckets[i].AsSpan(0, counts[i]);
|
||||
for (int j = 0; j < bucket.Length; j++)
|
||||
span[newIndex++] = bucket[j];
|
||||
|
||||
ArrayPool<SKColor>.Shared.Return(buckets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal static class RadixLikeSortGreen
|
||||
{
|
||||
#region Methods
|
||||
|
||||
public static void Sort(in Span<SKColor> span)
|
||||
{
|
||||
Span<int> counts = stackalloc int[256];
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
counts[span[i].Green]++;
|
||||
|
||||
Span<SKColor[]> buckets = ArrayPool<SKColor[]>.Shared.Rent(256).AsSpan(0, 256);
|
||||
for (int i = 0; i < counts.Length; i++)
|
||||
buckets[i] = ArrayPool<SKColor>.Shared.Rent(counts[i]);
|
||||
|
||||
Span<int> currentBucketIndex = stackalloc int[256];
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
SKColor color = span[i];
|
||||
int index = color.Green;
|
||||
SKColor[] bucket = buckets[index];
|
||||
int bucketIndex = currentBucketIndex[index];
|
||||
currentBucketIndex[index]++;
|
||||
bucket[bucketIndex] = color;
|
||||
}
|
||||
|
||||
int newIndex = 0;
|
||||
for (int i = 0; i < buckets.Length; i++)
|
||||
{
|
||||
Span<SKColor> bucket = buckets[i].AsSpan(0, counts[i]);
|
||||
for (int j = 0; j < bucket.Length; j++)
|
||||
span[newIndex++] = bucket[j];
|
||||
|
||||
ArrayPool<SKColor>.Shared.Return(buckets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
internal static class RadixLikeSortBlue
|
||||
{
|
||||
#region Methods
|
||||
|
||||
public static void Sort(in Span<SKColor> span)
|
||||
{
|
||||
Span<int> counts = stackalloc int[256];
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
counts[span[i].Blue]++;
|
||||
|
||||
Span<SKColor[]> buckets = ArrayPool<SKColor[]>.Shared.Rent(256).AsSpan(0, 256);
|
||||
for (int i = 0; i < counts.Length; i++)
|
||||
buckets[i] = ArrayPool<SKColor>.Shared.Rent(counts[i]);
|
||||
|
||||
Span<int> currentBucketIndex = stackalloc int[256];
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
SKColor color = span[i];
|
||||
int index = color.Blue;
|
||||
SKColor[] bucket = buckets[index];
|
||||
int bucketIndex = currentBucketIndex[index];
|
||||
currentBucketIndex[index]++;
|
||||
bucket[bucketIndex] = color;
|
||||
}
|
||||
|
||||
int newIndex = 0;
|
||||
for (int i = 0; i < buckets.Length; i++)
|
||||
{
|
||||
Span<SKColor> bucket = buckets[i].AsSpan(0, counts[i]);
|
||||
for (int j = 0; j < bucket.Length; j++)
|
||||
span[newIndex++] = bucket[j];
|
||||
|
||||
ArrayPool<SKColor>.Shared.Return(buckets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum SortTarget
|
||||
{
|
||||
None, Red, Green, Blue
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1,10 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Image.Screens"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.VisualScripting.Nodes.Image.Screens.CaptureScreenNodeCustomView"
|
||||
x:DataType="screens:CaptureScreenNodeCustomViewModel">
|
||||
<TextBlock Text="Test" />
|
||||
</UserControl>
|
||||
@ -1,17 +0,0 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Image.Screens;
|
||||
|
||||
public partial class CaptureScreenNodeCustomView : ReactiveUserControl<CaptureScreenNodeCustomViewModel>
|
||||
{
|
||||
public CaptureScreenNodeCustomView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.VisualScripting;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Image.Screens;
|
||||
|
||||
public class CaptureScreenNodeCustomViewModel : CustomNodeViewModel
|
||||
{
|
||||
#region Properties & Fields
|
||||
|
||||
private readonly CaptureScreenNode _node;
|
||||
private readonly INodeEditorService _nodeEditorService;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <inheritdoc />
|
||||
public CaptureScreenNodeCustomViewModel(CaptureScreenNode node, INodeScript script, INodeEditorService nodeEditorService)
|
||||
: base(node, script)
|
||||
{
|
||||
this._node = node;
|
||||
this._nodeEditorService = nodeEditorService;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Methods
|
||||
|
||||
#endregion
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user