forked from amkovkov/GranuSightSoftware2
342 lines
13 KiB
C#
342 lines
13 KiB
C#
using Avalonia;
|
|
using Avalonia.Media;
|
|
using Avalonia.Input;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Media.Imaging;
|
|
|
|
using Mat = OpenCvSharp.Mat;
|
|
using Cv2 = OpenCvSharp.Cv2;
|
|
|
|
using GSS2.Core.Extentions;
|
|
|
|
namespace GSS2.UI.Core;
|
|
|
|
public class OpenCvImage : Control
|
|
{
|
|
private class OpenCvBitmap : Bitmap
|
|
{
|
|
public OpenCvBitmap(Mat mat)
|
|
: base(
|
|
format: Avalonia.Platform.PixelFormat.Rgba8888,
|
|
alphaFormat: Avalonia.Platform.AlphaFormat.Unpremul,
|
|
data: mat.Data,
|
|
size: new PixelSize(mat.Width, mat.Height),
|
|
dpi: new Vector(96, 96),
|
|
stride: (mat.DataEnd - mat.DataStart).ToInt32() / mat.Rows
|
|
)
|
|
{ }
|
|
}
|
|
|
|
private Point? _lastmousePosition = null;
|
|
private Dictionary<int, Point>? _lastTouchPositions = null;
|
|
private float _zoom = 1;
|
|
private float _panX = 0;
|
|
private float _panY = 0;
|
|
private OpenCvBitmap? _bitmap = null;
|
|
|
|
public static readonly StyledProperty<Color> BackgroundProperty = AvaloniaProperty.Register<OpenCvImage, Color>(nameof(Background), defaultValue: Colors.Transparent);
|
|
public Color Background
|
|
{
|
|
get => GetValue(BackgroundProperty);
|
|
set => SetValue(BackgroundProperty, value);
|
|
}
|
|
public static readonly StyledProperty<Stretch> StretchModeProperty = AvaloniaProperty.Register<OpenCvImage, Stretch>(nameof(StretchMode), defaultValue: Stretch.Uniform);
|
|
public Stretch StretchMode
|
|
{
|
|
get => GetValue(StretchModeProperty);
|
|
set => SetValue(StretchModeProperty, value);
|
|
}
|
|
public static readonly StyledProperty<TileMode> TileModeProperty = AvaloniaProperty.Register<OpenCvImage, TileMode>(nameof(TileMode), defaultValue: TileMode.None);
|
|
public TileMode TileMode
|
|
{
|
|
get => GetValue(TileModeProperty);
|
|
set => SetValue(TileModeProperty, value);
|
|
}
|
|
public static readonly StyledProperty<Mat?> ImageProperty = AvaloniaProperty.Register<OpenCvImage, Mat?>(nameof(Image), defaultValue: null);
|
|
public Mat? Image
|
|
{
|
|
get => GetValue(ImageProperty);
|
|
set => SetValue(ImageProperty, value);
|
|
}
|
|
|
|
public OpenCvImage()
|
|
: base()
|
|
{
|
|
DoubleTapped += (_, _) =>
|
|
{
|
|
_zoom = 1f;
|
|
_panX = 0f;
|
|
_panY = 0f;
|
|
};
|
|
}
|
|
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
{
|
|
if (change.Property == ImageProperty)
|
|
{
|
|
_bitmap?.Dispose();
|
|
_bitmap = null;
|
|
|
|
if (Image is null)
|
|
return;
|
|
|
|
if (Image.Type() != OpenCvSharp.MatType.CV_8UC3 &&
|
|
Image.Type() != OpenCvSharp.MatType.CV_8UC4 &&
|
|
Image.Type() != OpenCvSharp.MatType.CV_16UC3 &&
|
|
Image.Type() != OpenCvSharp.MatType.CV_16UC4)
|
|
return;
|
|
|
|
var image = new Mat();
|
|
Image.CopyTo(image);
|
|
|
|
if (image.Type() == OpenCvSharp.MatType.CV_16UC3)
|
|
image.ConvertTo(image, OpenCvSharp.MatType.CV_8UC3, 1 / 256.0);
|
|
if (image.Type() == OpenCvSharp.MatType.CV_16UC4)
|
|
image.ConvertTo(image, OpenCvSharp.MatType.CV_8UC4, 1 / 256.0);
|
|
|
|
var channels = image.Split();
|
|
var alphaChannel = image.Channels() == 4 ? channels[4] : new Mat(Image.Size(), OpenCvSharp.MatType.CV_8UC1, new OpenCvSharp.Scalar(255));
|
|
Cv2.Merge([channels[2], channels[0], channels[1], alphaChannel], image);
|
|
|
|
_bitmap = new OpenCvBitmap(image);
|
|
|
|
InvalidateVisual();
|
|
}
|
|
base.OnPropertyChanged(change);
|
|
}
|
|
|
|
protected override void OnPointerWheelChanged(PointerWheelEventArgs ea)
|
|
{
|
|
if (ea.Pointer.Type == PointerType.Mouse)
|
|
{
|
|
var position = ea.GetPosition(this);
|
|
|
|
// Рассчитываем позицию в координатах изображения (до масштабирования)
|
|
var imageX = (position.X - Bounds.Width / 2 - _panX) / _zoom;
|
|
var imageY = (position.Y - Bounds.Height / 2 - _panY) / _zoom;
|
|
|
|
// Применяем масштаб
|
|
float zoomFactor = ea.Delta.Y > 0 ? 1.1f : 1 / 1.1f;
|
|
_zoom *= zoomFactor;
|
|
_zoom = Math.Clamp(_zoom, 1f, 10f);
|
|
_zoom = (float)Math.Round(_zoom, 2);
|
|
|
|
if (_zoom != 1f)
|
|
{
|
|
// Пересчитываем смещение так, чтобы указатель остался на месте
|
|
_panX = (float)(position.X - Bounds.Width / 2 - imageX * _zoom);
|
|
_panY = (float)(position.Y - Bounds.Height / 2 - imageY * _zoom);
|
|
}
|
|
else
|
|
{
|
|
// Если зум == 1, то возвращаем изображение к исходному смещению
|
|
_panX = 0f;
|
|
_panY = 0f;
|
|
}
|
|
}
|
|
base.OnPointerWheelChanged(ea);
|
|
InvalidateVisual();
|
|
}
|
|
protected override void OnPointerPressed(PointerPressedEventArgs ea)
|
|
{
|
|
if (ea.Pointer.Type == PointerType.Mouse)
|
|
{
|
|
if (ea.Properties.IsLeftButtonPressed)
|
|
{
|
|
if (ea.ClickCount == 2) // По двойному клику возвращаем изображение к смещение и зум к стандартным значениям
|
|
{
|
|
_zoom = 1f;
|
|
_panX = 0f;
|
|
_panY = 0f;
|
|
}
|
|
else
|
|
_lastmousePosition = ea.GetPosition(this);
|
|
}
|
|
}
|
|
else if (ea.Pointer.Type == PointerType.Touch)
|
|
{
|
|
if (_lastTouchPositions is null)
|
|
_lastTouchPositions = new Dictionary<int, Point>();
|
|
|
|
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
|
|
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
|
|
else if (_lastTouchPositions.ContainsKey(ea.Pointer.Id))
|
|
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
|
|
}
|
|
base.OnPointerPressed(ea);
|
|
InvalidateVisual();
|
|
}
|
|
protected override void OnPointerReleased(PointerReleasedEventArgs ea)
|
|
{
|
|
if (ea.Pointer.Type == PointerType.Mouse)
|
|
{
|
|
if (!ea.Properties.IsLeftButtonPressed)
|
|
_lastmousePosition = null;
|
|
}
|
|
else if (ea.Pointer.Type == PointerType.Touch)
|
|
{
|
|
if (_lastTouchPositions is not null && _lastTouchPositions.ContainsKey(ea.Pointer.Id))
|
|
_lastTouchPositions.Remove(ea.Pointer.Id);
|
|
}
|
|
base.OnPointerReleased(ea);
|
|
InvalidateVisual();
|
|
}
|
|
protected override void OnPointerMoved(PointerEventArgs ea)
|
|
{
|
|
if (ea.Pointer.Type == PointerType.Mouse)
|
|
{
|
|
if (_lastmousePosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем
|
|
{
|
|
base.OnPointerMoved(ea);
|
|
return;
|
|
}
|
|
var position = ea.GetPosition(this);
|
|
_panX -= (float)(_lastmousePosition.Value.X - position.X);
|
|
_panY -= (float)(_lastmousePosition.Value.Y - position.Y);
|
|
_lastmousePosition = position;
|
|
}
|
|
else if (ea.Pointer.Type == PointerType.Touch)
|
|
{
|
|
if (_lastTouchPositions is null)
|
|
{
|
|
base.OnPointerMoved(ea);
|
|
return;
|
|
}
|
|
|
|
if (!_lastTouchPositions.ContainsKey(ea.Pointer.Id) && _lastTouchPositions.Count() < 2)
|
|
{
|
|
_lastTouchPositions.Add(ea.Pointer.Id, ea.GetPosition(this));
|
|
base.OnPointerMoved(ea);
|
|
return;
|
|
}
|
|
|
|
if (_lastTouchPositions.Count() == 1 && _zoom != 1) // Если есть только одно прикосновение и зум != 1, то токлько перемещение
|
|
{
|
|
var position = ea.GetPosition(this);
|
|
_panX -= (float)(_lastTouchPositions[ea.Pointer.Id].X - position.X);
|
|
_panY -= (float)(_lastTouchPositions[ea.Pointer.Id].Y - position.Y); // Ось Y инвертированна
|
|
_lastTouchPositions[ea.Pointer.Id] = position;
|
|
}
|
|
else if (_lastTouchPositions.Count() == 2) // Если два прикосновения, то перемещение с приближением
|
|
{
|
|
var lastPosition1 = _lastTouchPositions.Skip(0).First().Value;
|
|
var lastPosition2 = _lastTouchPositions.Skip(1).First().Value;
|
|
var lastDistance = Point.Distance(lastPosition1, lastPosition2);
|
|
var lastCenter = PointHelper.Center(lastPosition1, lastPosition2);
|
|
|
|
_lastTouchPositions[ea.Pointer.Id] = ea.GetPosition(this);
|
|
|
|
var newPosition1 = _lastTouchPositions.Skip(0).First().Value;
|
|
var newPosition2 = _lastTouchPositions.Skip(1).First().Value;
|
|
var newDistance = Point.Distance(newPosition1, newPosition2);
|
|
var newCenter = PointHelper.Center(newPosition1, newPosition2);
|
|
|
|
var zoomFactor = newDistance / lastDistance;
|
|
var newZoom = (float)(_zoom * zoomFactor);
|
|
newZoom = (float)Math.Clamp(newZoom, 1f, 10f);
|
|
|
|
var imageX = (newCenter.X - Bounds.Width / 2 - _panX) / _zoom;
|
|
var imageY = (newCenter.Y - Bounds.Height / 2 - _panY) / _zoom;
|
|
|
|
_zoom = newZoom;
|
|
|
|
if (_zoom > 1f)
|
|
{
|
|
_panX = (float)(newCenter.X - Bounds.Width / 2 - imageX * _zoom);
|
|
_panY = (float)(newCenter.Y - Bounds.Height / 2 - imageY * _zoom);
|
|
}
|
|
else
|
|
{
|
|
_zoom = 1f;
|
|
_panX = 0f;
|
|
_panY = 0f;
|
|
}
|
|
}
|
|
}
|
|
base.OnPointerMoved(ea);
|
|
InvalidateVisual();
|
|
}
|
|
|
|
public override void Render(DrawingContext context)
|
|
{
|
|
context.FillRectangle(new SolidColorBrush(Background), new Rect(0, 0, Bounds.Width, Bounds.Height));
|
|
if (_bitmap is null)
|
|
{
|
|
base.Render(context);
|
|
return;
|
|
}
|
|
|
|
var (sx, sy) = ComputeStretchScale(StretchMode, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height);
|
|
sx *= _zoom;
|
|
sy *= _zoom;
|
|
|
|
var drawWidth = _bitmap.PixelSize.Width * sx;
|
|
var drawHeight = _bitmap.PixelSize.Height * sy;
|
|
var destRect = new Rect((Bounds.Width - drawWidth) / 2 + _panX, (Bounds.Height - drawHeight) / 2 + _panY, drawWidth, drawHeight);
|
|
|
|
var srcRect = ComputeSourceRect(TileMode, _bitmap.PixelSize.Width, _bitmap.PixelSize.Height);
|
|
|
|
context.DrawImage(_bitmap, srcRect, destRect);
|
|
|
|
base.Render(context);
|
|
}
|
|
|
|
private (double sx, double sy) ComputeStretchScale(Stretch stretch, double imageWidth, double imageHeight)
|
|
{
|
|
double viewWidth = Bounds.Width;
|
|
double viewHeight = Bounds.Height;
|
|
|
|
var viewAspect = viewWidth / viewHeight;
|
|
var imageAspect = imageWidth / imageHeight;
|
|
|
|
switch (stretch)
|
|
{
|
|
case Stretch.None:
|
|
return (1, 1);
|
|
case Stretch.Fill:
|
|
return (viewWidth / imageWidth, viewHeight / imageHeight);
|
|
case Stretch.Uniform:
|
|
if (imageAspect > viewAspect)
|
|
{
|
|
var s = viewWidth / imageWidth;
|
|
return (s, s);
|
|
}
|
|
else
|
|
{
|
|
var s = viewHeight / imageHeight;
|
|
return (s, s);
|
|
}
|
|
case Stretch.UniformToFill:
|
|
if (imageAspect > viewAspect)
|
|
{
|
|
var s = viewHeight / imageHeight;
|
|
return (s, s);
|
|
}
|
|
else
|
|
{
|
|
var s = viewWidth / imageWidth;
|
|
return (s, s);
|
|
}
|
|
default:
|
|
return (1, 1);
|
|
}
|
|
}
|
|
private Rect ComputeSourceRect(TileMode mode, int imageWidth, int imageHeight)
|
|
{
|
|
switch (mode)
|
|
{
|
|
case TileMode.None:
|
|
return new Rect(0, 0, imageWidth, imageHeight);
|
|
case TileMode.Tile:
|
|
return new Rect(0, 0, imageWidth * 2, imageHeight * 2);
|
|
case TileMode.FlipX:
|
|
return new Rect(imageWidth, 0, -imageWidth, imageHeight);
|
|
case TileMode.FlipY:
|
|
return new Rect(0, imageHeight, imageWidth, -imageHeight);
|
|
case TileMode.FlipXY:
|
|
return new Rect(imageWidth, imageHeight, -imageWidth, -imageHeight);
|
|
default:
|
|
return new Rect(0, 0, imageWidth, imageHeight);
|
|
}
|
|
}
|
|
} |