using Avalonia; using Avalonia.Media; using Avalonia.Input; using Avalonia.Controls; using Avalonia.Media.Imaging; using Mat = OpenCvSharp.Mat; using Cv2 = OpenCvSharp.Cv2; 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? _lastPosition = null; private float _zoom = 1; private float _panX = 0; private float _panY = 0; private OpenCvBitmap? _bitmap = null; public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), defaultValue: Colors.Transparent); public Color Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } public static readonly StyledProperty StretchModeProperty = AvaloniaProperty.Register(nameof(StretchMode), defaultValue: Stretch.Uniform); public Stretch StretchMode { get => GetValue(StretchModeProperty); set => SetValue(StretchModeProperty, value); } public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode), defaultValue: TileMode.None); public TileMode TileMode { get => GetValue(TileModeProperty); set => SetValue(TileModeProperty, value); } public static readonly StyledProperty ImageProperty = AvaloniaProperty.Register(nameof(Image), defaultValue: null); public Mat? Image { get => GetValue(ImageProperty); set => SetValue(ImageProperty, value); } public OpenCvImage() : base() { } 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; } } else if (ea.Pointer.Type == PointerType.Touch) { Console.WriteLine(string.Format("PointerWheelChanged Type Touch {} {}", ea.GetPosition(this), ea.Delta)); } 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 _lastPosition = ea.GetPosition(this); } } else if (ea.Pointer.Type == PointerType.Touch) { } base.OnPointerPressed(ea); InvalidateVisual(); } protected override void OnPointerReleased(PointerReleasedEventArgs ea) { if (ea.Pointer.Type == PointerType.Mouse) { if (!ea.Properties.IsLeftButtonPressed) _lastPosition = null; } else if (ea.Pointer.Type == PointerType.Touch) { } base.OnPointerReleased(ea); InvalidateVisual(); } protected override void OnPointerMoved(PointerEventArgs ea) { if (ea.Pointer.Type == PointerType.Mouse) { if (_lastPosition is null || _zoom == 1) // Если кнопка не нажата или зум == 1, то ничего не делаем { base.OnPointerMoved(ea); return; } var position = ea.GetPosition(this); _panX -= (float)(_lastPosition.Value.X - position.X); _panY -= (float)(_lastPosition.Value.Y - position.Y); _lastPosition = position; base.OnPointerMoved(ea); } else if (ea.Pointer.Type == PointerType.Touch) { } 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); } } }