Search
Duplicate

C#으로 UWP 데스크톱 앱 만들기 4/5: 이미지 합성 - 배경과 사진 결합

Published On
2024/09/12
Lang
KR
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 Picture Library 권한 추가

배경 이미지를 로드하기 위해 Picture Library에서 파일에 접근할 수 있어야 합니다. 이를 위해 앱의 Package.appxmanifest 파일에 Picture Library 접근 권한을 추가해야 합니다.
1.
Package.appxmanifest 파일을 열고 Capabilities 탭을 선택합니다.
2.
"Pictures Library"에 체크하여 앱이 사용자 사진 라이브러리에 접근할 수 있도록 설정합니다.
<Capabilities> <Capability Name="internetClient" /> <uap:Capability Name="picturesLibrary" /> </Capabilities>
XML
복사

2. 전체 코드

2.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($"Error in 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-bit unsigned 4-channel image (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($"Error in 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 WriteableBitmapToMatMatToWriteableBitmap

이 함수들은 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