4
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PowerShell と C# で画像処理(ピクセル操作)

Last updated at Posted at 2016-12-31

概要

  • この記事でやっていること:
    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 で記述する

  1. まず、画像ファイルのデータをピクセルデータに変換する。

    • 変換には、上記の ImagingUtil.BitmapConverter を利用する。
    • 元となる画像ファイルへのファイルパスは、スクリプトの引数で指定する。
  2. ピクセルデータを操作する。

    • 今回はピクセルデータに対して、簡単なグレースケール変換を行っている。
      • RGB 値を加算して 3 で割るだけ:

        $grayValue = [byte](($pixel.r + $pixel.g + $pixel.b) / 3)
        
  3. ピクセルデータを画像データに戻す。

  4. 変換した画像データは、元となった画像ファイルと同じ場所に、ファイル名の末尾に "_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

ビフォー

neko.jpg
neko.jpg

アフター

neko_gray.jpg
neko_gray.jpg

考察

  • 今回の例では PowerShell 側での画像処理のしやすさを考慮して、C# 側で byte[]IList<Pixel> の相互変換を行っているが、このオーバーヘッドのせいで処理が遅くなってしまっている。
    • 処理速度を気にする場合は、画像処理の部分も C# 側に記述してもよいかもしれない(PowerShell 側はそれらを利用するだけにする)。

追記

画像処理のちょっと凝ったロジックを PowerShell で書くのは難しいと、他のフィルタを作っていて感じました。ロジックを C# に寄せて書いた版をここに載せます。

4
9
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
4
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?