午後わてんのブログ

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

指定色で減色+誤差拡散、減色結果を他のアプリと比較してみた

指定した色に減色する時に誤差拡散を使う

 
この前はメディアンカット法を使って選んだ色をパレットの色にして
その色を使って普通に減色していた
今回は誤差拡散も使って減色

 
いつもの画像を8色に減色
 
イメージ 1
できた!…と思う
これだけだと正解なのかわかんないので
減色処理に誤差拡散を使えるアプリと比べてみる
 
イメージ 2
画像ビューアのIrfanView
1色単位での減色もできるは今回知った
かなりきれいに減色できる、色の選び方が上手だと思う
 
 
COLGA
イメージ 3
減色の設定がいろいろできるCOLGA
ディザリングの強さも指定できる
Maxの10だとノイズっぽくなる
こういう調整はどう処理しているのかなあ
 
イメージ 4
ディザリングレベル5
だいぶ印象が変わる
 
 
Yukari
イメージ 5
減色アプリのYukari
こちらは今回初めて使ってみた
COLGA同様ディザリングの強さを指定できるのでMaxの100
色の選び方が他のアプリとは違うみたいねえ
電信柱の黒がない
 
 
並べてみる
イメージ 13
irfanViewとCOLGAは誤差拡散は自然な感じ
僕が作ったのはあんまり拡散していない感じなんだよねえ、境界線が見える
それにしてもアプリに依って色の選び方、誤差拡散も違うから
どれが正解ってのもないのかなあと思い始める
 
16色
イメージ 14
色の選び方で違いが出るねえ
 
イメージ 6
ピクセル数優先で色を選ぶと青空はきれいになるけど
トマトの花の色がイマイチになる
 
PixelFormatを変更して減色
イメージ 7
WPFでは画像のPixelFormatを変更すると自動で減色と誤差拡散処理される
Indexed1だと2色
Indexed2は4色
Indexed4は16色
この時に選ばれる色(パレット)が特徴があって
元画像にはなさそうな色も選ばれる
今回のアプリでは表示画像の色を取得して表示するのも作ったので
それを使ってみると
この画像だと赤系の色はなさそうなのに2色も入っている
少なくともk平均法やメディアンカット法だけじゃないねえ
面白いのでこれを使って自分の処理で減色と誤差拡散してみる
 
イメージ 8
誤差拡散なし
 
イメージ 9
誤差拡散あり
PixelFormat変換の減色にかなり近いものになった
 
 
Indexed1で2色
イメージ 10
PixelFormatをIndexed1にして2色にしたところ
このパレットで誤差拡散してみると
 
イメージ 11
じっくり見比べなければわからない程度には同じになっている
これをもって今回のアプリの誤差拡散は
ほぼ正解ってことになりました!
 
2色減色を比較
イメージ 12
これは極端にアプリの差が出た
僕のは葉っぱや枝が太く見えるのが気になる
そこで別の誤差拡散
 
イメージ 15
これはかなりいい結果だと思う(自画自賛) 
 
 
今回の誤差拡散はかなり時間がかかった
最初は今までどおりのつもりで誤差拡散を書いたら
こうなってしまった
イメージ 16
色が右下へ流れたような感じ
 
ピクセルに誤差拡散する処理は
本当は
イメージ 17
こうなるのがいい
 
右下へ流れたような感じになったのは
イメージ 18
右にずれる
 
右へずれてしまう処理
イメージ 19
パレットの色1か色2どちらに変換するのかは色の距離が近いほうを選ぶ
距離は変換前の色とパレットの全色の距離を比較、これがC欄で
赤文字が近い
この変換前の色っていうのが画像の元の色+誤差、これがA欄
 
色の距離は単純にRGBそれぞれの差の2乗を足したもの、これの平方根
 
処理の流れ
最初のピクセル(左端)のRGB(200,255,200)、これとパレットの色1、色2の距離はそれぞれ、218102色2のほうが距離が近いので最初のピクセルは色2へ変換
(200,255,200)から(170,200,120)へ変換したので誤差は(30,55,80)、B欄
この誤差を右ピクセルに足(拡散)して(230,310,280)、A欄
これで1ピクセルの減色と誤差拡散が完了
2ピクセル目の処理
誤差拡散された色(230,310,280)、これとパレットの色の距離を計算
それぞれ317,203なので色2のほうが近いので色2へ変換
(230,310,280)から(170,200,120)へ変換したので誤差は(60,110,160)、B欄
この誤差を右ピクセルに足(拡散)して(260,365,360)、A欄
これで2ピクセルの減色と誤差拡散が完了
 
こんな感じで進めていくと同じような色が並んでいると誤差がどんどん溜まっていって、いざ別の色が来た時にも溜まった誤差のせいで同じ色に変換されるのが続いてしまう、この状態が右へ色が流れたような感じになるみたい
 
そこで誤差がたまりすぎるのが良くないと思って、誤差拡散は1ピクセルの処理ごとにリセットしたのが
イメージ 20
これでいいんじゃないかってくらい良くなったけど
なんか違う
今回の誤差拡散法は右と下方向x3の合計4方向に
拡散するFloydSteinberg式を使っているんだけど
この結果は右ピクセルだけにしか拡散していない感じ
 
イメージ 21
右方向のこれだけ見るとあっているんだけどねえ
実際には下方向の拡散もあるから少し違う
次に思いついたのが誤差の蓄積に上限を付ける方法
実際にはマイナスにもなるので下限も付けて0以下は0、255以上は255にするようにした結果が、さっきから使っているこれ
イメージ 22
 
イメージ 23
これでできた!って思ったんだけど
他にもいくつか試して
その中で良さそうだったのが
誤差の蓄積の下限上限をRGBのそれぞれに設ける、
値はパレットの色から作成
これがさっき自画自賛したもの
イメージ 24
イメージ 25
上が0-255制限
下がパレットの各色各RGBからの制限
あんまり変わんないかな
 
 
16色に減色
イメージ 26
注目はやっぱり赤いいちごがどうなるのか
画像全体では少ない赤のピクセルの扱いはどうなるのってところ
一番元画像に近いのはCOLGA、素晴らしい再現度
僕のアプリもなかなかだと思う(自画自賛2回め)
Vieasはノイズっぽいけど床の木目の再現度が高い
Yukariの誤差拡散は暗い色が連続していても明るい色がポツポツ出る
JTrimだけ色合いが違う、これは色はパレット色の選び方でピクセル数が多いCubeを優先しているのかなあ
イメージ 27
メディアンカットでピクセル数優先分割すると
似たような色合いになる
と思ったけど見比べたら結構違うな…
アプリによるパレットの違い
イメージ 28
元画像に使われている色は34335色
これを元にしたり、しなかったりで16色パレットを作るわけだけど
アプリに依ってぜんぜん違うねえ
気になるのはVieas、派手な(彩度の高い)赤とピンク、黄緑を選んでいる
誤差拡散を使えば中間の曖昧な色は他の色と組み合わせれば再現できるから
こういう極端な色を選ぶのはありなんだよねえ
 
 
 
処理速度

f:id:gogowaten:20191212111849j:plain

いつもの1024x768ピクセルの画像を今回のアプリで16色へ変換
 

f:id:gogowaten:20191212111916p:plain

16色パレットを作るのに4秒
誤差拡散で減色するのに5秒で合計9秒もかかる!
 
他のアプリは
一瞬 Vieas
一瞬 JTrim
一瞬 PixelFormat.Indexed4
1秒 COLGA
5秒 Yukari
9秒 今回のアプリ
一瞬で終わるのはどうなっているのかなあ、すごい
 

f:id:gogowaten:20191212111936p:plain

Vieas
赤いノイズはクリックしてもとの大きさにすると
そんなに目立たないけどやっぱり気になる
って書いたんだけど投稿した記事を見たら全然目立たない
記事作成中と投稿した記事では見え方が違うんだなあ
 

f:id:gogowaten:20191212111947p:plain

 

JTrim
 

f:id:gogowaten:20191212112004p:plain

COLGA
 
 

f:id:gogowaten:20191212112039p:plain

Yukari
 
 
 
色相90のHSVグラデーション画像を16色へ
イメージ 38
Yukariは暗いところで明るい色が出てくるのが不自然だなあと思ったけど
これは設定で誤差拡散の強さをMaxの100にしているせいだった
初期値の80にしたら
イメージ 39
不自然さがなくなってなめらからになった
これはきれいだなあ
COLGAとYukariの2つは減色の設定ができるので
今回の比較よりも良い結果になる設定もあると思う
 
イメージ 40
これも0~255制限に変えたら少し良くなった
 
 
 
コードの一部、変換するとこだけ
指定したパレットの色に減色+誤差拡散、誤差蓄積は0-255制限
いままではPixelFormatPgbr32を使ってきたけわかりやすいRgb24にした
/// <summary>
/// 誤差拡散で減色、誤差蓄積は0~255に制限
/// </summary>
/// <param name="source">PixelFormatはRgb24限定</param>
/// <param name="palette">List<Color></param>
/// <param name="errorStack">Trueで誤差蓄積なのでたくさん拡散する、falseでなしは拡散少なめ。Trueのほうがいいかも</param>
/// <returns>PixelFormatはRgb</returns>
private BitmapSource ReduceColor指定色誤差拡散で減色Limit0_255(BitmapSource source, List<Color> palette, bool errorStack)
{
    if (palette.Count == 0) { return source; }
    //WriteableBitmapクラスのCopyPixelsを使って画像の色情報を配列に複製
    var wb = new WriteableBitmap(source);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    byte[] pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);
    //誤差計算のために小数点を使うのでfloat型の配列に複製したのを使う
    float[] iPixels = new float[pixels.Length];
    for (int i = 0; i < iPixels.Length; ++i)
    {
        iPixels[i] = pixels[i];
    }

    long p = 0, pp = 0;//注目するピクセルの配列のアドレス
    float gosa = 0, pGosa = 0;//誤差、+誤差
    Color myColor;//パレットから選んだ色
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + (x * 3);//注目するピクセルのアドレス
            //誤差拡散した色に一番近いパレットの色を取得する
            //誤差拡散後の色(小数点RGB)
            double[] iRGB = new double[] { iPixels[p + 0], iPixels[p + 1], iPixels[p + 2] };
            //一番近い色取得
            myColor = GetColorNearPlette(iRGB, palette);
            //パレットのRGB
            byte[] pRGB = new byte[] { myColor.R, myColor.G, myColor.B };

            //RGBごとに誤差拡散、0以下の場合は0、255以上の場合は255に丸める(制限する)
            for (int i = 0; i < 3; ++i)//RGBの3ループ
            {
                //gosa = (float)(pixels[p + i] - pRGB[i]) / 16f;//誤差(元の色-パレットの色)
                //gosa = (float)(iPixels[p + i] - pRGB[i]) / 16f;//誤差(元の色-パレットの色)
                gosa = (errorStack) ? (float)(iPixels[p + i] - pRGB[i]) / 16f : (float)(pixels[p + i] - pRGB[i]) / 16f;
                //右下ピクセルへ誤差拡散
                pp = p + i + 3;//右下ピクセルアドレス
                if (pp < pixels.Length && x != w - 1)//右ピクセル
                {
                    //誤差拡散後の値が0以下なら0、255以上なら255に丸める
                    pGosa = iPixels[pp] + (gosa * 7f);//誤差拡散先の値に誤差を足す                           
                    iPixels[pp] = (pGosa < 0) ? 0 : (pGosa > 255) ? 255 : pGosa;
                    //↑の1行は↓の3行と同じ処理
                    //if (pGosa < 0) { iPixels[pp] = 0; }
                    //else if (pGosa > 255) { iPixels[pp] = 255; }
                    //else { iPixels[pp] = pGosa; }
                }

                if (y < h - 1)//注目するピクセルが最下段じゃないなら
                {
                    //真下ピクセルへ誤差拡散
                    pp = p + stride + i;//真下ピクセルアドレス
                    pGosa = iPixels[pp] + (gosa * 5f);
                    iPixels[pp] = (pGosa < 0) ? 0 : (pGosa > 255) ? 255 : pGosa;
                    //左下ピクセルへ誤差拡散
                    if (x != 0)
                    {
                        pp = p + stride + i - 3;//左下ピクセルアドレス
                        pGosa = iPixels[pp] + (gosa * 3f);
                        iPixels[pp] = (pGosa < 0) ? 0 : (pGosa > 255) ? 255 : pGosa;
                    }
                    //右下ピクセルへ誤差拡散
                    if (x < w - 1)
                    {
                        pp = p + stride + i + 3;//右下ピクセルアドレス
                        pGosa = iPixels[pp] + (gosa * 1f);
                        iPixels[pp] = (pGosa < 0) ? 0 : (pGosa > 255) ? 255 : pGosa;
                    }
                }
            }
            //色変更
            pixels[p + 0] = myColor.R;
            pixels[p + 1] = myColor.G;
            pixels[p + 2] = myColor.B;
        }
    }
    wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
    return wb;
}


//一番近いパレット色取得、小数点RGB用、RGB距離
private Color GetColorNearPlette(double[] rgb, List<Color> palette)
{
    double min = 0;
    double distance = 0;
    int pIndex = 0;
    min = GetColorDistanceDouble(rgb, palette[0]);
    for (int i = 1; i < palette.Count; ++i)
    {
        distance = GetColorDistanceDouble(rgb, palette[i]);
        if (min > distance)
        {
            min = distance;
            pIndex = i;
        }
    }
    return palette[pIndex];
}
 
画像のほうが見やすい

f:id:gogowaten:20191212112205p:plain

 

f:id:gogowaten:20191212112223p:plain

誤差蓄積している色情報の配列変数名がiPixelsで
元の画像の色情報の配列変数名がpixels
誤差蓄積は0以下や255以上は切り捨てて0~255までに制限は494行目
条件演算子?:は初めて使ってみた
 
使い方(書き方)はエクセルのIF関数そっくりで
(条件)?Trueのときの処理:falseのときの処理
今まで使い所がわからなかったけど
 
iPixels[pp] = (pGosa < 0) ? 0 : (pGosa > 255) ? 255 : pGosa;
↑の1行は↓の3行と同じ処理
if (pGosa < 0) { iPixels[pp] = 0; }
else if (pGosa > 255) { iPixels[pp] = 255; }
else { iPixels[pp] = pGosa; }
 
3行以上かかっていたのが1行で済むので楽ちん
見た目が分かりづらく感じるけど、これは慣れかな
 
 
参照したところ、素晴らしいアプリ
IrfanView - Official Homepage - One of the Most Popular Viewers Worldwide
http://www.irfanview.com/
IrfanView」定番の画像ビューワー - 窓の杜ライブラリ
https://forest.watch.impress.co.jp/library/software/irfanview/

COLGAのページ
http://www14.plala.or.jp/lptrans/colga/colgatop.html

Yukari
結社「障泥烏賊ライブラリ」用地
http://aoriika.exout.net/

JTrim
WoodyBells
http://www.woodybells.com/

フリーソフト&壁紙ギャラリー - Vieas Web
http://www.vieas.com/
 
 
 
今回の記事で画像の減色処理の大まかなところは全部試したかなあ
あとは
色の距離の測り方
色の選び方の調整
処理時間の短縮
どれも難しそう
 
コード全部
 
アプリダウンロード先(ヤフーボックス)
 
 
関連記事
 
誤差拡散の続き、2018/03/27は18日後
パレットを使った減色で誤差拡散 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15432449.html

2018/03/06パレット作成は3日前
メディアンカット法で色の選択、減色してみた、難しい ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
 
 
2018/03/04パレット作成
k平均法で減色してみた、設定と結果と処理時間 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
 
2018/03/02
単純減色と誤差拡散とディザリング ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
27色+誤差拡散
 
 
2018/02/23
FloydSteinberg他いくつかの誤差拡散を試してみた、白黒2値をディザリング ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
 
2018/02/22
誤差拡散法を使ってディザリング、右隣だけへの誤差拡散、グレースケール画像だけ ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ