Skip to content

Instantly share code, notes, and snippets.

@tomeko
Created April 22, 2023 17:57
Show Gist options
  • Save tomeko/6b779d37e73e733aed50dcdb88733ddf to your computer and use it in GitHub Desktop.
Save tomeko/6b779d37e73e733aed50dcdb88733ddf to your computer and use it in GitHub Desktop.
Fuzzy select fill simple (Avalonia UI)
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using System.Collections.Generic;
using System;
using System.IO;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Media;
using Avalonia.Platform;
using System.Runtime.InteropServices;
using System.Linq;
namespace floodfillava
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
const string TEST_IMAGE_PATH = @"t1.jpg";
private WriteableBitmap _ImSrc;
public WriteableBitmap ImSrc { get { return _ImSrc; } set { _ImSrc = value; NotifyPropertyChanged(); } }
private double _Threshold = 50;
public double Threshold { get { return _Threshold; } set { _Threshold = value; NotifyPropertyChanged(); } }
public MainWindow()
{
InitializeComponent();
DataContext = this;
ctlImSrc.PointerPressed += ImSrc_PointerPressed;
}
private void ImSrc_PointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
{
Point pt_click = e.GetPosition(ctlImSrc);
Point pt_im = new Point((ImSrc.Size.Width / ctlImSrc.Bounds.Width) * pt_click.X, (ImSrc.Size.Height / ctlImSrc.Bounds.Height) * pt_click.Y);
FloodFill(pt_im, (int)Threshold);
}
void FloodFill(Point start, int thresh)
{
int im_w = (int)ImSrc.Size.Width;
int im_h = (int)ImSrc.Size.Height;
UInt32[,] imdat = new UInt32[im_h, im_w];
var imp = ImSrc.Lock();
unsafe
{
fixed (UInt32* ptr = imdat)
{
int sz = imp.RowBytes * im_h * 4;
CopyMemory((void*)imp.Address, ptr, imp.RowBytes * im_h);
}
}
Color targetColor = imdat.GetColor((int)start.X, (int)start.Y);
HashSet<Point> selectedPoints = new HashSet<Point>();
Queue<Point> pointsToCheck = new Queue<Point>();
pointsToCheck.Enqueue(start);
while (pointsToCheck.Count > 0)
{
Point currentPoint = pointsToCheck.Dequeue();
if (selectedPoints.Contains(currentPoint)) continue;
Color currentColor = imdat.GetColor((int)currentPoint.X, (int)currentPoint.Y);
int colorDifference = ColorDifference(targetColor, currentColor);
if (colorDifference <= thresh)
{
selectedPoints.Add(currentPoint);
IEnumerable<Point> neighbors = GetNeighbors(currentPoint, im_w, im_h);
foreach (Point neighbor in neighbors)
{
if (!selectedPoints.Contains(neighbor))
{
pointsToCheck.Enqueue(neighbor);
}
}
}
}
Color color = new Color(255, 255, 0, 0);
foreach (var pt in selectedPoints)
imp.SetPixel((int)pt.X, (int)pt.Y, color);
imp.Dispose();
ctlImSrc.Source = null;
ctlImSrc.Source = ImSrc;
}
private static IEnumerable<Point> GetNeighbors(Point point, int width, int height)
{
List<Point> neighbors = new List<Point>();
var x = point.X;
var y = point.Y;
if (x > 0) neighbors.Add(new Point(x - 1, y));
if (x < width - 1) neighbors.Add(new Point(x + 1, y));
if (y > 0) neighbors.Add(new Point(x, y - 1));
if (y < height - 1) neighbors.Add(new Point(x, y + 1));
return neighbors;
}
private static int ColorDifference(Color color1, Color color2)
{
int redDifference = Math.Abs(color1.R - color2.R);
int greenDifference = Math.Abs(color1.G - color2.G);
int blueDifference = Math.Abs(color1.B - color2.B);
return redDifference + greenDifference + blueDifference;
}
private async void Load_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var ofd = new OpenFileDialog() { Title = "Load Image", AllowMultiple = false, Filters = new List<FileDialogFilter>() { new FileDialogFilter() { Extensions = new List<string>() { "jpg", "jpeg", "JPG" }, Name = "JPG" } } };
var file = await ofd.ShowAsync(this);
if (file != null && File.Exists(file[0]))
{
loadtestim(file[0]);
}
}
void loadtestim(string fpath)
{
WriteableBitmap wb = null;
using (FileStream fs = new FileStream(TEST_IMAGE_PATH, FileMode.Open))
{
ImSrc = WriteableBitmap.Decode(fs);
}
ctlImSrc.Source = ImSrc;
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public unsafe static void CopyMemory(void* srcPtr, void* dstPtr, int count) => new Span<byte>(srcPtr, count).CopyTo(new Span<byte>(dstPtr, count));
}
public static class Extensions
{
public static Color GetColor(this UInt32[,] dat, int x, int y)
{
return Color.FromArgb((byte)(dat[y,x] >> 24), (byte)(dat[y, x] >> 16), (byte)(dat[y, x] >> 8), (byte)dat[y, x]);
}
}
}
<Window 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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="floodfillava.MainWindow"
Title="floodfillava">
<Grid RowDefinitions="50,*" ColumnDefinitions="*,*">
<StackPanel Orientation="Horizontal" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Center" Margin="20,0,0,0">
<Button Content="Load" Click="Load_Click" Margin="0,0,30,0"/>
<Slider Minimum="0" Maximum="255" Value="{Binding Threshold, Mode=TwoWay}" Width="300" Margin="0,0,10,0"/>
<TextBlock Text="{Binding Threshold, StringFormat={}{0:N0}}" VerticalAlignment="Center" Width="60"/>
<TextBlock Text="Threshold" VerticalAlignment="Center"/>
</StackPanel>
<Image x:Name="ctlImSrc" Stretch="Uniform" StretchDirection="DownOnly" Grid.Row="1" Grid.Column="0"/>
<Image x:Name="ctlImDst" Stretch="Uniform" StretchDirection="DownOnly" Grid.Row="1" Grid.Column="1"/>
</Grid>
</Window>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment