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