Extending the AZ-220 Digital Twins hands-on lab with 3D visualization

Azure Digital Twins is advertised as a “platform as a service (PaaS) offering that enables the creation of twin graphs based on digital models of entire environments, which could be buildings, factories, farms, energy networks, railways, stadiums, and more—even entire cities”.

This sounds promising but it does not really ring a bell, does it?

Fortunately, besides the excellent documentation, Microsoft provides a great learning path in MS Learn as part of the AZ-220 Azure IoT developer exam preparations.

There, you will learn how Azure Digital Twins offers new opportunities for representing an Internet of Things solution via twin models, twin relations, and a runtime environment.

You finish the learning path with a hands-on lab where you build a model around a cheese factory and ingest sensor telemetry:

In the demo, the telemetry flows through the runtime and ends up in Time Series Insights.

Yes, the learning path is a good start and will prepare you for the exam or the assessment (you need to pass this assessment for a free one-year certification renewal).

On the other hand, many extra features could be added to turn this good start into a great start!

Think about propagating Azure Digital Twins events and twin property changes through the graph and visualizing live updates of twins in a 3D model, complete with alerts.

Let’s check out some of these additional features and see what you need to do to extend the ADT example.

Doorgaan met het lezen van “Extending the AZ-220 Digital Twins hands-on lab with 3D visualization”
Advertentie

3D illusie met XBox 360 Kinect

Het is al weer een enige tijd geleden dat Johnny Chung Lee  opzien baarde door met een eenvoudige Wii Remote hele leuke toepassingen wist te verzinnen die Nintendo nog niet verkocht.

Voor diegene die het gemist heeft, er staat een leuke demonstratie op het web van zijn TED presentatie. Hierin wordt oa. een 3D illusie geïllustreerd door met een bril te bewegen die via infrarood de positie van het hoofd verraden. En die positie wordt door de remote opgevangen via de interne camera. Meer informatie is ook te vinden op zijn website.

Toen Microsoft de eerste Kinect los verkocht werd deze met een adapter geleverd om op een reguliere USB poort aan te sluiten en al snel werd ook de Kinect ‘gehacked’. De opnames van de Kinect worden niet versleuteld dus al snel waren er open-source bibliotheken om de ruwe informatie uit Kinect te kunnen benutten voor de PC. Oa. mijn gewaardeerde collega Jan Saris heeft hier heel wat uurtjes aan gesleuteld met leuke projecten tot gevolg.

Ik heb destijds ook met verbazing gekeken. Ik vond vooral de ‘stadion’ illusie erg leuk. Sindsdien heb ik dit altijd nog eens over willen doen met de Kinect. Deze kan dus de positie van meerdere personen bepalen en dus ook van ook die van een hoofd. En je hoeft geen brilletje op te zetten.

Nu is dit al vaker gedaan :-). Zo is de  Kinect gecombineerd met 3D TV.

Wellicht was het geldingsdrang maar ook ik wilde dit nog eens uitzoeken. De afgelopen week heb ik er eindelijk vaart achter gezet en tada, hier is het resultaat.

Kinect SDK plus Kinect Toolkit

Om met de Kinect te kunnen knutselen moet je beschikken over Visual Studio en je hebt Kinect for Windows SDK v1.6 nodig. Volgens de EULA mag dit alleen i.c.m. de “Kinect for PC” (een duurder broertje met een verbeterd blikveld) toegepast worden. Het is niet toegestaan de Xbox360 Kinect te promoten bij je eigen software. Gelukkig  mag je wel testen met de Xbox360 Kinect en dat gaan we hier dus doen…

Ook is het handig om dan direct ook de Kinect for Windows Developer Toolkit v1.6 te downloaden. Dit is echt goud! Hierin zitten voorbeelden hoe je de Kinect kunt gebruiken voor spraak besturing (inclusief richtinggevoeligheid) of hoe je door een applicatie kunt navigeren met die typische Kinect handbesturing.

En natuurlijk zijn er in de Kinect ook voorbeelden van applicaties die skelet tracking uitvoeren, zoals de Skeletons basic –WPF app.

Deze laatste WPF applicatie gaan wij ombouwen.

Wat is het idee? Ik toon een mooi vergezicht overmaats op het scherm, dit geeft de illusie als dat je door een raam kijkt. Als je naar links loopt, dan gaat de foto ook naar links, en naar rechts als naar rechts loopt. Hierdoor is het net alsof je om de hoek kijkt van een raam. Of zo J. En dat kan herhaald worden door boven, onder, dichterbij en verder weg.

Absoluut werd relatief

Ik heb enkele manieren onderzocht en bleef een beetje steken in een ViewModel. Ik wilde graag MVVM toepassen. Nu kan ik de absolute posities achterhalen en die doorgeven naar de X, Y, Z en zoom factor maar dit wordt erg schokkerig. Ik kreeg het niet voor elkaar om een storyboard hier aan te koppelen. Een trigger gaat pas af als een bepaalde waarde gehaald is, niet als een waarde verandert?

Dus ik heb geen storyboard toegepast. Wel heb ik de positiedoorgifte van een gevoeligheid voorzien. Als een gemeten positie  flink afwijkt vanaf een vorige geaccepteerde positie dan wordt de foto verschoven en dan wordt dit de nieuwe te onthouden positie.

Daarom heb ik voor de verschillende richtingen in totaal zes storyboard gemaakt die vanuit de code behind worden aangeroepen. En ik heb een extra code-behind geschreven om de foto op de start positie te brengen.

De Xaml ziet er dan als volgt uit; zeven storyboard en een image op een grid:

<Window x:Class="MainWindow"
  xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
  xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>"
  Title="PictureZoom"
  Loaded="WindowLoaded"
  Closing="WindowClosing"
  WindowState="Maximized"
  WindowStyle="None"
  WindowStartupLocation="CenterScreen"
  mc:Ignorable="d"
  xmlns:d="<a href="http://schemas.microsoft.com/expression/blend/2008">http://schemas.microsoft.com/expression/blend/2008</a>"
  xmlns:mc="<a href="http://schemas.openxmlformats.org/markup-compatibility/2006">http://schemas.openxmlformats.org/markup-compatibility/2006</a>"
  d:DesignHeight="710"
  d:DesignWidth="1300"
  SizeToContent="WidthAndHeight">
<Window.Resources>
      <Storyboard x:Key="Start">
        <DoubleAnimation Storyboard.TargetName="Scale"
                         Storyboard.TargetProperty="(ScaleTransform.ScaleX)"
                         To="1.6"
                         Duration="0:0:0.0" />
        <DoubleAnimation Storyboard.TargetName="Scale"
                         Storyboard.TargetProperty="(ScaleTransform.ScaleY)"
                         To="1.6"
                         Duration="0:0:0.0" />
      </Storyboard>
      <Storyboard x:Key="ZoomIn">
        <DoubleAnimation Storyboard.TargetName="Scale"
                         Storyboard.TargetProperty="(ScaleTransform.ScaleX)"
                         By="0.010"
                         Duration="0:0:0.20" />
        <DoubleAnimation Storyboard.TargetName="Scale"
                         Storyboard.TargetProperty="(ScaleTransform.ScaleY)"
                         By="0.010"
                         Duration="0:0:0.20">
          <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
          </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
      </Storyboard>
      <Storyboard x:Key="ZoomOut">
        <DoubleAnimation Storyboard.TargetName="Scale"
                         Storyboard.TargetProperty="(ScaleTransform.ScaleX)"
                         By="-0.010"
                         Duration="0:0:0.25" />
        <DoubleAnimation Storyboard.TargetName="Scale"
                         Storyboard.TargetProperty="(ScaleTransform.ScaleY)"
                         By="-0.010"
                         Duration="0:0:0.25">
          <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
          </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
      </Storyboard>
      <Storyboard x:Key="PanLeft">
        <DoubleAnimation Storyboard.TargetName="Pan"
                         Storyboard.TargetProperty="(TranslateTransform.X)"
                         By="-10"
                         Duration="0:0:0.25">
          <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
          </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
      </Storyboard>
      <Storyboard x:Key="PanRight">
        <DoubleAnimation Storyboard.TargetName="Pan"
                         Storyboard.TargetProperty="(TranslateTransform.X)"
                         By="10"
                         Duration="0:0:0.25">
          <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
          </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
      </Storyboard>
      <Storyboard x:Key="PanUp">
        <DoubleAnimation Storyboard.TargetName="Pan"
                         Storyboard.TargetProperty="(TranslateTransform.Y)"
                         By="-10"
                         Duration="0:0:0.25">
          <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
          </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
      </Storyboard>
      <Storyboard x:Key="PanDown">
        <DoubleAnimation Storyboard.TargetName="Pan"
                         Storyboard.TargetProperty="(TranslateTransform.Y)"
                         By="10"
                         Duration="0:0:0.25">
          <DoubleAnimation.EasingFunction>
            <SineEase EasingMode="EaseInOut" />
          </DoubleAnimation.EasingFunction>
        </DoubleAnimation>
      </Storyboard>
    </Window.Resources>
  <Grid>
    <Border Grid.Row="1"
            Name="border"
            ClipToBounds="True">
      <Image Name="PictureToScale"
             RenderTransformOrigin="0.5, 0.5"
             Source="XYZ.JPG">
        <Image.RenderTransform>
          <TransformGroup>
            <ScaleTransform x:Name="Scale"
                            ScaleX="1"
                            ScaleY="1" />
            <TranslateTransform x:Name="Pan" />
          </TransformGroup>
        </Image.RenderTransform>
      </Image>
    </Border>
  </Grid>
</Window>

De code-behind is een afgeslankte variant op het demo project van Microsoft. Ik onthoud de laatst positie gemeten positie en hou referenties vast naar de storyboards. De precisie en de Kinect Sensor blijven onveranderd.

Kinect3d

Bij het starten van de applicatie laat ik de Kinect initialiseren en reageer ik alleen op de positie van de nek. Want het hoofd kan ook schuin gehouden worden en dat wil ik negeren. De magie zit in het “SensorSkeletonFrameReady” event waarbij de positie van dit nekgewricht wordt gemeten.

De code behind is dus:

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
  private Storyboard storyboardZoomIn;
  private Storyboard storyboardZoomOut;
  private Storyboard storyboardPanLeft;
  private Storyboard storyboardPanRight;
  private Storyboard storyboardPanUp;
  private Storyboard storyboardPanDown;

  private KinectSensor sensor;

  // Kinect precision: less is more responsive
  private double _precision = 0.025;

  private double _lastPositionX;
  private double _lastPositionY;
  private double _lastPositionZ;

  /// <summary>
  /// Initializes a new instance of the MainWindow class.
  /// </summary>
  public MainWindow()
  {
    InitializeComponent();

    // wait some time to get to the starting position...
    Thread.Sleep(4000);

    Mouse.OverrideCursor = Cursors.None;

    Storyboard storyboardStart = (Storyboard)TryFindResource("Start");
    storyboardStart.Begin(this);

    storyboardZoomIn = (Storyboard)TryFindResource("ZoomIn");
    storyboardZoomOut = (Storyboard)TryFindResource("ZoomOut");
    storyboardPanLeft = (Storyboard)TryFindResource("PanLeft");
    storyboardPanRight = (Storyboard)TryFindResource("PanRight");
    storyboardPanUp = (Storyboard)TryFindResource("PanUp");
    storyboardPanDown = (Storyboard)TryFindResource("PanDown");
  }

  /// <summary>
  /// Execute startup tasks
  /// </summary>
  /// <param name="sender">object sending the event</param>
  /// <param name="e">event arguments</param>
  private void WindowLoaded(object sender, RoutedEventArgs e)
  {
    // Look through all sensors and start the first connected one.
    // This requires that a Kinect is connected at the time of app startup.
    // To make your app robust against plug/unplug,
    // it is recommended to use KinectSensorChooser provided in Microsoft.Kinect.Toolkit
    foreach (var potentialSensor in KinectSensor.KinectSensors)
    {
      if (potentialSensor.Status == KinectStatus.Connected)
      {
        this.sensor = potentialSensor;
        break;
      }
    }

    if (null != this.sensor)
    {
      // Turn on the skeleton stream to receive skeleton frames
      this.sensor.SkeletonStream.Enable();

      // Add an event handler to be called whenever there is new color frame data
      this.sensor.SkeletonFrameReady += this.SensorSkeletonFrameReady;

      // Start the sensor!
      try
      {
        this.sensor.Start();
      }
      catch (IOException)
      {
        this.sensor = null;
      }
    }
  }

  /// <summary>
  /// Execute shutdown tasks
  /// </summary>
  /// <param name="sender">object sending the event</param>
  /// <param name="e">event arguments</param>
  private void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
  {
    if (null != this.sensor)
    {
      this.sensor.Stop();
    }
  }

  /// <summary>
  /// Event handler for Kinect sensor's SkeletonFrameReady event
  /// </summary>
  /// <param name="sender">object sending the event</param>
  /// <param name="e">event arguments</param>
  private void SensorSkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e)
  {
    Skeleton[] skeletons = new Skeleton[0];

    using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame())
    {
      if (skeletonFrame != null)
      {
        skeletons = new Skeleton[skeletonFrame.SkeletonArrayLength];
        skeletonFrame.CopySkeletonDataTo(skeletons);
      }
    }

    if (skeletons.Length != 0)
    {
      foreach (Skeleton skel in skeletons)
      {
        if (skel.TrackingState == SkeletonTrackingState.Tracked)
        {
          // Otherwise use JointType.ShoulderCenter
          this.MoveImage(skel, JointType.Head);

          break;
        }
      }
    }
  }

  private bool PersonMovedLeftRight(SkeletonPoint skeletonPoint)
  {
    return (Math.Abs(skeletonPoint.X - _lastPositionX) > _precision);
  }

  private bool PersonMovedUpDown(SkeletonPoint skeletonPoint)
  {
    return (Math.Abs(skeletonPoint.Y - _lastPositionY) > (_precision));
  }

  private bool PersonMovedInOut(SkeletonPoint skeletonPoint)
  {
    return (Math.Abs(skeletonPoint.Z - _lastPositionZ) > _precision);
  }

  /// <summary>
  /// Move the image
  /// </summary>
  /// <param name="skeleton">skeleton to draw bones from</param>
  /// <param name="jointType">joint to start drawing from</param>
  private void MoveImage(Skeleton skeleton, JointType jointType)
  {
    Joint joint = skeleton.Joints[jointType];

    if (joint.TrackingState == JointTrackingState.NotTracked
          || joint.TrackingState == JointTrackingState.Inferred)
    {
      return;
    }

    if ((_lastPositionX == 0)
          && (_lastPositionY == 0)
          && (_lastPositionZ == 0))
    {
      _lastPositionX = joint.Position.X;
      _lastPositionY = joint.Position.Y;
      _lastPositionZ = joint.Position.Z;
    }

    if (PersonMovedLeftRight(joint.Position))
    {
      if (joint.Position.X < _lastPositionX)
      {
        //left
        storyboardPanLeft.Begin(this);
      }
      else
      {
        if (joint.Position.X > _lastPositionX)
        {
          //right
          storyboardPanRight.Begin(this);
        }
      }

      _lastPositionX = joint.Position.X;
    }

    if (PersonMovedUpDown(joint.Position))
    {
      if (joint.Position.Y < _lastPositionY)
      {
        //down
        storyboardPanDown.Begin(this);
      }
      else
      {
        if (joint.Position.Y > _lastPositionY)
        {
          //up
          storyboardPanUp.Begin(this);
        }
      }

      _lastPositionY = joint.Position.Y;
    }

    if (PersonMovedInOut(joint.Position))
    {
      if (joint.Position.Z < _lastPositionZ)
      {
        //in
        storyboardZoomOut.Begin(this);
      }
      else
      {
        if (joint.Position.Z > _lastPositionZ)
        {
          //out
          storyboardZoomIn.Begin(this);
        }
      }

      _lastPositionZ = joint.Position.Z;
    }
  }
}

Is dit voldoende voor een 3D illusie? Bekijk het op http://youtu.be/5dupe4_UIQo en http://youtu.be/nMk5oSMXZmI .

Ik vind het zelf een heel aardig effect hebben. Maar er valt wel wat op aan te merken. Er gaat na enige tijd een afwijking optreden in het aansturen met relatieve stappen. Een stuk naar links bewegen en een stuk naar rechts bewegen wilt niet zeggen dat de foto weer op het zelfde punt terug is. Hoewel de gevoeligheid fantastisch is (je hoofd/nek een paar centimeter bewegen wordt al geregistreerd) kan ik het verloop niet goed compenseren. Ik ken zelfs over de rand van de foto ‘vallen’ en dan is de illusie in één keer verdwenen. Daarom heb ik ook een sleep van een paar seconden er in staan. Dit geeft mij de tijd om in het midden van de kamer te gaan staan.

Maar zoals ik al gezegd heb, het is een leuke illusie en het kostte alleen wat prutsen in de avond. En mijn zonen vinden het ook gaaf, prima toch?