Mientras trabajaba en una herramienta para organizar automáticamente archivos de media (mover imágenes nuevas a su directorio correcto) pensé: ¿podría sacar ventaja del motor de reconocimiento de caras de la Galería Fotográfica de Windows Live (WLPG) ?Las fotos que quiero organizar son de personas, y normalmente trato de marcar las caras con WLPG antes de correr mi aplicación (que usa las caras embebidas en un jpeg junto con otros metadatos para ayudar en la seleccion del directorio final). Quiza podría usar el motor de caras para reconocer algunas caras dentro de mi aplicación.

En realidad, el API no esta bien preparada para que otros la usen, y el motor de reconocimiento no es perfecto, pero he conseguido usarlo hasta cierto punto. Asi es como lo he hecho usando Embarcadero RAD Studio 2010 (Delphi), y con Galería Fotográfica de Windows Live 2011.

Primeros pasos

Lo primero, debes importar la librería de tipos “WLPG FaceRecognitionObjects” (que está en WLXFaceRecognition.dll en el directorio de WLPG ). Esto te dará acceso al API expuesto.

Para procesar un archivo de imagen (para detección o reconocimiento) tienes que cargarla en un objeto IWICBitmap para acceder a sus datos. No he usado control de excepciones para hacer el código más claro, pero debes capturar las excepciones y asegurarte de que liberas cualquier objeto que hayas creado.

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);

La variable theImageData será usada en las siguientes llamadas para detectar o reconocer caras.

Detección de caras

La detección es una tarea sencilla.

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 contiene las coordenadas del rectangulo de la cara (left, top, width, height) en porcentajes
             end;
           if theFaceRegion.GetFacePose(theFacePose) = S_OK then
             begin
               // theFacePose contiene la orientación de la cara detectada (ver los enumerados correspondientes en la libreria de tipos)
             end;
         end;
    end;
end;

Por cada cara puedes usar los métodos GetFaceRegionRect y GetFacePose para obtener el rectángulo y la orientación de la cara. Los objetos de cara tienen otros metodos, pero como solo hemos pedido la detección estos no retornarán información util en este momento. Desgraciadamente solo detectar la cara no es tan util si no puedes reconocerla.

Reconocimiento de caras

El reconocimiento de caras es una tarea algo más complicada. Veamos primero el código.

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 contiene las coordenadas del rectangulo de la cara (left, top, width, height) en porcentajes
             end;

           if theFaceRegion.GetFacePose(theFacePose) = S_OK then
             begin
               // theFacePose contiene la orientación de la cara  (ver los enumerados correspondientes en la libreria de tipos)
             end;

           theFacePersonId := 0;
           if faceregion.GetPersonId(theFacePersonId) <> S_OK then
             begin
               // theFacePersonId contiene el identificador interno de la persona detectada
             end;

           theFacePersonName := '';
           if faceregion.GetPersonName(theFacePersonName) <> S_OK then
             begin
               // theFacePersonName contiene el nombre de la persona detectada
             end;

           // Ahora leemos las recomendaciones de personas y su nivel de confianza
           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
                       // obtienes el nivel de confianza de pFaceRecommendations^[r].flConfidence (un porcentaje).
                     end;
               end;

         end;
    end;
end;

Parece muy similar al código de detección, pero si miras de cerca verás que solo devuelve un identificador numérico para las personas reconocidas, y el método principal tiene un parametro adicional (theFaceCache). El metodo GetPersonName solo devuelve algo cuando el motor está 100% seguro del reconocimiento. Asi que, ¿de donde sacamos los nombres de las personas recomendadas? y ¿que es el caché de caras?

Caché de ejemplos de caras

Este caché de caras es el objeto que contiene la información de las caras, permite al motor reconocer, y asocia un identificador de una persona con su nombre. Y para usar correctamente las caras reconocidas en WLPG necesitamos rellenarlo a partir de la base de datos de la aplicación, ya que el API no nos da acceso a esos datos.

Necesitaremos crear una clase basada en la interfaz IExemplarCache que contendrá la información de caras de la base de datos.

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\';

    // leemos datos de ejemplos de caras
    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;

    // leemos nombres de personas
    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;

Un objeto de tipo TExemplarCache, rellenado usando el método ImportFromWLPG se pasará al método RecognizeFacesFromImage , que usará sus datos para reconocer las caras. Luego, por cada cara podemos usar el metodo GetPersonName del caché para saber el nombre asociado al identificador devuelto.

Desgraciadamente, el motor de reconocimiento es muy lento, y necesita estar bien entrenado (dentro de WLPG) para devolver buenos niveles de confianza para las caras.

Recomiendo no usar WLPG al mismo tiempo que llamas al método ImportFromWLPG . El código podria lanzar una excepción porque la base de datos esta ya en uso.

ACTUALIZACIÓN: Mi base de datos de WLPG se ha corrompido. No estoy seguro de que sea porque hice algunas pruebas mientras tenía el WLPG abierto. Por suerte, tenía una copia de seguridad. Te sugiero que te asegures de que WLPG no esta corriendo, o hacer copias de seguridad del directorio de la base de datos completo.

Espero que Microsoft abra más su API en una próxima versión, para dar acceso mejor y más sencillo al caché de ejemplos de caras.