Power Queryで画像処理

出来らあっ！
え!!　マクロを使わずExcelで画像処理を!?

# 概要
Power Queryで画像処理の畳み込みフィルタを動かしてみた

### フィルタ実行前
元画像を表に読み込んだだけ
くっきりしたインコ（かわいい）
![畳み込みなし.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/5164b927-9e42-b666-4d00-2578a737c7b9.png)

文字を表示して拡大した図
![テーブル.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/e72e0c60-81d0-9cfc-3105-e33464f673ff.png)


### フィルタ実行後
ラプラシアンフィルタ(エッジ強調)
||||
| ---- | ---- |---- |
|0| 1|0|
|1|-4|1|
|0|1|0|

インコの輪郭（かわいい）
![ラプラシアンフィルタ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/5a42294f-a02d-3676-d4d7-985883c961dd.png)

ガウシアンフィルタ(平滑化) 
||||
| ---- | ---- |---- |
|1/16|1/8|1/16|
|1/8|1/4|1/8|
|1/16|1/8|1/16|

ぼやけたインコ（かわいい）
![ガウシアンフィルタ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/ec8c09ef-68fd-ff69-3b27-1a9ba86b61ae.png)



# 解説
PowerQueryのBinaryFormatを使って、BMPのバイナリを読み込んでます
BITMAPFILEHEADER構造を定義して、そこにバイナリを放り込むことで、
幅、高さ等の情報を取り出せるようにしています
また、BMPではヘッダ部以降のデータ部にRGB3色の輝度値(0黒～255白)が格納されており、
RGB3色のBMP画像では中心のインデックス＋3(RGB分)のデータが隣の画素の輝度値になります。
また、中心のインデックス＋画像の幅×3のデータはちょうど真下の輝度値になります。
中心画素へ周囲画素の輝度値に前述のフィルタの係数を乗算した合計を代入することで、フィルタ処理ができます。

読み込んだ輝度データをリスト＆バッファ化してみたら、
AddColumn内での参照がそこそこ早くなり、Convolution(畳み込みフィルタ)
がちゃんと動くようになりました（感動）
まだ重いので改善方法が知りたい





# 表示に関する補足
以下の手順で、条件付き書式を利用して画像を表示してます。

PowerQueryの展開先テーブル（ワークシート側）の設定
①テーブルが配置されたシートの列幅を2にする
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/884379b9-7bda-ada6-331c-1ff2cdf7bac2.png" width="120">
②テーブルプロパティから、列の幅を調整するのチェックを外す
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/73e0617f-c4c0-171d-4b98-e6537ccb137b.png" width="250">
③テーブルが配置されたシートのセルの書式設定を;;;(値を非表示)にする
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/61f7f484-f721-7020-3235-e1b8a6b7f84b.png" width="250">
④テーブルが配置されたシートの条件付き書式を0黒～255白と設定する
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1225394/6de2b774-0e88-e531-79cc-d435536f6663.png" width="250">






# 実行したソースとか


bmpPathに画像のパスを指定する。画像はカラービットマップしか読めません
Filterにはリスト形式で、9要素の係数を指定する
下記サンプルはラプラシアンフィルタの例
※4/26カンマ抜け修正（試した人がいたらゴメン）
```C#:Sample
let    
    bmpPath = "BMP画像のパス",
    Filter   = 
            {
                0, 1, 0,
                1,-4, 1,
                0, 1, 0
            },
    RawTable = Convolution(bmpReader(bmpPath,0),Filter),
    Result   = DispImage(RawTable)
in
    Result
```
24ビットのbmpを想定してます。他はたぶん動かん。
4バイト境界の計算これであってるかな…
```C#:bmpReader(BMP画像を読み込む関数)
(fpath,optional color)=>
let    
    BYTE2LONG = BinaryFormat.ByteOrder(BinaryFormat.UnsignedInteger32, ByteOrder.LittleEndian),
    BYTE2INT  = BinaryFormat.ByteOrder(BinaryFormat.UnsignedInteger16, ByteOrder.LittleEndian),

    BITMAPFILEHEADER = BinaryFormat.Record([
        etc1 = BinaryFormat.Binary(18),
        biWidth= BinaryFormat.Binary(4),
        biHeight = BinaryFormat.Binary(4),
        biPlanes = BinaryFormat.Binary(2),
        biBitCount = BinaryFormat.Binary(2),
        etc2 = BinaryFormat.Binary(24),
        biData = BinaryFormat.Binary()
    ]),

    imageData = BinaryFormat.ByteOrder(BinaryFormat.Byte, ByteOrder.LittleEndian),

    GetPixels = BinaryFormat.List(imageData),

    FileHeader = BITMAPFILEHEADER(File.Contents(fpath)),
    BitCount   = BYTE2INT(FileHeader[biBitCount]),
    ByteCount  = (BitCount / 8),
    
    width      = BYTE2LONG(FileHeader[biWidth]),
    Amari      = if Number.Mod(ByteCount * width,4) = 0 then 0 else 4 - Number.Mod(ByteCount * width,4),

    height     = BYTE2LONG(FileHeader[biHeight]),

    buffer     = List.Buffer(GetPixels(FileHeader[biData])),
    count      = List.Count(buffer),

    Entries    = List.Buffer(List.Generate(()=>
                        [
                            Index  = 0,
                            Row    = 0,
                            Column = 0,
                            Val    = GrayScale(buffer,Index,color)
                        ],
                 each 
                        [Index] < count,
                 each 
                        if [Column] + 1 = width then
                        [
                            Index  = [Index] + ByteCount + Amari,
                            Row    = [Row]   + 1,
                            Column = 0,
                            Val    = GrayScale(buffer,Index,color)
                        ]
                        else
                        [
                            Index  = [Index] + ByteCount,
                            Row    = [Row],
                            Column = [Column] + 1,
                            Val    = GrayScale(buffer,Index,color)                
                        ]

    )),

    GrayScale = (buf,index,optional color) => 
            if color = 0 then 
                (buf{index}+buf{index+1}+buf{index+2})*0.333
            else if color = 1 then
                buf{index}
            else if color = 2 then
                buf{index+1}
            else if color = 3 then
                buf{index+2}
            else
                buf{index+3},
    
    List2Table  = Table.FromList(Entries, Splitter.SplitByNothing(), null, null, ExtraValues.Error),

    Expanded    = Table.ExpandRecordColumn(List2Table, "Column1", {"Index", "Row", "Column", "Val"}, {"Index", "Row", "Column", "Val"}),
    Removed     = Table.RemoveColumns(Expanded,{"Index"}),

    Transformed = Table.TransformColumnTypes(Removed,{{"Val", Number.Type},{"Column", Int64.Type},{"Row", Int64.Type}}),
    Result      = Table.Buffer(Table.Distinct(Transformed, {"Row", "Column"}))
in
    Result
```


テーブルを参照したら、くっそ遅くて計算できなかった。
(AddColumn内で、bmpTable{Index}[Val]を読み込んでた）
ので、リストを参照先にしたら動いた🤗。
もっといい方法があるような気はするが…
```C#:Convolution(bmpReaderで読み込んだテーブルに畳み込みフィルタを適用する関数)
(bmpTable as table,filter as list)=>
let
    f        = List.Buffer(filter),
    vals     = List.Buffer(bmpTable[Val]),
    clmax    = Table.Max(bmpTable, "Column")[Column],
    rwmax    = Table.Max(bmpTable, "Row")[Row],
    AddIndex = Table.AddIndexColumn(bmpTable, "Index", 0, 1),
    buf      = Table.Buffer(Table.RemoveColumns(AddIndex,{"Val"})),
    cnvl     = Table.AddColumn(buf, "Val", each if [Column] = 0 or [Column] = clmax or [Row] = 0 or [Row] = rwmax then
                                         0
                                     else
                                       + f{0}*vals{[Index] - clmax - 1}
                                       + f{1}*vals{[Index] - clmax} 
                                       + f{2}*vals{[Index] - clmax + 1}
                                       + f{3}*vals{[Index] - 1}     
                                       + f{4}*vals{[Index]}   
                                       + f{5}*vals{[Index] + 1} 
                                       + f{6}*vals{[Index] + clmax - 1} 
                                       + f{7}*vals{[Index] + clmax} 
                                       + f{8}*vals{[Index] + clmax + 1}),


    Result  = Table.TransformColumnTypes(Table.RemoveColumns(cnvl,{"Index"}),{{"Val", type number}})
in
    Result
```
これでピクセル配列にしてる
バッファ化の意味があるかは知らぬ
```C#:DispImage(読み込んだテーブルをピボットしてピクセル配列にするだけ)
(bmpTable as table)=>
let
    Table   = Table.Buffer(Table.TransformColumnTypes(bmpTable,{{"Column", Text.Type}})),
    Columns = List.Buffer(List.Distinct(Table[Column])),
    Result  = Table.Sort(Table.Pivot(Table,Columns, "Column", "Val"),{{"Row", Order.Descending}})
in
    Result
```


# 余談
なお、Excelで画像処理やる場合、VBAからgdi等を呼び出してやっても可能です。
ファイルの読み書きはgdiplusのGdipLoadImageFromFile/GdipSaveImageToFile
画像⇔2次元配列のデータのやり取りはgdi32のGetDIBits/SetDIBitsでできる。
先駆者がインターネット界にいっぱいいるので、
やりたい人は"gdiplus VBA"等で検索してみてください。