Обрезка изображения по кругу

200
08 октября 2018, 00:30

Необходимо реализовать обрезку изображения по кругу с выбором области обрезки по типу, как это сделано, например, тут либо другой способ выбора области обрезки, лишь бы ее было удобно выбирать мышкой с возможностью масштаба/перемещения. В какую сторону смотреть? Может, кто-то сталкивался с инструкциями на эту тему?

Answer 1

Пример получается довольно большой, поэтому я его упрощу: пути для входного и выходного файла захардкожены, диалоги для их выбора добавите самостоятельно; исходное изображение не масштабируется, вместо этого масштабируется окружность; для перемещения области вырезания используются простые клики левой кнопкой мыши.

Итак, содержимое окна:

<Grid Margin="5">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="128"/>
        <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <StackPanel>
        <TextBlock Text="Исходное:"/>
        <Image Source="{Binding BlankImage}" Margin="0,0,0,5"/>
        <TextBlock Text="Обрезанное:"/>
        <Image Source="{Binding CroppedImage}"/>
    </StackPanel>
    <Grid Grid.Column="1" Margin="5,0,0,0">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid local:GesturesEx.MouseWheelDownCommand="{Binding DecRadius}"
              local:GesturesEx.MouseWheelUpCommand="{Binding IncRadius}"
              local:GesturesEx.MouseLeftButtonDownCommand="{Binding MoveTo}">
            <Image Source="{Binding BlankImage}" Stretch="None"
                   VerticalAlignment="Top" HorizontalAlignment="Left"/>
            <Path Fill="#66000000">
                <Path.Data>
                    <CombinedGeometry GeometryCombineMode="Exclude">
                        <CombinedGeometry.Geometry1>
                            <!--Чтобы накрыть всю картинку, в идеале здесь должна быть привязка к размерам родительского Grid-->
                            <RectangleGeometry Rect="0,0,10000,10000"/>
                        </CombinedGeometry.Geometry1>
                        <CombinedGeometry.Geometry2>
                            <EllipseGeometry Center="{Binding Center}"
                                             RadiusX="{Binding Radius}"
                                             RadiusY="{Binding Radius}"/>
                        </CombinedGeometry.Geometry2>
                    </CombinedGeometry>
                </Path.Data>
            </Path>
        </Grid>
        <UniformGrid Grid.Row="1" Margin="0,5,0,0" Rows="1"
                     HorizontalAlignment="Right">
            <UniformGrid.Resources>
                <Style TargetType="Button">
                    <Setter Property="Margin" Value="5,0,0,0"/>
                    <Setter Property="Padding" Value="10,2"/>
                </Style>
            </UniformGrid.Resources>
            <Button Content="Вырезать" Command="{Binding Crop}"/>
            <Button Content="Сохранить" Command="{Binding Save}"/>
        </UniformGrid>
    </Grid>
</Grid>

Вроде ничего сильно сложного, обычные привязки, всё самое интересное происходит в VM. Ну и для запуска команд при прокрутке колеса мыши и кликов написан простой класс с тремя AP:

public static class GesturesEx
{
    public static ICommand GetMouseWheelUpCommand(DependencyObject obj)
        => (ICommand)obj.GetValue(MouseWheelUpCommandProperty);
    public static void SetMouseWheelUpCommand(DependencyObject obj, ICommand value)
        => obj.SetValue(MouseWheelUpCommandProperty, value);
    public static readonly DependencyProperty MouseWheelUpCommandProperty =
        DependencyProperty.RegisterAttached("MouseWheelUpCommand", typeof(ICommand),
            typeof(GesturesEx), new PropertyMetadata(null, MouseWheelUpCommandChangedCallback));
    static void MouseWheelUpCommandChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var element = (UIElement)d;
        if (e.OldValue != null) element.MouseWheel -= OnMouseWheel;
        if (e.NewValue != null) element.MouseWheel += OnMouseWheel;
        void OnMouseWheel(object sender, MouseWheelEventArgs args)
        {
            if (args.Delta > 0) GetMouseWheelUpCommand(d).Execute(null);
        }
    }
    public static ICommand GetMouseWheelDownCommand(DependencyObject obj)
        => (ICommand)obj.GetValue(MouseWheelDownCommandProperty);
    public static void SetMouseWheelDownCommand(DependencyObject obj, ICommand value)
        => obj.SetValue(MouseWheelDownCommandProperty, value);
    public static readonly DependencyProperty MouseWheelDownCommandProperty =
        DependencyProperty.RegisterAttached("MouseWheelDownCommand", typeof(ICommand),
            typeof(GesturesEx), new PropertyMetadata(null, MouseWheelDownCommandChangedCallback));
    static void MouseWheelDownCommandChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var element = (UIElement)d;
        if (e.OldValue != null) element.MouseWheel -= OnMouseWheel;
        if (e.NewValue != null) element.MouseWheel += OnMouseWheel;
        void OnMouseWheel(object sender, MouseWheelEventArgs args)
        {
            if (args.Delta < 0) GetMouseWheelDownCommand(d).Execute(null);
        }
    }
    public static ICommand GetMouseLeftButtonDownCommand(DependencyObject obj)
        => (ICommand)obj.GetValue(MouseLeftButtonDownCommandProperty);
    public static void SetMouseLeftButtonDownCommand(DependencyObject obj, ICommand value)
        => obj.SetValue(MouseLeftButtonDownCommandProperty, value);
    public static readonly DependencyProperty MouseLeftButtonDownCommandProperty =
        DependencyProperty.RegisterAttached("MouseLeftButtonDownCommand", typeof(ICommand),
            typeof(GesturesEx), new PropertyMetadata(null, MouseLeftButtonDownCommandChangedCallback));
    static void MouseLeftButtonDownCommandChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var element = (UIElement)d;
        if (e.OldValue != null) element.MouseLeftButtonDown -= OnMouseLeftButtonDown;
        if (e.NewValue != null) element.MouseLeftButtonDown += OnMouseLeftButtonDown;
        void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
            => GetMouseLeftButtonDownCommand(element).Execute(args.GetPosition(element));
    }
}

В нем тоже ничего сложного, обычные AP, да и к делу он не сильно относится.

Теперь VM, используется стандартный базовый класс для нее:

abstract class Vm : INotifyPropertyChanged
{
    protected bool Set<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        NotifyPropertyChanged(propertyName);
        return true;
    }
    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    public event PropertyChangedEventHandler PropertyChanged;
}

Также повсеместно в VM используется следующая реализация ICommand:

public class DelegateCommand : ICommand
{
    protected readonly Predicate<object> _canExecute;
    protected readonly Action<object> _execute;
    public event EventHandler CanExecuteChanged;
    public DelegateCommand(Action<object> execute) : this(execute, _ => true) { }
    public DelegateCommand(Action<object> execute, Predicate<object> canExecute)
    {
        _execute = execute ?? throw new ArgumentNullException(nameof(execute));
        _canExecute = canExecute ?? throw new ArgumentNullException(nameof(canExecute));
    }
    public bool CanExecute(object parameter) => _canExecute(parameter);
    public void Execute(object parameter) => _execute(parameter);
    public void RaiseCanExecuteChanged()
        => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

Сама главная VM:

class MainVm : Vm
{
    // Исходное изображение
    ImageSource blankImage;
    public ImageSource BlankImage
    {
        get => blankImage;
        set => Set(ref blankImage, value);
    }
    // Обрезанное изображение
    BitmapFrame croppedImage;
    public BitmapFrame CroppedImage
    {
        get => croppedImage;
        set => Set(ref croppedImage, value);
    }
    // Координаты центра окружности
    Point center;
    public Point Center
    {
        get => center;
        set => Set(ref center, value);
    }
    // Радиус окружности
    double radius;
    public double Radius
    {
        get => radius;
        set => Set(ref radius, value);
    }
    // Команды для увеличения/уменьшения радиуса и перемещения окружности
    public ICommand IncRadius { get; }
    public ICommand DecRadius { get; }
    public ICommand MoveTo { get; }
    // Команды для обрезки и сохранения картинки
    public ICommand Crop { get; }
    public ICommand Save { get; }
    public MainVm()
    {
        IncRadius = new DelegateCommand(_ => Radius = Math.Min(Radius + 10, 1000));
        DecRadius = new DelegateCommand(_ => Radius = Math.Max(Radius - 10, 10));
        MoveTo = new DelegateCommand(o => Center = (Point)o);
        Crop = new DelegateCommand(_ => CropImage());
        Save = new DelegateCommand(_ => SaveImage());
        BlankImage = new BitmapImage(new Uri(@"...\bear.jpg"));
        Radius = 50;
        Center = new Point(BlankImage.Width / 2, BlankImage.Height / 2);
    }
    void CropImage()
    {
        var drawingVisual = new DrawingVisual();
        using (var drawingContext = drawingVisual.RenderOpen())
        {
            // Прямоугольник (квадрат), который нужно взять в исходном изображении
            var cropRect = new Rect(
                x: Center.X - Radius,
                y: Center.Y - Radius,
                width: 2 * Radius,
                height: 2 * Radius);
            // Вырезаем из исходного изображения и делаем из него кисть
            var brush = new ImageBrush(BlankImage)
            {
                ViewboxUnits = BrushMappingMode.Absolute,
                Viewbox = cropRect
            };
            // Рисуем эллипс, закрашенный полученной кистью
            drawingContext.DrawEllipse(
                brush, pen: null,
                center: new Point(Radius, Radius),
                radiusX: Radius,
                radiusY: Radius);
        }
        // Переносим полученное рисование в точечное изображение
        var bmp = new RenderTargetBitmap(
            (int)(2 * Radius), (int)(2 * Radius),
            96, 96, PixelFormats.Pbgra32);
        bmp.Render(drawingVisual);
        var frame = BitmapFrame.Create(bmp);
        frame.Freeze();
        CroppedImage = frame;
    }
    void SaveImage()
    {
        if (CroppedImage == null) return;
        // Сохраняем точечное изображение в файл в PNG-формате
        var encoder = new PngBitmapEncoder();
        encoder.Frames.Add(CroppedImage);
        using (var stream = File.Create(@"...\crop.png"))
            encoder.Save(stream);
    }
}

Ну и не забудьте установить DataContext для окна, например, как здесь в App.xaml.cs

Пример работы программы:

На выходе получаем:

READ ALSO
Отравка фото на API OK

Отравка фото на API OK

Делаю постер товаров в ОдноклассникиВсе права получены, использую вечный токен доступа

186
Doctrine не поддгружает связанные обьекты

Doctrine не поддгружает связанные обьекты

Например у меня есть таблицы campaigns и campaign_channels, я создаю обьект Campaign и CampaignChannels, в базе создаются воответствующие записи, но когда я хочу вытянуть...

171
Оптимизация и сжатие изображений FuelPHP

Оптимизация и сжатие изображений FuelPHP

Какие есть бесплатные способы сжатия изображений при загрузке на сайт? Сайт построен на FuelPHP

181