1. ガンマカーブについて考える
ガンマカーブというのがある.こういうカーブだ.
この絵は gluplot で F(x,g) = x**g
と入力してプロットさせてみたものだ.
で,「せっかくだから俺はこの関数を{横軸を入力,縦軸を出力}として画像の明るさを補正するのに使うぜ!」とかいう話をやってみて楽しもうと思った.(…っていう「遊び」の話です)
例えば,画像が全体的に暗い時には 1.0より小さい g
の値を用いてやれば,補正結果は元の画像よりも明るく,コントラストも高くなる.
逆に画像が全体的に明るいならば 1.0よりも大きい g
の値を用いてやればよい,と.
はい.ここで個人的に気に入らない箇所が2つあります.
1つ目はグラフ形状 だ.
上図の5つの曲線を眺めていると「なんだこいつら,集団でアーモンドみたいな形を作りやがってからに」とか思えてくる.
カーブの膨らみ方が何か気に入らない.
例えば暗い画像を補正するときに上図で最も上に膨らんでいるカーブ( F(x,0.25)
)を使うのだとしたら,
明るい画像を補正するときのカーブってのはこう↓あって欲しい.補正効果の対称性と言うか,そういう意味において.
まぁそう思うなら勝手にそうすれば良いだけの話なのでこれは大した問題ではない.
double g = 0.25;
if( x > 0.5 ) //明るい時は
{//まぁこうすればいいね
出力 = 1.0 - pow( 1.0-x, g );
}
(※世間一般でこういうことをするか否か? みたいなのは知らないし,ここではどうでもいい.単なる「遊び」なので.)
2つ目は, g
の値が 1.0 のとき,直線になるということ だ.
画像が暗いなら g < 1.0
とし,明るいなら g > 1.0
とするならば「中間的な明るさの画像に対して用いる g
の値は 1.0 付近だよね」という話になるであろうが,その場合,ほぼ何も起きない( g=1.0
ならば本当に何も起きない)ということになる.
そんなのは嫌だ.楽しくない.俺は中間的なやつのコントラストも改善したいのである.
2. S字カーブについて考える
…というわけでS字カーブの登場です.
ガンマカーブはクビにして,こんな感じ↓の形をしたカーブを採用してやれば,中間的明るさな画像のコントラストを上げられるハズ.
S字と言えばシグモイドが思い浮かぶが,シグモイドは「0と1に漸近するよ」というところが微妙なので,
俺のS字カーブ(x) = ( シグモイド(x-0.5) - シグモイド(-0.5) ) / ( シグモイド(0.5) - シグモイド(-0.5) )
として,「漸近~」じゃない部分を適当に切り出して→入出力が共に [0,1] な範囲になるように調整したのが上図の関数 SC(x,a)
だ.
(ここで a
はシグモイドのパラメタ.「ゲイン」とか呼ぶらしい.)
//シグモイド関数.a は関数のパラメタ(ゲイン)
inline double Sigmoid( double x, double a ){ return 1.0 / ( 1.0 + exp( -a*x ) ); }
//S-shaped Curve
class SSC
{
public:
SSC( double a )
: m_a( a )
, m_Term_in_Numerator( Sigmoid( -0.5, a ) )
, m_Denominator( 1.0 - 2*m_Term_in_Numerator )
{}
double operator()( double x ) const { return ( Sigmoid(x-0.5,m_a) - m_Term_in_Numerator ) / m_Denominator; }
private:
const double m_a;
const double m_Term_in_Numerator;
const double m_Denominator;
};
(カーブの名前がうっかり "SC" → "SSC" と変わっているけど気にしない)
このカーブを用いて画像の明るさを補正するコードをさくっと書く(画像データの型として OpenCV の cv::Mat
を使用):
void Test1( const cv::Mat &SrcImg8UC1, cv::Mat &DstResult, double a )
{
unsigned char Out[256]; //出力画素値[入力画素値]
{
SSC TheCurve(a);
for( int In=0; In<=255; ++In )
{ Out[In] = cvRound( 255.0 * TheCurve( In / 255.0 ) ); }
}
DstResult.create( SrcImg8UC1.size(), CV_8U );
for( int y=0; y<SrcImg8UC1.rows; ++y )
{
const unsigned char *pS = SrcImg8UC1.ptr<unsigned char>(y);
unsigned char *pR = DstResult.ptr<unsigned char>(y);
for( int x=0; x<SrcImg8UC1.cols; ++x, ++pS, ++pR )
{ *pR = Out[ *pS ]; }
}
}
{暗い画像,中間な画像,明るい画像}への効果具合がわかるように,入力画像は,明るさの異なる3パターンの絵を作ってそれを横並びにくっつけることで用意した.
狙い通り,中間のコントラストが上がっている.
そのかわり(当たり前だが)暗い画像はより暗く,明るい画像はより明るくなる形で割を食っている.
3. 中間的明るさではない画像にも使いたいんですけど?
S字カーブは用意できた.
だが,暗い画像や明るい画像をターゲットとする場合には,果たしてこのカーブをどう用いれば良いのか?
…
単純に「横軸を入力」としていたところを,「横軸は f(入力)
の値」とすれば良いのではあるまいか.
この関数 f
は,値域が [0,1]
な入力を [0,1]
な出力に変換するものであるが,狙いの明るさが 0.5 付近にくるようにいい感じに調整してくれる物だ.
例えば暗い画像を補正したいときは,f(0.2)
が 0.5 らへんに来るような感じになっていれば, 0.2 付近の暗さの箇所のコントラストが上がるというわけだ.
すばらしい.
あとはそのような関数 f
を用意するだけだ.
どこかにそんな関数は無い物だろうか?
…
そういうの,さっき見たような気がする.
確か「ガンマ」とかいう名前だったわ.
つまり,こうかよ!
//TgtIntencity が狙いの明るさ.引数の値域としては 8bit画像想定で [0,255].
void Test2( const cv::Mat &SrcImg8UC1, cv::Mat &DstResult, double a, unsigned char TgtIntencity )
{
//引数を [0.0, 0.1] な値域の世界に変換.
//ただし,次の処理の都合上, 0.0 とか 1.0 だと困るのでそのあたりは適当に除外している.
const double tgt = std::max( 5, std::min(250, (int)TgtIntencity) ) / 255.0;
//狙いの明るさ tgt が 0.5 になるようなガンマカーブのパラメタ g の値を求める.
const double g = log(0.5) / log( tgt<=0.5 ? tgt : 1.0-tgt ); //※ここの分岐は「カーブの形が気に入らない」とかいう話に対応
unsigned char Out[256]; //出力画素値[入力画素値]
{
SSC TheCurve(a);
for( int In=0; In<=255; ++In )
{
double s = In / 255.0;
//まずはガンマカーブに食わせて…
double g_s = ( tgt<=0.5 ? pow(s,g) : 1.0-pow(1.0-s,g) ); //※ここの分岐は「カーブの形が気に入らない」とかいう話に対応
//その結果をS字カーブに食わせる
Out[In] = cvRound( 255.0 * TheCurve( g_s ) );
}
}
DstResult.create( SrcImg8UC1.size(), CV_8U );
for( int y=0; y<SrcImg8UC1.rows; ++y )
{
const unsigned char *pS = SrcImg8UC1.ptr<unsigned char>(y);
unsigned char *pR = DstResult.ptr<unsigned char>(y);
for( int x=0; x<SrcImg8UC1.cols; ++x, ++pS, ++pR )
{ *pR = Out[ *pS ]; }
}
}
できた.
4. 適応型がどうの
そしたらもう「狙いの明るさ」を画像の各所の明るさに合わせて変動させればいいよね,とか思うわけなので,やってみる.
「各所の明るさ」とは何か? → とりあえず入力画像を適当にぼかした結果を用いることとする.
void Test3( const cv::Mat &SrcImg8UC1, cv::Mat &DstResult, double a )
{
//テーブルが2次元になったよ
unsigned char Out[256][256]; //出力画素値[狙い輝度値][入力画素値]
{
SSC TheCurve(a);
for( int TgtIntencity=0; TgtIntencity<=255; ++TgtIntencity )
{
const double tgt = std::max( 5, std::min(250, (int)TgtIntencity) ) / 255.0;
const double g = log(0.5) / log( tgt<=0.5 ? tgt : 1.0-tgt );
for( int In=0; In<=255; ++In )
{
double s = In / 255.0;
double g_s = ( tgt<=0.5 ? pow(s,g) : 1.0-pow(1.0-s,g) );
Out[TgtIntencity][In] = cvRound( 255.0 * TheCurve( g_s ) );
}
}
}
DstResult.create( SrcImg8UC1.size(), CV_8U );
{
cv::Mat LocalIntencityMap;
{//てきとーにぼかした画像を作る
int KernelSize = ( std::min(SrcImg8UC1.rows,SrcImg8UC1.cols) / 3) | 1;
cv::GaussianBlur( SrcImg8UC1, LocalIntencityMap, cv::Size(KernelSize,KernelSize), 0 );
}
for( int y=0; y<SrcImg8UC1.rows; ++y )
{
const unsigned char *pS = SrcImg8UC1.ptr<unsigned char>(y);
const unsigned char *pLI = LocalIntencityMap.ptr<unsigned char>(y);
unsigned char *pR = DstResult.ptr<unsigned char>(y);
for( int x=0; x<SrcImg8UC1.cols; ++x, ++pS, ++pLI, ++pR )
{ *pR = Out[ *pLI ][ *pS ]; }
}
}
}
というわけで,今回の遊びの最終結果がコレだ!
- 3領域とも,{中間調付近の明るさになり,コントラストは上がっている}と思う.
- てきとーなぼかし画像を用いた結果として激しくハローが生じている(領域間のつなぎ目の箇所とか尻尾の周りとかがわかりやすい).
正直「どうなん…?」って感じだけど,「"CLAHE" で良くね?」とか言わないことが大事.きっと.