Powershellで輪郭を抜き出す

コンピュータ

最近GIMPという無料のフォトレタッチソフトの使い方を学習中なのですが、この手のソフトはレイヤーという透明色を含む画像を多層に重ね合わせて1枚の像をつくるような構造になっています。透明な部分は下層の画像が見えるので画像同士を合成したり、下絵の上から本線をトレースしたりと現実の紙とペンで出来ることから、コンピュータならでは加工処理が出来たりします。

今のところGMIPで出来ることと、自分がしたい加工のすり合わせをしているのですが、自由自在に使えるようになるとは到底思えないレベルです。
それでも自分の意思で何かを作って結果が見えるとい事は中々楽しいです。

学習が進まない理由は、画像加工の用語が理解できない場面が多々あり、それが専門用語なのか普通の用語で自分の知識不足で理解出来ないだけなのか、はたまた用語の翻訳があやしいだけなのか、色々有ってGoogle先生に質問しかたが分らないというかなりめんどくさいことになっています。
誰かリアルな先生について勉強するのが良さそうな気もしますが、Google先生にうまく伝えられない質問を人間の先生にぶつける勇気がありません。

画像内の人などの物体を線だけの画像に変換したかったのですが、何といって問い合わせれば良いか…。
思いつく用語は「エッジ 境界線 輪郭 アウトライン」辺りと「変換」や「抽出」を組み合わせてみます。

それでGoogle先生に「輪郭 抽出」で問い合わせたところ、C言語を用いた画像加工のサンプルソースが公開されていました。
GIMPの機能では無いですが、ちょっと興味が惹かれました。

画像加工の理屈は全く理解できませんでしたが、サンプルのC言語のソースはシンプルで解りやすいので、Powershellに移植してみました。

<#
.SYNOPSIS
 輪郭を抽出

#>


using namespace System.Windows.Forms
using namespace System.Drawing

Set-StrictMode -Version Latest
$ErrorActionPreference = "STOP"

# アセンブリのロード
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing




function Main
{
    function Extract-Contour
    {
        param(
            [PictureBox]$src_pb,
            [PictureBox]$dst_pb
        )


        # 幅と高さ
        $width = $src_pb.Image.Width
        $height = $src_pb.Image.Height

        # 元画像
        $src_bmp = [Bitmap]($src_pb.Image)

        # 先画像
        $dst_bmp = [Bitmap]::new($width, $height)


        $cx = @(0, 0, 0, 0, 1, 0, 0, 0, -1)
        $cy = @(0, 0, 0, 0, 0, 1, 0, -1, 0)

        $amp = 3.0

        $d = @(0, 0, 0, 0, 0, 0, 0, 0, 0)




        for($y=1; $y -lt ($height-1); $y++) {
            for($x=1; $x -lt ($width-1); $x++) {
                #$c = $src_bmp.GetPixel($x, $y)
                
                $d[0] = $src_bmp.GetPixel($x-1, $y-1).R

                #Write-Host "0"

                $d[1] = $src_bmp.GetPixel($x-1, $y).R
                #Write-Host "1"
                $d[2] = $src_bmp.GetPixel($x-1, $y+1).R
                #Write-Host "2"
                $d[3] = $src_bmp.GetPixel($x, $y-1).R
                #Write-Host "3"
                $d[4] = $src_bmp.GetPixel($x, $y).R
                #Write-Host "4"
                $d[5] = $src_bmp.GetPixel($x, $y+1).R
                #Write-Host ("5 x:{0} y:{1}" -f $x, ($y+1))
                $d[6] = $src_bmp.GetPixel($x+1, $y-1).R
                #Write-Host ("6 x:{0} y:{1}" -f ($x+1), $y)
                $d[7] = $src_bmp.GetPixel($x+1, $y).R
                #Write-Host "7"
                $d[8] = $src_bmp.GetPixel($x+1, $y+1).R
                #Write-Host "8"
                
                $xx = [float]($cx[0]*$d[0] + $cx[1]*$d[1] + $cx[2]*$d[2] `
                            + $cx[3]*$d[3] + $cx[4]*$d[4] + $cx[5]*$d[5] `
                            + $cx[6]*$d[6] + $cx[7]*$d[7] + $cx[8]*$d[8])

                $yy = [float]($cy[0]*$d[0] + $cy[1]*$d[1] + $cy[2]*$d[2] `
                            + $cy[3]*$d[3] + $cy[4]*$d[4] + $cy[5]*$d[5] `
                            + $cy[6]*$d[6] + $cy[7]*$d[7] + $cy[8]*$d[8])

                $zz = [float]($amp * [Math]::Sqrt($xx*$xx+$yy*$yy))

                $dat = [int]$zz
                if ($dat -gt 255) { $dat = 255 }

                # $dst_bmp.SetPixel($x, $y, [color]::FromArgb(255, ($dat), ($dat), ($dat)))
                if ($dat -eq 255)
                {
                    $dst_bmp.SetPixel($x, $y, [color]::FromArgb(255, 0, 0, 0))
                } else {
                    $dst_bmp.SetPixel($x, $y, [color]::FromArgb(0, 255, 255, 255))
                }

            }
        }


        # 結果画像をピクチャボックスにセット
        if ($dst_pb.Image) { $dst_pb.Image.Dispose() } 

        Write-Host $dst_pb.SizeMode
        Write-Host $src_pb.Width
        $dst_pb.Image = [Image]$dst_bmp

    }


    $form = [Form]::new() | % {
        $_.Name = "MAIN_FORM"
        $_.Size = "800,600"
        $_
    }


    $controls = @(


        [Button]::new() | % {
            $_.Name = "BTN_GO"
            $_.Location = "720,40"
            $_.Text = "実行"
            $_.TextAlign = "MiddleCenter"
            $_.Size = "40,20"
            $_.Add_Click({
                $src_pb = $this.parent.Controls["PIC_BEFORE"]
                $dst_pb = $this.parent.Controls["PIC_AFTER"]
                Extract-Contour $src_pb $dst_pb
            })
            $_
        }

        [PictureBox]::new() | % {
            $_.Name = "PIC_BEFORE"

            $_.Image = [Bitmap]::FromFile("H:\ps1\ocero\logo.png")

            $_.Location = "10,80"
            $_.Size = "360,460"
            $_.SizeMode = "Zoom"
            $_.BorderStyle = "FixedSingle"
            $_.AllowDrop = $true
            $_.Add_DragEnter({$_.Effect = "ALL"})


            $_.Add_DragDrop({
                $file = @($_.Data.GetData("FileDrop"))[0]

                if ($fs = [FileStream]::new($file, [FileMode]::Open,[FileAccess]::Read)) {
                    try {
                        
                        if ($this.Image) { $this.Image.Dispose() }

                        $this.Image = [System.Drawing.Image]::FromStream($fs)

                        $num_w = $this.parent.Controls["NUM_W"]
                        $num_h = $this.parent.Controls["NUM_H"]

                        $num_w.Text = $this.Image.Width

                        $num_h.Text = $this.Image.Height

                        trim

                    } finally {
                        $fs.Close()
                    }
                }

            })

            $_
        }

        [PictureBox]::new() | % {
            $_.Name = "PIC_AFTER"
            $_.Location = "410,80"
            $_.Size = "360,460"
            $_.SizeMode = "Zoom"
            $_.BorderStyle = "FixedSingle"
            $_
        }
    )

    $form.Controls.AddRange($controls)

    $form.ShowDialog()
}
Main

「右側の枠」に画像ファイルをドラック&ドロップし「実行」ボタンを押してしばらくまつと左側に輪郭が抜き出された画像が表示されます。

笑えるほど時間がかかります。Powershellの遅さと私のスクリプトの作り方も相まって非常にゆっくりと動作します。
同じ内容を他の言語で作ったらどれだけ速くなるか試したくなりります。

$ampの値を調整することで抽出の具合が変わります。何故変わるのか理解していませんが。

あと、ボタンを押してからの待ち時間を考えると非同期処理の出番なのかもしれません。

実はスレッドや非同期処理のプログラミングの経験がなく、学びたいとは思うのですが、何度トライしても身についていないのが現状です。
ボタンを押す、別スレッドで時間がかかる処理を任せるような流れに成ると思いますが、その次に具体的に何をしたらよいのか思いつかない。

「しばらくお待ちください」と表示させることぐらいしか思いつかず、それだとあまり意味が…。
でもアプリケーションをフリーズさせない、嗜みとして、覚えるておくのも良さそうです。

コメント