UWPでWriteableBitmapに矩形・ポリゴン描画して表示する方法|SoftwareBitmapとCanvasの使い分け

UWPで画面やカメラをキャプチャした画像に、検出結果の矩形やポリゴン(多角形)を描いて表示したい――この要件は「画像に焼き付けるか」「UIで重ねるか」で最適解が変わります。WriteableBitmapとSoftwareBitmapの違い、Canvasの必要性、実装パターンと落とし穴を具体例つきで整理します。

目次

結論:焼き付けるならWriteableBitmap、重ね表示ならCanvasが最短

最初に迷いがちなポイントを、実務目線で一言にまとめると次の通りです。

  • 最終的に「画像ファイル」として保存・共有したい(図形を画像に含めたい) → WriteableBitmapにピクセルを書き込んで“焼き付け”
  • 画面上で見せられればよい(あとで動かす/編集する可能性がある) → 画像はそのまま表示し、Canvas(または同等レイヤー)にRectangle/Polygonを重ねる
やりたいことおすすめ理由
保存した画像にも枠線や多角形を残したいWriteableBitmap(PixelBuffer編集)ピクセルに直接描けるため、出力画像が1枚で完結する
検出枠を後から動かす・編集する・表示ON/OFFしたいCanvasで重ね表示画像を毎回再生成せずにUI要素として管理でき、変更が速い
キャプチャ直後のフレームを解析したい(色変換やモデル入力整形)SoftwareBitmap中心+表示は別手段画像処理APIやカメラ/フレームパイプラインと相性がよい

WriteableBitmapとSoftwareBitmapの違いを押さえる

両者は似た名前でも役割が異なります。UWPで「画像を扱う」ときの基礎として、まずは用途の住み分けを押さえておくと迷いが減ります。

観点WriteableBitmapSoftwareBitmap
主な居場所XAML表示寄り(Image.Sourceに直接渡せる)画像処理・入出力寄り(Windows.Graphics.Imaging)
描画(ピクセル編集)PixelBuffer(IBuffer)を編集して反映(Invalidate)LockBufferでBitmapBufferを取得して編集(形式・手順がやや複雑)
ピクセル形式基本はBGRA8(アルファは多くの場合プリマルチ)BGRA8/NV12/Gray8など柔軟(カメラはNV12になりやすい)
表示そのままImageに表示できるSoftwareBitmapSource等に変換して表示するのが一般的
向いている場面UIに出す/焼き付け画像を作る/軽いピクセル描画デコード/エンコード、色変換、解析パイプライン(フレーム処理)

質問の主目的が「キャプチャ画像に図形を描いてUIに表示したい」なら、まずは図形を画像に含めるか(焼き付け)、UIとして重ねるかを決めるのが最短です。

Canvasは必要?必須ではないが、重ね表示の第一候補

Canvasは「絶対に必要」ではありません。画像の上にUI要素を重ねられるレイアウトであれば、GridでもRelativePanelでも成立します。ただし、矩形やポリゴンのように座標で配置する図形を扱うなら、Canvasは次の理由で扱いやすいです。

  • Canvas.Left / Canvas.Topで座標指定ができ、検出結果(ピクセル座標)と考え方が近い
  • 複数の図形を重ねても、Z順(重なり)が直感的
  • 後から動かす、編集する、選択する、といった拡張がしやすい

逆に「焼き付け画像を1枚作ってImageに出すだけ」ならCanvas自体は不要です。WriteableBitmapをImage.Sourceにセットすれば完了します。

パターンA:CanvasでRectangle/Polygonを“重ねて”表示する(おすすめ)

UIとして最も作りやすいのがこの方式です。画像は画像として表示し、図形は図形として別レイヤーで表示します。検出枠の更新頻度が高い、枠の色や太さを切り替えたい、ユーザーが枠をドラッグして調整するといった要件に強いです。

基本レイアウト(Image + Canvasの2層)

よくあるのはGridに重ねる形です。Canvasは透明で、上から図形だけ描きます。

<Grid>
  <Image x:Name="CapturedImage"
         Stretch="Uniform"
         HorizontalAlignment="Stretch"
         VerticalAlignment="Stretch" />


Rectangleを追加する例(検出枠)

そのままCanvasに置くと「ズレ」が起きやすいので、まずは素直な追加例を見たうえで、次の節の座標変換とセットで使ってください。

using Windows.UI;
using Windows.UI.Xaml.Shapes;
using Windows.UI.Xaml.Media;

private void AddBoxOnCanvas(double x, double y, double w, double h)
{
var rect = new Rectangle
{
Width = w,
Height = h,
Stroke = new SolidColorBrush(Colors.Lime),
StrokeThickness = 2
};


Canvas.SetLeft(rect, x);
Canvas.SetTop(rect, y);

OverlayCanvas.Children.Add(rect);


}

Polygonを追加する例(多角形)

using System.Collections.Generic;
using Windows.Foundation;

private void AddPolygonOnCanvas(IReadOnlyList points)
{
var poly = new Windows.UI.Xaml.Shapes.Polygon
{
Stroke = new SolidColorBrush(Colors.DeepSkyBlue),
StrokeThickness = 2
};


foreach (var p in points)
{
    poly.Points.Add(p);
}

OverlayCanvas.Children.Add(poly);


}

最大の落とし穴:Imageの表示サイズとピクセル座標がズレる

多くのUIでは Image.Stretch="Uniform" を使うため、画像はコントロールの縦横比に合わせて縮小・拡大され、余白(レターボックス)が出ます。検出結果が「画像ピクセル座標」なのに、そのままCanvasに置くと枠がズレます。

対策は、元画像サイズ(PixelWidth/PixelHeight)とImageの表示領域(ActualWidth/ActualHeight)から、スケールとオフセットを計算して座標変換することです。

値意味取得例
srcW, srcH元画像のピクセル幅・高さWriteableBitmap.PixelWidth / PixelHeight
viewW, viewHImageコントロールの表示領域CapturedImage.ActualWidth / ActualHeight
scaleUniform時の拡大率min(viewW/srcW, viewH/srcH)
offsetX, offsetY余白分のズレ(viewW – srcW*scale)/2 など

変換をまとめた例です。表示リサイズに追従させるため、ImageのSizeChangedやLoaded後に再計算すると安定します。

using System;
using Windows.Foundation;

private (double scale, double offsetX, double offsetY) GetUniformTransform(
double srcW, double srcH, double viewW, double viewH)
{
var scale = Math.Min(viewW / srcW, viewH / srcH);
var drawnW = srcW * scale;
var drawnH = srcH * scale;


var offsetX = (viewW - drawnW) / 2.0;
var offsetY = (viewH - drawnH) / 2.0;

return (scale, offsetX, offsetY);


}

private Point ToViewPoint(Point p, double scale, double offsetX, double offsetY)
{
return new Point(offsetX + p.X * scale, offsetY + p.Y * scale);
}

矩形を置くときは、(x,y,w,h) を変換してからCanvasに渡します。

private void AddBoxFromPixelCoords(double x, double y, double w, double h,
    double srcW, double srcH)
{
    var (scale, ox, oy) = GetUniformTransform(
        srcW, srcH,
        CapturedImage.ActualWidth, CapturedImage.ActualHeight);


var rect = new Rectangle
{
    Width = w * scale,
    Height = h * scale,
    Stroke = new SolidColorBrush(Colors.Lime),
    StrokeThickness = 2
};

Canvas.SetLeft(rect, ox + x * scale);
Canvas.SetTop(rect,  oy + y * scale);

OverlayCanvas.Children.Add(rect);


}

ポリゴンも各PointをToViewPointで変換してから追加すれば、表示が縮小されても枠がピタッと一致します。

検出結果が「0〜1の正規化座標」で返ってくる場合

モデルやライブラリによっては、座標がピクセルではなく「画像幅・高さで割った値(0〜1)」で返ることがあります。その場合は、いったんピクセル座標へ戻してから上の変換にかけると混乱しません。

// 例:正規化座標(0〜1) → ピクセル座標
private static (double x, double y, double w, double h) NormToPixel(
    double nx, double ny, double nw, double nh, double srcW, double srcH)
{
    return (nx * srcW, ny * srcH, nw * srcW, nh * srcH);
}

重ね表示方式が強いケース

  • 検出枠が毎フレーム更新される(フレーム解析、物体追跡など)
  • 枠をタップして詳細表示、色分け、ラベル表示をしたい
  • 拡大縮小(Zoom)や回転に追従させたい

パターンB:WriteableBitmapのPixelBufferに図形を“焼き付ける”

「最終画像として保存したい」「他アプリに共有したい」「画像1枚だけを渡したい」という要件なら、図形をピクセルとして描き込み、WriteableBitmapを完成画像として扱うのが分かりやすいです。

やっていること(全体像)

  • 画像ファイルを読み込み → WriteableBitmapを作成 → SetSourceAsync()で画像を入れる
  • WriteableBitmap.PixelBufferをストリームとして開き、BGRAの4バイトを書き換えて線を描く
  • 描画後にInvalidate()して表示を更新し、Image.Sourceに設定する

画像ファイルからWriteableBitmapを作る

using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Media.Imaging;

public static async Task LoadWriteableBitmapAsync(StorageFile file)
{
using (IRandomAccessStream stream = await file.OpenReadAsync())
{
// 1x1で作ってSetSourceAsyncで実サイズに展開するのが定番
var wb = new WriteableBitmap(1, 1);
await wb.SetSourceAsync(stream);
return wb;
}
}

PixelBufferの読み書き(BGRAの基本)

WriteableBitmapのPixelBufferは、1ピクセルあたり4バイト(B, G, R, A)の並びです。たとえば不透明な緑の線を引くなら、(B=0, G=255, R=0, A=255) を書き込みます。

まずは「指定座標の1ピクセルを塗る」関数を用意します。境界チェックを入れておくと事故が減ります。

using System;
using System.Runtime.InteropServices.WindowsRuntime;

private static void SetPixelBgra(byte[] pixels, int width, int height,
int x, int y, byte b, byte g, byte r, byte a)
{
if (x < 0 || x >= width || y < 0 || y >= height) return;


int index = (y * width + x) * 4;
pixels[index + 0] = b;
pixels[index + 1] = g;
pixels[index + 2] = r;
pixels[index + 3] = a;


}

線を引く(Bresenham風のシンプル実装)

矩形やポリゴンは「線分の集合」です。そこでまず線分描画を用意すると、矩形も多角形も一気に作れます。

private static void DrawLine(byte[] pixels, int width, int height,
    int x0, int y0, int x1, int y1,
    byte b, byte g, byte r, byte a, int thickness = 1)
{
    int dx = Math.Abs(x1 - x0);
    int sx = x0 < x1 ? 1 : -1;
    int dy = -Math.Abs(y1 - y0);
    int sy = y0 < y1 ? 1 : -1;
    int err = dx + dy;


while (true)
{
    // 太さを簡易的に正方形で表現(見栄え優先なら別手段推奨)
    for (int ty = -thickness / 2; ty <= thickness / 2; ty++)
    for (int tx = -thickness / 2; tx <= thickness / 2; tx++)
        SetPixelBgra(pixels, width, height, x0 + tx, y0 + ty, b, g, r, a);

    if (x0 == x1 && y0 == y1) break;
    int e2 = 2 * err;
    if (e2 >= dy) { err += dy; x0 += sx; }
    if (e2 <= dx) { err += dx; y0 += sy; }
}


}

矩形の枠線を描く

private static void DrawRectOutline(byte[] pixels, int width, int height,
    int x, int y, int w, int h,
    byte b, byte g, byte r, byte a, int thickness = 2)
{
    if (w <= 0 || h <= 0) return;


// 終点は「最後のピクセル」になるよう -1 しておく
int x2 = x + w - 1;
int y2 = y + h - 1;

DrawLine(pixels, width, height, x,  y,  x2, y,  b, g, r, a, thickness); // 上
DrawLine(pixels, width, height, x2, y,  x2, y2, b, g, r, a, thickness); // 右
DrawLine(pixels, width, height, x2, y2, x,  y2, b, g, r, a, thickness); // 下
DrawLine(pixels, width, height, x,  y2, x,  y,  b, g, r, a, thickness); // 左


}

ポリゴン(多角形)を描く

点列を順番につないで、最後に始点へ戻します。

using System.Collections.Generic;
using Windows.Foundation;

private static void DrawPolygonOutline(byte[] pixels, int width, int height,
IReadOnlyList points,
byte b, byte g, byte r, byte a, int thickness = 2)
{
if (points == null || points.Count < 2) return;


for (int i = 0; i < points.Count; i++)
{
    var p0 = points[i];
    var p1 = points[(i + 1) % points.Count];

    DrawLine(pixels, width, height,
        (int)Math.Round(p0.X), (int)Math.Round(p0.Y),
        (int)Math.Round(p1.X), (int)Math.Round(p1.Y),
        b, g, r, a, thickness);
}


}

WriteableBitmapに反映して表示する(完成形)

PixelBufferを一度配列に読み出して編集し、書き戻してInvalidateします。図形が複数ある場合も、同じ配列上で連続して描けばOKです。

using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.UI.Xaml.Media.Imaging;

private async Task DrawAndShowAsync(StorageFile file)
{
var wb = await LoadWriteableBitmapAsync(file);


using (var stream = wb.PixelBuffer.AsStream())
{
    int length = (int)stream.Length;
    var pixels = new byte[length];

    // PixelBufferは基本的に一括で読めますが、念のため読み切りループにしておくと安全
    int read = 0;
    while (read < length)
    {
        int r = await stream.ReadAsync(pixels, read, length - read);
        if (r == 0) break;
        read += r;
    }

    // 例:矩形1つ(不透明な緑)
    DrawRectOutline(pixels, wb.PixelWidth, wb.PixelHeight,
        x: 80, y: 60, w: 300, h: 200,
        b: 0, g: 255, r: 0, a: 255, thickness: 3);

    // 例:ポリゴン1つ(不透明なオレンジ)
    var poly = new List<Windows.Foundation.Point>
    {
        new Windows.Foundation.Point(120, 120),
        new Windows.Foundation.Point(260, 90),
        new Windows.Foundation.Point(340, 200),
        new Windows.Foundation.Point(180, 240)
    };
    DrawPolygonOutline(pixels, wb.PixelWidth, wb.PixelHeight,
        poly, b: 0, g: 200, r: 255, a: 255, thickness: 3);

    stream.Seek(0, SeekOrigin.Begin);
    await stream.WriteAsync(pixels, 0, pixels.Length);
}

wb.Invalidate();
CapturedImage.Source = wb; // Imageに表示


}

アルファと色が薄く見えるときの考え方

WriteableBitmapは内部的にプリマルチプライドアルファ(PBGRA)で扱われることが多く、半透明色をそのまま書くと見え方が想定とズレることがあります。枠線を完全不透明(A=255)で描く限りは問題になりにくいですが、半透明の塗りを入れたい場合は「RGBをAで乗算した値」を書く前提で設計すると安全です。

やりたい表現注意点回避策
半透明の枠線RGBがアルファに追従しないと白っぽく/薄く見えることがあるRGBをA/255で乗算した値を書き込む(プリマルチ前提)
斜め線を綺麗にしたいピクセル描画だとギザギザになりやすいUI重ね表示、またはWin2D等のベクター描画を検討

焼き付けた画像をファイルとして保存する(PNG/JPEG)

焼き付け方式の価値は「完成画像を保存できる」点です。保存はBitmapEncoderで行います。ここではPNGで保存する例を示します。

using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;

public static async Task SaveAsPngAsync(WriteableBitmap wb, StorageFile outFile)
{
using (IRandomAccessStream stream = await outFile.OpenAsync(FileAccessMode.ReadWrite))
{
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);


    byte[] pixels;
    using (var pixelStream = wb.PixelBuffer.AsStream())
    {
        int length = (int)pixelStream.Length;
        pixels = new byte[length];

        int read = 0;
        while (read < length)
        {
            int r = await pixelStream.ReadAsync(pixels, read, length - read);
            if (r == 0) break;
            read += r;
        }
    }

    encoder.SetPixelData(
        BitmapPixelFormat.Bgra8,
        BitmapAlphaMode.Premultiplied,
        (uint)wb.PixelWidth,
        (uint)wb.PixelHeight,
        96, 96,
        pixels);

    await encoder.FlushAsync();
}


}

JPEGにする場合はエンコーダIDをBitmapEncoder.JpegEncoderIdにします。透過を含めたいならPNGが無難です。

SoftwareBitmapが向いているケースと、表示までの現実的な流れ

キャプチャが「カメラフレーム」「動画フレーム」「Direct3Dサーフェス」などから来る場合、最初からSoftwareBitmapとして受け取る(または変換する)流れが自然です。SoftwareBitmapは解析処理(色変換、回転補正、モデル入力への整形)で強みがあります。

SoftwareBitmapをUIに表示する方法(SoftwareBitmapSource)

SoftwareBitmapはそのままImage.Sourceに入れられないため、SoftwareBitmapSourceにセットして表示します。

using System.Threading.Tasks;
using Windows.Graphics.Imaging;
using Windows.UI.Xaml.Media.Imaging;

private readonly SoftwareBitmapSource _sbSource = new SoftwareBitmapSource();

private async Task ShowSoftwareBitmapAsync(SoftwareBitmap sb)
{
// BGRA8 + Premultiplied に揃えると表示が安定しやすい
SoftwareBitmap sbForDisplay = sb;
if (sb.BitmapPixelFormat != BitmapPixelFormat.Bgra8 || sb.BitmapAlphaMode != BitmapAlphaMode.Premultiplied)
{
sbForDisplay = SoftwareBitmap.Convert(sb, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}


await _sbSource.SetBitmapAsync(sbForDisplay);
CapturedImage.Source = _sbSource;


}

SoftwareBitmapで解析 → 結果はCanvasで重ねる、が実務では一番ラク

SoftwareBitmapを選ぶ場面の多くは「画像処理や解析が主目的」です。その場合、描画までSoftwareBitmapに寄せるよりも、

  • 解析はSoftwareBitmapで行う
  • 表示はImage(SoftwareBitmapSourceまたはWriteableBitmap)で行う
  • 枠やポリゴンはCanvasで重ねる

という分業がトータルで安定します。焼き付けが必要になったタイミングだけWriteableBitmapへ変換して描き込む、という段階的な設計がしやすいからです。

SoftwareBitmap → WriteableBitmapに変換して焼き付けたいとき

解析結果を画像として保存するタイミングでは、WriteableBitmapに寄せるのが手っ取り早いです。一方で、毎フレーム焼き付けを行うと負荷が跳ね上がります。更新頻度が高い場合は、焼き付けではなく重ね表示方式に寄せる方が現実的です。

実務で迷わない判断フロー(チェックリスト)

最後に、迷いを最小化するための判断フローをまとめます。

チェック項目YesならNoなら
図形を含めた完成画像を保存/共有したい?WriteableBitmapに焼き付けCanvasで重ね表示
枠が頻繁に更新される(動画/追跡)?Canvas重ね表示(焼き付けは重い)どちらでもOK
枠をユーザーが編集する(ドラッグ/選択)?Canvas(UI要素として扱う)焼き付けでも可
入力がカメラ/フレームでSoftwareBitmapが主流?解析はSoftwareBitmap、表示は重ね表示が無難静止画中心ならWriteableBitmapでも十分

よくある落とし穴と対策

枠がズレる(Stretchによる座標変換漏れ)

Uniform/UniformToFill/Fillで計算が変わります。まずはUIのStretchを固定し、その前提で変換するのがコツです。迷ったらUniformを基準にし、scaleとoffsetで合わせてください。

焼き付けが遅い(毎回全ピクセルを読み書きしている)

PixelBufferを毎回配列にコピーして更新すると、それだけでコストがかかります。枠が頻繁に動く用途(リアルタイム解析)なら、焼き付け方式は不利です。表示はCanvas重ねに寄せ、保存が必要な瞬間だけ焼き付け画像を生成する設計にするとスムーズです。

線がギザギザで見栄えが悪い

ピクセルでの線描画はアンチエイリアスが難しく、斜め線や小さな多角形が目立ちます。見栄えが重要なら、XAMLのShape(Rectangle/Polygon)で重ねるか、Win2Dのような描画ライブラリでベクター描画する方が品質を上げやすいです。

画像が回転している(EXIFやカメラの向き)

キャプチャ元によっては、画像データの向きと表示の向きが一致しないことがあります。解析結果が「回転前の座標」で返ってくるケースもあるため、回転補正(表示側で回転しているのか、データ側で回転しているのか)を整理し、座標変換のどこで吸収するか決めておくと事故が減ります。

まとめ

  • 焼き付け(最終画像として保存したい)なら、WriteableBitmapのPixelBufferを書き換えて描画する
  • 重ね表示(動かす/編集する/頻繁に更新)なら、Image + CanvasでRectangle/Polygonを重ねる
  • Canvasは必須ではないが、座標で配置する図形の管理が楽で、結果的に保守性が高い
  • SoftwareBitmapは解析・入出力の主役。表示やUI重ねとは役割分担すると安定する

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次