午後わてんのブログ

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

ガンマ補正してから画像の2値化ディザリングしてみた

2値化ディザリングするときにガンマ補正をしないと、元の画像より明るい画像になる
輝度値がそれぞれ
0(黒)
255(白)
の2値を使って、その中間になる128(灰色)の画像をディザリング処理すると

f:id:gogowaten:20200506125019p:plain
128(灰色)

f:id:gogowaten:20200506125109p:plain
2値化ディザリング

元の画像よりかなり明るく見える、本当ならディザリングで0と255を交互に並べているので、全体の輝度は0と255の中間127.5になっているから、元の画像128とほぼ同じ明るさに見えるはず…そうならないのは、ガンマ補正ってのが関係しているみたいで、普通のパソコン用モニタならこう見えるのが正しいみたい

f:id:gogowaten:20200506130009p:plain

輝度値0~255を0~1に変換して考えて、ガンマ補正を計算すると
f:id:gogowaten:20200506130456p:plain

f:id:gogowaten:20200506131953p:plain
ガンマ補正
赤のグラフがガンマ補正、横軸が入力値、縦軸がガンマ補正後の値

普通の環境でのガンマ値は2.2で
計算式は
入力値1/2.2
エクセル風に書くと、入力値^(1/2.2)

さっきの輝度128の元画像の場合だと
128/255=0.50196078、これにガンマ補正を掛けると
0.501960781/2.2=0.73103945、これを輝度値に戻すと
0.73103945*255=186.41506
ってことで128にガンマ補正すると186になる

2値化ディザリング後の0と255の中間127.5もガンマ補正すると
(127.5/255)1/2.2*255=186.08371で
186なので、結果的にそう見えるのが正しいみたい
よくわからんけど、2値化ディザリングしても元の画像と同じ明るさにしたい
ガンマ補正によって元の画像より明るくなるなら、逆側にガンマ補正してから2値化ディザリングすればいい
ってのが
Libcaca study - 5. Greyscale dithering
http://caca.zoy.org/study/part5.html
ここに書いてあるんだと思う、グーグル翻訳だとそんな感じ

逆側にガンマ補正の計算式は
入力値2.2
エクセル風に書くと、入力値^2.2

さっきの輝度128の元画像の場合だと
(128/255)2.2*255=55.977528、これをもとに2値化ディザリングすればいい
f:id:gogowaten:20200506132819p:plain
右下が逆側にガンマ補正してから2値化ディザリングした画像、ディザリング方式は誤差拡散のFloydSteinberg、これで元の画像と同じ明るさの2値化ができた

白と黒の割合
f:id:gogowaten:20200506133651p:plain
ガンマ補正なしでディザリング
これの黒の割合は
f:id:gogowaten:20200506133821p:plain
黒50%

f:id:gogowaten:20200506133924p:plain
ガンマ補正してから2値化ディザリング
これの黒の割合は
f:id:gogowaten:20200506133951p:plain
黒78.14%
白は100-78.14=21.86%で、全体の輝度は、0.2186*255=55.743これは、128を逆側にガンマ補正したときの値の55.977に近い、いいね



0~255のbyte型配列から逆側にガンマ補正して、double型配列を返す

        /// <summary>
        /// もとの値を逆側にガンマ補正して返す、double型
        /// </summary>
        /// <param name="pixels">もとの値の配列</param>
        /// <param name="gamma">ガンマ値、2.2が標準</param>
        /// <returns></returns>
        private double[] InvertGammaDouble(byte[] pixels, double gamma)
        {
            double[] vs = new double[pixels.Length];
            for (int i = 0; i < pixels.Length; i++)
            {
                double d = pixels[i] / 255.0;
                vs[i] = Math.Pow(d, gamma) * 255.0;
            }
            return vs;
        }

0~255の値しか扱わないからテーブルにしたほうがいいかも

ピクセルフォーマットがGray8のBitmapSourceを2値化ディザリングして返す
2値化の前に逆側にガンマ補正する
ディザリング方式は誤差拡散のFloydSteinberg

  private BitmapSource D4_Color2GammaDouble(BitmapSource source)
        {
            int width = source.PixelWidth;
            int height = source.PixelHeight;
            int stride = width;
            byte[] pixels = new byte[height * stride];
            source.CopyPixels(pixels, stride, 0);

            //ここをdouble型にしただけ、それ以外はD2と同じ
            //逆ガンマ補正したPixelsを取得して誤差計算用にする
            double[] gosaPixels = InvertGammaDouble(pixels, 2.2);

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

            //  * 7
            //3 5 1
            // ̄16 ̄

            //double型で計算
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    //注目ピクセルのインデックス
                    p = y * stride + x;
                    //しきい値127.5未満なら0にする、それ以外は255にする
                    pixels[p] = (gosaPixels[p] < 127.5) ? (byte)0 : (byte)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:20200506140450p:plain
ダウンロード:20200506_ガンマ補正してから2値化誤差拡散.zip

  • 画像ファイルドロップで画像表示
  • Copy:表示している画像をクリップボードにコピー
  • Paste:クリップボードから画像貼り付け
  • D1:普通に2値化誤差拡散
  • D2:逆側にガンマ補正して2値化、byte型で計算
  • D3:逆側にガンマ補正して2値化、byte型で計算、蛇行走査で誤差拡散
  • D4:逆側にガンマ補正して2値化、double型で計算
  • D5:逆側にガンマ補正して2値化、double型で計算、蛇行走査で誤差拡散
  • 表示画像:クリックで2値化前の画像表示

github.com

f:id:gogowaten:20200506142314p:plain

D5が一番キレイになると思うけど誤差かなあ

128の画像
f:id:gogowaten:20200506143032p:plain
これは蛇行走査しないほうがきれいに感じる

f:id:gogowaten:20200506144238p:plain
左下がガンマ補正版、普通の画像でもガンマ補正したほうが自然な感じ

f:id:gogowaten:20200506145240p:plain
ガンマ補正すると暗い部分はより暗くなるから細部は再現されにくいねえ、黒つぶれみたいになる

ガンマ補正ってよくわからん、0と255を交互に並べて中間の127.5を表現するのと、直接127.5に近い128を表示するのとではぜんぜん違う色(輝度)に見えるってのがねえ、同じモニタで出力しているのに違う



関連記事

前回のWPF記事は2週間前
gogowaten.hatenablog.com

1ヶ月前
gogowaten.hatenablog.com

1年前
gogowaten.hatenablog.com これもガンマ補正が関係してる?

6年前
gogowaten.hatenablog.com ガンマ補正はこのときが最初、いまだにわからんw

画像の色数をカウントするアプリ
gogowaten.hatenablog.com