* notes / aws
AWS Rekognition + S3
— Una arquitectura serverless para identificar asistentes en un evento: S3 como almacenamiento, Rekognition como motor biométrico y un puñado de lecciones sobre umbrales de confianza y privacidad.
Contexto: qué hace exactamente Rekognition
Rekognition es el servicio de visión por computador gestionado de AWS. Bajo el mismo paraguas conviven varias capacidades: detección de objetos y escenas, moderación de contenido, OCR, análisis de vídeo y, lo que nos interesa, análisis facial. Dentro del análisis facial, distinguimos tres operaciones esenciales:
DetectFaces: localiza rostros en una imagen y devuelve metadatos (edad estimada, expresión, gafas, ojos cerrados), pero no identifica a nadie.IndexFaces: extrae vectores faciales de los rostros encontrados en una imagen y los almacena en una colección identificados con unFaceId. Es el equivalente a "enrolar" a una persona.SearchFacesByImage: dada una imagen, busca dentro de una colección losFaceIdmás parecidos, ordenados por similitud.
Una colección no es más que un contenedor lógico identificado por nombre, creado con CreateCollection. Internamente, AWS guarda los vectores; nosotros nunca vemos los embeddings crudos. Esa opacidad es a la vez una ventaja (no necesitas entender vectores de 512 dimensiones) y un inconveniente (no puedes portarlos si decides abandonar Rekognition).
El precio, a fecha de redacción, ronda 1 dólar por cada 1 000 imágenes analizadas, con un coste adicional muy pequeño por cada vector almacenado en una colección. Para un evento de seiscientas personas y dos puntos de check-in, hablamos de unos pocos dólares de factura. La operativa, no obstante, exige cuidar dos detalles que ahora explicaré: el rol IAM para que Rekognition pueda leer S3 directamente, y el umbral de confianza, que determina cuántas confusiones aceptamos.
Implementación: del bucket a la búsqueda
El flujo es serverless de manual. Una Lambda se dispara con el evento s3:ObjectCreated:Put sobre el prefijo faces/checkin/, lee el objeto recién subido (no por su contenido, sino pasando la referencia S3Object a Rekognition) y consulta la colección. Si encuentra una coincidencia por encima del umbral, registra el check-in en DynamoDB.
Para que Rekognition pueda leer el bucket sin que nosotros descarguemos los bytes, basta con conceder al rol de la Lambda la política AmazonRekognitionReadOnlyAccess y, complementariamente, la política de bucket que permita a Rekognition (rekognition.amazonaws.com) ejecutar s3:GetObject sobre los prefijos relevantes. Esta delegación es la que justifica usar el parámetro S3Object en lugar de Bytes: nuestra Lambda no necesita memoria adicional para sostener una imagen de varios megabytes ni añade latencia descargándola.
El código TypeScript con el SDK v3 queda razonablemente conciso:
import {
RekognitionClient,
CreateCollectionCommand,
IndexFacesCommand,
SearchFacesByImageCommand,
} from "@aws-sdk/client-rekognition";
import {
DynamoDBClient,
PutItemCommand,
} from "@aws-sdk/client-dynamodb";
const rek = new RekognitionClient({ region: "eu-west-1" });
const ddb = new DynamoDBClient({ region: "eu-west-1" });
const COLLECTION = "evento-2026";
const BUCKET = "akzu-faces";
export async function ensureCollection() {
try {
await rek.send(new CreateCollectionCommand({ CollectionId: COLLECTION }));
} catch (err: unknown) {
if ((err as { name?: string }).name !== "ResourceAlreadyExistsException") {
throw err;
}
}
}
export async function enrollFace(s3Key: string, externalId: string) {
const res = await rek.send(
new IndexFacesCommand({
CollectionId: COLLECTION,
Image: { S3Object: { Bucket: BUCKET, Name: s3Key } },
ExternalImageId: externalId,
DetectionAttributes: ["DEFAULT"],
MaxFaces: 1,
QualityFilter: "AUTO",
}),
);
return res.FaceRecords?.[0]?.Face?.FaceId;
}La búsqueda en la Lambda de check-in es igual de directa, pero introduce el parámetro clave: FaceMatchThreshold.
export async function identify(s3Key: string) {
const res = await rek.send(
new SearchFacesByImageCommand({
CollectionId: COLLECTION,
Image: { S3Object: { Bucket: BUCKET, Name: s3Key } },
FaceMatchThreshold: 92,
MaxFaces: 1,
QualityFilter: "AUTO",
}),
);
const match = res.FaceMatches?.[0];
if (!match || !match.Face?.ExternalImageId) {
return null;
}
return {
externalId: match.Face.ExternalImageId,
similarity: match.Similarity ?? 0,
};
}
export const handler = async (event: { Records: { s3: { object: { key: string } } }[] }) => {
for (const record of event.Records) {
const key = record.s3.object.key;
const hit = await identify(key);
if (!hit) continue;
await ddb.send(
new PutItemCommand({
TableName: "check-ins",
Item: {
attendeeId: { S: hit.externalId },
ts: { S: new Date().toISOString() },
similarity: { N: hit.similarity.toFixed(2) },
imageKey: { S: key },
},
}),
);
}
};La política del bucket que permite a Rekognition leer las imágenes es la siguiente:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowRekognitionRead",
"Effect": "Allow",
"Principal": { "Service": "rekognition.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::akzu-faces/faces/*",
"Condition": {
"StringEquals": { "aws:SourceAccount": "123456789012" }
}
}
]
}Hallazgos: umbrales, falsos positivos y privacidad
La parte más reveladora del proyecto no fue arquitectónica sino estadística. Empezamos con el umbral por defecto sugerido en la documentación, FaceMatchThreshold: 80. En nuestras pruebas iniciales con cincuenta personas, todo parecía funcionar de maravilla: precisión cercana al 100%, ninguna confusión. Cuando subimos a la colección real de seiscientos asistentes, empezaron a aparecer falsos positivos sutiles: dos personas con barba y gafas similares se confundían con un 83% de similitud; gemelos prácticamente intercambiables (sí, había gemelos en el evento) compartían un score del 91%; y, lo más inquietante, un participante con sombrero ancho era confundido con otro sombrero ancho diferente por encima del 85%.
Subir el umbral a 92 eliminó casi todos los falsos positivos a costa de un puñado de falsos negativos: personas que el sistema no reconocía y debían identificarse manualmente. Esa asimetría, preferir un check-in manual a admitir a la persona equivocada, es la única política defendible en biometría: el coste de una falsa aceptación es desproporcionadamente mayor que el de un falso rechazo.
Otro descubrimiento incómodo: las fotografías de enrolado importan mucho más que las del momento del check-in. Si la imagen de referencia es pequeña, mal iluminada o frontal con sonrisa exagerada, el sistema produce vectores de baja calidad y la búsqueda posterior degrada rápidamente. Acabamos exigiendo una foto de enrolado de al menos 800 píxeles de ancho, sin filtros y con expresión neutra, lo cual añadió fricción al proceso de registro pero elevó la fiabilidad sustantivamente.
Por último, lo no técnico pero esencial: la privacidad. En la Unión Europea, los datos biométricos son una categoría especial bajo el RGPD (artículo 9), y procesarlos exige base jurídica explícita (típicamente, consentimiento informado) además de una evaluación de impacto. Para nuestro caso, redactamos un consentimiento claro, ofrecimos alternativa por código QR a quien no quisiera usar reconocimiento facial, y borramos la colección completa (con DeleteCollection) y todos los objetos S3 una semana después del evento. No es opcional; es la línea de base ética y legal.
Closure
Rekognition + S3 es una combinación pragmática que resuelve, con muy pocas líneas de código, un problema que hace cinco años exigía un equipo de ML dedicado. La tentación de construir todo desde cero sigue siendo poderosa entre ingenieros con vocación de artesano, pero rara vez está justificada para casos de uso acotados como el nuestro. No obstante, la facilidad de la herramienta no exime de pensar en los umbrales, en los falsos positivos y, sobre todo, en las personas cuyos rostros estás procesando. Por consiguiente, mi recomendación a quien aborde un proyecto similar es invertir el grueso del tiempo en validación estadística y en política de privacidad, no en infraestructura: lo primero es lo que separa una prueba de concepto vistosa de un sistema responsable.