C#でWPF学習中「OpenCVSharpでガンマ補正」

C# コンピュータ
C#

OpenCVSharpでガンマ補正をする方法を調べたのでGUIを作成してみました。

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new wpf
dotnet add package Microsoft.Xaml.Behaviors.Wpf
dotnet add package ReactiveProperty.WPF
dotnet add package OpenCvSharp4.Windows
code .

ソースコード

ファイル名:DragOverBehavior.cs

using System.Diagnostics;
using System.Xml.Serialization;
using System.Windows;
using Microsoft.Xaml.Behaviors;
using System;

namespace GammaCorrection
{
    public class DragOverBehavior : Behavior<UIElement>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.DragOver += this.DragOver;
        }

        private void DragOver(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effects = DragDropEffects.Copy;
                e.Handled = true;
                return;
            }

            e.Effects = DragDropEffects.None;
            e.Handled = false;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.DragOver -= this.DragOver;
        }
    }
}

ファイル名:GraphicsUtil.cs

using OpenCvSharp;
using OpenCvSharp.Extensions;
using OpenCvSharp.WpfExtensions;
using System.Windows.Media.Imaging;

namespace GammaCorrection
{
    public class GraphicsUtil
    {
        static public BitmapSource LoadBitmapSource(string file)
        {
            using (Mat mat = new(file))
            {
                var fb = BitmapSourceConverter.ToBitmapSource(mat);
                fb.Freeze();
                return fb;
            }

        }
        static public BitmapSource Filter(BitmapSource src, double gamma)
        {
            if (gamma == 0.0d) return src;

            using (Mat mat = BitmapSourceConverter.ToMat(src))
            {
                // ガンマ補正
                var lut = new byte[256];

                for(var i = 0; i < 256; i += 1)
                    lut[i] = (byte)(System.Math.Pow((double)(i / 255.0d), 1.0d / gamma) * 255.0d);
                
                Cv2.LUT(mat, lut, mat);

                var img = BitmapSourceConverter.ToBitmapSource(mat);
                img.Freeze();
                return img;
            }
        }
    }
}

ファイル名:MainWindow.xaml

<Window x:Class="GammaCorrection.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        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"
        xmlns:local="clr-namespace:GammaCorrection"
        mc:Ignorable="d"
        xmlns:interactivity="clr-namespace:Reactive.Bindings.Interactivity;assembly=ReactiveProperty.WPF"
        xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors"
        Title="ガンマ補正" Height="600" Width="800">
    
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <i:Interaction.Behaviors>
        <local:ViewModelCleanupBehavior />
    </i:Interaction.Behaviors>

    <Grid>
        
        <Grid.RowDefinitions>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        
        <ScrollViewer
            HorizontalScrollBarVisibility="Visible" 
            VerticalScrollBarVisibility="Visible" 
            Margin="10,10,10,0"
            Background="Pink"
            AllowDrop="True"
            Grid.Row="0">

            <i:Interaction.Behaviors>
                <local:DragOverBehavior />
            </i:Interaction.Behaviors>
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Drop">
                    <interactivity:EventToReactiveCommand Command="{Binding DropCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
            
            <Canvas
                Margin="0,0,0,0"
                Height="{Binding CanvasHeight.Value}"
                Width="{Binding CanvasWidth.Value}"
                Background="AliceBlue">
                <Image
                    Name="ViewImage" 
                    Stretch="None"
                    Source="{Binding ViewImage.Value}">
                </Image>
            </Canvas>
        </ScrollViewer>
        
        <StackPanel
            Grid.Row="1">
            <CheckBox
                Margin="10"
                IsEnabled="{Binding IsCheckBoxEnabled.Value}"
                IsChecked="{Binding IsCheckBoxChecked.Value}">
                フィルター有効・無効
            </CheckBox>

            <Expander IsExpanded="True" Header="ガンマ補正">
                <StackPanel>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock  Text="Gamma:" />
                        <TextBox  IsEnabled="{Binding SliderEnabled.Value}"
                                    Text="{Binding Gamma.Value}" />
                    </StackPanel>
                    <Slider IsEnabled="{Binding SliderEnabled.Value}"
                            Minimum="-3"
                            Maximum="3"
                            Value="{Binding GammaInt.Value}"/>
                    <ProgressBar 
                        IsEnabled="{Binding IsProgressBarIndeterminate.Value}"
                        IsIndeterminate="{Binding IsProgressBarIndeterminate.Value}" />
                </StackPanel>
            </Expander>
            
            
        </StackPanel>
    </Grid>
</Window>

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System;
using System.Windows;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Media.Imaging;
using System.Reactive.Disposables;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace GammaCorrection
{
    public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
    {
        public event PropertyChangedEventHandler PropertyChanged;

        // オリジナル画像ビットマップ
        public ReactiveProperty<BitmapSource> OriginalImage { get; private set;} = new ReactiveProperty<BitmapSource>();
        // 表示用ビットマップ
        public ReactiveProperty<BitmapSource> ViewImage { get; private set;} = new ReactiveProperty<BitmapSource>();
        // ドロップコマンド
        public AsyncReactiveCommand<DragEventArgs> DropCommand { get; }
        // キャンバス高さ
        public ReactiveProperty<double> CanvasHeight { get; private set; }

        // キャンバス幅
        public ReactiveProperty<double> CanvasWidth { get; private set; }
        // フィルター有効無効チェックボックス
        public ReactiveProperty<bool> IsCheckBoxEnabled { get; private set; } = new ReactiveProperty<bool>(false);
        public ReactiveProperty<bool> IsCheckBoxChecked { get; private set; }

        // スライドバーの有効フラグ
        public ReactiveProperty<bool> SliderEnabled {get; private set;} = new(false);

        // プログレスバー
        public ReactiveProperty<bool> IsProgressBarIndeterminate {get; private set;} = new (false);

        // ガンマ補正パラメタ(スライダー用)
        public ReactiveProperty<int> GammaInt { get; }
        // ガンマ補正パラメタ
        public ReactiveProperty<double> Gamma { get; }


        protected virtual void OnPropertyChanged(string name) => PropertyChanged(this, new PropertyChangedEventArgs(name));
        private CompositeDisposable Disposable { get; } = new();
        public MainWindowViewModel()
        {
            PropertyChanged += (o, e) => {};

            // キャンバス高さ初期化
            CanvasHeight = new ReactiveProperty<double>().AddTo(Disposable);
            // キャンバス幅初期化
            CanvasWidth = new ReactiveProperty<double>().AddTo(Disposable);
            // オリジナルビットマップの初期化
            OriginalImage = new ReactiveProperty<BitmapSource>().AddTo(Disposable);
            // 表示用ビットマップの初期化
            ViewImage = new ReactiveProperty<BitmapSource>().AddTo(Disposable);
            ViewImage.Subscribe(
                img => {
                    if (img == null) return;

                    // キャンバスサイズの変更
                    CanvasHeight.Value = (double)ViewImage.Value.PixelHeight;
                    CanvasWidth.Value = (double)ViewImage.Value.PixelWidth;

                }
            );
            // ドロップコマンドの初期化
            DropCommand = new AsyncReactiveCommand<DragEventArgs>().WithSubscribe(
                async e=>{
            
                    if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
                    string[] args = (string[])e.Data.GetData(DataFormats.FileDrop);

                    List<string> exts = new() {".JPEG", ".JPG", ".PNG", ".BMP"};

                    var files = args.Where(e => exts.Contains(Path.GetExtension(e).ToUpper()));
                    if (files.Any() == false) return;

                    foreach(var file in files)
                    {
                        var img = await Task.Run(() => GraphicsUtil.LoadBitmapSource(file));
                        ViewImage.Value = img;
                        OriginalImage.Value = img.Clone();
                        OriginalImage.Value.Freeze();

                        IsCheckBoxChecked.Value = false;
                        IsCheckBoxEnabled.Value = true;
                        SliderEnabled.Value = true;
                    }
                }
            );

            // フィルター有効無効チェックボックス初期化
            IsCheckBoxChecked = new ReactiveProperty<bool>().AddTo(Disposable);
            IsCheckBoxChecked.Subscribe(
                async x => {
                    if (OriginalImage.Value == null) return;
                    
                    if (x == false)
                    {
                        // 無効の場合、オリジナル画像を表示
                        ViewImage.Value = OriginalImage.Value.Clone();
                        ViewImage.Value.Freeze();
                        // SliderEnabled.Value = true;
                        return;
                    }

                    IsProgressBarIndeterminate.Value = true;
                    SliderEnabled.Value = false;
                    IsCheckBoxEnabled.Value = false;

                    ViewImage.Value = await Task.Run(() => GraphicsUtil.Filter(OriginalImage.Value, Gamma.Value));
                    
                    IsProgressBarIndeterminate.Value = false;
                    SliderEnabled.Value = true;
                    IsCheckBoxEnabled.Value = true;
                }
            );
            // ガンマ補正パラメタの初期化
            Gamma = new ReactiveProperty<double>().AddTo(Disposable);
            Gamma.Subscribe( x => {
                if (GammaInt == null) return;
                var intValue = 0;
                if (x == 3.0d) intValue = 3;
                if (x == 2.0d) intValue = 2;
                if (x == 1.5d) intValue = 1;
                if (x == 0.0d) intValue = 0;
                if (x == 0.66d) intValue = -1;
                if (x == 0.5d) intValue = -2;
                if (x == 0.33d) intValue = -3;

                if (GammaInt.Value != intValue)
                    GammaInt.Value = intValue;
            });
            GammaInt = new ReactiveProperty<int>(0).AddTo(Disposable);
            GammaInt.Subscribe( async x => {
                if (Gamma == null) return;
                var doubleValue = 0.0d;
                if (x == 3) doubleValue = 3.0d;
                if (x == 2) doubleValue = 2.0d;
                if (x == 1) doubleValue = 1.5d;
                if (x == 0) doubleValue = 0.0d;
                if (x == -1) doubleValue = 0.66d;
                if (x == -2) doubleValue = 0.5d;
                if (x == -3) doubleValue = 0.33d;

                if (Gamma.Value != doubleValue)
                {
                    Gamma.Value = doubleValue;

                    if (IsCheckBoxChecked.Value == false) return;
                    
                    // 重い場合はコメントアウト
                    IsProgressBarIndeterminate.Value = true;
                    SliderEnabled.Value = false;
                    IsCheckBoxEnabled.Value = false;

                    ViewImage.Value = await Task.Run(() => GraphicsUtil.Filter(OriginalImage.Value, Gamma.Value));
                    
                    IsProgressBarIndeterminate.Value = false;
                    SliderEnabled.Value = true;
                    IsCheckBoxEnabled.Value = true;
                }
            });

        }
        public void Dispose()
        {
            Disposable.Dispose();
        }
    }// class
}// ns

ファイル名:ViewModelCleanupBehavior.cs

using System.Xml;
using System.Xml.Schema;

using Microsoft.Xaml.Behaviors;
using System;
using System.Windows;
using System.ComponentModel;

namespace GammaCorrection
{
    public class ViewModelCleanupBehavior : Behavior<Window>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.Closed += this.WindowClosed;
        }

        private void WindowClosed(object sender, EventArgs e)
        {
            (this.AssociatedObject.DataContext as IDisposable)?.Dispose();
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            this.AssociatedObject.Closed -= this.WindowClosed;
        }
    }
}

使い方


ピンク色の領域に画像が表示されます。そちらに画像ファイルをエクスプローラーからドラックアンドドロップします。

画像が表示されますので、フィルター有効無効のチェックボッスをONにしスライダーを動かします。

青色が濃くなったり。

薄くなったりします。

空の青色の濃淡は大きく変化していますが、雲の白色の変化は少ないところから、画素の数値を一律で加減算しているわけではなさそうです。

感想

ガンマ補正のフィルター処理に時間がかかると思われるので、フィルター処理をasync,awaitで別スレッドで実行してみました。さらにフィルター処理実行中はプログレスバーを動かすことでユーザーに処理中であることを知らせるように作ったつもりなのですが、ガンマ補正は、それほど処理時間がかからない処理だったらしく、あまり意味がなかったようです。別の重めのフィルタで動作確認をしていますので、作成者の意図どおりの振る舞いにはなっています。

コメント