Accessing Windows Live Photo Gallery Face API

While working on a tool to automatically organize media files (move new images to their correct directory) i though: could i use the Windows Live Photo Gallery (WLPG) face recognition engine to my advantage? The photos i want to organize are people, and i usually try to mark the faces with WLPG before running my program (which uses the faces embeded in the jpeg along other metadata to help select the final directory). Maybe i could use the face engine to recognize some faces inside my program.

As it happens, the API is not well prepared to be used by others, and the recognition engine is not perfect, but i managed to use it to some level. This is how i did it using Embarcadero RAD Studio 2010 (Delphi), and with Windows Live Photo Gallery 2011.

First steps

First, you have to import the Type Library “WLPG FaceRecognitionObjects” (which is in WLXFaceRecognition.dll on the WLPG directory). This will give you access to the exposed API.

To process an image file (for detection or recognition) you have to load it into an IWICBitmap object to access its data. I haven’t used exception check code to make it more clear, but you should capture exceptions and make sure you free any objects you create.

var
  theImageManager: IImageManager;
  theFaceWICBitmap: FaceRecognitionObjects_TLB.IWICBitmap;
  theImageData: IImageData;
  theWICImage: Graphics.TWICImage;
begin
  theWICImage := TWICImage.Create;
  theWICImage.LoadFromFile(filename);
  theFaceWICBitmap := FaceRecognitionObjects_TLB.IWICBitmap(theWICImage.Handle);
  theImageManager := CoImageManager.Create;
  theImageManager.CreateImageDataFromWICBitmap(theFaceWICBitmap, theImageData);

The theImageData variable will be used on the following calls to detect or recognize faces.

Face Detection

Detection is an easy task.

var
  theFaceDetection: IFaceDetection;
  theFaceRegionSet: IFaceRegionSet;
  theFaceRegion: IFaceRegion;
  faceCount: Cardinal;
  theFaceRegionRectangle: FaceRegionRect;
  theFacePose: FacePose;
  f: Integer;
begin
  theFaceDetection := CoFaceDetection.Create;
  if theFaceDetection.RunFaceDetection (theImageData, theFaceRegionSet) = S_OK then
    begin
      if theFaceRegionSet <> nil then theFaceRegionSet.GetCount(faceCount);
      for f := 0 to Integer(faceCount) - 1 do
       if theFaceRegionSet.GetAt(f, theFaceRegion) = S_OK then
         begin
           if theFaceRegion.GetFaceRegionRect(theFaceRegionRectangle) = S_OK then
             begin
               // theFaceRegionRectangle contains the face rectangle coordinates (left, top, width, height) in percentages
             end;
           if theFaceRegion.GetFacePose(theFacePose) = S_OK then
             begin
               // theFacePose contains the detected face pose (see the corresponding enumerations in the type library)
             end;
         end;
    end;
end;

For each face you can use the GetFaceRegionRect and GetFacePose methods to get the rectangle and the detected pose (angle of the face). The face objects have other methods but since we only asked to detect the faces they will not return useful data at this point. Unfortunately just detecting a face is not that useful if you cannot recognize it.

Face Recognition

Face recognition is a very different beast to deal with. Lets see first the code.

type
  TFaceRecommendations = array [0 .. 0] of FaceRecommendation;
var
  theFaceRecognitionPipeline: IFaceRecognitionPipeline;
  theFaceRegionSet: IFaceRegionSet;
  theFaceRegion: IFaceRegion;
  faceCount: Cardinal;
  theFaceRegionRectangle: FaceRegionRect;
  theFacePose: FacePose;
  theFacePersonId: Integer;
  theFacePersonName: WideString;
  pFaceRecommendations: ^TFaceRecommendations;
  faceRecommendationsCount: Cardinal;
  f, r: Integer;
begin
  theFaceRecognitionPipeline := CoFaceRecognitionPipeline.Create;
  theFaceRecognitionPipeline.SetMatchThreshold(MatchThresholdDefault);
  if theFaceRecognitionPipeline.RecognizeFacesFromImage (theImageData, theFaceCache, theFaceRegionSet) = S_OK then
    begin
      if theFaceRegionSet <> nil then theFaceRegionSet.GetCount(faceCount);
      for f := 0 to Integer(faceCount) - 1 do
       if theFaceRegionSet.GetAt(f, theFaceRegion) = S_OK then
         begin

           if theFaceRegion.GetFaceRegionRect(theFaceRegionRectangle) = S_OK then
             begin
               // theFaceRegionRectangle contains the face rectangle coordinates (left, top, width, height) in percentages
             end;

           if theFaceRegion.GetFacePose(theFacePose) = S_OK then
             begin
               // theFacePose contains the detected face pose (see the corresponding enumerations in the type library)
             end;

           theFacePersonId := 0;
           if faceregion.GetPersonId(theFacePersonId) <> S_OK then
             begin
               // theFacePersonId contains the internal id of the detected person
             end;

           theFacePersonName := '';
           if faceregion.GetPersonName(theFacePersonName) <> S_OK then
             begin
               // theFacePersonName contains the name of the detected person
             end;

           // Next we read the person recommendations with their confidence level
           pFaceRecommendations := nil; // the method will allocate memory
           if theFaceRegion.GetRecommendations (PUserType3(pFaceRecommendations), faceRecommendationsCount) = S_OK then
             if faceRecommendationsCount > 0 then
               begin
                 for r := 0 to Integer(faceRecommendationsCount) - 1 do
                   if pFaceRecommendations^[r].nPersonId <> 0 then // a personId of 0 means no one.
                     begin
                       // you get the confidence level from pFaceRecommendations^[r].flConfidence (a percentage).
                     end;
               end;

         end;
    end;
end;

It looks very similar to the detection code, but if you look closely you will notice that it only returns a numeric Id for the recognized person(s), and the main method has an additional parameter (theFaceCache). The GetPersonName method only returns something when the engine is 100% sure of the recognition. So, where do we get the person recommendations names? and what is that face cache?

Face exemplar cache

That face cache is the object that holds the faces information, allows the engine to recognize, and maps a person Id with its name. And to properly use the recognized faces from WLPG we will need to populate it from the application database, since the API does not give us access to that data.

We will have to define a class based on the IExemplarCache interface that will hold the face information from the database.

type
  TPersonFaceData = class
  public
    PersonId: cardinal;
    Representation: TBytes;
    FaceRep:array of FaceRepresentation;
    constructor Create(const id: integer; rep: TBytes);
    procedure AddRepresentation (rep:TBytes);
    destructor Destroy; override;
  end;

  TExemplarCache = class(TInterfacedObject, IExemplarCache)
  private
    cache: TObjectList;
    personnames: TStringList;
    statusCacheStamp:integer;
  public
    function GetPersonCount(out puPersonCount: LongWord): HResult; stdcall;
    function GetPersonData(uPersonIndex: LongWord; out pnPersonId: integer; out ppFaceRepresentations: PUserType4; out puRepresentationCount: LongWord) : HResult; stdcall;
    function GetCacheStamp(out pdwCacheStamp: LongWord): HResult; stdcall;

    constructor Create;
    destructor Destroy; override;
    procedure ImportFromWLPG;
    function GetPersonName(uPersonId: integer): string;
  end;

{ TPersonFaceData }

procedure TPersonFaceData.AddRepresentation(rep: TBytes);
begin
  setlength (FaceRep, length(FaceRep)+1);
  FaceRep[high(FaceRep)].uByteCount := length(rep);
  GetMem (FaceRep[high(FaceRep)].pbBytes, FaceRep[high(FaceRep)].uByteCount);
  move (rep[0], FaceRep[high(FaceRep)].pbBytes^, FaceRep[high(FaceRep)].uByteCount);
end;

constructor TPersonFaceData.Create(const id: integer; rep: TBytes);
begin
  PersonId := id;
  Representation := rep;
  setlength (FaceRep, 1);
  FaceRep[0].uByteCount := length(rep);
  GetMem (FaceRep[0].pbBytes, FaceRep[0].uByteCount);
  move (rep[0], FaceRep[0].pbBytes^, FaceRep[0].uByteCount);
end;

destructor TPersonFaceData.Destroy;
var
  I: Integer;
begin
  for I := 0 to high(FaceRep) do
    FreeMem (FaceRep[I].pbBytes);
  inherited;
end;

{ TExemplarCache }

constructor TExemplarCache.Create;
begin
  cache := TObjectList.Create;
  personnames := TStringList.Create;
end;

destructor TExemplarCache.Destroy;
begin
  personnames.Free;
  cache.Free;
  inherited;
end;

function TExemplarCache.GetCacheStamp(out pdwCacheStamp: LongWord): HResult;
begin
  pdwCacheStamp := statusCacheStamp;
  Result := S_OK;
end;

function TExemplarCache.GetPersonCount(out puPersonCount: cardinal): HResult;
begin
  puPersonCount := cache.Count;
  Result := S_OK;
end;

function TExemplarCache.GetPersonData(uPersonIndex: cardinal; out pnPersonId: integer; out ppFaceRepresentations: PUserType4; out puRepresentationCount: cardinal): HResult;
begin
  if (uPersonIndex < cache.Count) then
    begin
      pnPersonId := cache[uPersonIndex].PersonId;
      puRepresentationCount:=length(cache[uPersonIndex].FaceRep);
      ppFaceRepresentations := @(cache[uPersonIndex].FaceRep[0]);
      Result := S_OK;
    end
  else
    begin
      pnPersonId := 0;
      ppFaceRepresentations:=nil;
      puRepresentationCount:=0;
      Result := S_FALSE;
    end;
end;

function TExemplarCache.GetPersonName(uPersonId: integer): string;
var
  i: Integer;
begin
  if (uPersonId > 0) and (uPersonId-1 < personnames.Count) then
    begin
      i:=PersonNames.IndexOfObject(pointer(uPersonId));
      if i >= 0 then Result := PersonNames[i] else Result:='';
    end
  else
    Result := '';
end;

procedure TExemplarCache.ImportFromWLPG;
var
  conn: TADOConnection;
  query: TADOQuery;
  pName: TField;
  pId: TField;
  pData: TField;
  I: Integer;
  found: Boolean;
  wlpg_dir: String;
begin
  cache.Clear;
  personnames.Clear;

  try
    wlpg_dir:=GetSpecialFolderLocation(CSIDL_LOCAL_APPDATA)+'\Microsoft\Windows Live Photo Gallery\';

    // read face exemplar data
    conn:=TADOConnection.Create(nil);
    query:=TADOQuery.Create(nil);
    try
      conn.ConnectionString := 'Provider=Microsoft.SQLLITE.MOBILE.OLEDB.3.0;Data Source="'+wlpg_dir+'FaceExemplars.ed1";SSCE:Max Database Size=4000';
      conn.LoginPrompt := false;
      query.Connection := conn;
      query.CursorLocation := clUseServer;
      query.CursorType := ctOpenForwardOnly;
      query.LockType := ltReadOnly;
      query.SQL.Text := 'SELECT PersonId, Data FROM tblExemplar';
      query.Open;
      try
        while not query.Eof do
          begin
            try
              pId := query.FieldByName('PersonId');
              pData := query.FieldByName('Data');
              if (pId <> nil) and (pData <> nil) then
                begin
                  found := false;
                  for I := 0 to cache.Count - 1 do
                    if cache[i].PersonId = pId.AsInteger then
                      begin
                        found := true;
                        cache[i].AddRepresentation (pData.AsBytes);
                        break;
                      end;
                  if not found then cache.Add (TPersonFaceData.Create (pId.AsInteger, pData.AsBytes));
                end;
            except
            end;
            query.Next;
            application.processmessages;
          end;
      finally
        query.Close;
      end;
      query.SQL.Text := 'SELECT ExemplarCacheStamp FROM tblStatus';
      query.Open;
      try
        if not query.Eof then
          statusCacheStamp := query.FieldByName('ExemplarCacheStamp').AsInteger;
      finally
        query.Close;
      end;
    finally
      query.Free;
      conn.Close;
      conn.Free;
    end;

    // read person names
    conn:=TADOConnection.Create(nil);
    query:=TADOQuery.Create(nil);
    try
      conn.ConnectionString := 'Provider=Microsoft.SQLLITE.MOBILE.OLEDB.3.0;Data Source="'+wlpg_dir+'Pictures.pd6";SSCE:Max Database Size=4000';
      conn.LoginPrompt := false;
      query.Connection := conn;
      query.CursorType := ctOpenForwardOnly;
      query.LockType := ltReadOnly;
      query.CursorLocation := clUseServer;
      query.SQL.Text := 'SELECT PersonId, Name FROM tblPerson';
      query.Open;
      try
        while not query.Eof do
          begin
            try
              pName := query.FieldByName('Name');
              pId := query.FieldByName('PersonId');
              if (pName <> nil) and (pId <> nil) then
                if pId.AsInteger <> 0 then
                  personnames.AddObject (pName.AsString, pointer(pId.AsInteger));
            except
            end;
            query.Next;
            application.processmessages;
          end;
      finally
        query.Close;
        conn.Close;
      end;
    finally
      query.Free;
      conn.Free;
    end;
  except
    on e:exception do
      begin
        cache.Clear;
        personnames.Clear;
      end;
  end;
end;

An object of type TExemplarCache, populated using the ImportFromWLPG method will be passed to the RecognizeFacesFromImage method, which will use its data to recognize the faces. Then, for each face we can use the GetPersonName method of the cache to know the name associated to the id returned.

Unfortunately, the recognition engine is very slow and needs to be properly trained (within WLPG) to return good confidence levels for the faces.

It is recommended to not run WLPG at the same time you call the ImportFromWLPG method. The code could raise an exception because the database is already in use.

UPDATE: My WLPG database got corrupted. Not sure if it was because i ran some tests while having WLPG open. Thankfully, i had a backup. I suggest you make sure WLPG is not running, or make backups of the entire database directory.

I hope Microsoft will open more their API in a next version, to give easier and greater access to the face exemplar cache.