Search
Duplicate

C#でUWPデスクトップアプリを作る 4/5: 画像合成-背景と写真を組み合わせる

Published On
2024/09/12
Lang
JP
Tags
Programming
Window Development
UWP

シリーズ

こんにちは!今回の投稿では、カメラで撮影した写真を背景画像の透明な部分に合成する方法について説明します。撮影された写真を背景の透明な部分に比率を維持したまま収まるようにリサイズし、中央に配置する方法を実装していきます。
前回の記事では写真撮影とカウントダウン機能を実装しました。今回は背景画像の透明な部分を見つけ、そこに撮影した画像を正確に合成していきます。

1. 必要なパッケージと権限設定

今回の投稿で使用するパッケージと権限設定は以下の通りです。

1.1 OpenCvSharp4パッケージのインストール

まず、OpenCVSharpライブラリを使用して画像を処理するために、OpenCvSharp4.runtime.winOpenCvSharp4.Extensionsパッケージをインストールします。これにより、画像合成作業をOpenCVSharpの強力な画像処理機能を使用して行うことができます。
1.
NuGetパッケージマネージャーOpenCvSharp4.runtime.winOpenCvSharp4.Extensionsを検索してインストールします。
2.
プロジェクトにOpenCVSharpライブラリを追加して、画像処理作業を進めることができます。

1.2 ピクチャライブラリの権限追加

背景画像を読み込むためにピクチャライブラリからファイルにアクセスできる必要があります。そのため、アプリのPackage.appxmanifestファイルにピクチャライブラリのアクセス権限を追加する必要があります。
1.
Package.appxmanifestファイルを開き、Capabilitiesタブを選択します。
2.
「Pictures Library」にチェックを入れて、アプリがユーザーの写真ライブラリにアクセスできるように設定します。
<Capabilities> <Capability Name="internetClient" /> <uap:Capability Name="picturesLibrary" /> </Capabilities>
XML
복사

2. 全体のコード

2.1 背景画像と撮影された写真の結合フロー

この投稿の核心は、撮影された画像を背景画像の透明な部分に合わせて比率を維持しながらリサイズし、中央に配置することです。その後、背景画像に写真を合成して1つの画像として出力します。

全体のコード

using OpenCvSharp; using OpenCvSharp.Extensions; using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using Windows.Media.Capture; using Windows.Storage; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Media.Imaging; namespace PhotoBooth { public sealed partial class MainPage : Page { private MediaCapture _mediaCapture; private bool _isPreviewing = true; public MainPage() { this.InitializeComponent(); EnterFullScreenMode(); InitializeCameraAsync(); } private void EnterFullScreenMode() { var view = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView(); view.TryEnterFullScreenMode(); Windows.UI.ViewManagement.ApplicationView.PreferredLaunchWindowingMode = Windows.UI.ViewManagement.ApplicationViewWindowingMode.FullScreen; } private async void InitializeCameraAsync() { _mediaCapture = new MediaCapture(); await _mediaCapture.InitializeAsync(); CameraPreview.Source = _mediaCapture; await _mediaCapture.StartPreviewAsync(); } private async void CameraPreview_Tapped(object sender, Windows.UI.Xaml.Input.TappedRoutedEventArgs e) { if (_isPreviewing) { await StartCountdownAndTakePhoto(); } } private async Task StartCountdownAndTakePhoto() { try { CountdownText.Visibility = Visibility.Visible; for (int i = 2; i > 0; i--) { CountdownText.Text = i.ToString(); await Task.Delay(1000); } CountdownText.Visibility = Visibility.Collapsed; await TakePhotoAndCompositeWithOpenCVAsync(); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"StartCountdownAndTakePhotoでエラーが発生しました: {ex.Message}"); } } public static Mat WriteableBitmapToMat(WriteableBitmap wb) { // WriteableBitmapのピクセルデータを配列として取得 using (var stream = wb.PixelBuffer.AsStream()) { byte[] pixels = new byte[stream.Length]; stream.Read(pixels, 0, pixels.Length); // OpenCVのMatオブジェクトを作成(BGRAフォーマット) Mat mat = new Mat(wb.PixelHeight, wb.PixelWidth, MatType.CV_8UC4); // 8UC4: 8ビット符号なし4チャンネル画像(BGRA) // MatのメモリバッファにWriteableBitmapのピクセルデータをコピー Marshal.Copy(pixels, 0, mat.Data, pixels.Length); return mat; } } public static WriteableBitmap MatToWriteableBitmap(Mat mat) { WriteableBitmap wb = new WriteableBitmap(mat.Width, mat.Height); using (Stream stream = wb.PixelBuffer.AsStream()) { byte[] pixels = new byte[mat.Width * mat.Height * mat.ElemSize()]; Marshal.Copy(mat.Data, pixels, 0, pixels.Length); stream.Write(pixels, 0, pixels.Length); } return wb; } private async Task<Mat> LoadAndResizeBackgroundImageAsync(StorageFile backgroundFile, int targetWidth, int targetHeight) { // 背景画像を読み込む Mat backgroundMat; using (Stream backgroundStream = await backgroundFile.OpenStreamForReadAsync()) { backgroundMat = Mat.FromStream(backgroundStream, ImreadModes.Unchanged); // Unchanged: 透明度情報を保持 } // 背景画像を目的のサイズにリサイズ if (backgroundMat.Width > targetWidth || backgroundMat.Height > targetHeight) { Cv2.Resize(backgroundMat, backgroundMat, new OpenCvSharp.Size(targetWidth, targetHeight)); } return backgroundMat; } private async Task TakePhotoAndCompositeWithOpenCVAsync() { try { // 写真撮影 var renderTargetBitmap = new RenderTargetBitmap(); await renderTargetBitmap.RenderAsync(CameraPreview); var pixelBuffer = await renderTargetBitmap.GetPixelsAsync(); var capturedBitmap = new WriteableBitmap(renderTargetBitmap.PixelWidth, renderTargetBitmap.PixelHeight); using (var stream = capturedBitmap.PixelBuffer.AsStream()) { await stream.WriteAsync(pixelBuffer.ToArray(), 0, (int)pixelBuffer.Length); } // WriteableBitmapをOpenCVのMatに変換 Mat capturedMat = WriteableBitmapToMat(capturedBitmap); // 画像フォルダから背景画像を読み込み、サイズ変更 var picturesFolder = await StorageFolder.GetFolderFromPathAsync(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)); var backgroundFile = await picturesFolder.GetFileAsync("background.png"); // 背景画像の読み込みとリサイズ(例:1920x1080にリサイズ) Mat backgroundMat = await LoadAndResizeBackgroundImageAsync(backgroundFile, 1920, 1080); // 透明部分をマスクとして抽出(アルファチャンネル値が0の部分) Mat[] bgChannels = new Mat[4]; Cv2.Split(backgroundMat, out bgChannels); Mat alphaChannel = bgChannels[3]; // 透明部分を二値化(アルファ値が0の箇所を白、それ以外を黒に) Mat transparentMask = new Mat(); Cv2.Threshold(alphaChannel, transparentMask, 0, 255, ThresholdTypes.BinaryInv); // 連結成分のラベリングで透明領域を探す Mat labels = new Mat(); Mat stats = new Mat(); Mat centroids = new Mat(); int numLabels = Cv2.ConnectedComponentsWithStats(transparentMask, labels, stats, centroids); // 最大の透明領域を探す int largestLabel = 0; int largestArea = 0; for (int i = 1; i < numLabels; i++) // 0番目のラベルは背景なので除外 { int area = stats.At<int>(i, 4); // Areaは4番目のインデックス if (area > largestArea) { largestArea = area; largestLabel = i; } } // 最大領域の座標を取得 int x = stats.At<int>(largestLabel, 0); // Left (0番目のインデックス) int y = stats.At<int>(largestLabel, 1); // Top (1番目のインデックス) int width = stats.At<int>(largestLabel, 2); // Width (2番目のインデックス) int height = stats.At<int>(largestLabel, 3); // Height (3番目のインデックス) // アスペクト比を保持しながら撮影された画像を透明領域に合わせてリサイズ(透明領域いっぱいに) double aspectRatio = (double)capturedMat.Width / capturedMat.Height; int newWidth, newHeight; // 透明領域の比率と比較して、小さい方の比率に合わせてリサイズ if ((double)width / height > aspectRatio) { newWidth = width; newHeight = (int)(width / aspectRatio); // 幅に合わせる } else { newHeight = height; newWidth = (int)(aspectRatio * height); // 高さに合わせる } // 撮影された画像を該当サイズにリサイズ Mat resizedCapturedMat = new Mat(); Cv2.Resize(capturedMat, resizedCapturedMat, new OpenCvSharp.Size(newWidth, newHeight)); // 中央揃えのためのオフセット計算(はみ出る部分を切り取る) int offsetX = x - (newWidth - width) / 2; // はみ出る部分の半分を左に移動 int offsetY = y - (newHeight - height) / 2; // はみ出る部分の半分を上に移動 // 透明領域に中央配置 for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { int srcY = i + (newHeight - height) / 2; int srcX = j + (newWidth - width) / 2; if (srcY >= 0 && srcY < resizedCapturedMat.Height && srcX >= 0 && srcX < resizedCapturedMat.Width) { Vec3b fgPixel = resizedCapturedMat.At<Vec3b>(srcY, srcX); // 撮影された画像のピクセル(BGR) backgroundMat.Set(y + i, x + j, new Vec4b(fgPixel[0], fgPixel[1], fgPixel[2], 255)); // 背景画像に合成 } } } // MatをWriteableBitmapに変換 WriteableBitmap resultBitmap = MatToWriteableBitmap(backgroundMat); // 合成された画像を表示 CapturedImage.Source = resultBitmap; CapturedImage.Visibility = Visibility.Visible; CameraPreview.Visibility = Visibility.Collapsed; // ボタンを表示 ButtonPanel.Opacity = 1; // リソースの解放 capturedMat.Dispose(); resizedCapturedMat.Dispose(); backgroundMat.Dispose(); transparentMask.Dispose(); labels.Dispose(); stats.Dispose(); centroids.Dispose(); _isPreviewing = false; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"TakePhotoAndCompositeWithOpenCVAsyncでエラーが発生しました: {ex.Message}"); } } private void RetakeButton_Click(object sender, RoutedEventArgs e) { ButtonPanel.Opacity = 0; CapturedImage.Visibility = Visibility.Collapsed; CameraPreview.Visibility = Visibility.Visible; _isPreviewing = true; } private void PrintButton_Click(object sender, RoutedEventArgs e) { // 印刷機能の実装予定 } } }
C#
복사

3. コードの説明

3.1 WriteableBitmapToMatおよびMatToWriteableBitmap

これらの関数は、UWPで使用されるWriteableBitmapとOpenCVのMat間の変換を処理する役割を果たします。画像を処理する際にはOpenCVのMat形式に変換し、最終的にUIに表示する際には再びWriteableBitmapに変換します。

3.2 背景画像の読み込みと透明部分の検出

LoadAndResizeBackgroundImageAsync関数は背景画像を読み込み、サイズを調整します。その後、背景画像からアルファチャンネルを抽出して透明部分をマスクとして生成し、透明な領域の中で最大の領域を見つける作業を行います。

3.3 撮影された写真のリサイズと中央配置

TakePhotoAndCompositeWithOpenCVAsync関数は、撮影された画像を透明な領域に比率を維持したまま収まるようにリサイズし、中央に配置する役割を果たします。リサイズされた画像を透明な領域の中央に正確に配置し、その領域に合わせて画像を合成します。

4. まとめ

今回の投稿では、背景画像の透明な部分に合わせて撮影された画像を合成する方法を学びました。OpenCVSharpの強力な画像処理機能を使用して、比率を維持したままリサイズし、中央に配置する方法を実装しました。次回の投稿では、印刷機能を追加してアプリを完成させます。
次回予告:
C#でUWPデスクトップアプリを作る 5/5:印刷機能を追加してアプリを完成させる

他の言語で読む:

著者をサポートする:

私の記事を楽しんでいただけたら、一杯のコーヒーで応援してください!
Search
September 2024
Today
S
M
T
W
T
F
S