午後わてんのブログ

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

分割するCubeの選択、メディアンカットで減色パレット

おとといからの続き

 
ダウンロード先
イメージ 1
今回は分割するCubeの選択法を変えると
パレットはどうなるのか試してみた
 
選択方法は5つ
最大長辺 最も長い辺(軸)を持つCubeを分割対象にする
最大ピクセルピクセルが最も多いCube
最大体積 Cubeの体積が最大
最大分散Cube 色の分散値が最大、分散値はピクセルの数も加味したもの
最大分散辺 RGBごとの辺の分散値が最大、これもピクセル数を加味
 
 
今回はCubeの選択方法だけ変えて、分割場所と色の選択は固定
分割場所は辺の中央
色の選択はCubeの平均色なので使うパレットはPan1
 
この画像を減色
4色
イメージ 2
4色の時点だとピクセル数で選択するのはいまいちな結果になった
それ以外は全く同じ色になって花の赤が選択された、いいね
 

f:id:gogowaten:20191212124924p:plain

他のパレットも色の順番が違うだけで
ピクセル数以外はおなじに見える
 
16色
イメージ 4
16色でもピクセル数だけ他と違って青空重視
なんだけどこの色数では足りなくてガタガタ
それ以外は青、赤、黄色、黒と違う色重視な感じでバランスが取れている
辺長と体積の2つはおなじに見えるけど微妙に違う
 
64色
イメージ 5
64色まで増やすとピクセル数分割の青空もきれいになってきた
グラデーションや背景重視ならこれを選ぶ
 
256色
イメージ 6
迷うことなく右上のピクセル数分割を選ぶ
 
 
 
 
イメージ 8
4色
イメージ 7
辺長がいいねえ
分散の2つも赤が選択されると予想していたけど外れた
 
4色で最大分散辺での分割での色の選び方の違い
イメージ 9
色の選び方を変えたら赤系も入っていた
でも逆に緑がないw
 
 
 
16色
イメージ 10
16色まで増やしたらピクセル数でも赤が入った
背景と主題がはっきりしない込み入った画像でも
ピクセル数とそれ以外で分かれる
 
 
256色
イメージ 11
ピクセル数以外はかわらないねえ
 
 
グラデーション画像
イメージ 12
256x256、使用色数65536色
4色
イメージ 13
全部全く同じになったのは
結果を見せられればなんとなくわかる
 
256色
イメージ 14
256色でも全く同じ!
縦横一定割合で変化している色が並んでいるから
選択されるCubeが同じになるのかなあ
 
 
HSVの色相90のSV
イメージ 15
256x256、65536色
 
4色
イメージ 16
ピクセル数以外は全部同じ
 
256色
イメージ 17
よくわらかん
 
 
イメージ 18
128x128、16384色
白と黒のグラデーションの中に赤(255,0,0)
これから3色選ぶとすれば
白、黒、赤になればいい
イメージ 19
期待どおり
 
 
イメージ 20
256x192,49152色
 
4色
イメージ 21
辺長と体積が同じ、これはわかる
ピクセルと分散の2つが同じになった、これは意外
 
16色
イメージ 22
ピクセル数分割がいいねえ
 
 
イメージ 23
背景が複雑だけどぼやけている画像
 
16色
イメージ 24
256色
イメージ 25
ぼやけた背景だとピクセル数が有利かと思ったけど
どれも大差ないねえ
 
 
分割Cubeの選択法の感想
ピクセル数分割以外はどれも大差ない
 
背景重視や256色とか色数が多ければ
最大ピクセル数 >>>>> 辺長 ≒ 体積 > 分散Cube ≒ 分散辺
 
主題(主体、全景)重視や色数が4色とか少ないときはその反対
って感じかな
 
残念だったのが分散を使ったもの
もう少し他と差が出るかなあと思っていたし
分散の事自体よくわかっていなくて書くのに時間かかったから
使い方が間違っているかもしれない
もしかしたらピクセル数を加味しない分散のほうが良かったのかも?
 
 
 
Cubeクラスは少し変更
RGBそれぞれの平均値と分散値を持つフィールド追加
分散値は分割する時に計算して使う。
BitmapSourceを使うコンストラクタは削除してColorのListからだけにした
ほんとはBitmapSourceからColorのListに変換して、それをColorのListを使うコンストラクタに渡して作成したかったけど書き方がわからなくてこうなった
/// <summary>
/// RGBそれぞれの最小値と最大値を持つクラス
/// その他にRGBそれぞれの辺の長さ、平均値、画像の全ピクセルの色、使用されている色数を持つ
/// RGBそれぞれの分散値はNaNを入れておいて必要な時に入れる
/// コンストラクタはColorのListから作成するものだけ、それだけのクラス
/// </summary>
public class Cube
{
    public byte MinRed;//最小R
    public byte MinGreen;
    public byte MinBlue;
    public byte MaxRed;//最大赤
    public byte MaxGreen;
    public byte MaxBlue;
    public float RedPixAverage;//ピクセル数を加味した平均赤
    public float GreenPixAverage;
    public float BluePixAverage;
    public double RedVariance;//赤の分散
    public double GreenVariance;
    public double BlueVariance;
    public double VarianceMax;
    public List<Color> AllPixelsColor;//すべてのピクセルの色リスト     
    public List<Color> AllColor;//使用されているすべての色リスト
    public int LengthMax;//Cubeの最大辺長
    public int LengthRed;//赤の辺長
    public int LengthGreen;
    public int LengthBlue;

    //ColorのリストからCube作成
    public Cube(List<Color> color)
    {
        byte lR = 255, lG = 255, lB = 255, hR = 0, hG = 0, hB = 0;
        byte cR, cG, cB;
        long rAdd = 0, gAdd = 0, bAdd = 0;
        AllPixelsColor = new List<Color>();
        foreach (Color item in color)
        {
            cR = item.R; cG = item.G; cB = item.B;
            rAdd += cR; gAdd += cG; bAdd += cB;
            AllPixelsColor.Add(Color.FromRgb(cR, cG, cB));
            if (lR > cR) { lR = cR; }
            if (lG > cG) { lG = cG; }
            if (lB > cB) { lB = cB; }
            if (hR < cR) { hR = cR; }
            if (hG < cG) { hG = cG; }
            if (hB < cB) { hB = cB; }
        }
        //重複を除いて使用されている色リスト作成、Distinct
        AllColor = AllPixelsColor.Distinct().ToList();//これの処理コストが結構大きい?

        MinRed = lR; MinGreen = lG; MinBlue = lB;
        MaxRed = hR; MaxGreen = hG; MaxBlue = hB;
        LengthRed = 1 + MaxRed - MinRed;
        LengthGreen = 1 + MaxGreen - MinGreen;
        LengthBlue = 1 + MaxBlue - MinBlue;
        LengthMax = Math.Max(LengthRed, Math.Max(LengthGreen, LengthBlue));
        //平均値
        float count = color.Count;
        RedPixAverage = rAdd / count;
        GreenPixAverage = gAdd / count;
        BluePixAverage = bAdd / count;
        //分散はとりあえず非数を入れておく
        RedVariance = double.NaN;
        GreenVariance = double.NaN;
        BlueVariance = double.NaN;
        VarianceMax = double.NaN;
    }

//BitmapSourceをColorのListに変換する
private List<Color> GetColorList(BitmapSource source)
{
    var bitmap = new FormatConvertedBitmap(source, PixelFormats.Pbgra32, null, 0);
    var wb = new WriteableBitmap(bitmap);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    byte[] pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);
    long p = 0;

    var ColorList = new List<Color>();
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + (x * 4);
            ColorList.Add(Color.FromRgb(pixels[p + 2], pixels[p + 1], pixels[p]));
        }
    }
    return ColorList;
}
ほんとはこれをCubeクラスに書いておいて
 
//BitmapSourceからCubeを作成
public Cube(BitmapSource source)
{
    var bitmap = new FormatConvertedBitmap(source, PixelFormats.Pbgra32, null, 0);
    var wb = new WriteableBitmap(bitmap);
    int h = wb.PixelHeight;
    int w = wb.PixelWidth;
    int stride = wb.BackBufferStride;
    byte[] pixels = new byte[h * stride];
    wb.CopyPixels(pixels, stride, 0);
    long p = 0;

    var ColorList = new List<Color>();
    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            p = y * stride + (x * 4);
            ColorList.Add(Color.FromRgb(pixels[p + 2], pixels[p + 1], pixels[p]));
        }
    }
    new Cube(ColorList);//これだと何も入らない空っぽ

}
こうしたいんだけど違うみたい、書き方がわからん
 
 
ここから分割対象の選択法5つ

最大長辺を持つCubeを分割対象に選択
CubeのListを渡して対象になるCubeのインデックスを返す
//リストから最大長辺を持つCubeのIndexを取得
private int GetSelectIndexLongSideCube(List<Cube> cubeList)
{
    int max = 0, index = 0;
    for (int i = 0; i < cubeList.Count; ++i)
    {
        if (max < cubeList[i].LengthMax)
        {
            max = cubeList[i].LengthMax;
            index = i;
        }
    }
    return index;
}
いつも最大とか最小を得るときにはforとifを使っているんだけどLINQとかを使えばラクなのかなあ、LINQも使えるようになりたい
 
ピクセル数が最大のCubeを分割対象に選択
//Cubeのリストからピクセル数最大のCubeのIndexを取得
private int GetIndexSelectManyPidxelsCube(List<Cube> cubeList)
{
    int max = 0, index = 0;
    for (int i = 0; i < cubeList.Count; ++i)
    {
        if (max < cubeList[i].AllPixelsColor.Count)
        {
            max = cubeList[i].AllPixelsColor.Count;
            index = i;
        }
    }
    return index;
}

最大体積のCubeを分割対象に選択
//リストから最大体積を持つCubeのIndexを取得
private int GetSelectIndexCapacityMaxCube(List<Cube> cubeList)
{
    int max = 0, index = 0, capa = 0;
    Cube c;

    for (int i = 0; i < cubeList.Count; ++i)
    {
        c = cubeList[i];
        capa = (c.MaxRed - c.MinRed) * (c.MaxGreen - c.MinGreen) * (c.MaxBlue - c.MinBlue);
        if (max < capa)
        {
            max = capa;
            index = i;
        }
    }
    return index;
}

ここまでは普通、辺の長さや、カウント、直方体の体積は小学生でもわかる
次は分散
これは文系の高校では難しい、というか習わないから、えっ知らないってなる、なった、でも今度からは文系高校でも教えてくれるらしい、いいね!
 
分散で得られる値は対象の散らばり具合を表して、大きければそれだけ散らかっている、なので分散値が大きいものを分割していけば、散らばりの少ないCubeになる、つまり同じ色がまとまったCubeになるので、減色に都合のいいパレットができる、かも
 
分散は要素と平均値の差を2乗したのを合計して要素数で割る
イメージ 26
計算手順自体は簡単だけどめんどくさすぎる
これを手動で計算するのはムリだけどパソコン使えばラク、相性がいい
さらにエクセルには分散の関数があってVARPっての
これ使えば簡単に求めることができる
でもc#にはないから自分で書いて
//Cubeを渡してRGBごとの分散を求めてフィールド(プロパティ)に入れる
//RGBごとの分散を配列にして返す
private double[] GetRGBごとの分散(Cube iCube)
{
    //Cubeの分散の変数に数値が入っていなければ計算して入れて返す
    if (double.IsNaN(iCube.VarianceMax) == true)
    {
        double rVar = 0, gVar = 0, bVar = 0;
        Color pxColor;
        long count = 0;
        count = iCube.AllPixelsColor.Count;

        rVar = 0; gVar = 0; bVar = 0;
        for (int j = 0; j < count; ++j)
        {
            pxColor = iCube.AllPixelsColor[j];
            rVar += Math.Pow(pxColor.R - iCube.RedPixAverage, 2f);//偏差の2乗の合計
            gVar += Math.Pow(pxColor.G - iCube.GreenPixAverage, 2f);
            bVar += Math.Pow(pxColor.B - iCube.BluePixAverage, 2f);
        }
        //Cubeの分散の変数に分散を入れる
        iCube.RedVariance = rVar / count;
        iCube.GreenVariance = gVar / count;
        iCube.BlueVariance = bVar / count;
        iCube.VarianceMax = Math.Max(iCube.RedVariance, Math.Max(iCube.GreenVariance, iCube.BlueVariance));
    }
    return new double[] { iCube.RedVariance, iCube.GreenVariance, iCube.BlueVariance };
}

AllPixelsColorがCubeにあるすべてのピクセルのColor
今回はこれ全部を計算しているけど、もしかしたらピクセルの数は無視して純粋に色の数だけで分散を求めたほうが良かったのかも
 
上のを使って
分散が最大のCubeを分割対象に選択
//Cubeを渡してRGBごとの分散を求めてフィールド(プロパティ)に入れる
//RGBごとの分散を配列にして返す
private double[] GetRGBごとの分散(Cube iCube)
{
    //Cubeの分散の変数に数値が入っていなければ計算して入れて返す
    if (double.IsNaN(iCube.VarianceMax) == true)
    {
        double rVar = 0, gVar = 0, bVar = 0;
        Color pxColor;
        long count = 0;
        count = iCube.AllPixelsColor.Count;

        rVar = 0; gVar = 0; bVar = 0;
        for (int j = 0; j < count; ++j)
        {
            pxColor = iCube.AllPixelsColor[j];
            rVar += Math.Pow(pxColor.R - iCube.RedPixAverage, 2f);//偏差の2乗の合計
            gVar += Math.Pow(pxColor.G - iCube.GreenPixAverage, 2f);
            bVar += Math.Pow(pxColor.B - iCube.BluePixAverage, 2f);
        }
        //Cubeの分散の変数に分散を入れる
        iCube.RedVariance = rVar / count;
        iCube.GreenVariance = gVar / count;
        iCube.BlueVariance = bVar / count;
        iCube.VarianceMax = Math.Max(iCube.RedVariance, Math.Max(iCube.GreenVariance, iCube.BlueVariance));
    }
    return new double[] { iCube.RedVariance, iCube.GreenVariance, iCube.BlueVariance };
}
 
分散が最大の辺を持つCubeを分割対象に選択
//最大分散辺を持つCubeのindex取得
private int GetSelectIndexOfMaxVarianceSide(List<Cube> cubeList)
{
    int index = 0;
    double max = 0;
    double variance = 0;
    for (int i = 0; i < cubeList.Count; ++i)
    {
        double[] rgbVariance = GetRGBごとの分散(cubeList[i]);
        variance = Math.Max(rgbVariance[0], Math.Max(rgbVariance[1], rgbVariance[2]));
        if (max < variance)
        {
            max = variance;
            index = i;
        }
    }
    return index;
}




/// <summary>
/// bitmapからCubeを作って指定数まで分割
/// </summary>
/// <param name="source">PixelFormat.Pbgr32限定</param>
/// <param name="splitCount">いくつまで分割するのか</param>
/// <param name="GetIndexOfSplitCube">分割するCubeの選択方法の関数</param>
/// <param name="SplitBy">分割する場所の指定</param>
/// <returns></returns>
private List<Cube> SplitCube(
    Cube cube,
    int splitCount,
    Func<List<Cube>, int> GetIndexOfSplitCube,
    Func<Cube, List<Cube>> SplitBy)
{
    int loopCount = 1;
    var cubeList = new List<Cube>() { cube };//元のCubeのリスト
    var tempCubeList = new List<Cube>();//2分割されたCubeを一時的に入れるリスト
    var completionList = new List<Cube>();//これ以上分割できないCubeのリスト
    int index;
    //指定数まで分割されるか、これ以上分割できなくなるまでループ
    while (splitCount > loopCount && cubeList.Count > 0)
    {
        // どのCubeを分割するのか選定(最大長辺or最大ピクセル数or分散など)
        index = GetIndexOfSplitCube(cubeList);

        //分割してリストに追加
        tempCubeList.Clear();
        tempCubeList.AddRange(SplitBy(cubeList[index]));//2分割

        //2分割した結果どちらかのCubeのピクセル数が0なら、それ以上分割できないってことなので
        //別のリストに追加する
        if (tempCubeList[0].AllPixelsColor.Count == 0 || tempCubeList[1].AllPixelsColor.Count == 0)
        {
            if (tempCubeList[0].AllPixelsColor.Count == 0)
            {
                completionList.Add(tempCubeList[1]);
            }
            else { completionList.Add(tempCubeList[0]); }
        }
        //普通に2分割できたら元のリストに追加
        else
        {
            cubeList.AddRange(tempCubeList);
        }

        //分割のもとになったCubeをリストから削除
        cubeList.RemoveAt(index);

        loopCount++;
    }
    //
    cubeList.AddRange(completionList);
    return cubeList;
}
private List<Cube> SplitCube(
    List<Color> listColors,
    int splitCount,
    Func<List<Cube>, int> GetIndexOfSplitCube,
    Func<Cube, List<Cube>> SplitBy)
{
    return SplitCube(new Cube(listColors), splitCount, GetIndexOfSplitCube, SplitBy);
}
分割ループのところを直した
それ以上分割できないCubeを分割した場合、一方に全てのピクセルが入って、もう一方はピクセルが0個になって返ってくる。0個の方は破棄していたんだけど全部入った方はそのままにしていて、次のループでまた分割処理に入るっていうムダなことになっていたので、どちらかかが0個になって返ってきたらそれ以上分割できないと判断して、別のListにストックするようにした
 
 
 
参照したところ
 
 
分散の意味と求め方、分散公式の使い方
https://sci-pursuit.com/math/statistics/variance.html
 
 
 
コード全部(GitHub)
 
アプリダウンロード(ヤフーボックス)
20180315_メディアンカット法での色の選び方1.1.zip
関連記事
2018/03/20はおととい
Cubeから色の選び方、メディアンカットで減色パレット ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15421887.html