Grouping similar faces
If you have several images of faces, one thing you may want to do is group the faces. Typically, you will want to group faces based on similarity, which is a feature the Face API provides.
By providing the API with a list of face IDs, it will respond with one or more groups. One group consists of faces that are similar looking. Usually, this means that the faces belong to the same person. Faces that cannot find any similar counterparts are placed in a group we'll call MessyGroup
.
Create a new View
called FaceGroupingView.xaml
. The View
should have six image elements, with corresponding titles and textboxes for face IDs. It should also have a button for our group command and a textbox to output the grouping result.
In the corresponding FaceGroupingViewModel.xaml
View
model, you should add the BitmapImage
properties for all images. You should also add the string
properties for the face IDs and one for the result. There is also a need for an ICommand
property.
At the start of the ViewModel
, we declare some private
variables, as follows:
private FaceServiceClient _faceServiceClient; private List<string> _imageFiles = new List<string>(); private List<Guid> _faceIds = new List<Guid>();
The first one is used to access the Face API. The second one contains a list of strings that in turn contain the location of our images. The last list contains the detected face IDs.
The constructor accepts a parameter of the FaceServiceClient
type. It assigns it to the corresponding variable and calls the Initialize
function. This creates our ICommand
object and calls a function to add our images to the application.
In the function that adds images, we add hardcoded image paths to our _imageFiles
list. For this example, we add six. Using a for
loop, we generate each BitmapImage
property. When we have an image, we want to detect faces in it:
try { using (Stream fileStream = File.OpenRead(_imageFiles[i])) { Face[] faces = await _faceServiceClient.DetectAsync(fileStream);
We do not need any more data than the generated face ID, which we know is stored for 24 hours after detection:
_faceIds.Add(faces[0].FaceId); CreateImageSources(image, i, faces[0].FaceId); } }
Assuming that there is only one face per image, we add that face ID to our _faceIds
list. The image, face ID, and current iteration number in the loop are passed on to a new function, CreateImageSources
. This function contains a switch
case based on the iteration number. Based on the number, we assign the image and face ID to the corresponding image and image ID property. This is then shown in the UI.
We have a button to group the images. To group the images, we call the Face API's GroupAsync
method, passing on an array of face IDs, as shown in the following code. The array of face IDs must contain at least two elements, and it cannot contain more than 1,000 elements:
private async void GroupFaces(object obj) { try { GroupResultfaceGroups = await _faceServiceClient.GroupAsync(_faceIds.ToArray());
The response is a GroupResult
type, which may contain one or more groups, as well as the messy group. We check to see whether there is a response and then we parse it, as shown in the following code:
if (faceGroups != null) FaceGroupingResult = ParseGroupResult(faceGroups); }
Before looking at the ParseGroupResult
method, add the corresponding catch
clause and close-up GroupFaces
function.
When parsing the results, we first create a StringBuilder
class to hold our text. Then we get the groups
from the result. A group is an array of face IDs of the images in that group. All groups are stored in a list, and we append the number of groups to the StringBuilder
class, as shown in the following code:
private string ParseGroupResult(GroupResultfaceGroups) { StringBuilder result = new StringBuilder(); List<Guid[]>groups = faceGroups.Groups; result.AppendFormat("There are {0} group(s)\n", groups.Count);
We loop through the list of groups. Inside this loop, we loop through each item in the group. For the sake of readability, we have a helper function to find the image name from the ID. It finds the index in our _faceIds
list. This is then used in the image name, so if the index is 2
, the image name would be Image 3
. For this to give the intended effect, you must have placed the images in a logical order, as follows:
result.Append("Groups:\t"); foreach(Guid[] guid in groups) { foreach(Guid id in guid) { result.AppendFormat("{0} - ", GetImageName(id)); } result.Append("\n"); }
The GroupResult
method may also contain a MessyGroup
array. This is an array of Guid
variables containing the face IDs in that group. We loop through this array and append the image name, the same way we did with the regular groups, as shown in the following code:
result.Append("Messy group:\t"); Guid[] messyGroup = faceGroups.MessyGroup; foreach(Guidguid in messyGroup) { result.AppendFormat("{0} - ", GetImageName(guid)); }
We end the function by returning the StringBuilder
function's text, which will output it to the screen, as follows:
return result.ToString(); }
Make sure that the ViewModel
instances have been created in the MainViewModel.cs
file. Also, make sure that the View
has been added as a TabItem
property in the MainView.xaml
file. Compile and test the application.
If you are using the sample images provided, you may end up with something like the following: