Accediendo al API de caras de la Galería Fotográfica de Windows Live
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.





