午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

ガンマ補正してから黒灰白の3色に減色+ディザリングしてみた、グレースケール画像

前回の続きで、今回は3色に減色、3色は0, 127, 255
ディザリングはいつもの誤差拡散FloydSteinberg方式

github.com

今回のアプリ
ダウンロード:20200507_ガンマ補正してから3色誤差拡散.zip

f:id:gogowaten:20200508201107p:plain
今回のアプリ

  • 画像ファイルドロップで画像表示、カラー画像もPixelFormats.Gray8に変換して表示する
  • Copy:表示している画像をクリップボードにコピー
  • Paste:クリップボードから画像貼り付け
  • button1:普通に3色に減色+誤差拡散
  • button2:逆側に2.2でガンマ補正
  • button3:逆側にガンマ補正、0~0.5、0.5~1.0別々に2.2で補正
  • button4:逆側にガンマ補正、0~0.5を2.2で補正、0.5~1.0は計算
  • button5:D4を蛇行走査で誤差拡散
  • 表示画像:クリックで減色前のグレースケール画像表示

f:id:gogowaten:20200508114245p:plain
D0がもとのグレースケール画像、D1が普通に減色+ディザリング
これでも十分綺麗に変換できているけど、全体が少し明るくなっている、特に下の黒に近い部分
ここからガンマ補正を色々試してできたのが
f:id:gogowaten:20200508115020p:plain
D5が今回の結果
画面から離れて見るか、目を細めて見ると、D1よりD5のほうがD0に近いのがわかる


全体一律に2.2のガンマ補正

f:id:gogowaten:20200508120706p:plain
D2
前回の2値化(0と255の2色に減色)のときと同じように、0.0~1.0全て一律にガンマ値2.2で補正してから3色にしたのがD2、これは元の画像よりかなり暗くなってしまった
f:id:gogowaten:20200508121932p:plain
ガンマ補正
2値化のときはこれで良かったんだけど、3色だとこうじゃないみたいで、それは
caca.zoy.org
ここにあるグラフを見てもわかる
どうやら中間の色を使うなら、ガンマ補正も中間で一旦区切るみたいな感じになっていてグラフだと
f:id:gogowaten:20200508123247p:plain
0.0~0.5までのガンマ補正
この緑の線のようになっていて、これは普通の赤のグラフを半分に縮小した感じ
たぶんガンマ値は2.2のままでいいとして、入力値0.5のときに補正後の値が0.5になるような曲線はどうしたらいいものかと
f:id:gogowaten:20200508124029p:plain
((入力値 * 2) ^ γ) / 2
入力値を2倍にしてからガンマ補正して、最後に半分にしただけ
これでそれっぽくなった
次の0.5~1.0までも同じようにすればいいのかと思ってコピペしたら
f:id:gogowaten:20200508125135p:plain
違う
突き抜けていったので、これは違うと
0.5で一旦リセットされる感じなので、入力値から0.5を引いて置いて、あとはさっきの緑グラフと同じ、つまり2倍してから補正して半分にする
f:id:gogowaten:20200508130256p:plain
まだ違う
惜しい、下にずれているだけだから、あとは0.5を足せば良さそう
f:id:gogowaten:20200508130503p:plain
できた
それっぽくなった
この緑と紫のグラフを使ってガンマ補正してから減色したのが
f:id:gogowaten:20200508142409p:plain
D3
D2よりだいぶ良くなった、特に半分から下(127~0)の部分はもうこれで良さそう
あとは上半分が暗い、暗いってことはガンマ補正が効きすぎているってことだから、ガンマ値を2.2より下げれば良さそう
さっきのリンク先のグラフをよく見ると、上半分に当たる曲線部分は緩やかになっている、つまり2.2より下
じゃあどれだけ下げればいいのかってことなんだけど、ガンマ値は初期の2.2をMaxってことにして、ここから下げていって最後Minは1.0、1.0ってのはどんな値でも1.0乗なら変化なし(補正無しと同じ)だから
ってことで、ガンマ値が取る値はMax~Minで、2.2~1.0だから、全体の長さは2.2-1.0で1.2、これの割合で計算すればいいのかなと
f:id:gogowaten:20200508154949p:plain
割合
割合は、上半分の入力値は0.5~1.0で、開始は0.5ってことで、0.5を正側にガンマ補正すると 0.51/2.2=0.72974005で、これを割合の距離として見るとガンマ値は1.3より少し大きい値になりそう

05~1.0区間のガンマ値の計算は

f:id:gogowaten:20200508155910p:plain
2.2からの距離 = (初期ガンマ値 - ガンマ値) / 全体距離
2.2からの距離は0.51/2.2=0.72974005、初期ガンマ値は2.2、全体距離は1.2
0.72974005=(2.2-ガンマ値) / 1.2
ガンマ値 = -(0.72974005*1.2) + 2.2
ガンマ値 = 1.3243119
これをグラフにしてみると
f:id:gogowaten:20200508162842p:plain
0.5~1.0
水色のグラフがそれ
0~0.5まではD3と同じ緑色のグラフ
これを使って減色してみると
f:id:gogowaten:20200508163348p:plain
D4
D4がそれ、D3では上半分が暗かったのが明るくなって元の画像に近くなった、今回はこれで完成
ガンマ値を求める計算がこれであっているのかは、わかんないけど完成
あとはD4まで誤差拡散の走査が片方向走査だったので、これを蛇行走査にしたのがD5、これが今回の完成形

リンク先の画像と比べてみる
f:id:gogowaten:20200508165057p:plain
比較
並べてみると随分違うけど、雰囲気はあっているからこれでいいじゃんって気もするし、これいじょうわからん

画像を個別に並べてみる
f:id:gogowaten:20200508165504p:plain f:id:gogowaten:20200508165516p:plain f:id:gogowaten:20200508165527p:plain f:id:gogowaten:20200508165540p:plain f:id:gogowaten:20200508165555p:plain f:id:gogowaten:20200508165606p:plain f:id:gogowaten:20200508165504p:plain
両端が元画像のD0、間は左からD1~D5
D1は全体的に明るくなっている
D2は全体的にかなり暗くなっている
D3は上半分が暗い
D4、D5はほぼ同じ明るさに見る




ガンマ補正なしのD1

/// <summary>
/// ガンマ補正なしで0 127 255の3色に減色、FloydSteinbergで誤差拡散、PixelFormat.Gray8グレースケール画像専用
/// 0~84を0に変換、85~170を127、それ以外を255に変換
/// </summary>
/// <param name="source"></param>
/// <returns></returns>
private BitmapSource D1_Color3(BitmapSource source)
{
    //画素値の配列作成
    int width = source.PixelWidth;
    int height = source.PixelHeight;
    int stride = width;
    byte[] pixels = new byte[height * stride];//もとの値Pixels
    source.CopyPixels(pixels, stride, 0);

    //誤差拡散計算用
    double[] gosaPixels = new double[height * stride];
    Array.Copy(pixels, gosaPixels, height * stride);

    int p;//座標を配列のインデックスに変換した値用
    double gosa;//誤差(変換前 - 変換後)

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            p = y * stride + x;
            //3色に変換
            if (gosaPixels[p] < 85) pixels[p] = 0;
            else if (gosaPixels[p] < 171) pixels[p] = 127;
            else pixels[p] = 255;

            //誤差拡散
            gosa = (gosaPixels[p] - pixels[p]) / 16.0;
            if (x != width - 1)
                gosaPixels[p + 1] += gosa * 7;//右
            if (y < height - 1)
            {
                p += stride;
                gosaPixels[p] += gosa * 5;//下
                if (x != 0)
                    gosaPixels[p - 1] += gosa * 3;//左下
                if (x != width - 1)
                    gosaPixels[p + 1] += gosa * 1;//右下
            }
        }
    }
    return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
}

これをガンマ補正するように改変したD4が

/// <summary>
/// 3色用ガンマ補正、0~0.5までと0.5~1.0までを指定したガンマ値で補正する
/// </summary>
/// <param name="pixels"></param>
/// <param name="gamma1">0.0~0.5までのガンマ値</param>
/// <param name="gamma2">0.5~1.0までのガンマ値</param>
/// <returns></returns>
private double[] InvertEachGamma3Color(byte[] pixels, double gamma1, double gamma2)
{
    double[] vs = new double[pixels.Length];
    //0~255を0~1に変換して補正してから、また0~255に戻す
    for (int i = 0; i < pixels.Length; i++)
    {
        double d = pixels[i] / 255.0;
        if (d <= 0.5)
        {
            vs[i] = Math.Pow(d * 2, gamma1) / 2.0 * 255;
        }
        else
        {
            //0.5~1.0区間は
            // = 開始値 + ((入力値 - 開始値) * 分割数) ^ ガンマ値 / 分割数
            // = 0.5 + ((入力値 - 0.5) * 2) ^ ガンマ値 / 2
            vs[i] = (0.5 + (Math.Pow((d - 0.5) * 2, gamma2) / 2.0)) * 255;
        }
    }
    return vs;
}

//0.0~0.5と0.5~1.0を別々にガンマ補正、ガンマ値は
//0.0~0.5は2.2
//0.5~1.0は計算して1.3243119で補正
private BitmapSource D4_Color3EachGamma2(BitmapSource source)
{
    int width = source.PixelWidth;
    int height = source.PixelHeight;
    int stride = width;
    byte[] pixels = new byte[height * stride];//もとの値Pixels
    source.CopyPixels(pixels, stride, 0);
    //0~0.5区間で使うガンマ値は初期の2.2
    double gamma1 = 2.2;

    //0.5~1.0区間で使うガンマ値を計算
    //開始値は0.5これを初期γで補正する

    //補正後 = 0.72974005
    // = 開始値 ^ (1 / 初期γ)
    // = 0.5 ^ (1 / 2.2)
    // = 0.72974005
    //0.5のガンマ補正後の値 = 0.7297401
    double correct = Math.Pow(0.5, 1.0 / gamma1);

    //0.5~1.0区間で使うガンマ値を、0.5の補正後値を使って計算すると
    // = (-ガンマ値全体距離 * 補正後) + 初期γ
    // = -(2.2 - 1.0) * 0.72974005) + 2.2
    // = 1.3243119
    double gamma2 = -(gamma1 - 1.0) * correct + gamma1;

    //逆ガンマ補正した値の配列、誤差拡散計算用
    double[] gosaPixels = InvertEachGamma3Color(pixels, gamma1, gamma2);

    int p;//座標を配列のインデックスに変換した値用
    double gosa;//誤差(変換前 - 変換後)

    //  * 7
    //3 5 1
    // ̄16 ̄
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            //注目ピクセルのインデックス
            p = y * stride + x;
            //3色に変換
            if (gosaPixels[p] < 85) pixels[p] = 0;
            else if (gosaPixels[p] < 171) pixels[p] = 127;
            else pixels[p] = 255;

            //誤差拡散
            gosa = (gosaPixels[p] - pixels[p]) / 16.0;
            if (x != width - 1)
                gosaPixels[p + 1] += gosa * 7;
            if (y < height - 1)
            {
                p += stride;
                gosaPixels[p] += gosa * 5;
                if (x != 0)
                    gosaPixels[p - 1] += gosa * 3;
                if (x != width - 1)
                    gosaPixels[p + 1] += gosa * 1;
            }
        }
    }
    return BitmapSource.Create(width, height, 96, 96, PixelFormats.Gray8, null, pixels, stride);
}

普通の画像を減色

f:id:gogowaten:20200508202246p:plain
トマト
いいねえ、2値化のときは黒つぶれみたいになったところも灰色が入ったことで再現性が上がっている

f:id:gogowaten:20200508202625p:plain
空と電線
この画像はガンマ補正なしでもありでもあんまり変化ないかな

f:id:gogowaten:20200508202742p:plain
空とリナリア
花のコントラスト具合の様子でかなり差が出た、ガンマ補正ありのほうが再現性が高い、面白い

f:id:gogowaten:20200508204610p:plain
輝度値127
元の画像が127一色だから、そのまま127でいいはずなんだけど、ガンマ補正ありだと黒(0)が出てしまっている

今回のような計算だと色数が増えるほどガンマ値が減っていくはずだから、そうするとますます補正無しとの差がなくなることになる

f:id:gogowaten:20200508195437p:plain
4色のとき?
こんな感じ



関連記事
次回のWPF記事は3週間後

gogowaten.hatenablog.com

前回は2日前

gogowaten.hatenablog.com