概要
- 以下の記事の C# プログラムを少しアレンジして、PowerShell から利用してみた。
-
この記事でやっていること:
0. C# で、画像ファイルをピクセルデータ(バイト配列)に変換したり、戻したりする。
0. PowerShell で、画像処理(ピクセル操作)を行い、加工した画像を別のファイルに保存する。 -
PowerShell で手軽に画像処理のプログラムが書けるようになる、かもしれない。
コード
ビットマップとピクセルデータ間の変換処理を C# で記述する
- 今回は、以下の 3 つのクラスを詰め込んだ名前空間 ImagingUtil を用意した。
0. RGBA のバイト情報をピクセル単位で扱えるようにするための Pixel クラス
0. ビットマップとピクセルデータ間の変換処理を行う BitmapConverter クラス
0. 画像ファイルの保存形式 ImageFormat を決定する ImageFormatResolver クラス
ImagingUtil.cs
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace ImagingUtil
{
// RGBA のバイト情報をピクセル単位で扱えるようにするためのクラス
public class Pixel
{
public byte a;
public byte r;
public byte g;
public byte b;
public Pixel(byte b, byte g, byte r, byte a)
{
this.a = a;
this.r = r;
this.g = g;
this.b = b;
}
}
// ビットマップとピクセルデータ間の変換処理を行うクラス
public static class BitmapConverter
{
// ビットマップをピクセルデータに変換する
public static IList<Pixel> ConvertBitmapToPixels(Bitmap bitmap)
{
byte[] byteArray = null;
using (bitmap)
{
byteArray = ConvertBitmapToByteArray(bitmap);
}
var ret = new List<Pixel>();
for (var i = 0; i < byteArray.Length; i += 4)
{
ret.Add(new Pixel(byteArray[i + 0],
byteArray[i + 1],
byteArray[i + 2],
byteArray[i + 3]));
}
return ret;
}
// ビットマップをバイト配列に変換する
private static byte[] ConvertBitmapToByteArray(Bitmap bitmap)
{
var ret = new byte[bitmap.Width * bitmap.Height * 4];
BitmapData bitmapData = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
Marshal.Copy(bitmapData.Scan0, ret, 0, ret.Length);
bitmap.UnlockBits(bitmapData);
return ret;
}
// ピクセルデータをビットマップに戻す
public static Bitmap ConvertPixelsToBitmap(IList<Pixel> pixels, int width, int height)
{
var byteArray = new byte[width * height * 4];
var index = 0;
foreach (var pixel in pixels)
{
byteArray[index++] = pixel.b;
byteArray[index++] = pixel.g;
byteArray[index++] = pixel.r;
byteArray[index++] = pixel.a;
}
return ConvertByteArrayToBitmap(byteArray, width, height);
}
// バイト配列をビットマップに戻す
private static Bitmap ConvertByteArrayToBitmap(byte[] byteArray, int width, int height)
{
Bitmap ret = new Bitmap(width, height);
BitmapData bitmapData = ret.LockBits(
new Rectangle(0, 0, ret.Width, ret.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format32bppArgb);
Marshal.Copy(byteArray, 0, bitmapData.Scan0, byteArray.Length);
ret.UnlockBits(bitmapData);
return ret;
}
}
// 画像ファイルの保存形式 ImageFormat を決定するクラス
public static class ImageFormatResolver
{
// 画像ファイルの拡張子から決定する
public static ImageFormat ResolveFromExtension(string extension)
{
switch (extension.ToLower())
{
case ".bmp": return ImageFormat.Bmp;
case ".gif": return ImageFormat.Gif;
case ".jpg":
case ".jpeg": return ImageFormat.Jpeg;
case ".png":
default: return ImageFormat.Png;
}
}
}
}
画像処理の部分を PowerShell で記述する
-
まず、画像ファイルのデータをピクセルデータに変換する。
- 変換には、上記の ImagingUtil.BitmapConverter を利用する。
- 元となる画像ファイルへのファイルパスは、スクリプトの引数で指定する。
-
ピクセルデータを操作する。
- 今回はピクセルデータに対して、簡単なグレースケール変換を行っている。
-
RGB 値を加算して 3 で割るだけ:
$grayValue = [byte](($pixel.r + $pixel.g + $pixel.b) / 3)
-
- 今回はピクセルデータに対して、簡単なグレースケール変換を行っている。
-
ピクセルデータを画像データに戻す。
-
変換した画像データは、元となった画像ファイルと同じ場所に、ファイル名の末尾に "_gray" と付けて保存する。
ConvertTo-Grayscale.ps1
param(
[Parameter(Mandatory = $true)]
[string]
$SourceImageFilePath
)
# 画像ファイルのパスを解決
$resolvedFilePath = Resolve-Path $SourceImageFilePath
# 画像の情報を取得
Add-Type -AssemblyName System.Drawing
$sourceImage = [System.Drawing.Image]::FromFile($resolvedFilePath)
$imageWidth = $sourceImage.Width
$imageHeight = $sourceImage.Height
# 'ImagingUtil.cs' をロード
$thisDirectoryPath = Split-Path $MyInvocation.MyCommand.Path -Parent
Add-Type -Path (Join-Path $thisDirectoryPath 'ImagingUtil.cs') `
-ReferencedAssemblies System.Drawing
# 画像データをピクセルデータに変換
$pixels = [ImagingUtil.BitmapConverter]::ConvertBitmapToPixels($sourceImage)
if ($sourceImage -ne $null)
{
$sourceImage.Dispose()
}
# 画像処理 (グレースケール変換)
foreach ($pixel in $pixels)
{
$grayValue = [byte](($pixel.r + $pixel.g + $pixel.b) / 3)
$pixel.r = $grayValue
$pixel.g = $grayValue
$pixel.b = $grayValue
}
# ピクセルデータを画像データに戻す
$outputImage = [ImagingUtil.BitmapConverter]::ConvertPixelsToBitmap($pixels, $imageWidth, $imageHeight)
# 変換した画像データを保存する(元画像と同じ場所に、ファイル名の末尾に "_gray" と付けて保存)
$outputFileLocation = Split-Path $resolvedFilePath -Parent
$outputFileBaseName = (Get-ChildItem $resolvedFilePath).BaseName + "_gray"
$outputFileExtention = (Get-ChildItem $resolvedFilePath).Extension
$outputFileFullName = $outputFileBaseName + $outputFileExtention
$outputFilePath = Join-Path $outputFileLocation $outputFileFullName
$outputImageFormat = [ImagingUtil.ImageFormatResolver]::ResolveFromExtension($outputFileExtention)
$outputImage.Save($outputFilePath, $outputImageFormat)
実行結果
PS C:\> & .\ConvertTo-GrayScale.ps1 neko.jpg
ビフォー
アフター
考察
- 今回の例では PowerShell 側での画像処理のしやすさを考慮して、C# 側で
byte[]
とIList<Pixel>
の相互変換を行っているが、このオーバーヘッドのせいで処理が遅くなってしまっている。- 処理速度を気にする場合は、画像処理の部分も C# 側に記述してもよいかもしれない(PowerShell 側はそれらを利用するだけにする)。
追記
画像処理のちょっと凝ったロジックを PowerShell で書くのは難しいと、他のフィルタを作っていて感じました。ロジックを C# に寄せて書いた版をここに載せます。