3
2

Power Queryで画像処理

Last updated at Posted at 2024-03-23

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

概要

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

フィルタ実行前

元画像を表に読み込んだだけ
くっきりしたインコ(かわいい)
畳み込みなし.PNG

文字を表示して拡大した図
テーブル.PNG

フィルタ実行後

ラプラシアンフィルタ(エッジ強調)

0 1 0
1 -4 1
0 1 0

インコの輪郭(かわいい)
ラプラシアンフィルタ.PNG

ガウシアンフィルタ(平滑化)

1/16 1/8 1/16
1/8 1/4 1/8
1/16 1/8 1/16

ぼやけたインコ(かわいい)
ガウシアンフィルタ.PNG

解説

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

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

表示に関する補足

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

PowerQueryの展開先テーブル(ワークシート側)の設定
①テーブルが配置されたシートの列幅を2にする

②テーブルプロパティから、列の幅を調整するのチェックを外す

③テーブルが配置されたシートのセルの書式設定を;;;(値を非表示)にする

④テーブルが配置されたシートの条件付き書式を0黒~255白と設定する

実行したソースとか

bmpPathに画像のパスを指定する。画像はカラービットマップしか読めません
Filterにはリスト形式で、9要素の係数を指定する
下記サンプルはラプラシアンフィルタの例
※4/26カンマ抜け修正(試した人がいたらゴメン)

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バイト境界の計算これであってるかな…

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]を読み込んでた)
ので、リストを参照先にしたら動いた🤗。
もっといい方法があるような気はするが…

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

これでピクセル配列にしてる
バッファ化の意味があるかは知らぬ

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"等で検索してみてください。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2