Terminator Program: Part 2
July 8, 2014 1 Comment
Following up on my last post, I decided to send the entire photograph to Sky Biometry and have them parse the photograph and identify individual people. This ability is built right into their API. For example, if you pass them this picture, you get the following json back.
I added the red highlight to show that Sky Biometry can recognize multiple people (it is an array of uids) and that each face tag has a center.x and center:y. Reading the API documentation, this point is center of the face tag point and their point is a percentage of the photo width.
So I need to translate the center point of the skeleton from the Kinect to eqiv center point of the sky biometry recognition output and I should be able to identify individual people within the Kinect’s field of vision. Going back to the Kinect code, I ditched the DrawBoxAroundHead method and altered the UpdateDisplay method like so
- private void UpdateDisplay(byte[] colorData, Skeleton[] skeletons)
- {
- if (_videoBitmap == null)
- {
- _videoBitmap = new WriteableBitmap(640, 480, 96, 96, PixelFormats.Bgr32, null);
- }
- _videoBitmap.WritePixels(new Int32Rect(0, 0, 640, 480), colorData, 640 * 4, 0);
- kinectColorImage.Source = _videoBitmap;
- var selectedSkeleton = skeletons.FirstOrDefault(s => s.TrackingState == SkeletonTrackingState.Tracked);
- if (selectedSkeleton != null)
- {
- var headPosition = selectedSkeleton.Joints[JointType.Head].Position;
- var adjustedHeadPosition =
- _sensor.CoordinateMapper.MapSkeletonPointToColorPoint(headPosition, ColorImageFormat.RgbResolution640x480Fps30);
- var adjustedSkeletonPosition = _sensor.CoordinateMapper.MapSkeletonPointToColorPoint(selectedSkeleton.Position, ColorImageFormat.RgbResolution640x480Fps30);
- skeletonCanvas.Children.Clear();
- Rectangle headRectangle = new Rectangle();
- headRectangle.Fill = new SolidColorBrush(Colors.Blue);
- headRectangle.Width = 10;
- headRectangle.Height = 10;
- Canvas.SetLeft(headRectangle, adjustedHeadPosition.X);
- Canvas.SetTop(headRectangle, adjustedHeadPosition.Y);
- skeletonCanvas.Children.Add(headRectangle);
- Rectangle skeletonRectangle = new Rectangle();
- skeletonRectangle.Fill = new SolidColorBrush(Colors.Red);
- skeletonRectangle.Width = 10;
- skeletonRectangle.Height = 10;
- Canvas.SetLeft(skeletonRectangle, adjustedHeadPosition.X);
- Canvas.SetTop(skeletonRectangle, adjustedHeadPosition.Y);
- skeletonCanvas.Children.Add(skeletonRectangle);
- String skeletonInfo = headPosition.X.ToString() + " : " + headPosition.Y.ToString() + " — ";
- skeletonInfo = skeletonInfo + adjustedHeadPosition.X.ToString() + " : " + adjustedHeadPosition.Y.ToString() + " — ";
- skeletonInfo = skeletonInfo + adjustedSkeletonPosition.X.ToString() + " : " + adjustedSkeletonPosition.Y.ToString();
- skeletonInfoTextBox.Text = skeletonInfo;
- }
- }
Notice that there are two rectangles because I was not sure if the Head.Position or the Skeleton.Position would match SkyBiometry. Turns out that I want the Head.Position for SkyBiometry (besides, the terminator would want head shots only)
So I ditched the Skeleton.Position. I then needed a way to translate the Head.Posotion.X to SkyBiometry.X and Head.Posotion.Y to SkyBiometry.Y. Fortunately, I know the size of each photograph (640 X 480) so calculating the percent is an exercise of altering UpdateDisplay:
- private void UpdateDisplay(byte[] colorData, Skeleton[] skeletons)
- {
- Int32 photoWidth = 640;
- Int32 photoHeight = 480;
- if (_videoBitmap == null)
- {
- _videoBitmap = new WriteableBitmap(photoWidth, photoHeight, 96, 96, PixelFormats.Bgr32, null);
- }
- _videoBitmap.WritePixels(new Int32Rect(0, 0, photoWidth, photoHeight), colorData, photoWidth * 4, 0);
- kinectColorImage.Source = _videoBitmap;
- var selectedSkeleton = skeletons.FirstOrDefault(s => s.TrackingState == SkeletonTrackingState.Tracked);
- if (selectedSkeleton != null)
- {
- var headPosition = selectedSkeleton.Joints[JointType.Head].Position;
- var adjustedHeadPosition =
- _sensor.CoordinateMapper.MapSkeletonPointToColorPoint(headPosition, ColorImageFormat.RgbResolution640x480Fps30);
- skeletonCanvas.Children.Clear();
- Rectangle headRectangle = new Rectangle();
- headRectangle.Fill = new SolidColorBrush(Colors.Blue);
- headRectangle.Width = 10;
- headRectangle.Height = 10;
- Canvas.SetLeft(headRectangle, adjustedHeadPosition.X);
- Canvas.SetTop(headRectangle, adjustedHeadPosition.Y);
- skeletonCanvas.Children.Add(headRectangle);
- var skyBiometryX = ((float)adjustedHeadPosition.X / photoWidth)*100;
- var skyBioMetryY = ((float)adjustedHeadPosition.Y / photoHeight)*100;
- String skeletonInfo = adjustedHeadPosition.X.ToString() + " : " + adjustedHeadPosition.Y.ToString() + " — ";
- skeletonInfo = skeletonInfo + Math.Round(skyBiometryX,2).ToString() + " : " + Math.Round(skyBioMetryY,2).ToString();
- skeletonInfoTextBox.Text = skeletonInfo;
- }
And so now I have
The next step is to get the Kinect photo to Sky Biometry. I decided to use Azure Blob Storage as my intermediately location. I updated the architectural diagram like so:
At this point, it made sense to move the project over to F# so I could better concentrate on the work that needs to be done and also getting the important code out of the UI code behind. I fired up a F# project in my solution added a couple different implementations of Storing Photos. To keep things consistent, I created a data structure and an interface:
- namespace ChickenSoftware.Terminator.Core
- open System
- type public PhotoImage (uniqueId:Guid, imageBytes:byte[]) =
- member this.UniqueId = uniqueId
- member this.ImageBytes = imageBytes
- type IPhotoImageProvider =
- abstract member InsertPhotoImage : PhotoImage -> unit
- abstract member DeletePhotoImage : Guid -> unit
- abstract member GetPhotoImage : Guid -> PhotoImage
My 1st stop was to replicate what Miles did with the Save File Dialog box with a File System Provider. It was very much like a C# implementation:
- namespace ChickenSoftware.Terminator.Core
- open System
- open System.IO
- open System.Drawing
- open System.Drawing.Imaging
- type LocalFileSystemPhotoImageProvider(folderPath: string) =
- member this.GetPhotoImageUri(uniqueIdentifier: Guid) =
- let fileName = uniqueIdentifier.ToString() + ".jpg"
- Path.Combine(folderPath, fileName)
- interface IPhotoImageProvider with
- member this.InsertPhotoImage(photoImage: PhotoImage) =
- let fullPath = this.GetPhotoImageUri(photoImage.UniqueId)
- use memoryStream = new MemoryStream(photoImage.ImageBytes)
- let image = Image.FromStream(memoryStream)
- image.Save(fullPath)
- member this.DeletePhotoImage(uniqueIdentifier: Guid) =
- let fullPath = this.GetPhotoImageUri(uniqueIdentifier)
- File.Delete(fullPath)
- member this.GetPhotoImage(uniqueIdentifier: Guid) =
- let fullPath = this.GetPhotoImageUri(uniqueIdentifier)
- use fileStream = new FileStream(fullPath,FileMode.Open)
- let image = Image.FromStream(fileStream)
- use memoryStream = new MemoryStream()
- image.Save(memoryStream,ImageFormat.Jpeg)
- new PhotoImage(uniqueIdentifier, memoryStream.ToArray())
To call the save method, I altered the SavePhoto method in the C# project to use a MemoryStream and not a FileStream:
- private void SavePhoto(byte[] colorData)
- {
- var bitmapSource = BitmapSource.Create(640, 480, 96, 96, PixelFormats.Bgr32, null, colorData, 640 * 4);
- JpegBitmapEncoder encoder = new JpegBitmapEncoder();
- encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
- using (MemoryStream memoryStream = new MemoryStream())
- {
- encoder.Save(memoryStream);
- PhotoImage photoImage = new PhotoImage(Guid.NewGuid(), memoryStream.ToArray());
- String folderUri = @"C:\Data";
- IPhotoImageProvider provider = new LocalFileSystemPhotoImageProvider(folderUri);
- provider.InsertPhotoImage(photoImage);
- memoryStream.Close();
- }
- _isTakingPicture = false;
- }
And sure enough, it saves the photo to disk:
One problem that took me 20 minutes to uncover is that if you get your file system path wrong, you get the unhelpful exception:
This has been well-bitched about on stack overflow so I won’t comment further.
With the file system up and running, I turned my attention to Azure. Like the File System provider, it is very close to a C# implementation
- namespace ChickenSoftware.Terminator.Core
- open System
- open System.IO
- open Microsoft.WindowsAzure.Storage
- open Microsoft.WindowsAzure.Storage.Blob
- type AzureStoragePhotoImageProvider(customerUniqueId: Guid, connectionString: string) =
- member this.GetBlobContainer(blobClient:Blob.CloudBlobClient) =
- let container = blobClient.GetContainerReference(customerUniqueId.ToString())
- if not (container.Exists()) then
- container.CreateIfNotExists() |> ignore
- let permissions = new BlobContainerPermissions()
- permissions.PublicAccess <- BlobContainerPublicAccessType.Blob
- container.SetPermissions(permissions)
- container
- member this.GetBlockBlob(uniqueIdentifier: Guid) =
- let storageAccount = CloudStorageAccount.Parse(connectionString)
- let blobClient = storageAccount.CreateCloudBlobClient()
- let container = this.GetBlobContainer(blobClient)
- let photoUri = this.GetPhotoImageUri(uniqueIdentifier)
- container.GetBlockBlobReference(photoUri)
- member this.GetPhotoImageUri(uniqueIdentifier: Guid) =
- uniqueIdentifier.ToString() + ".jpg"
- interface IPhotoImageProvider with
- member this.InsertPhotoImage(photoImage: PhotoImage) =
- let blockBlob = this.GetBlockBlob(photoImage.UniqueId)
- use memoryStream = new MemoryStream(photoImage.ImageBytes)
- blockBlob.UploadFromStream(memoryStream)
- member this.DeletePhotoImage(uniqueIdentifier: Guid) =
- let blockBlob = this.GetBlockBlob(uniqueIdentifier)
- blockBlob.Delete()
- member this.GetPhotoImage(uniqueIdentifier: Guid) =
- let blockBlob = this.GetBlockBlob(uniqueIdentifier)
- if blockBlob.Exists() then
- blockBlob.FetchAttributes()
- use memoryStream = new MemoryStream()
- blockBlob.DownloadToStream(memoryStream)
- let photoArray = memoryStream.ToArray()
- new PhotoImage(uniqueIdentifier,photoArray)
- else
- failwith "photo not found"
And when I pop it into the WPF application,
- private void SavePhoto(byte[] colorData)
- {
- var bitmapSource = BitmapSource.Create(640, 480, 96, 96, PixelFormats.Bgr32, null, colorData, 640 * 4);
- JpegBitmapEncoder encoder = new JpegBitmapEncoder();
- encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
- using (MemoryStream memoryStream = new MemoryStream())
- {
- encoder.Save(memoryStream);
- PhotoImage photoImage = new PhotoImage(Guid.NewGuid(), memoryStream.ToArray());
- Guid customerUniqueId = new Guid("7282AF48-FB3D-489B-A572-2EFAE80D0A9E");
- String connectionString =
- "DefaultEndpointsProtocol=http;AccountName=XXX;AccountKey=XXX";
- IPhotoImageProvider provider = new AzureStoragePhotoImageProvider(customerUniqueId, connectionString);
- provider.InsertPhotoImage(photoImage);
- memoryStream.Close();
- }
- _isTakingPicture = false;
- }
I can now write my images to Azure.
With that out of the way, I can now have SkyBiometry pick up my photo, analyze it, and push the results back. I went ahead and added in the .fs module that I had already created for this blog post. I then added FSharp.Data via NuGet and was ready to roll. In he Save photo event handler,after saving the photo to blob storage, it then calls Sky Biometry to compare against a base image that has already been trained:
- private void SavePhoto(byte[] colorData)
- {
- var bitmapSource = BitmapSource.Create(640, 480, 96, 96, PixelFormats.Bgr32, null, colorData, 640 * 4);
- JpegBitmapEncoder encoder = new JpegBitmapEncoder();
- encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
- PhotoImage photoImage = UploadPhotoImage(encoder);
- String skyBiometryUri = "http://api.skybiometry.com";
- String uid = "Kinect@ChickenFace";
- String apiKey = "XXXX";
- String apiSecret = "XXXX";
- var imageComparer = new SkyBiometryImageComparer(skyBiometryUri, uid, apiKey, apiSecret);
- String basePhotoUri = "XXXX.jpg";
- String targetPhotoUri = "XXXX/" + photoImage.UniqueId + ".jpg";
- currentImage.Source = new BitmapImage(new Uri(basePhotoUri));
- compareImage.Source = new BitmapImage(new Uri(targetPhotoUri)); ;
- var matchValue = imageComparer.CalculateFacialRecognitionConfidence(basePhotoUri, targetPhotoUri);
- FacialRecognitionTextBox.Text = "Match Value is: " + matchValue.ToString();
- _isTakingPicture = false;
- }
And I am getting a result back from Sky Biometry.
Finally, I added in the SkyBiometry X and Y coordinates for the photo and compared to the calculated ones based on the Kinect Skeleton Tracking:
- currentImage.Source = new BitmapImage(new Uri(basePhotoUri));
- compareImage.Source = new BitmapImage(new Uri(targetPhotoUri)); ;
- var matchValue = imageComparer.CalculateFacialRecognitionConfidence(basePhotoUri, targetPhotoUri);
- var selectedSkeleton = skeletons.FirstOrDefault(s => s.TrackingState == SkeletonTrackingState.Tracked);
- if (selectedSkeleton != null)
- {
- var headPosition = selectedSkeleton.Joints[JointType.Head].Position;
- var adjustedHeadPosition =
- _sensor.CoordinateMapper.MapSkeletonPointToColorPoint(headPosition, ColorImageFormat.RgbResolution640x480Fps30);
- var skyBiometryX = ((float)adjustedHeadPosition.X / 640) * 100;
- var skyBioMetryY = ((float)adjustedHeadPosition.Y / 480) * 100;
- StringBuilder stringBuilder = new StringBuilder();
- stringBuilder.Append("Match Value is: ");
- stringBuilder.Append(matchValue.Confidence.ToString());
- stringBuilder.Append("Sky Biometry X: ");
- stringBuilder.Append(matchValue.X.ToString());
- stringBuilder.Append("Sky Biometry Y: ");
- stringBuilder.Append(matchValue.Y.ToString());
- stringBuilder.Append("Kinect X: ");
- stringBuilder.Append(Math.Round(skyBiometryX, 2).ToString());
- stringBuilder.Append("Kinect Y: ");
- stringBuilder.Append(Math.Round(skyBioMetryY, 2).ToString());
- FacialRecognitionTextBox.Text = stringBuilder.ToString();
- }
- _isTakingPicture = false;
And the results are encouraging –> it looks like I can use the X and Y to identify different people on the screen:
Match Value is: 53
Sky Biometry X: 10
Sky Biometry Y: 13.33
Kinect X: 47.5
Kinect Y: 39.79
Up next will be pointing the laser and the target…
Pingback: F# Weekly #28, 2014 | Sergey Tihon's Blog