Elaborare fotogrammi multimediali con MediaFrameReader
Questo articolo illustra come utilizzare un MediaFrameReader con MediaCapture per ottenere fotogrammi multimediali da una o più origini disponibili, tra cui fotocamere a colori, di profondità e a infrarossi, dispositivi audio o persino origini di fotogrammi personalizzati, ad esempio quelle che producono fotogrammi di tracciamento scheletrici. Questa funzionalità è progettata per l'uso in app che eseguono elaborazioni in tempo reale dei fotogrammi multimediali, come le app di realtà aumentata o per fotocamere con riconoscimento della profondità.
Se interessa semplicemente acquisire video o foto, ad esempio un'app di fotografia tipica, probabilmente si vuole usare una delle altre tecniche di acquisizione supportate da MediaCapture. Per un elenco delle tecniche e degli articoli di acquisizione multimediale disponibili che illustrano come usarle, vedere Fotocamera.
Nota
Le funzionalità descritte in questo articolo sono disponibili solo a partire da Windows 10 versione 1607.
Nota
È disponibile un esempio di app di Windows universale che illustra l'uso di MediaFrameReader per visualizzare fotogrammi da origini di fotogrammi diverse, tra cui fotocamere a colori, profondità e infrarossi. Per altre informazioni, vedere l'esempio Camera frames.
Nota
In Windows 10 versione 1803 è stato introdotto un nuovo set di API per l'uso di MediaFrameReader con dati audio. Per altre informazioni, vedere Elaborare frame audio con MediaFrameReader.
Impostazione del progetto
Come per qualsiasi app che usa MediaCapture, si deve dichiarare che l'app usa la funzionalità webcam prima di tentare di accedere a qualsiasi dispositivo fotocamera. Se l'app acquisisce da un dispositivo audio, devi anche dichiarare la funzionalità del dispositivo microfono.
Aggiungere funzionalità al manifesto dell'app
- In Esplora soluzioni in Microsoft Visual Studio aprire la finestra di progettazione per il manifesto dell'applicazione facendo doppio clic sull'elemento package.appxmanifest.
- Fare clic sulla scheda Funzionalità.
- Selezionare la casella Webcam e la casella microfono.
- Per accedere alla raccolta immagini e video, selezionare le caselle Pictures Library e la casella Videos Library.
Questo codice di esempio in questo articolo usa le API degli spazi dei nomi seguenti, oltre a quelli inclusi nel modello di progetto predefinito.
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;
Selezionare le origini dei fotogrammi e i gruppi di origine dei frame
Molte app che elaborano fotogrammi multimediali devono ottenere fotogrammi da più origini contemporaneamente, ad esempio fotocamere a colori e profondità di un dispositivo. L'oggetto MediaFrameSourceGroup rappresenta un set di origini di fotogrammi multimediali che possono essere usati contemporaneamente. Chiamare il metodo statico MediaFrameSourceGroup.FindAllAsync per ottenere un elenco di tutti i gruppi di origini frame supportate dal dispositivo corrente.
var frameSourceGroups = await MediaFrameSourceGroup.FindAllAsync();
Si può anche creare un DeviceWatcher usando DeviceInformation.CreateWatcher e il valore restituito da MediaFrameSourceGroup.GetDeviceSelector per ricevere notifiche quando cambiano i gruppi di origine dei fotogrammi disponibili nel dispositivo, ad esempio quando una fotocamera esterna è collegata. Per altre informazioni, vedere Enumerare i dispositivi.
MediaFrameSourceGroup include una raccolta di oggetti MediaFrameSourceInfo che descrivono le origini dei fotogrammi incluse nel gruppo. Dopo aver recuperato i gruppi di origine dei frame disponibili nel dispositivo, è possibile selezionare il gruppo che espone le origini dei frame a cui si è interessati.
L'esempio seguente illustra il modo più semplice per selezionare un gruppo di origine frame. Questo codice esegue semplicemente un ciclo su tutti i gruppi disponibili e quindi esegue un ciclo su ogni elemento dell'insieme SourceInfos . Ogni MediaFrameSourceInfo viene controllato per verificare se supporta le funzionalità che stiamo cercando. In questo caso, la proprietà MediaStreamType viene controllata per il valore VideoPreview, ovvero il dispositivo fornisce un flusso di anteprima video e la proprietà SourceKind viene controllata per il valore Color, a indicare che l'origine fornisce fotogrammi a colori.
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;
}
}
Questo metodo di identificazione del gruppo di origine dei fotogrammi desiderato e delle origini frame funziona per casi semplici, ma se si desidera selezionare le origini dei fotogrammi in base a criteri più complessi, può diventare rapidamente complesso. Un altro metodo consiste nell'usare la sintassi Linq e gli oggetti anonimi per effettuare la selezione. Nell'esempio seguente viene utilizzato il metodo di estensione Select per trasformare gli oggetti MediaFrameSourceGroup nell'elenco frameSourceGroups in un oggetto anonimo con due campi: sourceGroup, che rappresenta il gruppo stesso e colorSourceInfo, che rappresenta l'origine del frame di colore nel gruppo. Il campo colorSourceInfo viene impostato sul risultato di FirstOrDefault, che seleziona il primo oggetto per cui il predicato specificato viene risolto in true. In questo caso, il predicato è true se il tipo di flusso è VideoPreview, il tipo di origine è Color e se la fotocamera si trova nel pannello anteriore del dispositivo.
Dall'elenco di oggetti anonimi restituiti dalla query descritta in precedenza, il metodo di estensione Where viene usato per selezionare solo gli oggetti in cui il campo colorSourceInfo non è Null. Viene infine chiamato FirstOrDefault per selezionare il primo elemento nell'elenco.
È ora possibile utilizzare i campi dell'oggetto selezionato per ottenere riferimenti all'oggetto MediaFrameSourceGroup selezionato e all'oggetto MediaFrameSourceInfo che rappresenta la fotocamera a colori. Questi verranno usati in un secondo momento per inizializzare l'oggetto MediaCapture e creare un MediaFrameReader per l'origine selezionata. Infine, è necessario verificare se il gruppo di origine è Null, ovvero il dispositivo corrente non ha le origini di acquisizione richieste.
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;
}
L'esempio seguente usa una tecnica simile come descritto in precedenza per selezionare un gruppo di origine che contiene fotocamere a colori, profondità e infrarossi.
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];
Nota
A partire da Windows 10, versione 1803, si può usare la classe MediaCaptureVideoProfile per selezionare un'origine fotogramma multimediale con un set di funzionalità desiderate. Per altre informazioni, vedere la sezione Usare i profili video per selezionare un'origine fotogramma più avanti in questo articolo.
Inizializzare l'oggetto MediaCapture per utilizzare il gruppo di origine frame selezionato
Il passaggio successivo consiste nell'inizializzare l'oggetto MediaCapture per usare il gruppo di origine frame selezionato nel passaggio precedente.
L'oggetto MediaCapture viene in genere usato da più posizioni all'interno dell'app, quindi devi dichiarare una variabile membro della classe per conservarla.
MediaCapture mediaCapture;
Creare un'istanza dell'oggetto MediaCapture chiamando il costruttore. Creare quindi un oggetto MediaCaptureInitializationSettings che verrà utilizzato per inizializzare l'oggetto MediaCapture. In questo esempio vengono usate le impostazioni seguenti:
- SourceGroup : indica al sistema quale gruppo di origine verrà usato per ottenere fotogrammi. Tenere presente che il gruppo di origine definisce un set di origini di fotogrammi multimediali che possono essere usate contemporaneamente.
- SharingMode: indica al sistema se è necessario il controllo esclusivo sui dispositivi di origine di acquisizione. Se si imposta questa opzione su ExclusiveControl, significa che si può modificare le impostazioni del dispositivo di acquisizione, ad esempio il formato dei fotogrammi che produce, ma ciò significa che se un'altra app ha già un controllo esclusivo, l'app avrà esito negativo quando tenta di inizializzare il dispositivo di acquisizione multimediale. Se si imposta questa opzione su SharedReadOnly, è possibile ricevere fotogrammi dalle origini dei fotogrammi anche se sono in uso da un'altra app, ma non è possibile modificare le impostazioni per i dispositivi.
- MemoryPreference: se si specifica CPU, il sistema userà la memoria CPU che garantisce che quando arrivano i fotogrammi saranno disponibili come oggetti SoftwareBitmap. Se si specifica Auto, il sistema sceglierà in modo dinamico la posizione di memoria ottimale per archiviare i fotogrammi. Se il sistema sceglie di usare la memoria GPU, i fotogrammi multimediali arriveranno come oggetto IDirect3DSurface e non come SoftwareBitmap.
- StreamingCaptureMode: impostare questa opzione su Video per indicare che l'audio non deve essere trasmesso in streaming.
Chiamare InitializeAsync per inizializzare MediaCapture con le impostazioni desiderate. Assicurarsi di chiamare questa operazione all'interno di un blocco try nel caso in cui l'inizializzazione abbia esito negativo.
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;
}
Impostare il formato preferito per l'origine dei fotogrammi
Per impostare il formato preferito per un'origine frame, è necessario ottenere un oggetto MediaFrameSource che rappresenta l'origine. Per ottenere questo oggetto, accedere al dizionario Frames di MediaCapture inizializzato, specificando l'identificatore dell'origine frame che si desidera utilizzare. Questo è il motivo per cui è stato salvato l'oggetto MediaFrameSourceInfo quando si seleziona un gruppo di origine frame.
La proprietà MediaFrameSource.SupportedFormats contiene un elenco di oggetti MediaFrameFormat che descrivono i formati supportati per l'origine dei fotogrammi. Utilizzare il metodo di estensione Where Linq per selezionare un formato in base alle proprietà desiderate. In questo esempio viene selezionato un formato con una larghezza di 1080 pixel e che può fornire fotogrammi in formato RGB a 32 bit. Il metodo di estensione FirstOrDefault seleziona la prima voce nell'elenco. Se il formato selezionato è Null, il formato richiesto non è supportato dall'origine del frame. Se il formato è supportato, è possibile richiedere che l'origine usi questo formato chiamando 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);
Creare un lettore di fotogrammi per l'origine frame
Per ricevere fotogrammi per un'origine di fotogrammi multimediali, usare MediaFrameReader.
MediaFrameReader mediaFrameReader;
Creare un'istanza del lettore di fotogrammi chiamando CreateFrameReaderAsync sull'oggetto MediaCapture inizializzato. Il primo argomento di questo metodo è l'origine dei fotogrammi da cui si desidera ricevere fotogrammi. È possibile creare un lettore di fotogrammi separato per ogni origine frame che si vuole usare. Il secondo argomento indica al sistema il formato di output in cui si desidera che i fotogrammi arrivino. In questo modo è possibile evitare la necessità di eseguire conversioni personalizzate in fotogrammi non appena arrivano. Si noti che se si specifica un formato non supportato dall'origine dei fotogrammi, verrà generata un'eccezione, quindi assicurarsi che questo valore si trovi nell'insieme SupportedFormats.
Dopo aver creato il lettore di fotogrammi, registrare un gestore per l'evento FrameArrived che viene generato ogni volta che un nuovo frame è disponibile dall'origine.
Indicare al sistema di avviare la lettura dei fotogrammi dall'origine chiamando StartAsync.
mediaFrameReader = await mediaCapture.CreateFrameReaderAsync(colorFrameSource, MediaEncodingSubtypes.Argb32);
mediaFrameReader.FrameArrived += ColorFrameReader_FrameArrived;
await mediaFrameReader.StartAsync();
Gestire l'evento frame arrivato
L'evento MediaFrameReader.FrameArrived viene generato ogni volta che un nuovo frame è disponibile. È possibile scegliere di elaborare ogni fotogramma che arriva o usare solo fotogrammi quando sono necessari. Poiché il lettore di fotogrammi genera l'evento nel proprio thread, potrebbe essere necessario implementare una logica di sincronizzazione per assicurarsi di non tentare di accedere agli stessi dati da più thread. Questa sezione illustra come sincronizzare i fotogrammi di colore del disegno con un controllo immagine in una pagina XAML. Questo scenario risolve il vincolo di sincronizzazione aggiuntivo che richiede l'esecuzione di tutti gli aggiornamenti ai controlli XAML nel thread dell'interfaccia utente.
Il primo passaggio nella visualizzazione dei fotogrammi in XAML consiste nel creare un controllo Immagine.
<Image x:Name="imageElement" Width="320" Height="240" />
Nella pagina code-behind dichiarare una variabile membro della classe di tipo SoftwareBitmap che verrà usata come buffer nascosto in cui verranno copiate tutte le immagini in ingresso. Si noti che i dati dell'immagine non vengono copiati, ma solo i riferimenti all'oggetto. Dichiarare anche un valore booleano per tenere traccia se l'operazione dell'interfaccia utente è attualmente in esecuzione.
private SoftwareBitmap backBuffer;
private bool taskRunning = false;
Poiché i fotogrammi arriveranno come oggetti SoftwareBitmap è necessario creare un oggetto SoftwareBitmapSource che consente di usare un SoftwareBitmap come origine per un controllo XAML . È necessario impostare l'origine dell'immagine in un punto qualsiasi del codice prima di avviare il lettore di fotogrammi.
imageElement.Source = new SoftwareBitmapSource();
È ora possibile implementare il gestore eventi FrameArrived. Quando viene chiamato il gestore, il parametro sender contiene un riferimento all'oggetto MediaFrameReader che ha generato l'evento. Chiamare TryAcquireLatestFrame su questo oggetto per tentare di ottenere il frame più recente. Come suggerisce il nome, TryAcquireLatestFrame potrebbe non riuscire a restituire un frame. Quindi, quando si accede alle proprietà VideoMediaFrame e quindi SoftwareBitmap, assicurarsi di testare per null. In questo esempio l'operatore condizionale Null ? viene usato per accedere a SoftwareBitmap e quindi l'oggetto recuperato viene controllato per null.
Il controlloImmagine può visualizzare solo immagini in formato BRGA8 con pre-moltiplicato o nessun alfa. Se il frame in arrivo non è in tale formato, il metodo statico Convert viene usato per convertire la bitmap software nel formato corretto.
Successivamente, il metodo Interlocked.Exchange viene usato per scambiare il riferimento di alla bitmap in arrivo con la bitmap backbuffer. Questo metodo scambia questi riferimenti in un'operazione atomica thread-safe. Dopo lo scambio, l'immagine backbuffer precedente, ora nella variabile softwareBitmap viene eliminata per pulire le risorse.
Successivamente, CoreDispatcher associato all'elemento Image viene usato per creare un'attività che verrà eseguita nel thread dell'interfaccia utente chiamando RunAsync. Poiché le attività asincrone verranno eseguite all'interno dell'attività, l'espressione lambda passata a RunAsync viene dichiarata con la parola chiave async.
All'interno dell'attività, la variabile _taskRunning viene controllata per assicurarsi che sia in esecuzione una sola istanza dell'attività alla volta. Se l'attività non è già in esecuzione, _taskRunning è impostata su true per impedire l'esecuzione dell'attività. In un ciclo while, Interlocked.Exchange viene chiamato per copiare dal backbuffer in un SoftwareBitmap temporaneo fino a quando l'immagine backbuffer non è null. Per ogni volta che viene popolata la bitmap temporanea, viene eseguito il cast della proprietà Source di Image in un SoftwareBitmapSource, quindi SetBitmapAsync per impostare l'origine dell'immagine.
Infine, la variabile _taskRunning viene impostata su false in modo che l'attività possa essere eseguita di nuovo alla chiamata del gestore.
Nota
Se si accede agli oggetti SoftwareBitmap o Direct3DSurface forniti dalla proprietà VideoMediaFrame di un MediaFrameReference, il sistema crea un riferimento sicuro a questi oggetti, il che significa che non verranno eliminati quando si chiama Dispose sul contenitore MediaFrameReference. È necessario chiamare in modo esplicito il metodo Dispose di SoftwareBitmap o Direct3DSurface direttamente affinché gli oggetti vengano eliminati immediatamente. In caso contrario, il Garbage Collector libera la memoria per questi oggetti, ma non è possibile sapere quando si verificherà e se il numero di bitmap o superfici allocate supera la quantità massima consentita dal sistema, il flusso dei nuovi frame verrà arrestato. È possibile copiare fotogrammi recuperati, usando il metodo SoftwareBitmap.Copy, ad esempio, e quindi rilasciare i fotogrammi originali per superare questa limitazione. Inoltre, se si crea MediaFrameReader usando l'overload CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype, Windows.Graphics.Imaging.BitmapSize outputSize) o CreateFrameReaderAsync(Windows.Media.Capture.Frames.MediaFrameSource inputSource, System.String outputSubtype), i fotogrammi restituiti sono copie dei dati dei frame originali e quindi non causano l'interruzione dell'acquisizione dei fotogrammi quando sono mantenuti.
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();
}
Risorse di pulizia
Al termine della lettura dei fotogrammi, assicurarsi di arrestare il lettore di fotogrammi multimediali chiamando StopAsync, annullando la registrazione del gestore FrameArrived ed eliminando l'oggetto MediaCapture.
await mediaFrameReader.StopAsync();
mediaFrameReader.FrameArrived -= ColorFrameReader_FrameArrived;
mediaCapture.Dispose();
mediaCapture = null;
Per altre informazioni sulla pulizia degli oggetti di acquisizione multimediale quando l'applicazione è sospesa, vedere Visualizzare l'anteprima della fotocamera.
Classe helper FrameRenderer
L'esempio di fotogrammi della fotocamera di Universal Windows fornisce una classe helper che semplifica la visualizzazione dei fotogrammi da origini a colori, infrarossi e profondità nell'app. In genere, è consigliabile eseguire operazioni più dettagliate e a infrarossi rispetto alla visualizzazione dello schermo, ma questa classe helper è uno strumento utile per illustrare la funzionalità di lettura dei fotogrammi e per il debug dell'implementazione del lettore di fotogrammi personalizzata.
La classe helper FrameRenderer implementa i metodi seguenti.
- Costruttore FrameRenderer: il costruttore inizializza la classe helper per usare l'elemento Image XAML passato per la visualizzazione di fotogrammi multimediali.
- ProcessFrame: questo metodo visualizza un fotogramma multimediale, rappresentato da MediaFrameReference, nell'elemento Image passato al costruttore. In genere è consigliabile chiamare questo metodo dal gestore eventi FrameArrived passando il frame restituito da TryAcquireLatestFrame.
- ConvertToDisplayableImage: questo metodo controlla il formato del frame multimediale e, se necessario, lo converte in un formato visualizzabile. Per le immagini a colori, ciò significa assicurarsi che il formato di colore sia BGRA8 e che la modalità alfa bitmap sia premoltiplicata. Per i fotogrammi di profondità o infrarossi, ogni linea di analisi viene elaborata per convertire i valori di profondità o infrarossi in una sfumatura psuedocolor, usando la classe PsuedoColorHelper inclusa anche nell'esempio e elencata di seguito.
Nota
Per eseguire la manipolazione dei pixel nelle immagini SoftwareBitmap, è necessario accedere a un buffer di memoria nativo. A tale scopo, è necessario usare l'interfaccia COM IMemoryBufferByteAccess inclusa nell'elenco di codice riportato di seguito ed è necessario aggiornare le proprietà del progetto per consentire la compilazione di codice unsafe. Per altre informazioni, vedere Creare, modificare e salvare immagini bitmap.
[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;
});
}
}
}
Usare MultiSourceMediaFrameReader per ottenere fotogrammi correlati al tempo da più origini
A partire da Windows 10, versione 1607, si può usare MultiSourceMediaFrameReader per ricevere frame correlati al tempo da più origini. Questa API semplifica l'elaborazione che richiede frame da più origini prese in prossimità temporale, ad esempio l'uso della classe DepthCorrelatedCoordinateMapper. Una limitazione dell'uso di questo nuovo metodo consiste nel fatto che gli eventi arrivati ai fotogrammi vengono generati solo alla velocità dell'origine di acquisizione più lenta. Verranno eliminati fotogrammi aggiuntivi da origini più veloci. Inoltre, poiché il sistema prevede che i fotogrammi arrivino da origini diverse a frequenze diverse, non riconosce automaticamente se un'origine ha interrotto completamente la generazione di fotogrammi. Il codice di esempio in questa sezione illustra come usare un evento per creare una logica di timeout personalizzata che viene richiamata se i frame correlati non arrivano entro un limite di tempo definito dall'app.
I passaggi per l'uso di MultiSourceMediaFrameReader sono simili ai passaggi per l'uso MediaFrameReader descritti in precedenza in questo articolo. Questo esempio userà un'origine colore e un'origine di profondità. Dichiarare alcune variabili stringa per archiviare gli ID di origine dei fotogrammi multimediali che verranno usati per selezionare fotogrammi da ogni origine. Successivamente, dichiarare un oggetto ManualResetEventSlim, un CancellationTokenSource e un EventHandler che verrà usato per implementare la logica di timeout per l'esempio.
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;
Usando le tecniche descritte in precedenza in questo articolo, eseguire una query per un MediaFrameSourceGroup che include le origini colore e profondità necessarie per questo scenario di esempio. Dopo aver selezionato il gruppo di origine dei fotogrammi desiderato, ottenere MediaFrameSourceInfo per ogni origine fotogramma.
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];
Creare e inizializzare un oggetto MediaCapture, passando il gruppo di origine frame selezionato nelle impostazioni di inizializzazione.
mediaCapture = new MediaCapture();
var settings = new MediaCaptureInitializationSettings()
{
SourceGroup = selectedGroup,
SharingMode = MediaCaptureSharingMode.ExclusiveControl,
MemoryPreference = MediaCaptureMemoryPreference.Cpu,
StreamingCaptureMode = StreamingCaptureMode.Video
};
await mediaCapture.InitializeAsync(settings);
Dopo aver inizializzato l'oggetto MediaCapture, recuperare gli oggetti MediaFrameSource per le fotocamere a colori e profondità. Archiviare l'ID per ogni origine in modo da poter selezionare il frame in arrivo per l'origine corrispondente.
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;
Creare e inizializzare MultiSourceMediaFrameReader chiamando CreateMultiSourceFrameReaderAsync e passando una matrice di origini frame che verrà usata dal lettore. Registrare un gestore dell'evento per l'evento FrameArrived. In questo esempio viene creata un'istanza della classe helper FrameRenderer, descritta in precedenza in questo articolo, per eseguire il rendering dei fotogrammi in un controllo Image. Infine, avviare il lettore di fotogrammi chiamando StartAsync.
Registrare un gestore eventi per l'evento IntegerlationFailed dichiarato in precedenza nell'esempio. Segnaleremo questo evento se una delle origini dei fotogrammi multimediali in uso smette di produrre fotogrammi. Chiamare infine Task.Run per chiamare il metodo helper di timeout, NotifyAboutCorrelationFailure, in un thread separato. L'implementazione di questo metodo viene illustrata più avanti in questo articolo.
_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));
L'evento FrameArrived viene generato ogni volta che un nuovo fotogramma è disponibile da tutte le origini dei fotogrammi multimediali gestite da MultiSourceMediaFrameReader. Ciò significa che l'evento verrà generato sulla cadenza dell'origine multimediale più lenta. Se un'origine produce più fotogrammi nel tempo in cui un'origine più lenta produce un fotogramma, verranno eliminati i fotogrammi aggiuntivi dall'origine veloce.
Ottenere l'oggetto MultiSourceMediaFrameReference associato all'evento chiamando TryAcquireLatestFrame. Ottenere MediaFrameReference associato a ogni origine di fotogrammi multimediali chiamando TryGetFrameReferenceBySourceId, passando le stringhe ID archiviate quando il lettore di fotogrammi è stato inizializzato.
Chiamare il metodo Set dell'oggetto ManualResetEventSlim per segnalare che i fotogrammi sono arrivati. Questo evento verrà controllato nel metodo NotifyCorrelationFailure in esecuzione in un thread separato.
Infine, eseguire qualsiasi elaborazione sui fotogrammi multimediali correlati al tempo. In questo esempio viene semplicemente visualizzato il fotogramma dall'origine profondità.
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);
}
}
Il metodo helper NotifyCorrelationFailure è stato eseguito su un thread separato dopo l'avvio del lettore di fotogrammi. In questo metodo verificare se il frame ha ricevuto l'evento è stato segnalato. Tenere presente che, nel gestore FrameArrived, questo evento viene impostato ogni volta che arriva un set di fotogrammi correlati. Se l'evento non è stato segnalato per un periodo di tempo definito dall'app, ovvero 5 secondi, è un valore ragionevole e l'attività non è stata annullata usando CancellationToken, è probabile che una delle origini dei fotogrammi multimediali abbia interrotto la lettura dei fotogrammi. In questo caso in genere si vuole arrestare il lettore di fotogrammi, quindi generare l'evento CorrelationFailed definito dall'app. Nel gestore per questo evento è possibile arrestare il lettore di fotogrammi e pulire le risorse associate, come illustrato in precedenza in questo articolo.
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;
}
Usare la modalità di acquisizione dei fotogrammi memorizzati nel buffer per mantenere la sequenza di fotogrammi acquisiti
A partire da Windows 10 versione 1709, si può impostare la proprietà AcquisitionMode di un MediaFrameReader o MultiSourceMediaFrameReader a Buffered per mantenere la sequenza di fotogrammi passati nell'app dall'origine del frame.
mediaFrameReader.AcquisitionMode = MediaFrameReaderAcquisitionMode.Buffered;
Nella modalità di acquisizione predefinita, Realtime, se più fotogrammi vengono acquisiti dall'origine mentre l'app gestisce ancora l'evento FrameArrived per un frame precedente, il sistema invierà l'app al frame acquisito più di recente e rilascia fotogrammi aggiuntivi in attesa nel buffer. In questo modo l'app offre sempre il frame più recente disponibile. Questa è in genere la modalità più utile per le applicazioni di visione artificiale in tempo reale.
In modalità di acquisizione con la modalità di acquisizione Buffered, il sistema manterrà tutti i fotogrammi nel buffer e li fornirà all'app tramite l'evento FrameArrived nell'ordine ricevuto. Tenere presente che in questa modalità, quando il buffer del sistema per i fotogrammi viene riempito, il sistema smetterà di acquisire nuovi fotogrammi finché l'app non completa l'evento FrameArrived per i fotogrammi precedenti, liberando più spazio nel buffer.
Usare MediaSource per visualizzare fotogrammi in mediaPlayerElement
A partire da Windows, versione 1709, puoi visualizzare i fotogrammi acquisiti da MediaFrameReader direttamente in un controllo MediaPlayerElement nella pagina XAML. Questo risultato viene ottenuto usando MediaSource.CreateFromMediaFrameSource per creare un oggetto MediaSource che può essere usato direttamente da un MediaPlayer associato a MediaPlayerElement. Per informazioni dettagliate sull'uso di MediaPlayer e MediaPlayerElement, vedere Riprodurre audio e video con MediaPlayer.
Gli esempi di codice seguenti illustrano un'implementazione semplice che mostra i fotogrammi da una fotocamera anteriore e posteriore contemporaneamente in una pagina XAML.
Aggiungere prima di tutto due controlli MediaPlayerElement alla pagina XAML.
<MediaPlayerElement x:Name="mediaPlayerElement1" Width="320" Height="240"/>
<MediaPlayerElement x:Name="mediaPlayerElement2" Width="320" Height="240"/>
Successivamente, usando le tecniche illustrate nelle sezioni precedenti di questo articolo, selezionare un MediaFrameSourceGroup che contiene oggetti MediaFrameSourceInfo per fotocamere a colori nel pannello anteriore e nel pannello posteriore. Si noti che MediaPlayer non converte automaticamente i fotogrammi da formati non di colore, ad esempio una profondità o dati a infrarossi, in dati di colore. L'uso di altri tipi di sensori può produrre risultati imprevisti.
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];
Inizializzare l'oggetto MediaCapture per utilizzare l'oggetto MediaFrameSourceGroup selezionato.
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;
}
Infine, chiamare MediaSource.CreateFromMediaFrameSource per creare un oggetto MediaSource per ogni origine frame usando la proprietà Id dell'oggetto MediaFrameSourceInfo associato per selezionare una delle origini frame nell'insieme FrameSources dell'oggetto MediaCapture. Inizializzare un nuovo oggetto MediaPlayer e assegnarlo a un MediaPlayerElement chiamando SetMediaPlayer. Impostare quindi la proprietà Source nell'oggetto MediaSource appena creato.
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;
Usare i profili video per selezionare un'origine fotogramma
Un profilo di fotocamera, rappresentato da un oggetto MediaCaptureVideoProfile, rappresenta un set di funzionalità offerte da un particolare dispositivo di acquisizione, ad esempio frequenza dei fotogrammi, risoluzioni o funzionalità avanzate come l'acquisizione HDR. Un dispositivo di acquisizione può supportare più profili, consentendo di selezionare quello ottimizzato per lo scenario di acquisizione. A partire da Windows 10 versione 1803, è possibile usare MediaCaptureVideoProfile per selezionare un'origine di fotogrammi multimediali con funzionalità particolari prima di inizializzare l'oggetto MediaCapture. Il metodo di esempio seguente cerca un profilo video che supporta HDR con Wide Color Gamut (WCG) e restituisce un oggetto MediaCaptureInitializationSettings che può essere usato per inizializzare MediaCapture per usare il dispositivo e il profilo selezionati.
Prima di tutto, chiamare MediaFrameSourceGroup.FindAllAsync per ottenere un elenco di tutti i gruppi di origine dei fotogrammi multimediali disponibili nel dispositivo corrente. Scorrere ogni gruppo di origine e chiamare MediaCapture.FindKnownVideoProfiles per ottenere un elenco di tutti i profili video per il gruppo di origine corrente che supporta il profilo specificato, in questo caso HDR con foto WCG. Se viene trovato un profilo che soddisfa i criteri, creare un nuovo oggetto MediaCaptureInitializationSettings e impostare VideoProfile sul profilo di selezione e videoDeviceId sulla proprietà Id del gruppo di origine dei fotogrammi multimediali corrente.
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);
}
Per altre informazioni sull'uso dei profili della fotocamera, vedere Profili della Fotocamera.
Argomenti correlati