出来らあっ!
え!! マクロを使わずExcelで画像処理を!?
概要
Power Queryで画像処理の畳み込みフィルタを動かしてみた
フィルタ実行前
フィルタ実行後
ラプラシアンフィルタ(エッジ強調)
0 | 1 | 0 |
1 | -4 | 1 |
0 | 1 | 0 |
ガウシアンフィルタ(平滑化)
1/16 | 1/8 | 1/16 |
1/8 | 1/4 | 1/8 |
1/16 | 1/8 | 1/16 |
解説
PowerQueryのBinaryFormatを使って、BMPのバイナリを読み込んでます
BITMAPFILEHEADER構造を定義して、そこにバイナリを放り込むことで、
幅、高さ等の情報を取り出せるようにしています
また、BMPではヘッダ部以降のデータ部にRGB3色の輝度値(0黒~255白)が格納されており、
RGB3色のBMP画像では中心のインデックス+3(RGB分)のデータが隣の画素の輝度値になります。
また、中心のインデックス+画像の幅×3のデータはちょうど真下の輝度値になります。
中心画素へ周囲画素の輝度値に前述のフィルタの係数を乗算した合計を代入することで、フィルタ処理ができます。
読み込んだ輝度データをリスト&バッファ化してみたら、
AddColumn内での参照がそこそこ早くなり、Convolution(畳み込みフィルタ)
がちゃんと動くようになりました(感動)
まだ重いので改善方法が知りたい
表示に関する補足
以下の手順で、条件付き書式を利用して画像を表示してます。
PowerQueryの展開先テーブル(ワークシート側)の設定
①テーブルが配置されたシートの列幅を2にする
②テーブルプロパティから、列の幅を調整するのチェックを外す
③テーブルが配置されたシートのセルの書式設定を;;;(値を非表示)にする
④テーブルが配置されたシートの条件付き書式を0黒~255白と設定する
実行したソースとか
bmpPathに画像のパスを指定する。画像はカラービットマップしか読めません
Filterにはリスト形式で、9要素の係数を指定する
下記サンプルはラプラシアンフィルタの例
※4/26カンマ抜け修正(試した人がいたらゴメン)
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バイト境界の計算これであってるかな…
(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]を読み込んでた)
ので、リストを参照先にしたら動いた🤗。
もっといい方法があるような気はするが…
(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
これでピクセル配列にしてる
バッファ化の意味があるかは知らぬ
(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"等で検索してみてください。