画像の明度調整を行う場合、画像のピクセル単位に R, G, B 値を操作しますが、単純に定数を加減すると全体に白っぽくなったり黒っぽくなったりしてしまうため、補正用の関数に基づいた値の変換をかけることがよく行われます。今回はガンマ値を利用して明度を調整する WPF なプログラムを書いてみます。
調整するガンマ値の範囲は、0.37 から 2.7 とします。ガンマ値は 1 で f(x) = x となり、関数グラフは次のようになります(上に膨らんでいるのが 2.7、下に凹んでいるのが 0.37 のグラフ)。
補正量の指示はスライダー コントロールで行うこととしますが、暗くする側の値が 1 から 0.37、明るくする側が 1 から 2.7 と非対称で、かつ入力値の変化量に対する出力値の変化量も非線型になることから、補正量の指示値からガンマ値への変換方法を考える必要があります。
今回は f(x) = -x + 255 な関数グラフ上で、f(x) = x な関数グラフとの交点からの距離(左上へ向かう方向を正とする)を補正量の指示値とし、f(x) = -x + 255 な関数グラフ上での指示値の座標 X、Y からガンマ値を求める方法をとりました(下図参照: 左下が (0, 0)、左上が(0, 255))。
X 軸の中心から f(x) = -x + 255 な関数グラフ上を移動した場合の X 軸、Y 軸上の移動量は直角二等辺三角形の の関係から求めることができます。次に X 軸、Y 軸上の移動量から座標を求め、この座標を通るガンマ値を求めることで、補正量の指示値からガンマ値への変換を行うことができます。
ガンマ補正の計算式は f(x) = 255 × (x ÷ 255)1/γ なので、ガンマを求める式は γ = 1 / log(127.5 – 移動量) / 255 ((127.5 + 移動量) / 255)となります。
プログラム実行時の画面は次のとおりです。
それではプログラムです。
「WPF アプリケーション」なプロジェクトを作成します(プロジェクト名は GammaCorrection としています)。プロジェクトの構成は、簡単にビューとビューモデルだけとしています。
MVVM パターンなので、お決まりのコマンド処理とプロパティチェンジイベントの発火を提供する ViewModelBase クラスは別記事の ViewModelBase で書いたものを使っています。別プロジェクトにして作成した DLL を参照設定で追加するなり、プロジェクト内にソースを取り入れるなりしてください(次のコードは DLL を参照する前提で書いています)。
プロジェクトに「ViewModels」フォルダを追加し、ViewModels フォルダに「MainViewModel」クラスを作成します。
using System;
using System.IO;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using MakCraft.ViewModels;
namespace GammaCorrection.ViewModels
{
class MainViewModel : ViewModelBase
{
private const double MEDIAN_VALUE = 127.5;
private const byte NUMBER_OF_ELEMENTS = 4; // RGBA で4つ
private const double DPI = 96;
private readonly double _rightTriangle = Math.Sqrt(2);
private byte[] _original;
private PixelFormat _pixelFormat;
public MainViewModel()
{
var path = @"画像ファイルのフルパス";
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.CacheOption = BitmapCacheOption.OnLoad;
bitmap.StreamSource = fs;
bitmap.EndInit();
Bitmap = bitmap;
}
if (_bitmap.Format != PixelFormats.Bgr24 && _bitmap.Format != PixelFormats.Bgr32 && _bitmap.Format != PixelFormats.Bgra32)
{
throw new FileFormatException("トゥルーカラー画像ファイルではありません。");
}
_pixelFormat = _bitmap.Format;
var width = (int)_bitmap.PixelWidth;
var height = (int)_bitmap.PixelHeight;
_original = new byte[width * height * NUMBER_OF_ELEMENTS];
_bitmap.CopyPixels(_original, width * NUMBER_OF_ELEMENTS, 0);
}
private double _gamma = 1;
public double Gamma
{
get { return _gamma; }
private set
{
_gamma = value;
base.RaisePropertyChanged(() => Gamma);
}
}
private double _changeAmount = 0;
public double ChangeAmount
{
get { return _changeAmount; }
set
{
_changeAmount = value;
base.RaisePropertyChanged(() => ChangeAmount);
var amount = _changeAmount / _rightTriangle;
Gamma = 1 / Math.Log((MEDIAN_VALUE + amount) / byte.MaxValue, (MEDIAN_VALUE - amount) / byte.MaxValue);
changeExecute();
}
}
private BitmapSource _bitmap;
public BitmapSource Bitmap
{
get { return _bitmap; }
set
{
_bitmap = value;
base.RaisePropertyChanged(() => Bitmap);
}
}
private void changeExecute()
{
changeBright(computeGammaLut(_gamma), _pixelFormat);
}
private ICommand _changeCommand;
public ICommand ChangeCommand
{
get
{
if (_changeCommand == null)
{
_changeCommand = new RelayCommand(changeExecute);
}
return _changeCommand;
}
}
private void changeBright(byte[] luTable, PixelFormat pixelFormat)
{
var width = (int)_bitmap.PixelWidth;
var height = (int)_bitmap.PixelHeight;
var temp = new byte[_original.Length];
Array.Copy(_original, temp, _original.Length);
for (var i = 0; i < temp.Length; ++i)
{
if (i % NUMBER_OF_ELEMENTS != NUMBER_OF_ELEMENTS - 1)
{ // ダミー又はアルファチャンネルでなければ変換テーブル値で補正する
temp[i] = luTable[temp[i]];
}
}
Bitmap = BitmapSource.Create(width, height, DPI, DPI, pixelFormat, null, temp, width * NUMBER_OF_ELEMENTS);
}
private static byte[] computeGammaLut(double gamma)
{
var result = new byte[byte.MaxValue + 1];
var temp1 = 0d;
var temp2 = 0d;
for (var i = 0; i <= byte.MaxValue; ++i)
{
temp1 = i / (double)byte.MaxValue;
temp2 = 1 / gamma;
result[i] = (byte)(Math.Pow(temp1, temp2) * byte.MaxValue);
}
return result;
}
}
}
「画像ファイルのフルパス」には、利用する画像ファイルのフルパスを記入してください(今回、ファイルの選択ダイアログの表示機能は組み込んでいません)。なお、画像ファイルは 32 ビットカラーを想定しています。
コンストラクタで、画像ファイル読み込み後にバイト配列 _original へピクセルデータをセットしています。
ガンマ値は、ChangeAmount プロパティの set アクセサー中で計算を行っています。また、void changeExecute() メソッドを呼び出すことで、プロパティ変更を行うと画像の明度の変更も行うようにしています。
changeExecute() メソッドでは、byte[] computeGammaLut(double gamma) メソッドを呼び出すことで作成したガンマ値による変換テーブルを引数として、void changeBright(byte[] luTable) メソッドを呼び出します。
changeBright(byte[] luTable) メソッドでは、ピクセルデータの R、G、B 値(配列内の順番は B、G、R、A)を変換テーブルにしたがって変更し、当該操作終了後にピクセルデータから System.Windows.Media.Imaging.BitmapSource を作成します。
次にビューです。
<Window x:Class="GammaCorrection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:GammaCorrection.ViewModels"
Title="MainWindow" SizeToContent="Width" Height="600">
<Window.DataContext>
<vm:MainViewModel />
</Window.DataContext>
<StackPanel>
<TextBlock
Text="ガンマ補正を用いた明度変更のテスト" FontSize="18" Margin="8" HorizontalAlignment="Center" />
<DockPanel>
<Border DockPanel.Dock="Right" BorderBrush="Brown" BorderThickness="1">
<TextBlock
Text="{Binding Gamma, StringFormat=#0.00}" VerticalAlignment="Center" Margin="6 0" />
</Border>
<Slider
Minimum="-60" Maximum="60" SmallChange="6.0" LargeChange="6.0"
TickPlacement="BottomRight" TickFrequency="6" ToolTip="{Binding ChangeAmount}"
Margin="10" Value="{Binding ChangeAmount}" />
</DockPanel>
<Image Source="{Binding Bitmap}" MaxHeight="480" />
</StackPanel>
</Window>
これで、ビルドすればプログラムが動きます 😉