비트맵 이미지 만들기, 편집 및 저장

이 문서에서는 BitmapDecoderBitmapEncoder를 사용하여 이미지 파일을 로드하고 저장하는 방법과 SoftwareBitmap 개체를 사용하여 비트맵 이미지를 나타내는 방법을 설명합니다.

SoftwareBitmap 클래스는 이미지 파일, WriteableBitmap 개체, Direct3D 표면 및 코드를 비롯한 여러 원본에서 만들 수 있는 다양한 API입니다. SoftwareBitmap을 사용하면 다양한 픽셀 형식과 알파 모드 간에 쉽게 변환할 수 있으며 픽셀 데이터에 대한 낮은 수준의 액세스를 허용합니다. 또한 SoftwareBitmap은 다음을 포함하여 Windows의 여러 기능에서 사용하는 일반적인 인터페이스입니다.

  • CapturedFrame - 카메라로 캡처된 프레임을 SoftwareBitmap으로 가져올 수 있습니다.

  • VideoFrame - VideoFrameSoftwareBitmap 표현을 가져올 수 있습니다.

  • FaceDetector - SoftwareBitmap에서 얼굴을 검색할 수 있습니다.

이 문서의 샘플 코드는 다음 네임스페이스의 API를 사용합니다.

using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.Graphics.Imaging;
using Windows.UI.Xaml.Media.Imaging;

BitmapDecoder를 사용하여 이미지 파일에서 SoftwareBitmap 만들기

파일에서 SoftwareBitmap을 만들려면 이미지 데이터가 포함된 StorageFile 인스턴스를 가져옵니다. 이 예제에서는 FileOpenPicker를 사용하여 사용자가 이미지 파일을 선택할 수 있도록 합니다.

FileOpenPicker fileOpenPicker = new FileOpenPicker();
fileOpenPicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileOpenPicker.FileTypeFilter.Add(".jpg");
fileOpenPicker.ViewMode = PickerViewMode.Thumbnail;

var inputFile = await fileOpenPicker.PickSingleFileAsync();

if (inputFile == null)
{
    // The user cancelled the picking operation
    return;
}

StorageFile 개체의 OpenAsync 메서드를 호출하여 이미지 데이터를 포함하는 임의 액세스 스트림을 가져옵니다. 정적 메서드 BitmapDecoder.CreateAsync 를 호출하여 지정된 스트림에 대한 BitmapDecoder 클래스의 인스턴스를 가져옵니다. GetSoftwareBitmapAsync를 호출하여 이미지가 포함된 SoftwareBitmap 개체를 가져옵니다.

SoftwareBitmap softwareBitmap;

using (IRandomAccessStream stream = await inputFile.OpenAsync(FileAccessMode.Read))
{
    // Create the decoder from the stream
    BitmapDecoder decoder = await BitmapDecoder.CreateAsync(stream);

    // Get the SoftwareBitmap representation of the file
    softwareBitmap = await decoder.GetSoftwareBitmapAsync();
}

BitmapEncoder를 사용하여 파일에 SoftwareBitmap 저장

SoftwareBitmap을 파일에 저장하려면 이미지를 저장할 StorageFile 인스턴스를 가져옵니다. 이 예제에서는 FileSavePicker를 사용하여 사용자가 출력 파일을 선택할 수 있도록 합니다.

FileSavePicker fileSavePicker = new FileSavePicker();
fileSavePicker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
fileSavePicker.FileTypeChoices.Add("JPEG files", new List<string>() { ".jpg" });
fileSavePicker.SuggestedFileName = "image";

var outputFile = await fileSavePicker.PickSaveFileAsync();

if (outputFile == null)
{
    // The user cancelled the picking operation
    return;
}

StorageFile 개체의 OpenAsync 메서드를 호출하여 이미지를 쓸 임의 액세스 스트림을 가져옵니다. 정적 메서드 BitmapEncoder.CreateAsync 를 호출하여 지정된 스트림에 대한 BitmapEncoder 클래스의 인스턴스를 가져옵니다. CreateAsync의 첫 번째 매개 변수는 이미지를 인코딩하는 데 사용해야 하는 코덱을 나타내는 GUID입니다. BitmapEncoder 클래스는 JpegEncoderId와 같이 인코더에서 지원하는 각 코덱의 ID를 포함하는 속성을 노출합니다.

SetSoftwareBitmap 메서드를 사용하여 인코딩할 이미지를 설정합니다. 인코딩되는 동안 이미지에 기본 변환을 적용하도록 BitmapTransform 속성의 값을 설정할 수 있습니다. IsThumbnailGenerated 속성은 썸네일이 인코더에 의해 생성되는지 여부를 결정합니다. 모든 파일 형식이 썸네일을 지원하는 것은 아니므로 이 기능을 사용하는 경우 썸네일이 지원되지 않는 경우 throw되는 지원되지 않는 작업 오류를 catch해야 합니다.

FlushAsync를 호출하여 인코더가 이미지 데이터를 지정된 파일에 쓰게 합니다.

private async void SaveSoftwareBitmapToFile(SoftwareBitmap softwareBitmap, StorageFile outputFile)
{
    using (IRandomAccessStream stream = await outputFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        // Create an encoder with the desired format
        BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.JpegEncoderId, stream);

        // Set the software bitmap
        encoder.SetSoftwareBitmap(softwareBitmap);

        // Set additional encoding parameters, if needed
        encoder.BitmapTransform.ScaledWidth = 320;
        encoder.BitmapTransform.ScaledHeight = 240;
        encoder.BitmapTransform.Rotation = Windows.Graphics.Imaging.BitmapRotation.Clockwise90Degrees;
        encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
        encoder.IsThumbnailGenerated = true;

        try
        {
            await encoder.FlushAsync();
        }
        catch (Exception err)
        {
            const int WINCODEC_ERR_UNSUPPORTEDOPERATION = unchecked((int)0x88982F81);
            switch (err.HResult)
            {
                case WINCODEC_ERR_UNSUPPORTEDOPERATION: 
                    // If the encoder does not support writing a thumbnail, then try again
                    // but disable thumbnail generation.
                    encoder.IsThumbnailGenerated = false;
                    break;
                default:
                    throw;
            }
        }

        if (encoder.IsThumbnailGenerated == false)
        {
            await encoder.FlushAsync();
        }


    }
}

BitmapPropertySet 개체를 만들고 인코더 설정을 나타내는 하나 이상의 BitmapTypedValue 개체로 채워 BitmapEncoder를 만들 때 추가 인코딩 옵션을 지정할 수 있습니다. 지원되는 인코더 옵션 목록은 BitmapEncoder 옵션 참조를 참조하세요.

var propertySet = new Windows.Graphics.Imaging.BitmapPropertySet();
var qualityValue = new Windows.Graphics.Imaging.BitmapTypedValue(
    1.0, // Maximum quality
    Windows.Foundation.PropertyType.Single
    );

propertySet.Add("ImageQuality", qualityValue);

await Windows.Graphics.Imaging.BitmapEncoder.CreateAsync(
    Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId,
    stream,
    propertySet
);

XAML 이미지 컨트롤과 함께 SoftwareBitmap 사용

이미지 컨트롤을 사용하여 XAML 페이지 내에서 이미지를 표시하려면 먼저 XAML 페이지에서 이미지 컨트롤을 정의합니다.

<Image x:Name="imageControl"/>

현재 이미지 컨트롤은 BGRA8 인코딩을 사용하고 미리 곱하거나 알파 채널이 없는 이미지만 지원합니다. 이미지를 표시하기 전에 올바른 형식이 있는지 테스트하고, 그렇지 않은 경우 SoftwareBitmap 정적 변환를 사용하여 이미지를 지원되는 형식으로 변환합니다.

SoftwareBitmapSource 개체를 만듭니다. SetBitmapAsync를 호출하고 SoftwareBitmap을 전달하여 원본 개체의 콘텐츠를 설정합니다. 그런 다음 이미지 컨트롤의 Source 속성을 새로 만든 SoftwareBitmapSource로 설정할 수 있습니다.

if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
    softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
{
    softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
}

var source = new SoftwareBitmapSource();
await source.SetBitmapAsync(softwareBitmap);

// Set the source of the Image control
imageControl.Source = source;

SoftwareBitmapSource를 사용하여 ImageBrush에 대한 ImageSourceSoftwareBitmap을 설정할 수도 있습니다.

WriteableBitmap에서 SoftwareBitmap 만들기

SoftwareBitmap.CreateCopyFromBuffer를 호출하고 WriteableBitmapPixelBuffer 속성을 제공하여 픽셀 데이터를 설정하여 기존 WriteableBitmap에서 SoftwareBitmap을 만들 수 있습니다. 두 번째 인수를 사용하면 새로 만든 WriteableBitmap에 대한 픽셀 형식을 요청할 수 있습니다. WriteableBitmapPixelWidthPixelHeight 속성을 사용하여 새 이미지의 차원을 지정할 수 있습니다.

SoftwareBitmap outputBitmap = SoftwareBitmap.CreateCopyFromBuffer(
    writeableBitmap.PixelBuffer,
    BitmapPixelFormat.Bgra8,
    writeableBitmap.PixelWidth,
    writeableBitmap.PixelHeight
);

프로그래밍 방식으로 SoftwareBitmap 만들기 또는 편집

지금까지 이 항목에서는 이미지 파일 작업을 다루었습니다. 코드에서 프로그래밍 방식으로 새 SoftwareBitmap을 만들고 동일한 기술을 사용하여 SoftwareBitmap의 픽셀 데이터에 액세스하고 수정할 수도 있습니다.

SoftwareBitmap은 COM interop을 사용하여 픽셀 데이터가 포함된 원시 버퍼를 노출합니다.

COM interop을 사용하려면 프로젝트에 System.Runtime.InteropServices 네임스페이스에 대한 참조를 포함해야 합니다.

using System.Runtime.InteropServices;

네임스페이스 내에 다음 코드를 추가하여 IMemoryBufferByteAccess COM 인터페이스를 초기화합니다.

[ComImport]
[Guid("5B0D3235-4DBA-4D44-865E-8F1D0E4FD04D")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe interface IMemoryBufferByteAccess
{
    void GetBuffer(out byte* buffer, out uint capacity);
}

원하는 픽셀 형식과 크기로 새 SoftwareBitmap을 만듭니다. 또는 픽셀 데이터를 편집하려는 기존 SoftwareBitmap을 사용합니다. SoftwareBitmap.LockBuffer를 호출하여 픽셀 데이터 버퍼를 나타내는 BitmapBuffer 클래스의 인스턴스를 가져옵니다. BitmapBufferIMemoryBufferByteAccess COM 인터페이스로 캐스팅한 다음 IMemoryBufferByteAccess.GetBuffer를 호출하여 바이트 배열을 데이터로 채웁니다. BitmapBuffer.GetPlaneDescription 메서드를 사용하여 각 픽셀에 대한 버퍼로 오프셋을 계산하는 데 도움이 되는 BitmapPlaneDescription 개체를 가져옵니다.

softwareBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8, 100, 100, BitmapAlphaMode.Premultiplied);

using (BitmapBuffer buffer = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Write))
{
    using (var reference = buffer.CreateReference())
    {
        byte* dataInBytes;
        uint capacity;
        ((IMemoryBufferByteAccess)reference).GetBuffer(out dataInBytes, out capacity);

        // Fill-in the BGRA plane
        BitmapPlaneDescription bufferLayout = buffer.GetPlaneDescription(0);
        for (int i = 0; i < bufferLayout.Height; i++)
        {
            for (int j = 0; j < bufferLayout.Width; j++)
            {

                byte value = (byte)((float)j / bufferLayout.Width * 255);
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 0] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 1] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 2] = value;
                dataInBytes[bufferLayout.StartIndex + bufferLayout.Stride * i + 4 * j + 3] = (byte)255;
            }
        }
    }
}

이 메서드는 Windows 런타임 형식의 기반이 되는 원시 버퍼에 액세스하므로 안전하지 않은 키워드(keyword)를 사용하여 선언해야 합니다. 또한 프로젝트의 속성 페이지를 열고 빌드 속성 페이지를 클릭하고 안전하지 않은 코드 허용 검사 상자를 선택하여 안전하지 않은 코드를 컴파일할 수 있도록 Microsoft Visual Studio에서 프로젝트를 구성해야 합니다.

Direct3D 화면에서 SoftwareBitmap 만들기

Direct3D 화면에서 SoftwareBitmap 개체를 만들려면 프로젝트에 Windows.Graphics.DirectX.Direct3D11 네임스페이스를 포함해야 합니다.

using Windows.Graphics.DirectX.Direct3D11;

CreateCopyFromSurfaceAsync를 호출하여 표면에서 새 SoftwareBitmap을 만듭니다. 이름에서 알 수 있듯이 새 SoftwareBitmap에는 이미지 데이터의 별도 복사본이 있습니다. SoftwareBitmap을 수정해도 Direct3D 화면에는 영향을 주지 않습니다.

private async void CreateSoftwareBitmapFromSurface(IDirect3DSurface surface)
{
    softwareBitmap = await SoftwareBitmap.CreateCopyFromSurfaceAsync(surface);
}

SoftwareBitmap을 다른 픽셀 형식으로 변환

SoftwareBitmap 클래스는 기존 SoftwareBitmap에서 지정한 픽셀 형식 및 알파 모드를 사용하는 새 SoftwareBitmap을 쉽게 만들 수 있는 정적 메서드인 Convert를 제공합니다. 새로 만든 비트맵에는 이미지 데이터의 별도 복사본이 있습니다. 새 비트맵을 수정해도 원본 비트맵에는 영향을 미치지 않습니다.

SoftwareBitmap bitmapBGRA8 = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);

이미지 파일 코드 변환

이미지 파일을 BitmapDecoder에서 BitmapEncoder로 직접 트랜스코딩할 수 있습니다. 코드 변환할 파일에서 IRandomAccessStream을 만듭니다. 입력 스트림에서 새 BitmapDecoder를 만듭니다. 메모리 내 스트림 및 디코더 개체를 전달하여 BitmapEncoder.CreateForTranscodingAsync에 쓰고 호출할 인코더에 대한 새 InMemoryRandomAccessStream을 만듭니다. 인코드 옵션은 코드 변환 시 지원되지 않습니다. 대신 CreateAsync를 사용해야 합니다. 인코더에서 특별히 설정하지 않은 입력 이미지 파일의 모든 속성은 변경되지 않은 출력 파일에 기록됩니다. FlushAsync를 호출하여 인코더가 메모리 내 스트림으로 인코딩되도록 합니다. 마지막으로 파일 스트림과 메모리 내 스트림을 처음부터 찾아 CopyAsync를 호출하여 메모리 내 스트림을 파일 스트림에 기록합니다.

private async void TranscodeImageFile(StorageFile imageFile)
{


    using (IRandomAccessStream fileStream = await imageFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fileStream);

        var memStream = new Windows.Storage.Streams.InMemoryRandomAccessStream();
        BitmapEncoder encoder = await BitmapEncoder.CreateForTranscodingAsync(memStream, decoder);

        encoder.BitmapTransform.ScaledWidth = 320;
        encoder.BitmapTransform.ScaledHeight = 240;

        await encoder.FlushAsync();

        memStream.Seek(0);
        fileStream.Seek(0);
        fileStream.Size = 0;
        await RandomAccessStream.CopyAsync(memStream, fileStream);

        memStream.Dispose();
    }
}