Share via


MediaFrameReader를 사용하여 미디어 프레임 처리

이 문서에서는 MediaFrameReaderMediaCapture를 사용하여 색, 깊이, 적외선 카메라, 오디오 장치, 사용자 지정 프레임 원본(예: 골격 추적 프레임을 생성하는 프레임 원본) 등 하나 이상의 사용 가능한 원본에서 미디어 프레임을 가져오는 방법을 보여 줍니다. 이 기능은 확대된 현실 및 깊이 인식 카메라 앱과 같이 미디어 프레임의 실시간 처리를 수행하는 앱에 사용되도록 설계되었습니다.

일반적인 사진 앱 등 단순히 비디오 또는 사진을 캡처하려면 MediaCapture에서 지원하는 다른 캡처 기술 중 하나를 사용할 수 있습니다. 미디어 캡처 기술 및 사용 방법을 보여 주는 문서 목록은 카메라를 참조하세요.

참고 항목

이 문서에 설명된 기능은 Windows 10 버전 1607부터 사용할 수 있습니다.

참고 항목

유니버설 Windows 앱 샘플에서는 MediaFrameReader를 사용하여 색, 깊이, 적외선 카메라 등 다른 프레임 원본의 프레임을 표시하는 방법을 보여 줍니다. 자세한 내용은 카메라 프레임 샘플을 참조하세요.

참고 항목

오디오 데이터가 포함된 MediaFrameReader를 사용하기 위한 새로운 API 세트가 Windows 10, 1803 버전에 도입되었습니다. 자세한 내용은 MediaFrameReader를 사용하여 오디오 프레임 처리를 참조하세요.

프로젝트 설정

MediaCapture를 사용하는 앱의 경우 카메라 장치에 액세스하기 전에 앱에서 웹캠 기능을 사용하도록 선언해야 합니다. 앱이 오디오 장치에서 캡처하는 경우 마이크 장치 기능도 선언해야 합니다.

앱 매니페스트에 접근 권한 값 추가

  1. Microsoft Visual Studio의 솔루션 탐색기에서 package.appxmanifest 항목을 두 번 클릭하여 응용 프로그램 매니페스트 디자이너를 엽니다.
  2. 기능 탭을 선택합니다.
  3. 웹캠 확인란과 마이크 상자를 선택합니다.
  4. 사진과 비디오 라이브러리에 액세스하려면 사진 라이브러리의 확인란과 비디오 라이브러리의 확인란을 선택합니다.

이 문서의 예제 코드에서는 기본 프로젝트 템플릿에 포함된 API뿐만 아니라 다음 네임스페이스의 API도 사용합니다.

using Windows.Media.Capture.Frames;
using Windows.Devices.Enumeration;
using Windows.Media.Capture;
using Windows.UI.Xaml.Media.Imaging;
using Windows.Media.MediaProperties;
using Windows.Graphics.Imaging;
using System.Threading;
using Windows.UI.Core;
using System.Threading.Tasks;
using Windows.Media.Core;
using System.Diagnostics;
using Windows.Media;
using Windows.Media.Devices;
using Windows.Media.Audio;

프레임 원본과 프레임 원본 그룹 선택

미디어 프레임을 처리하는 대부분의 앱은 디바이스의 색과 깊이 카메라 등 여러 원본의 프레임을 한 번에 가져와야 합니다. MediaFrameSourceGroup 개체는 동시에 사용할 수 있는 미디어 프레임 원본 집합을 나타냅니다. 정적 메서드 MediaFrameSourceGroup.FindAllAsync를 호출하여 현재 장치에서 지원하는 모든 프레임 원본 그룹의 목록을 가져옵니다.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

또한 DeviceInformation.CreateWatcherMediaFrameSourceGroup.GetDeviceSelector에서 반환한 값을 사용하여 DeviceWatcher를 만들면 외부 카메라가 연결될 때 등 디바이스에서 사용 가능한 프레임 원본 그룹이 변경될 때 알림을 받을 수 있습니다. 자세한 내용은 장치 열거를 참조하세요.

MediaFrameSourceGroup에는 그룹에 포함된 프레임 원본을 설명하는 MediaFrameSourceInfo 개체 컬렉션이 있습니다. 디바이스에서 사용할 수 있는 프레임 원본 그룹을 검색한 후에는 원하는 프레임 원본을 노출하는 그룹을 선택할 수 있습니다.

다음 예제에서는 프레임 원본 그룹을 간단히 선택하는 방법을 보여 줍니다. 이 코드는 단순히 사용 가능한 모든 그룹을 반복한 다음 SourceInfos 컬렉션 내에서 각 항목을 반복합니다. 각 MediaFrameSourceInfo에서 검색 중인 기능을 지원하는지 확인합니다. 이 경우 MediaStreamType 속성 값이 VideoPreview(장치에서 비디오 미리 보기 스트림 제공)와 SourceKind 속성 값이 Color(원본에서 색 프레임 제공)인지 확인합니다.

var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();

MediaFrameSourceGroup selectedGroup = null;
MediaFrameSourceInfo colorSourceInfo = null;

foreach (var sourceGroup in frameSourceGroups)
{
    foreach (var sourceInfo in sourceGroup.SourceInfos)
    {
        if (sourceInfo.MediaStreamType == MediaStreamType.VideoPreview
            && sourceInfo.SourceKind == MediaFrameSourceKind.Color)
        {
            colorSourceInfo = sourceInfo;
            break;
        }
    }
    if (colorSourceInfo != null)
    {
        selectedGroup = sourceGroup;
        break;
    }
}

이 방법은 간단한 경우에는 원하는 프레임 원본 그룹과 프레임 원본을 식별할 수 있지만 더 복잡한 기준을 기반으로 프레임 원본을 선택하려는 경우에는 까다로울 수 있습니다. 또 다른 방법은 Linq 구문과 익명 개체를 사용하여 선택하는 것입니다. 다음 예제에서는 Select 확장 메서드를 사용하여 frameSourceGroups 목록의 MediaFrameSourceGroup 개체를 두 필드가 포함된 익명 개체로 변환합니다. 이 두 필드는 그룹 자체를 나타내는 sourceGroup과 그룹의 색 프레임 원본을 나타내는 colorSourceInfo입니다. colorSourceInfo 필드는 제공된 조건자가 true로 확인되는 첫 번째 개체를 선택하는 FirstOrDefault의 결과로 설정됩니다. 이 경우 스트림 형식이 VideoPreview이고, 원본 종류가 Color이며, 카메라가 장치의 앞면에 있으면 조건자가 true입니다.

위에서 설명한 쿼리에서 반환한 익명 개체 목록에서 Where 확장 메서드는 colorSourceInfo 필드가 null이 아닌 개체만 선택하는 데 사용됩니다. 마지막으로 FirstOrDefault가 목록의 첫 번째 항목을 선택하기 위해 호출됩니다.

이제 선택한 개체의 필드를 사용하여 선택한 MediaFrameSourceGroup 및 컬러 카메라를 나타내는 MediaFrameSourceInfo 개체에 대한 참조를 가져올 수 있습니다. 이러한 참조는 나중에 MediaCapture 개체를 초기화하고 선택한 원본에 대한 MediaFrameReader를 만드는 데 사용됩니다. 마지막으로 원본 그룹이 null인지, 즉 현재 디바이스에 요청된 캡처 원본이 있는지 테스트해야 합니다.

var selectedGroupObjects = frameSourceGroups.Select(group =>
   new
   {
       sourceGroup = group,
       colorSourceInfo = group.SourceInfos.FirstOrDefault((sourceInfo) =>
       {
           // On Xbox/Kinect, omit the MediaStreamType and EnclosureLocation tests
           return sourceInfo.MediaStreamType == MediaStreamType.VideoPreview
           && sourceInfo.SourceKind == MediaFrameSourceKind.Color
           && sourceInfo.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front;
       })

   }).Where(t => t.colorSourceInfo != null)
   .FirstOrDefault();

MediaFrameSourceGroup selectedGroup = selectedGroupObjects?.sourceGroup;
MediaFrameSourceInfo colorSourceInfo = selectedGroupObjects?.colorSourceInfo;

if (selectedGroup == null)
{
    return;
}

다음 예제에서는 위에서 설명한 대로 유사한 기술을 사용하여 색, 깊이 및 적외선 카메라를 포함하는 원본 그룹을 선택합니다.

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Depth),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Infrared),
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with color, depth or infrared found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo colorSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[0];
MediaFrameSourceInfo infraredSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[1];
MediaFrameSourceInfo depthSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[2];

참고 항목

Windows 10 버전 1803부터 MediaCaptureVideoProfile 클래스를 사용하여 원하는 기능 세트와 함께 미디어 프레임 원본을 선택할 수 있습니다. 자세한 내용은 이 문서의 후반부에 나오는 비디오 프로필을 사용하여 프레임 원본 선택 섹션을 참조하세요.

선택한 프레임 원본 그룹을 사용하기 위해 MediaCapture 개체 초기화

다음 단계는 이전 단계에서 선택한 프레임 원본 그룹을 사용하기 위해 MediaCapture를 초기화하는 것입니다.

MediaCapture 개체는 일반적으로 앱 내의 여러 위치에서 사용되므로 이 개체를 포함하는 클래스 멤버 변수를 선언해야 합니다.

MediaCapture mediaCapture;

생성자를 호출하여 MediaCapture 개체의 인스턴스를 만듭니다. 다음으로 MediaCapture 개체를 초기화하는 데 사용할 MediaCaptureInitializationSettings 개체를 만듭니다. 이 예제에서는 다음 설정이 사용됩니다.

  • SourceGroup - 프레임을 가져오는 데 사용할 원본 그룹을 지정합니다. 원본 그룹은 동시에 사용할 수 있는 미디어 프레임 원본 집합을 정의합니다.
  • SharingMode - 캡처 원본 디바이스를 단독으로 제어해야 하는지 지정합니다. 이 값을 ExclusiveControl로 설정하면 생성할 프레임 형식 등의 캡처 디바이스 설정을 변경할 수 있다는 의미입니다. 그러나 다른 앱에서 이미 단독으로 제어하는 경우 미디어 캡처 디바이스를 초기화하려고 하면 오류가 발생합니다. 이 값을 SharedReadOnly로 설정하면 다른 앱에서 사용되어도 프레임 원본의 프레임을 수신할 수 있지만 장치의 설정을 변경할 수는 없습니다.
  • MemoryPreference - cpu를 지정 하는 경우 시스템은 cpu 메모리를 사용하여 프레임이 도착할 때 이를 보장 하는 SoftwareBitmap 개체를 사용할 수 있습니다. Auto로 지정할 경우 시스템에서는 프레임을 저장하는 데 가장 적합한 메모리 위치를 동적으로 선택합니다. 시스템에서 GPU 메모리를 사용하도록 선택한 경우 미디어 프레임이 SoftwareBitmap이 아닌 IDirect3DSurface 개체로 수신됩니다.
  • StreamingCaptureMode - 이 값을 Video로 설정하여 오디오를 스트리밍할 필요가 없다는 것을 나타냅니다.

InitializeAsync를 호출하여 MediaCapture를 원하는 설정으로 초기화합니다. 초기화가 실패하는 경우 try 블록 내에서 호출해야 합니다.

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

프레임 원본에 대한 기본 형식 설정

프레임 원본에 대한 기본 형식을 설정하려면 원본을 나타내는 MediaFrameSource 개체를 가져와야 합니다. 이 개체를 가져오려면 초기화된 MediaCapture 개체의 Frames 사전에 액세스하여 사용할 프레임 원본의 식별자를 지정합니다. 이러한 이유로 프레임 원본 그룹을 선택할 때 MediaFrameSourceInfo 개체를 저장한 것입니다.

MediaFrameSource.SupportedFormats 속성에는 프레임 원본에 대해 지원되는 형식을 설명하는 MediaFrameFormat 개체 목록이 있습니다. Where Linq 확장 메서드를 사용하여 원하는 속성을 기반으로 형식을 선택합니다. 이 예제에서는 너비가 1080픽셀이고 32비트 RGB 형식으로 제공할 수 있는 형식이 선택됩니다. FirstOrDefault 확장 메서드는 목록의 첫 번째 항목을 선택합니다. 선택한 형식이 null이면 요청한 형식이 프레임 원본에서 지원되지 않습니다. 형식이 지원되는 경우 SetFormatAsync를 호출하여 원본에서 이 형식을 사용하도록 요청할 수 있습니다.

var colorFrameSource = mediaCapture.FrameSources[colorSourceInfo.Id];
var preferredFormat = colorFrameSource.SupportedFormats.Where(format =>
{
    return format.VideoFormat.Width >= 1080
    && format.Subtype == MediaEncodingSubtypes.Argb32;

}).FirstOrDefault();

if (preferredFormat == null)
{
    // Our desired format is not supported
    return;
}

await colorFrameSource.SetFormatAsync(preferredFormat);

프레임 원본에 대한 프레임 읽기 프로그램 만들기

미디어 프레임 원본에 대한 프레임을 수신하려면 MediaFrameReader를 사용합니다.

MediaFrameReader mediaFrameReader;

초기화한 MediaCapture 개체에서 CreateFrameReaderAsync를 호출하여 프레임 읽기 프로그램을 인스턴스화합니다. 이 메서드에 대한 첫 번째 인수는 프레임을 수신하려는 프레임 원본입니다. 사용하려는 각 프레임 원본에 대해 개별 프레임 읽기 프로그램을 만들 수 있습니다. 두 번째 인수는 도착할 프레임의 출력 형식을 지정합니다. 이렇게 하면 프레임이 도착한 후 직접 변환할 필요가 없습니다. 프레임 원본에서 지원하지 않는 형식을 지정하려는 경우 예외가 발생하므로 이 변수는 SupportedFormats 컬렉션에 있어야 합니다.

프레임 읽기 프로그램을 만든 다음 원본에서 새 프레임을 사용할 수 있을 때마다 발생하는 FrameArrived 이벤트에 대한 처리기를 등록합니다.

StartAsync를 호출하여 원본에서 프레임 읽기를 시작하도록 지정합니다.

mediaFrameReader = await mediaCapture.CreateFrameReaderAsync(colorFrameSource, MediaEncodingSubtypes.Argb32);
mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await mediaFrameReader.StartAsync();

프레임 도착 이벤트 처리

새 프레임을 사용할 수 있을 때마다 MediaFrameReader.FrameArrived 이벤트가 발생합니다. 도착하는 모든 프레임을 처리하거나 필요한 프레임만 사용할 수 있습니다. 프레임 읽기 프로그램은 해당 자체 스레드에서 이벤트를 발생시키므로 여러 스레드에서 동일한 데이터에 액세스하지 않도록 일부 동기화 논리를 구현해야 합니다. 이 섹션에서는 그리기 색 프레임을 XAML 페이지의 이미지 컨트롤과 동기화하는 방법을 보여 줍니다. 이 시나리오에서는 XAML 컨트롤에 대한 모든 업데이트가 UI 스레드에서 수행되어야 하는 추가 동기화 제약 조건을 설명합니다.

XAML에서 프레임을 표시하는 첫 번째 단계는 이미지 컨트롤을 만드는 것입니다.

<Image x:Name="imageElement" Width="320" Height="240" />

코드 숨김 페이지에서 들어오는 모든 이미지가 복사되는 백 버퍼로 사용할 SoftwareBitmap의 클래스 멤버 변수를 선언합니다. 이미지 데이터 자체가 아닌 개체 참조만 복사됩니다. 또한 UI 작업이 현재 실행 중인지를 추적하는 부울을 선언합니다.

private SoftwareBitmap backBuffer;
private bool taskRunning = false;

프레임은 SoftwareBitmap 개체로 도착하므로 SoftwareBitmap을 XAML Control의 원본으로 사용할 수 있도록 SoftwareBitmapSource 개체를 만들어야 합니다. 프레임 읽기 프로그램을 시작하기 전에 코드 내에 이미지 원본을 설정해야 합니다.

imageElement.Source = new SoftwareBitmapSource();

이제 FrameArrived 이벤트 처리기를 구현해야 합니다. 처리기가 호출되면 sender 매개 변수에 이벤트를 발생시킬 MediaFrameReader 개체에 대한 참조가 포함됩니다. 이 개체에서 TryAcquireLatestFrame을 호출하여 최신 프레임을 가져옵니다. 이름에서 알 수 있듯이 TryAcquireLatestFrame은 프레임을 반환하지 못할 수 있습니다. 따라서 VideoMediaFrame에 액세스한 다음 SoftwareBitmap 속성에 액세스할 때는 null인지 테스트해야 합니다. 이 예제에서는 null 조건부 연산자 ?를 사용하여 SoftwareBitmap에 액세스하고 검색된 개체가 null인지 확인합니다.

Image 컨트롤은 프리멀티플라이되거나 알파가 없는 BRGA8 형식으로만 이미지를 표시할 수 있습니다. 도착하는 프레임이 해당 형식이 아니면 정적 메서드 Convert가 소프트웨어 비트맵을 올바른 형식으로 변환합니다.

다음으로 Interlocked.Exchange 메서드를 사용하여 도착하는 비트맵의 참조를 백 버퍼 비트맵으로 바꿉니다. 이 메서드는 스레드로부터 안전한 원자성 작업에서 이러한 참조를 바꿉니다. 바꾼 후에는 이제 softwareBitmap 변수에 있는 이전 백 버퍼 이미지가 삭제되어 해당 리소스가 정리됩니다.

다음으로 Image 요소와 연관된 CoreDispatcher를 사용하여 RunAsync를 호출하여 UI 스레드에서 실행할 작업을 만듭니다. 작업 내에서 비동기 작업이 수행되므로 async 키워드를 사용하여 RunAsync로 전달된 람다 식이 선언됩니다.

작업 내에서 _taskRunning 변수를 검사하여 한 번에 작업의 한 인스턴스만 실행 중인지 확인합니다. 작업이 실행되고 있지 않은 경우 작업이 다시 실행되지 않도록 _taskRunning이 true로 설정됩니다. while 루프에서 백 버퍼 이미지가 null이 될 때까지 백 버퍼에서 임시 SoftwareBitmap으로 복사하기 위해 Interlocked.Exchange를 호출합니다. 임시 비트맵을 채울 때마다 ImageSource 속성이 SoftwareBitmapSource로 캐스트된 다음 SetBitmapAsync가 호출되어 이미지의 원본을 설정합니다.

마지막으로, 다음에 처리기를 호출할 때 작업이 다시 실행될 수 있도록 _taskRunning 변수를 다시 false로 설정합니다.

참고 항목

MediaFrameReferenceVideoMediaFrame 속성에서 제공하는 SoftwareBitmap 또는 Direct3DSurface 개체에 액세스하는 경우 시스템에서 해당 개체에 대한 강력한 참조를 만듭니다. 즉, 이러한 참조는 포함하는 MediaFrameReference에서 Dispose를 호출할 때 삭제되지 않습니다. 개체를 즉시 삭제하려면 SoftwareBitmap 또는 Direct3DSurfaceDispose 메서드를 직접 명시적으로 호출해야 합니다. 그러지 않으면 가비지 수집기에서 결국 이러한 개체의 메모리를 해제하지만 언제 수행될지 알 수 없으며, 할당된 비트맵 또는 화면 수가 시스템에서 허용된 최대 크기를 초과할 경우 새 프레임의 흐름이 중단됩니다. 예를 들어 SoftwareBitmap.Copy 메서드를 사용하여 검색된 프레임을 복사한 후 원래 프레임을 해제하여 이 제한을 해결할 수 있습니다. 또한 오버로드 CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize) 또는 CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype)을 사용하여 MediaFrameReader를 만들 경우 반환된 프레임은 원래 프레임 데이터의 사본이므로 이것을 유지할 때에도 프레임 획득을 중지하지 않습니다.

private void ColorFrameReader_FrameArrived(MediaFrameReader sender, MediaFrameArrivedEventArgs args)
{
    var mediaFrameReference = sender.TryAcquireLatestFrame();
    var videoMediaFrame = mediaFrameReference?.VideoMediaFrame;
    var softwareBitmap = videoMediaFrame?.SoftwareBitmap;

    if (softwareBitmap != null)
    {
        if (softwareBitmap.BitmapPixelFormat != Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8 ||
            softwareBitmap.BitmapAlphaMode != Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied)
        {
            softwareBitmap = SoftwareBitmap.Convert(softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }

        // Swap the processed frame to _backBuffer and dispose of the unused image.
        softwareBitmap = Interlocked.Exchange(ref backBuffer, softwareBitmap);
        softwareBitmap?.Dispose();

        // Changes to XAML ImageElement must happen on UI thread through Dispatcher
        var task = imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
            async () =>
            {
                // Don't let two copies of this task run at the same time.
                if (taskRunning)
                {
                    return;
                }
                taskRunning = true;

                // Keep draining frames from the backbuffer until the backbuffer is empty.
                SoftwareBitmap latestBitmap;
                while ((latestBitmap = Interlocked.Exchange(ref backBuffer, null)) != null)
                {
                    var imageSource = (SoftwareBitmapSource)imageElement.Source;
                    await imageSource.SetBitmapAsync(latestBitmap);
                    latestBitmap.Dispose();
                }

                taskRunning = false;
            });
    }

    mediaFrameReference.Dispose();
}

리소스 정리

프레임 읽기가 완료된 후에는 StopAsync를 호출하고, FrameArrived 처리기의 등록을 취소하고, MediaCapture 개체를 삭제하여 미디어 프레임 읽기 프로그램을 중지해야 합니다.

await mediaFrameReader.StopAsync();
mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
mediaCapture.Dispose();
mediaCapture = null;

응용 프로그램이 일시 중지될 때 미디어 캡처 개체 정리에 대한 자세한 내용은 카메라 미리 보기 표시를 참조하세요.

FrameRenderer 도우미 클래스

유니버설 Windows 카메라 프레임 샘플은 앱의 색, 적외선 및 깊이 원본을 통해 프레임을 쉽게 표시할 수 있는 도우미 클래스를 제공합니다. 일반적으로 단순히 화면에 표시하는 대신 색과 적외선 데이터를 사용하여 더 많은 작업을 수행하려고 하지만 이 도우미 클래스를 사용하면 프레임 읽기 프로그램 기능을 보여 주고 프레임 읽기 프로그램 구현을 디버깅할 수 있습니다.

FrameRenderer 도우미 클래스는 다음 메서드를 구현합니다.

  • FrameRenderer 생성자 - 생성자는 미디어 프레임 표시를 위해 전달한 XAML Image 요소를 사용하도록 도우미 클래스를 초기화합니다.
  • ProcessFrame - 이 메서드는 생성자로 전달한 Image 요소에서 미디어 프레임을 MediaFrameReference로 표시합니다. 일반적으로 FrameArrived 이벤트 처리기에서 이 메서드를 호출하고 TryAcquireLatestFrame에서 반환한 프레임을 전달합니다.
  • ConvertToDisplayableImage - 이 메서드는 미디어 프레임의 형식을 확인하고 필요한 경우 표시 가능한 형식으로 변환합니다. 컬러 이미지의 경우 색 형식이 BGRA8이고 비트맵 알파 모드가 프리멀티플라이되어야 함을 의미입니다. 깊이 또는 적외선 프레임의 경우 샘플에 포함되고 아래에 나열된 PsuedoColorHelper 클래스를 사용하여 깊이 또는 적외선 값을 의사 색 그라데이션으로 변환하도록 각 스캐닝선이 처리됩니다.

참고 항목

SoftwareBitmap 이미지에서 픽셀 조작을 수행하려면 원시 메모리 버퍼에 액세스해야 합니다. 이렇게 하려면 아래 나열된 코드에 포함된 IMemoryBufferByteAccess COM 인터페이스를 사용하고 안전하지 않은 코드의 컴파일을 허용하도록 프로젝트 속성을 업데이트해야 합니다. 자세한 내용은 비트맵 이미지 만들기, 편집 및 저장을 참조하세요.

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

class FrameRenderer
{
    private Image _imageElement;
    private SoftwareBitmap _backBuffer;
    private bool _taskRunning = false;

    public FrameRenderer(Image imageElement)
    {
        _imageElement = imageElement;
        _imageElement.Source = new SoftwareBitmapSource();
    }

    // Processes a MediaFrameReference and displays it in a XAML image control
    public void ProcessFrame(MediaFrameReference frame)
    {
        var softwareBitmap = FrameRenderer.ConvertToDisplayableImage(frame?.VideoMediaFrame);
        if (softwareBitmap != null)
        {
            // Swap the processed frame to _backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);

            // UI thread always reset _backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                async () =>
                {
                    // Don't let two copies of this task run at the same time.
                    if (_taskRunning)
                    {
                        return;
                    }
                    _taskRunning = true;

                    // Keep draining frames from the backbuffer until the backbuffer is empty.
                    SoftwareBitmap latestBitmap;
                    while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }



    // Function delegate that transforms a scanline from an input image to an output image.
    private unsafe delegate void TransformScanline(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes);
    /// <summary>
    /// Determines the subtype to request from the MediaFrameReader that will result in
    /// a frame that can be rendered by ConvertToDisplayableImage.
    /// </summary>
    /// <returns>Subtype string to request, or null if subtype is not renderable.</returns>

    public static string GetSubtypeForFrameReader(MediaFrameSourceKind kind, MediaFrameFormat format)
    {
        // Note that media encoding subtypes may differ in case.
        // https://docs.microsoft.com/en-us/uwp/api/Windows.Media.MediaProperties.MediaEncodingSubtypes

        string subtype = format.Subtype;
        switch (kind)
        {
            // For color sources, we accept anything and request that it be converted to Bgra8.
            case MediaFrameSourceKind.Color:
                return Windows.Media.MediaProperties.MediaEncodingSubtypes.Bgra8;

            // The only depth format we can render is D16.
            case MediaFrameSourceKind.Depth:
                return String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.D16, StringComparison.OrdinalIgnoreCase) ? subtype : null;

            // The only infrared formats we can render are L8 and L16.
            case MediaFrameSourceKind.Infrared:
                return (String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L8, StringComparison.OrdinalIgnoreCase) ||
                    String.Equals(subtype, Windows.Media.MediaProperties.MediaEncodingSubtypes.L16, StringComparison.OrdinalIgnoreCase)) ? subtype : null;

            // No other source kinds are supported by this class.
            default:
                return null;
        }
    }

    /// <summary>
    /// Converts a frame to a SoftwareBitmap of a valid format to display in an Image control.
    /// </summary>
    /// <param name="inputFrame">Frame to convert.</param>

    public static unsafe SoftwareBitmap ConvertToDisplayableImage(VideoMediaFrame inputFrame)
    {
        SoftwareBitmap result = null;
        using (var inputBitmap = inputFrame?.SoftwareBitmap)
        {
            if (inputBitmap != null)
            {
                switch (inputFrame.FrameReference.SourceKind)
                {
                    case MediaFrameSourceKind.Color:
                        // XAML requires Bgra8 with premultiplied alpha.
                        // We requested Bgra8 from the MediaFrameReader, so all that's
                        // left is fixing the alpha channel if necessary.
                        if (inputBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8)
                        {
                            System.Diagnostics.Debug.WriteLine("Color frame in unexpected format.");
                        }
                        else if (inputBitmap.BitmapAlphaMode == BitmapAlphaMode.Premultiplied)
                        {
                            // Already in the correct format.
                            result = SoftwareBitmap.Copy(inputBitmap);
                        }
                        else
                        {
                            // Convert to premultiplied alpha.
                            result = SoftwareBitmap.Convert(inputBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
                        }
                        break;

                    case MediaFrameSourceKind.Depth:
                        // We requested D16 from the MediaFrameReader, so the frame should
                        // be in Gray16 format.
                        if (inputBitmap.BitmapPixelFormat == BitmapPixelFormat.Gray16)
                        {
                            // Use a special pseudo color to render 16 bits depth frame.
                            var depthScale = (float)inputFrame.DepthMediaFrame.DepthFormat.DepthScaleInMeters;
                            var minReliableDepth = inputFrame.DepthMediaFrame.MinReliableDepth;
                            var maxReliableDepth = inputFrame.DepthMediaFrame.MaxReliableDepth;
                            result = TransformBitmap(inputBitmap, (w, i, o) => PseudoColorHelper.PseudoColorForDepth(w, i, o, depthScale, minReliableDepth, maxReliableDepth));
                        }
                        else
                        {
                            System.Diagnostics.Debug.WriteLine("Depth frame in unexpected format.");
                        }
                        break;

                    case MediaFrameSourceKind.Infrared:
                        // We requested L8 or L16 from the MediaFrameReader, so the frame should
                        // be in Gray8 or Gray16 format. 
                        switch (inputBitmap.BitmapPixelFormat)
                        {
                            case BitmapPixelFormat.Gray16:
                                // Use pseudo color to render 16 bits frames.
                                result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor16BitInfrared);
                                break;

                            case BitmapPixelFormat.Gray8:
                                // Use pseudo color to render 8 bits frames.
                                result = TransformBitmap(inputBitmap, PseudoColorHelper.PseudoColorFor8BitInfrared);
                                break;
                            default:
                                System.Diagnostics.Debug.WriteLine("Infrared frame in unexpected format.");
                                break;
                        }
                        break;
                }
            }
        }

        return result;
    }



    /// <summary>
    /// Transform image into Bgra8 image using given transform method.
    /// </summary>
    /// <param name="softwareBitmap">Input image to transform.</param>
    /// <param name="transformScanline">Method to map pixels in a scanline.</param>

    private static unsafe SoftwareBitmap TransformBitmap(SoftwareBitmap softwareBitmap, TransformScanline transformScanline)
    {
        // XAML Image control only supports premultiplied Bgra8 format.
        var outputBitmap = new SoftwareBitmap(BitmapPixelFormat.Bgra8,
            softwareBitmap.PixelWidth, softwareBitmap.PixelHeight, BitmapAlphaMode.Premultiplied);

        using (var input = softwareBitmap.LockBuffer(BitmapBufferAccessMode.Read))
        using (var output = outputBitmap.LockBuffer(BitmapBufferAccessMode.Write))
        {
            // Get stride values to calculate buffer position for a given pixel x and y position.
            int inputStride = input.GetPlaneDescription(0).Stride;
            int outputStride = output.GetPlaneDescription(0).Stride;
            int pixelWidth = softwareBitmap.PixelWidth;
            int pixelHeight = softwareBitmap.PixelHeight;

            using (var outputReference = output.CreateReference())
            using (var inputReference = input.CreateReference())
            {
                // Get input and output byte access buffers.
                byte* inputBytes;
                uint inputCapacity;
                ((IMemoryBufferByteAccess)inputReference).GetBuffer(out inputBytes, out inputCapacity);
                byte* outputBytes;
                uint outputCapacity;
                ((IMemoryBufferByteAccess)outputReference).GetBuffer(out outputBytes, out outputCapacity);

                // Iterate over all pixels and store converted value.
                for (int y = 0; y < pixelHeight; y++)
                {
                    byte* inputRowBytes = inputBytes + y * inputStride;
                    byte* outputRowBytes = outputBytes + y * outputStride;

                    transformScanline(pixelWidth, inputRowBytes, outputRowBytes);
                }
            }
        }

        return outputBitmap;
    }



    /// <summary>
    /// A helper class to manage look-up-table for pseudo-colors.
    /// </summary>

    private static class PseudoColorHelper
    {
        #region Constructor, private members and methods

        private const int TableSize = 1024;   // Look up table size
        private static readonly uint[] PseudoColorTable;
        private static readonly uint[] InfraredRampTable;

        // Color palette mapping value from 0 to 1 to blue to red colors.
        private static readonly Color[] ColorRamp =
        {
            Color.FromArgb(a:0xFF, r:0x7F, g:0x00, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0x00, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0x7F, b:0x00),
            Color.FromArgb(a:0xFF, r:0xFF, g:0xFF, b:0x00),
            Color.FromArgb(a:0xFF, r:0x7F, g:0xFF, b:0x7F),
            Color.FromArgb(a:0xFF, r:0x00, g:0xFF, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x7F, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0xFF),
            Color.FromArgb(a:0xFF, r:0x00, g:0x00, b:0x7F),
        };

        static PseudoColorHelper()
        {
            PseudoColorTable = InitializePseudoColorLut();
            InfraredRampTable = InitializeInfraredRampLut();
        }

        /// <summary>
        /// Maps an input infrared value between [0, 1] to corrected value between [0, 1].
        /// </summary>
        /// <param name="value">Input value between [0, 1].</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]  // Tell the compiler to inline this method to improve performance

        private static uint InfraredColor(float value)
        {
            int index = (int)(value * TableSize);
            index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
            return InfraredRampTable[index];
        }

        /// <summary>
        /// Initializes the pseudo-color look up table for infrared pixels
        /// </summary>

        private static uint[] InitializeInfraredRampLut()
        {
            uint[] lut = new uint[TableSize];
            for (int i = 0; i < TableSize; i++)
            {
                var value = (float)i / TableSize;
                // Adjust to increase color change between lower values in infrared images

                var alpha = (float)Math.Pow(1 - value, 12);
                lut[i] = ColorRampInterpolation(alpha);
            }

            return lut;
        }



        /// <summary>
        /// Initializes pseudo-color look up table for depth pixels
        /// </summary>
        private static uint[] InitializePseudoColorLut()
        {
            uint[] lut = new uint[TableSize];
            for (int i = 0; i < TableSize; i++)
            {
                lut[i] = ColorRampInterpolation((float)i / TableSize);
            }

            return lut;
        }



        /// <summary>
        /// Maps a float value to a pseudo-color pixel
        /// </summary>
        private static uint ColorRampInterpolation(float value)
        {
            // Map value to surrounding indexes on the color ramp
            int rampSteps = ColorRamp.Length - 1;
            float scaled = value * rampSteps;
            int integer = (int)scaled;
            int index =
                integer < 0 ? 0 :
                integer >= rampSteps - 1 ? rampSteps - 1 :
                integer;

            Color prev = ColorRamp[index];
            Color next = ColorRamp[index + 1];

            // Set color based on ratio of closeness between the surrounding colors
            uint alpha = (uint)((scaled - integer) * 255);
            uint beta = 255 - alpha;
            return
                ((prev.A * beta + next.A * alpha) / 255) << 24 | // Alpha
                ((prev.R * beta + next.R * alpha) / 255) << 16 | // Red
                ((prev.G * beta + next.G * alpha) / 255) << 8 |  // Green
                ((prev.B * beta + next.B * alpha) / 255);        // Blue
        }


        /// <summary>
        /// Maps a value in [0, 1] to a pseudo RGBA color.
        /// </summary>
        /// <param name="value">Input value between [0, 1].</param>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]

        private static uint PseudoColor(float value)
        {
            int index = (int)(value * TableSize);
            index = index < 0 ? 0 : index > TableSize - 1 ? TableSize - 1 : index;
            return PseudoColorTable[index];
        }

        #endregion

        /// <summary>
        /// Maps each pixel in a scanline from a 16 bit depth value to a pseudo-color pixel.
        /// </summary>
        /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>
        /// <param name="depthScale">Physical distance that corresponds to one unit in the input scanline.</param>
        /// <param name="minReliableDepth">Shortest distance at which the sensor can provide reliable measurements.</param>
        /// <param name="maxReliableDepth">Furthest distance at which the sensor can provide reliable measurements.</param>

        public static unsafe void PseudoColorForDepth(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes, float depthScale, float minReliableDepth, float maxReliableDepth)
        {
            // Visualize space in front of your desktop.
            float minInMeters = minReliableDepth * depthScale;
            float maxInMeters = maxReliableDepth * depthScale;
            float one_min = 1.0f / minInMeters;
            float range = 1.0f / maxInMeters - one_min;

            ushort* inputRow = (ushort*)inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                var depth = inputRow[x] * depthScale;

                if (depth == 0)
                {
                    // Map invalid depth values to transparent pixels.
                    // This happens when depth information cannot be calculated, e.g. when objects are too close.
                    outputRow[x] = 0;
                }
                else
                {
                    var alpha = (1.0f / depth - one_min) / range;
                    outputRow[x] = PseudoColor(alpha * alpha);
                }
            }
        }



        /// <summary>
        /// Maps each pixel in a scanline from a 8 bit infrared value to a pseudo-color pixel.
        /// </summary>
        /// /// <param name="pixelWidth">Width of the input scanline, in pixels.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>

        public static unsafe void PseudoColorFor8BitInfrared(
            int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
        {
            byte* inputRow = inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                outputRow[x] = InfraredColor(inputRow[x] / (float)Byte.MaxValue);
            }
        }

        /// <summary>
        /// Maps each pixel in a scanline from a 16 bit infrared value to a pseudo-color pixel.
        /// </summary>
        /// <param name="pixelWidth">Width of the input scanline.</param>
        /// <param name="inputRowBytes">Pointer to the start of the input scanline.</param>
        /// <param name="outputRowBytes">Pointer to the start of the output scanline.</param>

        public static unsafe void PseudoColorFor16BitInfrared(int pixelWidth, byte* inputRowBytes, byte* outputRowBytes)
        {
            ushort* inputRow = (ushort*)inputRowBytes;
            uint* outputRow = (uint*)outputRowBytes;

            for (int x = 0; x < pixelWidth; x++)
            {
                outputRow[x] = InfraredColor(inputRow[x] / (float)UInt16.MaxValue);
            }
        }
    }


    // Displays the provided softwareBitmap in a XAML image control.
    public void PresentSoftwareBitmap(SoftwareBitmap softwareBitmap)
    {
        if (softwareBitmap != null)
        {
            // Swap the processed frame to _backBuffer and trigger UI thread to render it
            softwareBitmap = Interlocked.Exchange(ref _backBuffer, softwareBitmap);

            // UI thread always reset _backBuffer before using it.  Unused bitmap should be disposed.
            softwareBitmap?.Dispose();

            // Changes to xaml ImageElement must happen in UI thread through Dispatcher
            var task = _imageElement.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
                async () =>
                {
                    // Don't let two copies of this task run at the same time.
                    if (_taskRunning)
                    {
                        return;
                    }
                    _taskRunning = true;

                    // Keep draining frames from the backbuffer until the backbuffer is empty.
                    SoftwareBitmap latestBitmap;
                    while ((latestBitmap = Interlocked.Exchange(ref _backBuffer, null)) != null)
                    {
                        var imageSource = (SoftwareBitmapSource)_imageElement.Source;
                        await imageSource.SetBitmapAsync(latestBitmap);
                        latestBitmap.Dispose();
                    }

                    _taskRunning = false;
                });
        }
    }
}

MultiSourceMediaFrameReader를 사용하여 여러 원본에서 시간 연관 프레임 가져오기

Windows 10 버전 1607부터 MultiSourceMediaFrameReader를 사용하여 다양한 원본에서 시간 연관 프레임을 가져올 수 있습니다. 이 API를 사용하면 DepthCorrelatedCoordinateMapper 클래스를 사용한 임시 근접 연결에서 얻은 여러 원본의 프레임을 필요로 하는 프로세싱을 더 쉽게 처리할 수 있습니다. 이 새로운 메서드의 한 가지 제한은 프레임 도착 이벤트가 가장 느린 캡처 원본의 속도로만 발생한다는 점입니다. 더 빠른 원본의 추가 프레임은 삭제됩니다. 또한 시스템이 다른 원본의 프레임이 다른 속도로 도착할 것으로 예상하기 때문에, 원본에서 프레임 생성을 모두 함께 중단한 경우에도 자동으로 인식하지 못합니다. 이 섹션의 예제 코드에서는 시간 연관 프레임이 앱에서 정의한 시간 제한 내에 도착하지 않으면 호출되는 자체 시간 초과 논리를 만들기 위해 이벤트를 사용하는 방법을 보여 줍니다.

MultiSourceMediaFrameReader를 사용하는 단계는 이 문서의 앞에서 설명한 MediaFrameReader를 사용하는 단계와 유사합니다. 이 예에서는 색상 원본과 깊이 원본을 사용합니다. 몇몇 문자열 변수를 정의하여 각 원본의 프레임을 선택하는 데 사용되는 미디어 프레임 소스 ID를 저장합니다. 다음, 이 예의 시간 초과 논리를 구현하는 데 사용되는 ManualResetEventSlim, CancellationTokenSourceEventHandler를 선언합니다.

private MultiSourceMediaFrameReader _multiFrameReader = null;
private string _colorSourceId = null;
private string _depthSourceId = null;


private readonly ManualResetEventSlim _frameReceived = new ManualResetEventSlim(false);
private readonly CancellationTokenSource _tokenSource = new CancellationTokenSource();
public event EventHandler CorrelationFailed;

이 문서에서 설명한 기술을 사용하여, 이 예제 시나리오에 필요한 색상 및 깊이 원본을 포함하는 MediaFrameSourceGroup을 쿼리합니다. 원하는 프레임 원본 그룹을 선택한 후, 각 프레임 원본에서 MediaFrameSourceInfo를 가져옵니다.

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.SourceKind == MediaFrameSourceKind.Depth)
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with color, depth or infrared found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo colorSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[0];
MediaFrameSourceInfo depthSourceInfo = eligibleGroups[selectedGroupIndex].SourceInfos[1];

MediaCapture 개체를 만들어 초기화하고, 초기화 설정에서 선택된 프레임 원본 그룹에 전달합니다.

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};

await mediaCapture.InitializeAsync(settings);

MediaCapture 개체를 초기화한 후, 색상 및 깊이 카메라에 대한 MediaFrameSource 개체를 가져옵니다. 각 원본의 ID를 저장하여 해당 원본에서 도착하는 프레임을 선택할 수 있도록 합니다.

MediaFrameSource colorSource =
    mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Color);

MediaFrameSource depthSource =
    mediaCapture.FrameSources.Values.FirstOrDefault(
        s => s.Info.SourceKind == MediaFrameSourceKind.Depth);

if (colorSource == null || depthSource == null)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture doesn't have the Color and Depth streams");
    return;
}

_colorSourceId = colorSource.Info.Id;
_depthSourceId = depthSource.Info.Id;

CreateMultiSourceFrameReaderAsync를 호출하고 읽기 프로그램이 사용할 프레임 원본 배열을 전달하여 MultiSourceMediaFrameReader를 만들고 초기화합니다. FrameArrived 이벤트에 대한 이벤트 처리기를 등록합니다. 이 예에서는 이 문서에서 이미 설명한 FrameRenderer 도우미 클래스의 인스턴스를 만들어 프레임을 Image 컨트롤로 렌더링합니다. StartAsync를 호출하여 프레임 읽기 프로그램을 시작합니다.

이 예의 앞에서 선언한 CorellationFailed 이벤트에 대한 이벤트 처리기를 등록합니다. 사용 중인 미디어 프레임 원본 중 하나가 프레임 생성을 중단하면 이 이벤트에 신호를 보낼 것입니다. 마지막으로, Task.Run을 호출하여 시간 초과 도우미 메서드인 NotifyAboutCorrelationFailure를 별도 스레드에서 호출합니다. 이 메서드의 구현 방법은 이 문서의 뒷부분에서 설명합니다.

_multiFrameReader = await mediaCapture.CreateMultiSourceFrameReaderAsync(
    new[] { colorSource, depthSource });

_multiFrameReader.FrameArrived += MultiFrameReader_FrameArrived;

_frameRenderer = new FrameRenderer(imageElement);

MultiSourceMediaFrameReaderStartStatus startStatus =
    await _multiFrameReader.StartAsync();

if (startStatus != MultiSourceMediaFrameReaderStartStatus.Success)
{
    throw new InvalidOperationException(
        "Unable to start reader: " + startStatus);
}

this.CorrelationFailed += MainPage_CorrelationFailed;
Task.Run(() => NotifyAboutCorrelationFailure(_tokenSource.Token));

MultiSourceMediaFrameReader에서 관리하는 모든 미디어 프레임 원본에서 새 프레임을 사용할 수 있게 되면 FrameArrived 이벤트가 발생합니다. 즉, 이벤트가 가장 느린 미디어 원본의 흐름에 따라 발생하게 됩니다. 한 원본에서 더 느린 원본이 한 프레임을 생성하는 동안 여러 프레임을 생성한 경우, 빠른 원본의 추가 프레임은 삭제됩니다.

TryAcquireLatestFrame을 호출하여 이벤트와 관련된 MultiSourceMediaFrameReference를 가져옵니다. TryGetFrameReferenceBySourceId를 호출하고 프레임 읽기 프로그램이 초기화되었을 때 저장된 ID 문자열을 전달하여 각 미디어 프레임 원본에 관련된 MediaFrameReference를 가져옵니다.

ManualResetEventSlim 개체의 Set 메서드를 호출하여 프레임이 도착했다는 신호를 보냅니다. 이 이벤트는 별도의 스레드에서 실행 중인 NotifyCorrelationFailure 메서드에서 확인합니다.

마지막으로, 시간 연관 프레임을 위한 처리를 수행합니다. 이 예에서는 간단히 깊이 원본의 프레임만 표시합니다.

private void MultiFrameReader_FrameArrived(MultiSourceMediaFrameReader sender, MultiSourceMediaFrameArrivedEventArgs args)
{
    using (MultiSourceMediaFrameReference muxedFrame =
        sender.TryAcquireLatestFrame())
    using (MediaFrameReference colorFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(_colorSourceId))
    using (MediaFrameReference depthFrame =
        muxedFrame.TryGetFrameReferenceBySourceId(_depthSourceId))
    {
        // Notify the listener thread that the frame has been received.
        _frameReceived.Set();
        _frameRenderer.ProcessFrame(depthFrame);
    }
}

프레임 읽기 프로그램이 다시 시작된 후 NotifyCorrelationFailure 도우미 메서드가 별도 스레드에서 실행됩니다. 이 메서드는 프레임 수신 이벤트의 신호를 받았는지 확인합니다. FrameArrived 처리기에서 상호 관련 프레임의 집합이 도착할 때마다 이 이벤트가 발생하도록 설정했습니다. 앱에서 정의한 기간(5초 정도가 적당) 동안 이벤트에서 신호를 보내지 않을 경우나 CancellationToken을 사용하여 작업이 취소된 경우는 미디어 프레임 중 하나에서 읽기를 중지한 것일 수 있습니다. 이런 경우 일반적으로 프레임 읽기 프로그램을 종료해야 하므로, 앱에서 정의한 CorrelationFailed 이벤트를 발생시킵니다. 이 이벤트의 처리기에서 프레임 읽기 프로그램을 중지하고 이 문서 앞부분에서 설명한 방법으로 관련된 리소스를 정리할 수 있습니다.

private void NotifyAboutCorrelationFailure(CancellationToken token)
{
    // If in 5 seconds the token is not cancelled and frame event is not signaled,
    // correlation is most likely failed.
    if (WaitHandle.WaitAny(new[] { token.WaitHandle, _frameReceived.WaitHandle }, 5000)
            == WaitHandle.WaitTimeout)
    {
        CorrelationFailed?.Invoke(this, EventArgs.Empty);
    }
}
private async void MainPage_CorrelationFailed(object sender, EventArgs e)
{
    await _multiFrameReader.StopAsync();
    _multiFrameReader.FrameArrived -= MultiFrameReader_FrameArrived;
    mediaCapture.Dispose();
    mediaCapture = null;
}

버퍼링된 프레임 획득 모드를 사용하여 획득된 프레임의 시퀀스를 유지합니다.

Windows 10 버전 1709부터 MediaFrameReader 또는 MultiSourceMediaFrameReaderAcquisitionMode 속성을 Buffered로 설정하여 프레임 소스에서 앱으로 전달되는 프레임의 시퀀스를 유지할 수 있습니다.

mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;

기본 획득 모드 Realtime에서 앱이 이전 프레임에 대해 FrameArrived 이벤트를 처리하는 동안 소스에서 여러 프레임을 얻으면 시스템은 앱에 가장 최근에 획득한 프레임을 보내고 버퍼에서 대기하고 있는 프레임을 삭제합니다. 따라서 앱에 항상 가장 최근에 사용 가능한 프레임이 제공됩니다. 이것은 일반적으로 실시간 컴퓨터 비전 애플리케이션에 가장 유용한 모드입니다.

Buffered 획득 모드에서 시스템은 모든 프레임을 버퍼에 보관하고 FrameArrived 이벤트를 통해 받은 순서대로 앱에 제공합니다. 이 모드에서 프레임에 대한 시스템의 버퍼가 채워지면 앱이 이전 프레임에 대해 FrameArrived 이벤트를 완료할 때까지 새로운 프레임 획득을 중지하여 버퍼에서 더 많은 공간을 확보합니다.

MediaSource를 사용하여 MediaPlayerElement에 프레임 표시

Windows 버전 1709부터 XAML 페이지의 MediaPlayerElement 컨트롤에서 직접 MediaFrameReader에서 획득한 프레임을 표시할 수 있습니다. 이 작업을 하려면 MediaSource.CreateFromMediaFrameSource를 사용하여 MediaPlayerElement와 관련된 MediaPlayer가 직접 사용할 수 있는 MediaSource 개체를 만듭니다. MediaPlayerMediaPlayerElement 작업에 대한 자세한 내용은 MediaPlayer를 사용하여 오디오 및 비디오 재생을 참조하세요.

다음 코드 예제는 XAML 페이지에서 정면 및 후면 카메라의 프레임을 동시에 표시하는 간단한 구현을 보여줍니다.

첫째, MediaPlayerElement 컨트롤 2개를 XAML 페이지에 추가합니다.

<MediaPlayerElement x:Name="mediaPlayerElement1" Width="320" Height="240"/>
<MediaPlayerElement x:Name="mediaPlayerElement2" Width="320" Height="240"/>

그런 다음 이 문서의 이전 섹션에 나오는 기술을 사용하여 전면 패널 및 후면 패널에서 컬러 카메라용 MediaFrameSourceInfo 개체를 포함하는 MediaFrameSourceGroup을 선택합니다. MediaPlayer은 프레임을 깊이 또는 적외선 데이터와 같은 색상 이외의 형식에서 색 데이터로 자동 변환하지 않습니다. 다른 센서 유형을 사용하면 예상치 못한 결과가 발생할 수 있습니다.

var allGroups = await MediaFrameSourceGroup.FindAllAsync();
var eligibleGroups = allGroups.Select(g => new
{
    Group = g,

    // For each source kind, find the source which offers that kind of media frame,
    // or null if there is no such source.
    SourceInfos = new MediaFrameSourceInfo[]
    {
        g.SourceInfos.FirstOrDefault(info => info.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Front
            && info.SourceKind == MediaFrameSourceKind.Color),
        g.SourceInfos.FirstOrDefault(info => info.DeviceInformation?.EnclosureLocation.Panel == Windows.Devices.Enumeration.Panel.Back
            && info.SourceKind == MediaFrameSourceKind.Color)
    }
}).Where(g => g.SourceInfos.Any(info => info != null)).ToList();

if (eligibleGroups.Count == 0)
{
    System.Diagnostics.Debug.WriteLine("No source group with front and back-facing camera found.");
    return;
}

var selectedGroupIndex = 0; // Select the first eligible group
MediaFrameSourceGroup selectedGroup = eligibleGroups[selectedGroupIndex].Group;
MediaFrameSourceInfo frontSourceInfo = selectedGroup.SourceInfos[0];
MediaFrameSourceInfo backSourceInfo = selectedGroup.SourceInfos[1];

선택한 MediaFrameSourceGroup을 사용하기 위해 MediaCapture 개체를 초기화합니다.

mediaCapture = new MediaCapture();

var settings = new MediaCaptureInitializationSettings()
{
    SourceGroup = selectedGroup,
    SharingMode = MediaCaptureSharingMode.ExclusiveControl,
    MemoryPreference = MediaCaptureMemoryPreference.Cpu,
    StreamingCaptureMode = StreamingCaptureMode.Video
};
try
{
    await mediaCapture.InitializeAsync(settings);
}
catch (Exception ex)
{
    System.Diagnostics.Debug.WriteLine("MediaCapture initialization failed: " + ex.Message);
    return;
}

마지막으로 MediaSource.CreateFromMediaFrameSource를 호출하고 관련 MediaFrameSourceInfo 개체의 Id 속성을 사용하여 각 프레임 원본별로 MediaSource를 만들고 나서 MediaCapture object's FrameSources 컬렉션에서 프레임 원본 중 하나를 선택합니다. SetMediaPlayer를 호출하여 새로운 MediaPlayer 개체를 초기화하고 이것을 MediaPlayerElement에 할당합니다. 그리고 나서 Source 속성을 새로 만든 MediaSource 개체로 설정합니다.

var frameMediaSource1 = MediaSource.CreateFromMediaFrameSource(mediaCapture.FrameSources[frontSourceInfo.Id]);
mediaPlayerElement1.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement1.MediaPlayer.Source = frameMediaSource1;
mediaPlayerElement1.AutoPlay = true;

var frameMediaSource2 = MediaSource.CreateFromMediaFrameSource(mediaCapture.FrameSources[backSourceInfo.Id]);
mediaPlayerElement2.SetMediaPlayer(new Windows.Media.Playback.MediaPlayer());
mediaPlayerElement2.MediaPlayer.Source = frameMediaSource2;
mediaPlayerElement2.AutoPlay = true;

비디오 프로필을 사용하여 프레임 원본 선택

MediaCaptureVideoProfile 개체로 표시되는 카메라 프로필은 프레임 속도, 해상도 또는 HDR 캡처와 같은 고급 기능 등 특정 캡처 디바이스에서 제공하는 기능 세트를 나타냅니다. 캡처 디바이스는 여러 프로필을 지원할 수 있으므로 캡처 시나리오에 맞게 최적화된 프로필을 선택할 수 있습니다. Windows 10 버전 1803부터 MediaCaptureVideoProfile을 사용하여 특정 기능과 함께 미디어 프레임 원본을 선택한 후 MediaCapture 개체를 초기화할 수 있습니다. 다음 예제 메서드는 WCG(Wide Color Gamut)로 HDR을 지원하는 비디오 프로필을 찾고 선택한 디바이스와 프로필을 사용하기 위해 MediaCapture를 초기화하는 데 사용할 수 있는 MediaCaptureInitializationSettings 개체를 반환합니다.

먼저, MediaFrameSourceGroup.FindAllAsync를 호출하여 현재 디바이스에서 사용할 수 있는 전체 미디어 프레임 소스 그룹 목록을 가져옵니다. 각 소스 그룹을 반복 실행하고 MediaCapture.FindKnownVideoProfiles를 호출하여 지정된 프로필을 지원하는 현재 소스 그룹에 대한 모든 비디오 프로필 목록을 가져옵니다(이 경우는 WCG 사진이 있는 HDR). 기준을 충족하는 프로필이 발견되면 새로운 MediaCaptureInitializationSettings 개체를 만들고 VideoProfile을 프로필 선택으로 설정하고 VideoDeviceId를 현재 미디어 프레임 소스 그룹의 Id 속성으로 설정합니다.

public async Task<MediaCaptureInitializationSettings> FindHdrWithWcgPhotoProfile()
{
    IReadOnlyList<MediaFrameSourceGroup> sourceGroups = await MediaFrameSourceGroup.FindAllAsync();
    MediaCaptureInitializationSettings settings = null;

    foreach (MediaFrameSourceGroup sourceGroup in sourceGroups)
    {
        // Find a device that support AdvancedColorPhoto
        IReadOnlyList<MediaCaptureVideoProfile> profileList = MediaCapture.FindKnownVideoProfiles(
                                      sourceGroup.Id,
                                      KnownVideoProfile.HdrWithWcgPhoto);

        if (profileList.Count > 0)
        {
            settings = new MediaCaptureInitializationSettings();
            settings.VideoProfile = profileList[0];
            settings.VideoDeviceId = sourceGroup.Id;
            break;
        }
    }
    return settings;
}

private void StartDeviceWatcherButton_Click(object sender, RoutedEventArgs e)
{
    var remoteCameraHelper = new RemoteCameraPairingHelper(this.Dispatcher);
}

카메라 프로필 사용에 대한 자세한 내용은 카메라 프로필을 참조하세요.