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? _lastTouchPositions = 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() { 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(); 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); } } }