// ----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ----------------------------------------------------------------------- namespace Microsoft.Samples.Kinect.Webserver.Sensor { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; using System.Windows; using Microsoft.Kinect; using Microsoft.Kinect.Toolkit.BackgroundRemoval; using Microsoft.Samples.Kinect.Webserver.Properties; using Microsoft.Samples.Kinect.Webserver.Sensor.Serialization; /// /// Implementation of ISensorStreamHandler that exposes background removal streams. /// [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Disposable background removal stream is disposed when sensor is set to null")] public class BackgroundRemovalStreamHandler : SensorStreamHandlerBase { /// /// JSON name of background removal stream. /// internal const string BackgroundRemovalStreamName = "backgroundRemoval"; /// /// JSON name for property representing the tracking user id. /// internal const string TrackingIdPropertyName = "trackingId"; /// /// JSON name for property representing the background removed color image resolution. /// internal const string ResolutionPropertyName = "resolution"; /// /// Regular expression that matches the background removed frame resolution property. /// private static readonly Regex BackgroundRemovalResolutionRegex = new Regex(@"^(?i)(\d+)x(\d+)$"); private static readonly KeyValuePair[] BackgroundRemovalResolutions = { new KeyValuePair(ColorImageFormat.RgbResolution640x480Fps30, new Size(640, 480)), new KeyValuePair(ColorImageFormat.RgbResolution1280x960Fps12, new Size(1280, 960)) }; /// /// Context that allows this stream handler to communicate with its owner. /// private readonly SensorStreamHandlerContext ownerContext; /// /// Serializable background removal stream message, reused as background removed color frames arrive. /// private readonly BackgroundRemovalStreamMessage backgroundRemovalStreamMessage = new BackgroundRemovalStreamMessage { stream = BackgroundRemovalStreamName }; /// /// Sensor providing data to background removal stream. /// private KinectSensor sensor; /// /// Entry point for background removal stream functionality. /// private BackgroundRemovedColorStream backgroundRemovalStream; /// /// Id of the user we choose to track. /// private int trackingId; /// /// True if background removal stream is enabled. Background removal stream is disabled by default. /// private bool backgroundRemovalStreamIsEnabled; /// /// The background removed color image format. /// private ColorImageFormat colorImageFormat = ColorImageFormat.RgbResolution640x480Fps30; /// /// Keep track if we're in the middle of processing a background removed color frame. /// private bool isProcessingBackgroundRemovedFrame; /// /// Initializes a new instance of the class /// and associates it with a context that allows it to communicate with its owner. /// /// /// An instance of class. /// internal BackgroundRemovalStreamHandler(SensorStreamHandlerContext ownerContext) { this.ownerContext = ownerContext; this.AddStreamConfiguration(BackgroundRemovalStreamName, new StreamConfiguration(this.GetProperties, this.SetProperty)); } /// /// Lets ISensorStreamHandler know that Kinect Sensor associated with this stream /// handler has changed. /// /// /// New KinectSensor. /// public override void OnSensorChanged(KinectSensor newSensor) { if (this.sensor != null) { try { this.backgroundRemovalStream.BackgroundRemovedFrameReady -= this.BackgroundRemovedFrameReadyAsync; this.backgroundRemovalStream.Dispose(); this.backgroundRemovalStream = null; this.sensor.ColorStream.Disable(); } catch (InvalidOperationException) { // KinectSensor might enter an invalid state while enabling/disabling streams or stream features. // E.g.: sensor might be abruptly unplugged. } } this.sensor = newSensor; if (newSensor != null) { this.backgroundRemovalStream = new BackgroundRemovedColorStream(newSensor); this.backgroundRemovalStream.BackgroundRemovedFrameReady += this.BackgroundRemovedFrameReadyAsync; // Force enabling the background removal stream because it hasn't been enabled before. this.UpdateBackgroundRemovalFrameFormat(this.colorImageFormat, true); } } /// /// Process data from one Kinect color frame. /// /// /// Kinect color data. /// /// /// from which we obtained color data. /// public override void ProcessColor(byte[] colorData, ColorImageFrame colorFrame) { if (colorData == null) { throw new ArgumentNullException("colorData"); } if (colorFrame == null) { throw new ArgumentNullException("colorFrame"); } if (this.backgroundRemovalStreamIsEnabled) { this.backgroundRemovalStream.ProcessColor(colorData, colorFrame.Timestamp); } } /// /// Process data from one Kinect depth frame. /// /// /// Kinect depth data. /// /// /// from which we obtained depth data. /// public override void ProcessDepth(DepthImagePixel[] depthData, DepthImageFrame depthFrame) { if (depthData == null) { throw new ArgumentNullException("depthData"); } if (depthFrame == null) { throw new ArgumentNullException("depthFrame"); } if (this.backgroundRemovalStreamIsEnabled) { this.backgroundRemovalStream.ProcessDepth(depthData, depthFrame.Timestamp); } } /// /// Process data from one Kinect skeleton frame. /// /// /// Kinect skeleton data. /// /// /// from which we obtained skeleton data. /// public override void ProcessSkeleton(Skeleton[] skeletons, SkeletonFrame skeletonFrame) { if (skeletons == null) { throw new ArgumentNullException("skeletons"); } if (skeletonFrame == null) { throw new ArgumentNullException("skeletonFrame"); } if (this.backgroundRemovalStreamIsEnabled) { this.backgroundRemovalStream.ProcessSkeleton(skeletons, skeletonFrame.Timestamp); } } /// /// Event handler for BackgroundRemovedColorStream's BackgroundRemovedFrameReady event /// /// object sending the event /// event arguments internal async void BackgroundRemovedFrameReadyAsync(object sender, BackgroundRemovedColorFrameReadyEventArgs e) { if (!this.backgroundRemovalStreamIsEnabled) { // Directly return if the stream is not enabled. return; } if (this.isProcessingBackgroundRemovedFrame) { // Re-entered BackgroundRemovedFrameReadyAsync while a previous frame is already being processed. // Just ignore new frames until the current one finishes processing. return; } this.isProcessingBackgroundRemovedFrame = true; try { bool haveFrameData = false; using (var backgroundRemovedFrame = e.OpenBackgroundRemovedColorFrame()) { if (backgroundRemovedFrame != null) { this.backgroundRemovalStreamMessage.UpdateBackgroundRemovedColorFrame(backgroundRemovedFrame); haveFrameData = true; } } if (haveFrameData) { await this.ownerContext.SendTwoPartStreamMessageAsync(this.backgroundRemovalStreamMessage, this.backgroundRemovalStreamMessage.Buffer); } } finally { this.isProcessingBackgroundRemovedFrame = false; } } /// /// Get the size for the given color image format. /// /// The color image format. /// The width and height of the given image. private static Size GetColorImageSize(ColorImageFormat format) { try { var q = from item in BackgroundRemovalResolutions where item.Key == format select item.Value; return q.Single(); } catch (InvalidOperationException) { throw new ArgumentException(Resources.UnsupportedColorFormat, "format"); } } /// /// Get the color image format for the specified width and height. /// /// /// Image width. /// /// /// Image height. /// /// /// The color image format enumeration value. /// private static ColorImageFormat GetColorImageFormat(int width, int height) { try { var q = from item in BackgroundRemovalResolutions where (int)item.Value.Width == width && (int)item.Value.Height == height select item.Key; return q.Single(); } catch (InvalidOperationException) { throw new ArgumentException(Resources.UnsupportedColorFormat); } } /// /// Set the background removed color frame format. /// /// /// The given color image format. /// /// /// Streams should be enabled even if new color image format is the same as the old one. /// This is useful for the initial enabling of the stream. /// private void UpdateBackgroundRemovalFrameFormat(ColorImageFormat format, bool forceEnable) { if (!forceEnable && (format == this.colorImageFormat)) { // No work to do return; } if (this.sensor != null) { try { this.sensor.ColorStream.Enable(format); this.backgroundRemovalStream.Enable(format, DepthImageFormat.Resolution640x480Fps30); } catch (InvalidOperationException) { // KinectSensor might enter an invalid state while enabling/disabling streams or stream features. // E.g.: sensor might be abruptly unplugged. } } // Update the image format property if the action succeeded. this.colorImageFormat = format; } /// /// Gets a background removal stream property value. /// /// /// Property name->value map where property values should be set. /// private void GetProperties(Dictionary propertyMap) { propertyMap.Add(KinectRequestHandler.EnabledPropertyName, this.backgroundRemovalStreamIsEnabled); propertyMap.Add(TrackingIdPropertyName, this.trackingId); var size = GetColorImageSize(this.colorImageFormat); propertyMap.Add(ResolutionPropertyName, string.Format(CultureInfo.InvariantCulture, @"{0}x{1}", (int)size.Width, (int)size.Height)); } /// /// Set a background removal stream property value. /// /// /// Name of property to set. /// /// /// Property value to set. /// /// /// null if property setting was successful, error message otherwise. /// private string SetProperty(string propertyName, object propertyValue) { bool recognized = true; if (propertyValue == null) { return Resources.PropertyValueInvalidFormat; } try { switch (propertyName) { case KinectRequestHandler.EnabledPropertyName: this.backgroundRemovalStreamIsEnabled = (bool)propertyValue; break; case TrackingIdPropertyName: { var oldTrackingId = this.trackingId; this.trackingId = (int)propertyValue; if (this.trackingId != oldTrackingId) { this.backgroundRemovalStream.SetTrackedPlayer(this.trackingId); } } break; case ResolutionPropertyName: var match = BackgroundRemovalResolutionRegex.Match((string)propertyValue); if (!match.Success || (match.Groups.Count != 3)) { return Resources.PropertyValueInvalidFormat; } int width = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); int height = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); try { var format = GetColorImageFormat(width, height); this.UpdateBackgroundRemovalFrameFormat(format, false); } catch (ArgumentException) { return Resources.PropertyValueUnsupportedResolution; } break; default: recognized = false; break; } if (!recognized) { return Resources.PropertyNameUnrecognized; } } catch (InvalidCastException) { return Resources.PropertyValueInvalidFormat; } return null; } } }