bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] ポインター実践例① — unsafe コードによるビットマップ画像処理の高速化

[C#] ポインター実践例① — unsafe コードによるビットマップ画像処理の高速化

Jun 11, 2026 C# , .NET bucket-sort

このシリーズは全 4 記事で構成されています。

  • ① なぜポインターが必要か / unsafe の基本 / * と & 演算子
  • ② -> 演算子 / stackalloc / fixed / sizeof
  • ③ 本記事 — ビットマップ画像処理の高速化
  • ④ 実践例:C++ DLL との相互運用

前回(①)で述べたように、C# でポインターを使う主な理由の一つはパフォーマンス最適化です。本記事では「ビットマップ画像のピクセル処理」を題材に、安全な API との速度差と、ポインターを使った実装方法を解説します。

なぜ画像処理でポインターが有効か

System.Drawing.Bitmap には GetPixel(x, y) / SetPixel(x, y, color) というメソッドがあります。これらは安全で使いやすいですが、1 ピクセルごとに内部でロックや境界チェックが発生するため、全ピクセルをループで処理すると非常に低速です。

一方、Bitmap.LockBits でビットマップのメモリをロックしてポインターを取得すれば、生のバイト配列に直接アクセスできます。ロックや境界チェックがなくなるため、同じ処理が数十倍速くなることがあります。

方法 特徴
GetPixel / SetPixel 安全・簡単・低速(1 ピクセルごとにオーバーヘッド)
LockBits + ポインター unsafe・高速(メモリに直接アクセス)
LockBits + Span<byte> unsafe 不要・高速(.NET 6 以降で推奨)

準備:プロジェクトの設定

System.Drawing は Windows 固有のため、.csproj に次を追加します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0-windows</TargetFramework>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Drawing.Common" Version="9.0.0" />
  </ItemGroup>
</Project>

LockBits の基本

Bitmap.LockBits は BitmapData を返します。BitmapData.Scan0 が先頭ピクセルへのポインターです。

using System.Drawing;
using System.Drawing.Imaging;

// ビットマップをロックして BitmapData を取得
Bitmap bmp = new Bitmap("input.png");
BitmapData data = bmp.LockBits(
    new Rectangle(0, 0, bmp.Width, bmp.Height),
    ImageLockMode.ReadWrite,
    PixelFormat.Format32bppArgb  // 1ピクセル = 4バイト (B, G, R, A の順)
);

// 先頭アドレスを取得
// data.Scan0 は IntPtr 型(ポインターのラッパー)
IntPtr ptr = data.Scan0;

// 処理後は必ずアンロック
bmp.UnlockBits(data);

Format32bppArgb は 1 ピクセルが 4 バイト(Blue, Green, Red, Alpha の順)で格納されます。

グレースケール変換

GetPixel / SetPixel 版(遅い)

static void ToGrayscaleSlow(Bitmap bmp)
{
    for (int y = 0; y < bmp.Height; y++)
    {
        for (int x = 0; x < bmp.Width; x++)
        {
            Color c = bmp.GetPixel(x, y);
            // 輝度 = 0.299R + 0.587G + 0.114B(ITU-R BT.601)
            int luminance = (int)(c.R * 0.299 + c.G * 0.587 + c.B * 0.114);
            bmp.SetPixel(x, y, Color.FromArgb(c.A, luminance, luminance, luminance));
        }
    }
}

ポインター版(速い)

static unsafe void ToGrayscaleFast(Bitmap bmp)
{
    BitmapData data = bmp.LockBits(
        new Rectangle(0, 0, bmp.Width, bmp.Height),
        ImageLockMode.ReadWrite,
        PixelFormat.Format32bppArgb
    );

    try
    {
        byte* ptr = (byte*)data.Scan0.ToPointer();
        int stride = data.Stride; // 1行のバイト数(パディング込み)

        for (int y = 0; y < bmp.Height; y++)
        {
            byte* row = ptr + y * stride;
            for (int x = 0; x < bmp.Width; x++)
            {
                // 各ピクセルは B, G, R, A の順
                byte b = row[x * 4 + 0];
                byte g = row[x * 4 + 1];
                byte r = row[x * 4 + 2];
                // A はそのまま

                byte luminance = (byte)(r * 0.299 + g * 0.587 + b * 0.114);
                row[x * 4 + 0] = luminance; // B
                row[x * 4 + 1] = luminance; // G
                row[x * 4 + 2] = luminance; // R
            }
        }
    }
    finally
    {
        // 例外が発生してもアンロックを保証
        bmp.UnlockBits(data);
    }
}

stride(ストライド)は 1 行のバイト数です。幅 × チャンネル数にアラインメントのパディングが加わるため、bmp.Width * 4 と一致しない場合があります。stride を使って行の先頭アドレスを計算するのが重要なポイントです。

明度反転(ネガティブ変換)

static unsafe void InvertColors(Bitmap bmp)
{
    BitmapData data = bmp.LockBits(
        new Rectangle(0, 0, bmp.Width, bmp.Height),
        ImageLockMode.ReadWrite,
        PixelFormat.Format32bppArgb
    );

    try
    {
        byte* ptr = (byte*)data.Scan0.ToPointer();
        int byteCount = Math.Abs(data.Stride) * bmp.Height;

        for (int i = 0; i < byteCount; i += 4)
        {
            ptr[i + 0] = (byte)(255 - ptr[i + 0]); // B
            ptr[i + 1] = (byte)(255 - ptr[i + 1]); // G
            ptr[i + 2] = (byte)(255 - ptr[i + 2]); // R
            // ptr[i + 3] はアルファ値。変更しない
        }
    }
    finally
    {
        bmp.UnlockBits(data);
    }
}

行ごとのループを使わず、全バイトを 1 本のループで処理しています。パディングのないフォーマットかつ Stride が正のときだけ使えるシンプルな書き方です。

利用例:ファイルを読み込んで変換して保存

using System.Drawing;

// グレースケール変換
using var bmp1 = new Bitmap("input.png");
ToGrayscaleFast(bmp1);
bmp1.Save("output_gray.png");

// 明度反転
using var bmp2 = new Bitmap("input.png");
InvertColors(bmp2);
bmp2.Save("output_invert.png");

Console.WriteLine("変換完了");

Span<byte> を使った安全版(.NET 6 以降)

ポインター演算(byte*)を使わずに Span<byte> で同等の処理を書けます。unsafe コンテキスト自体は引き続き必要ですが、添字アクセスに範囲チェックが入るためより安全です。

using System.Runtime.InteropServices;

static unsafe void ToGrayscaleSpan(Bitmap bmp)
{
    BitmapData data = bmp.LockBits(
        new Rectangle(0, 0, bmp.Width, bmp.Height),
        ImageLockMode.ReadWrite,
        PixelFormat.Format32bppArgb
    );

    try
    {
        // IntPtr → Span<byte>
        int byteCount = Math.Abs(data.Stride) * bmp.Height;
        Span<byte> pixels = new Span<byte>(data.Scan0.ToPointer(), byteCount);

        for (int i = 0; i < pixels.Length; i += 4)
        {
            byte b = pixels[i + 0];
            byte g = pixels[i + 1];
            byte r = pixels[i + 2];
            byte lum = (byte)(r * 0.299 + g * 0.587 + b * 0.114);
            pixels[i + 0] = lum;
            pixels[i + 1] = lum;
            pixels[i + 2] = lum;
        }
    }
    finally
    {
        bmp.UnlockBits(data);
    }
}

Span<byte> を使うことで、ポインター演算(ptr++ や *(ptr + i) など)なしに安全なインデックスアクセスが可能になります。new Span<byte>(void*, int) コンストラクターと IntPtr.ToPointer() はどちらも unsafe コンテキストが必要なため、メソッドには unsafe が必要です。

まとめ

アプローチ 速度 unsafe 要否 主な用途
GetPixel / SetPixel 低速 不要 数ピクセルの部分変更
LockBits + byte* ポインター 高速 必要 全ピクセル処理(高速化優先)
LockBits + Span<byte> 高速 必要 全ピクセル処理(ポインター演算なし)

画像処理のようにメモリを連続アクセスするユースケースでは、ポインターによる直接操作は数十倍のパフォーマンス差を生むことがあります。ただし LockBits / UnlockBits のペアを確実に呼ぶこと(try/finally 必須)、ストライドを正確に使うことが実装上の重要な注意点です。

次回(④)は、C++ で実装された DLL を P/Invoke で呼び出す際にポインターをどう扱うかを解説します。

C# .NET Unsafe ポインター Pointer 画像処理 Bitmap GDI+
← [C#] ポインター型(Pointer Types)② — -> 演算子 / stackalloc / fixed / sizeof [C#] ポインター実践例② — P/Invoke を使った C++ DLL との相互運用 →

Related Posts

  • [C#] ポインター実践例② — P/Invoke を使った C++ DLL との相互運用 Jun 12, 2026
  • [C#] ポインター型(Pointer Types)② — -> 演算子 / stackalloc / fixed / sizeof Jun 10, 2026
  • [C#] ポインター型(Pointer Types)入門 ① — unsafe コードの基本と * / & 演算子 Jun 9, 2026
  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026

Table of Contents

  • なぜ画像処理でポインターが有効か
  • 準備:プロジェクトの設定
  • LockBits の基本
  • グレースケール変換
    • GetPixel / SetPixel 版(遅い)
    • ポインター版(速い)
  • 明度反転(ネガティブ変換)
  • 利用例:ファイルを読み込んで変換して保存
  • Span<byte> を使った安全版(.NET 6 以降)
  • まとめ

Recent Posts

  • [C#] プラグインで拡張できるアプリケーションを設計する Jul 9, 2026
  • [C#] 属性でコードに意味を与える Jul 8, 2026
  • [C#] 外部アセンブリを読み込み実行時に型を呼び出す Jul 7, 2026
  • [C#] リフレクションで型の中身を調べる Jul 6, 2026
  • [C#] 型メタデータとリフレクションの基礎を理解する Jul 5, 2026

Categories

  • C#120
  • .NET119
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Diagnostics1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • コレクション
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • パフォーマンス
  • Apache
  • System.Collections.Generic
  • デリゲート
  • Code Snippet
  • DynamoDB
  • LINQ
  • NoSQL
  • PHP-FPM
  • RDS
  • System.Collections
  • Windows
  • メモリ管理
  • リフレクション
  • DoS
  • Nginx
  • WordPress
  • メタデータ
  • ラムダ式
  • 監視
  • 設計
  • Amazon Linux 2023
  • Delegate
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • LINQ to Objects
  • OPCache
  • Pointer
  • Reflection
  • System.Collections.Specialized
  • Unsafe
  • Webサーバー
  • インターフェース
  • オブジェクト指向
  • クラス設計
  • デザインパターン
  • パターンマッチング
  • ポインター
  • 継承
Powered by Hugo & Explore Theme.