PowerShellで空間フィルタリング(画像処理)を実装してみた

# はじめに
:::note info
FUJITSU Advent Calendar 2022 14日目の記事です。
:::

今年は画像処理の勉強に力を入れてました。なので、学んだことのアウトプットとして画像処理の記事を書きます。また、去年勉強したPowerShellでコーディングもします。

この記事では、PowerShellで「空間フィルタリング」という画像処理を実装します。

記事の最後には、コピペしてそのまま実行できるコードをのせてますので、適当な画像で空間フィルタリングを動かしてみると楽しいかもしれません。

# 空間フィルタリングとは
空間フィルタリングは、出力画像の画素値を求めるのに、注目画素の周囲の画素の値を使って計算します。
入力画像とフィルタを重ね合わせて、重なった画素のフィルタの値と入力画像の画素値の掛け算の合計を出力画像の画素値とします。これを入力画像の全ての画素に対して行います。

![filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/f3c1cbe6-9fe6-2874-5741-56c9ed044da3.png)

空間フィルタリングを適用することで、画像をぼかしたり、エッジを抽出したりといった画像処理を実現できます。

:::note info
この記事では、3x3の空間フィルタのみ説明していますが、一般にはもっと大きな空間フィルタも使用できます。
:::

# 実装方法
## 画像の「端」の考慮
画像の端でも特殊な考慮をしなくていいように、以下のように入力画像の周囲を0で囲ったビットマップ配列を作り、その配列に対してフィルタを適用します。
フィルタを適用する時の画素位置のインデックスは、0で囲った分のずれを考慮する必要があります。
![bitmap.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/4c2c9725-f1b2-4675-7bd6-e8b0ac12e1c6.png)

## ベースとするコード
以下のコードをベースにして、フィルタリングの関数(Xxx-Filter関数)のみを各種空間フィルタリングの関数で置き換えます。

```powershell
# コマンドライン引数
Param(
    $in_file, # 入力画像ファイルのパス
    $out_file # 出力画像ファイルのパス
)

Add-Type -AssemblyName System.Drawing

# ビットマップ配列を作成する関数
function Initialize-Bitmap($src_image) {
    $width = $src_image.Width   # 入力画像の幅
    $height = $src_image.Height # 入力画像の高さ

    # ビットマップ配列を作成(3はR、G、Bをそれぞれ保持する必要があるため)
    $bitmap = New-Object "System.Int32[,,]" 3,($width+2),($height+2)

    # 0で初期化
    0..2 | %{
        $i = $_
        0..($width+1) | %{
            $x = $_
            0..($height+1) | %{
                $y = $_
                $bitmap[$i,$x,$y] = 0
            }
        }
    }

    # 入力画像の画素値をコピー
    0..($width-1) | %{
        $x = $_
        0..($height-1) | %{
            $y = $_

            # Rの画素値をコピー
            $bitmap[0,($x+1),($y+1)] = $src_image.GetPixel($x, $y).R
            # Gの画素値をコピー
            $bitmap[1,($x+1),($y+1)] = $src_image.GetPixel($x, $y).G
            # Bの画素値をコピー
            $bitmap[2,($x+1),($y+1)] = $src_image.GetPixel($x, $y).B
        }
    }

    return ,$bitmap
}

# フィルタリングの関数
function Xxx-Filter($bitmap, $src_image, $dst_image) {
    # 実装はフィルタごとに異なる
}

# 入力画像を読み込み
$src_image = [System.Drawing.Image]::FromFile($in_file)

# 出力画像のオブジェクトを作成
$dst_image = New-Object System.Drawing.Bitmap($src_image.Width, $src_image.Height)

# ビットマップ配列を作成する関数を呼び出し
$bitmap = (Initialize-Bitmap $src_image)

# フィルタリングの関数を適用
$dst_image = (Xxx-Filter $bitmap $src_image $dst_image)

# 出力画像をPNGで保存
$dst_image.Save($out_file, [System.Drawing.Imaging.ImageFormat]::Png)

# オブジェクトを破棄
$src_image.Dispose()
$dst_image.Dispose()
```

# 各種空間フィルタリングの説明とフィルタリング関数の実装
## 平均化フィルタ
平均化フィルタは、フィルタをかける領域内の画素値の平均を求めます。このフィルタをかけることで、画像をぼかす「平滑化」の効果を実現できます。

![average_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/739e03e6-dd94-cb05-30e4-86014a111558.png)

:::note info
フィルタのサイズを3x3より大きくすると、より平滑化の効果が強くなります。
:::

入力画像
![src_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/a0afcaac-0fdf-cde1-77cf-5e68f7d9743c.png)

出力画像
![average_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/fb6f7844-22a2-3d61-0fdb-beda93d5d56f.png)


実装
```powershell
function Average-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,($x-1),($y-1)] `
                         + $bitmap[$i,$x,($y-1)]     `
                         + $bitmap[$i,($x+1),($y-1)] `
                         + $bitmap[$i,($x-1),$y]     `
                         + $bitmap[$i,$x,$y]         `
                         + $bitmap[$i,($x+1),$y]     `
                         + $bitmap[$i,($x-1),($y+1)] `
                         + $bitmap[$i,$x,($y+1)]     `
                         + $bitmap[$i,($x+1),($y+1)]
            }

            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/9), ($rgb[1]/9), ($rgb[2]/9))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}
```

## ガウシアンフィルタ
ガウシアンフィルタは、平均化に正規分布(ガウス分布)に基づく重みを付け、フィルタの中心に近いほど大きな重みを付けます。平均化フィルタに比べ、より滑らかな平滑化の効果を実現できます。

![gaussian_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/ba700470-b7ee-c872-634f-a3914afa833b.png)

入力画像
![src_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/73f957de-261b-7719-ba91-41bf92719610.png)

出力画像
![gaussian_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/10ffcc90-7f34-aabb-49a2-aac02b87a2c1.png)


実装
```powershell
function Gaussian-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,($x-1),($y-1)] `
                         + 2 * $bitmap[$i,$x,($y-1)] `
                         + $bitmap[$i,($x+1),($y-1)] `
                         + 2 * $bitmap[$i,($x-1),$y] `
                         + 4 * $bitmap[$i,$x,$y]     `
                         + 2 * $bitmap[$i,($x+1),$y] `
                         + $bitmap[$i,($x-1),($y+1)] `
                         + 2 * $bitmap[$i,$x,($y+1)] `
                         + $bitmap[$i,($x+1),($y+1)]
            }
            
            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/16), ($rgb[1]/16), ($rgb[2]/16))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}
```

## 横方向の微分フィルタ
微分フィルタは、注目画素の隣接画素間の差を求めることにより、画像のエッジを抽出します。

:::note info
デジタル画像の画素値は連続ではないため、微分ではなく「差分」を求めます。
注目画素と隣接画素との差を求めるやり方もあります。
:::

横方向の微分フィルタは、横方向の差を求め、縦のエッジを抽出します。

![horizontal_difference_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/b71a3fd1-54c2-f9c6-ba56-3efa60f9457c.png)

入力画像
![src_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/3bad6804-9b9d-d1a1-3100-06aa69cfdbc3.png)

出力画像
![horizontal_differential_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/98824a98-ab52-366d-9ee6-f583e5f8cdaf.png)


実装
```powershell
function Horizontal-Differential-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = - $bitmap[$i,($x-1),$y] `
                         + $bitmap[$i,($x+1),$y]

                # 画素値が0より小さくなった場合の考慮
                If($rgb[$i] -lt 0) {$rgb[$i] = 0}
            }

            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/2), ($rgb[1]/2), ($rgb[2]/2))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}
```

## 縦方向の微分フィルタ
縦方向の微分フィルタは、横方向の微分フィルタと同じ考え方で縦方向の差を求め、横方向のエッジを抽出します。

![vertica_difference_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/f0badc96-536d-335a-a110-cf27235ef8d9.png)

入力画像
![src_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/5aef23df-a1ce-6b08-5d01-ec9bd07b0be5.png)

出力画像
![vertical_differential_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/a55673e6-048a-a479-5839-f826db6e5129.png)

実装
```powershell
function Vertical-Differential-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,$x,($y-1)] `
                         - $bitmap[$i,$x,($y+1)]
               
                # 画素値が0より小さくなった場合の考慮
                If($rgb[$i] -lt 0) {$rgb[$i] = 0}
            }

            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/2), ($rgb[1]/2), ($rgb[2]/2))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}
```

## ラプラシアンフィルタ
ラプラシアンフィルタは、横方向の2次微分と縦方向の2次微分を同時に行い、方向に依存しないエッジを抽出します。

![laplacian_filter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/dbbefb58-d869-c317-e478-9b4b4402ad80.png)

入力画像
![src_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/85b17f2c-b35c-cd01-68f5-9327a261315e.png)


出力画像
![laplacian_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/920487/525ec11b-8623-610f-23a8-051207a379e5.png)

実装
```powershell
function Laplacian-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,$x,($y-1)]   `
                         + $bitmap[$i,($x-1),$y]   `
                         -4 * $bitmap[$i,$x,$y]    `
                         + $bitmap[$i,($x+1),$y]   `
                         + $bitmap[$i,$x,($y+1)]

                # 画素値が0より小さくなった場合の考慮
                If($rgb[$i] -lt 0) {$rgb[$i] = 0}
                # 画素値が255より大きくなった場合の考慮
                If($rgb[$i] -gt 255) {$rgb[$i] = 255}
            }

            $color = [System.Drawing.Color]::FromArgb($a, $rgb[0], $rgb[1], $rgb[2])
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}
```

# 各種空間フィルタリングの実行可能コード
ここのコードは、ファイルに保存してWindowsのコマンドプロンプトやPowerShellのプロンプトで以下のようにして実行できます。

```
> PowerShell.exe -ExecutionPolicy RemoteSigned -File 【保存したファイルのフルパス】
【入力画像ファイルのフルパス】 【出力画像ファイルのフルパス】
```

:::note warn
コードのファイルは「.ps1」、画像ファイルは「.png」の拡張子を付けます。
:::

平均化フィルタ
```powershell
# コマンドライン引数
Param(
    $in_file, # 入力画像ファイルのパス
    $out_file # 出力画像ファイルのパス
)

Add-Type -AssemblyName System.Drawing

# ビットマップ配列を作成する関数
function Initialize-Bitmap($src_image) {
    $width = $src_image.Width   # 入力画像の幅
    $height = $src_image.Height # 入力画像の高さ

    # ビットマップ配列を作成
    $bitmap = New-Object "System.Int32[,,]" 3,($width+2),($height+2)

    # 0で初期化
    0..2 | %{
        $i = $_
        0..($width+1) | %{
            $x = $_
            0..($height+1) | %{
                $y = $_
                $bitmap[$i,$x,$y] = 0
            }
        }
    }

    # 入力画像の画素値をコピー
    0..($width-1) | %{
        $x = $_
        0..($height-1) | %{
            $y = $_

            # Rの画素値をコピー
            $bitmap[0,($x+1),($y+1)] = $src_image.GetPixel($x, $y).R
            # Gの画素値をコピー
            $bitmap[1,($x+1),($y+1)] = $src_image.GetPixel($x, $y).G
            # Bの画素値をコピー
            $bitmap[2,($x+1),($y+1)] = $src_image.GetPixel($x, $y).B
        }
    }

    return ,$bitmap
}

# 平均化フィルタの関数
function Average-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,($x-1),($y-1)] `
                         + $bitmap[$i,$x,($y-1)]     `
                         + $bitmap[$i,($x+1),($y-1)] `
                         + $bitmap[$i,($x-1),$y]     `
                         + $bitmap[$i,$x,$y]         `
                         + $bitmap[$i,($x+1),$y]     `
                         + $bitmap[$i,($x-1),($y+1)] `
                         + $bitmap[$i,$x,($y+1)]     `
                         + $bitmap[$i,($x+1),($y+1)]
            }

            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/9), ($rgb[1]/9), ($rgb[2]/9))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}

# 入力画像を読み込み
$src_image = [System.Drawing.Image]::FromFile($in_file)

# 出力画像のオブジェクトを作成
$dst_image = New-Object System.Drawing.Bitmap($src_image.Width, $src_image.Height)

# ビットマップ配列を作成する関数を呼び出し
$bitmap = (Initialize-Bitmap $src_image)

# 平均化フィルタの関数を適用
$dst_image = (Average-Filter $bitmap $src_image $dst_image)

# 出力画像をPNGで保存
$dst_image.Save($out_file, [System.Drawing.Imaging.ImageFormat]::Png)

# オブジェクトを破棄
$src_image.Dispose()
$dst_image.Dispose()
```

ガウシアンフィルタ
```powershell
# コマンドライン引数
Param(
    $in_file, # 入力画像ファイルのパス
    $out_file # 出力画像ファイルのパス
)

Add-Type -AssemblyName System.Drawing

# ビットマップ配列を作成する関数
function Initialize-Bitmap($src_image) {
    $width = $src_image.Width   # 入力画像の幅
    $height = $src_image.Height # 入力画像の高さ

    # ビットマップ配列を作成
    $bitmap = New-Object "System.Int32[,,]" 3,($width+2),($height+2)

    # 0で初期化
    0..2 | %{
        $i = $_
        0..($width+1) | %{
            $x = $_
            0..($height+1) | %{
                $y = $_
                $bitmap[$i,$x,$y] = 0
            }
        }
    }

    # 入力画像の画素値をコピー
    0..($width-1) | %{
        $x = $_
        0..($height-1) | %{
            $y = $_

            # Rの画素値をコピー
            $bitmap[0,($x+1),($y+1)] = $src_image.GetPixel($x, $y).R
            # Gの画素値をコピー
            $bitmap[1,($x+1),($y+1)] = $src_image.GetPixel($x, $y).G
            # Bの画素値をコピー
            $bitmap[2,($x+1),($y+1)] = $src_image.GetPixel($x, $y).B
        }
    }

    return ,$bitmap
}

# ガウシアンフィルタの関数
function Gaussian-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,($x-1),($y-1)] `
                         + 2 * $bitmap[$i,$x,($y-1)] `
                         + $bitmap[$i,($x+1),($y-1)] `
                         + 2 * $bitmap[$i,($x-1),$y] `
                         + 4 * $bitmap[$i,$x,$y]     `
                         + 2 * $bitmap[$i,($x+1),$y] `
                         + $bitmap[$i,($x-1),($y+1)] `
                         + 2 * $bitmap[$i,$x,($y+1)] `
                         + $bitmap[$i,($x+1),($y+1)]
            }
            
            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/16), ($rgb[1]/16), ($rgb[2]/16))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}

# 入力画像を読み込み
$src_image = [System.Drawing.Image]::FromFile($in_file)

# 出力画像のオブジェクトを作成
$dst_image = New-Object System.Drawing.Bitmap($src_image.Width, $src_image.Height)

# ビットマップ配列を作成する関数を呼び出し
$bitmap = (Initialize-Bitmap $src_image)

# ガウシアンフィルタの関数を適用
$dst_image = (Gaussian-Filter $bitmap $src_image $dst_image)

# 出力画像をPNGで保存
$dst_image.Save($out_file, [System.Drawing.Imaging.ImageFormat]::Png)

# オブジェクトを破棄
$src_image.Dispose()
$dst_image.Dispose()
```

横方向の微分フィルタ
```powershell
# コマンドライン引数
Param(
    $in_file, # 入力画像ファイルのパス
    $out_file # 出力画像ファイルのパス
)

Add-Type -AssemblyName System.Drawing

# ビットマップ配列を作成する関数
function Initialize-Bitmap($src_image) {
    $width = $src_image.Width   # 入力画像の幅
    $height = $src_image.Height # 入力画像の高さ

    # ビットマップ配列を作成
    $bitmap = New-Object "System.Int32[,,]" 3,($width+2),($height+2)

    # 0で初期化
    0..2 | %{
        $i = $_
        0..($width+1) | %{
            $x = $_
            0..($height+1) | %{
                $y = $_
                $bitmap[$i,$x,$y] = 0
            }
        }
    }

    # 入力画像の画素値をコピー
    0..($width-1) | %{
        $x = $_
        0..($height-1) | %{
            $y = $_

            # Rの画素値をコピー
            $bitmap[0,($x+1),($y+1)] = $src_image.GetPixel($x, $y).R
            # Gの画素値をコピー
            $bitmap[1,($x+1),($y+1)] = $src_image.GetPixel($x, $y).G
            # Bの画素値をコピー
            $bitmap[2,($x+1),($y+1)] = $src_image.GetPixel($x, $y).B
        }
    }

    return ,$bitmap
}

# 横方向の微分フィルタの関数
function Horizontal-Differential-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = - $bitmap[$i,($x-1),$y] `
                         + $bitmap[$i,($x+1),$y]

                # 画素値が0より小さくなった場合の考慮
                If($rgb[$i] -lt 0) {$rgb[$i] = 0}
            }

            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/2), ($rgb[1]/2), ($rgb[2]/2))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}

# 入力画像を読み込み
$src_image = [System.Drawing.Image]::FromFile($in_file)

# 出力画像のオブジェクトを作成
$dst_image = New-Object System.Drawing.Bitmap($src_image.Width, $src_image.Height)

# ビットマップ配列を作成する関数を呼び出し
$bitmap = (Initialize-Bitmap $src_image)

# 横方向の微分フィルタの関数を適用
$dst_image = (Horizontal-Differential-Filter $bitmap $src_image $dst_image)

# 出力画像をPNGで保存
$dst_image.Save($out_file, [System.Drawing.Imaging.ImageFormat]::Png)

# オブジェクトを破棄
$src_image.Dispose()
$dst_image.Dispose()
```

縦方向の微分フィルタ
```powershell
# コマンドライン引数
Param(
    $in_file, # 入力画像ファイルのパス
    $out_file # 出力画像ファイルのパス
)

Add-Type -AssemblyName System.Drawing

# ビットマップ配列を作成する関数
function Initialize-Bitmap($src_image) {
    $width = $src_image.Width   # 入力画像の幅
    $height = $src_image.Height # 入力画像の高さ

    # ビットマップ配列を作成
    $bitmap = New-Object "System.Int32[,,]" 3,($width+2),($height+2)

    # 0で初期化
    0..2 | %{
        $i = $_
        0..($width+1) | %{
            $x = $_
            0..($height+1) | %{
                $y = $_
                $bitmap[$i,$x,$y] = 0
            }
        }
    }

    # 入力画像の画素値をコピー
    0..($width-1) | %{
        $x = $_
        0..($height-1) | %{
            $y = $_

            # Rの画素値をコピー
            $bitmap[0,($x+1),($y+1)] = $src_image.GetPixel($x, $y).R
            # Gの画素値をコピー
            $bitmap[1,($x+1),($y+1)] = $src_image.GetPixel($x, $y).G
            # Bの画素値をコピー
            $bitmap[2,($x+1),($y+1)] = $src_image.GetPixel($x, $y).B
        }
    }

    return ,$bitmap
}

# 縦方向の微分フィルタの関数
function Vertical-Differential-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,$x,($y-1)] `
                         - $bitmap[$i,$x,($y+1)]
               
                # 画素値が0より小さくなった場合の考慮
                If($rgb[$i] -lt 0) {$rgb[$i] = 0}
            }

            $color = [System.Drawing.Color]::FromArgb($a, ($rgb[0]/2), ($rgb[1]/2), ($rgb[2]/2))
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}

# 入力画像を読み込み
$src_image = [System.Drawing.Image]::FromFile($in_file)

# 出力画像のオブジェクトを作成
$dst_image = New-Object System.Drawing.Bitmap($src_image.Width, $src_image.Height)

# ビットマップ配列を作成する関数を呼び出し
$bitmap = (Initialize-Bitmap $src_image)

# 縦方向の微分フィルタの関数を適用
$dst_image = (Vertical-Differential-Filter $bitmap $src_image $dst_image)

# 出力画像をPNGで保存
$dst_image.Save($out_file, [System.Drawing.Imaging.ImageFormat]::Png)

# オブジェクトを破棄
$src_image.Dispose()
$dst_image.Dispose()
```

ラプラシアンフィルタ
```powershell
# コマンドライン引数
Param(
    $in_file, # 入力画像ファイルのパス
    $out_file # 出力画像ファイルのパス
)

Add-Type -AssemblyName System.Drawing

# ビットマップ配列を作成する関数
function Initialize-Bitmap($src_image) {
    $width = $src_image.Width   # 入力画像の幅
    $height = $src_image.Height # 入力画像の高さ

    # ビットマップ配列を作成
    $bitmap = New-Object "System.Int32[,,]" 3,($width+2),($height+2)

    # 0で初期化
    0..2 | %{
        $i = $_
        0..($width+1) | %{
            $x = $_
            0..($height+1) | %{
                $y = $_
                $bitmap[$i,$x,$y] = 0
            }
        }
    }

    # 入力画像の画素値をコピー
    0..($width-1) | %{
        $x = $_
        0..($height-1) | %{
            $y = $_

            # Rの画素値をコピー
            $bitmap[0,($x+1),($y+1)] = $src_image.GetPixel($x, $y).R
            # Gの画素値をコピー
            $bitmap[1,($x+1),($y+1)] = $src_image.GetPixel($x, $y).G
            # Bの画素値をコピー
            $bitmap[2,($x+1),($y+1)] = $src_image.GetPixel($x, $y).B
        }
    }

    return ,$bitmap
}

# ラプラシアンフィルタの関数
function Laplacian-Filter($bitmap, $src_image, $dst_image) {
    $rgb = New-Object "System.Int32[]" 3

    0..($src_image.Width-1) | %{
        $x = $_ + 1
        0..($src_image.Height-1) | %{
            $y = $_ + 1

            $a = $src_image.GetPixel(($x-1), ($y-1)).A

            0..2 | %{
                $i = $_ 

                $rgb[$i] = $bitmap[$i,$x,($y-1)]   `
                         + $bitmap[$i,($x-1),$y]   `
                         -4 * $bitmap[$i,$x,$y]    `
                         + $bitmap[$i,($x+1),$y]   `
                         + $bitmap[$i,$x,($y+1)]

                # 画素値が0より小さくなった場合の考慮
                If($rgb[$i] -lt 0) {$rgb[$i] = 0}
                # 画素値が255より大きくなった場合の考慮
                If($rgb[$i] -gt 255) {$rgb[$i] = 255}
            }

            $color = [System.Drawing.Color]::FromArgb($a, $rgb[0], $rgb[1], $rgb[2])
            $dst_image.SetPixel(($x-1), ($y-1), $color)
        }
    }

    return $dst_image
}

# 入力画像を読み込み
$src_image = [System.Drawing.Image]::FromFile($in_file)

# 出力画像のオブジェクトを作成
$dst_image = New-Object System.Drawing.Bitmap($src_image.Width, $src_image.Height)

# ビットマップ配列を作成する関数を呼び出し
$bitmap = (Initialize-Bitmap $src_image)

# ラプラシアンフィルタの関数を適用
$dst_image = (Laplacian-Filter $bitmap $src_image $dst_image)

# 出力画像をPNGで保存
$dst_image.Save($out_file, [System.Drawing.Imaging.ImageFormat]::Png)

# オブジェクトを破棄
$src_image.Dispose()
$dst_image.Dispose()
```

# 参考リンク
「画像ファイルの読み込み」「画素単位のアクセス」「画像のファイル出力」の処理はこちらの記事を参考にさせて頂きました。

https://qiita.com/S_SenSq/items/75185845b716f712a087

画像処理はこちらの書籍で勉強しました。画像処理全般を学べてとてもいい本です。

https://www.amazon.co.jp/%E3%83%87%E3%82%A3%E3%82%B8%E3%82%BF%E3%83%AB%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86-%E6%94%B9%E8%A8%82%E7%AC%AC%E4%BA%8C%E7%89%88-%E3%83%87%E3%82%A3%E3%82%B8%E3%82%BF%E3%83%AB%E7%94%BB%E5%83%8F%E5%87%A6%E7%90%86%E7%B7%A8%E9%9B%86%E5%A7%94%E5%93%A1%E4%BC%9A/dp/490347464X