Rekognize your serverless photo album
In this blog post I explain how I used AWS to build a serverless photo album with facial recognition. This is not a step-by-step tutorial but rather an overview of the architecture, setup, and code.
I started my online photo album, finpics.com, back in 1999. I originally built finpics.com using php and mysql.
I also had a custom Java Applet to upload the images. Pretty cool, right.
Code: https://github.com/rgfindl/finpics
Website: http://finpics.com
Serverless
Serverless architectures refer to applications that significantly depend on third-party services (knows as Backend as a Service or “BaaS”) or on custom code that’s run in ephemeral containers (Function as a Service or “FaaS”), the best known vendor host of which currently is AWS Lambda. http://martinfowler.com/articles/serverless.html
finpics.com
finpics.com gets almost no traffic. Good thing too because there are a lot of embarrassing picture of myself, family, and friends. A serverless architectures is more cost effective because I’m not paying for a server to run that gets very little traffic. There is also very little maintenance with serverless apps.
Once AWS Rekognition was released I knew that I wanted to use it to improve the searchability of finpics.com. It is great to click on someones face and see more pictures of them.
AWS resources
AWS Resource | Usage |
---|---|
Route53 | DNS for finpics.com, points to static S3 bucket. |
S3 | Hosts static website and images. |
Cognito | Authorization. Lets the web client assume an IAM Role to make calls to AWS resources. |
DynamoDB | Store image metadata. |
Lambda | Serverless compute. |
Rekognition | Facial recognition. |
Architecture
Web Requests
Static Web Site
Fetch the html, js, css, and image assets directly from S3.
Get Unauth Creds
Make a call to Cognito to get AWS credentials to use for all the calls to AWS. The user assumes the unauthenticated IAM role that you define in Cognito.
Fetch Pictures
The pictures are structured into picture sets. A call is made to DynamoDB to get all the picture sets, and the featured pic for each set. Then another call is made to DynamoDB to get the pictures for each set.
Search Faces
Search Rekognition given a face id. When you click on a persons face the results are pictures with that face sorted by highest probability.
Upload Images
Images are uploaded directly to S3. There is an S3 event that triggers a Lambda function to perform the following tasks on each new image:
- Create a thumbnail
- Index the faces with Rekognition
- Store metadata in DynamoDB
I have a script that uploads a new picture set.
DynamoDB Tables
pics table
- primaykey (Primary Key)
- sortKey (Sort Key)
- data (Rekognition IndexFaces response)
The pics table stores all the picture sets, with featured image, which is used by the index page.
Picsets
- primaykey: ‘/‘ (Primary Key)
- sortkey: ‘014_newportboston’ (Sort Key)
- pic: ‘Newport_pic_3.jpg’
The pics table also stores all the pictures associated with a picture set, which is used by each picture set page.
Pics
- primaykey: ‘014_newportboston’ (Primary Key)
- sortkey: ‘Newport_pic_3.jpg’ (Sort Key)
- … Rekognition IndexFaces response
pics_by_image_id table
- image_id (Primary Key)
- data (Rekognition IndexFaces response)
- image_path
The pics_by_image_id table stores the same facial recognition data as the pics table but is index by the AWS Rekognition image_id. When we search Rekognition for facial matches we use the image_id’s in the response to fetch the picture information via this table.
Setup & Code Samples
AWS S3
I’m using 3 buckets for finpics.com.
- finpics.com which serves the static web content.
- finpics-pics which serves the original (large) images.
- finpics-thumbs which serves the thumbnails.
I used different buckets for 2 reasons:
- AWS Rekognition fails when your bucket name has a period in it. Awesome!
- For every new picture added to finpics-pics a Lambda function is triggered to create the thumbnail. I didn’t want to create a circular loop.
For each bucket I have Web Hosting enabled.
I want all assets in these 3 buckets to be publicly available. Under Permissions I have the following bucket policy for each. Make sure to change the Resource name to match your bucket name.
Now all my web assets are publicly available and served via S3. Cheap, serverless, and performant.
AWS Cognito
Cognito allows the client-side JavaScript code permission to access AWS resources like DynamoDD & Lambda functions.
I created a Cognito Federated Identity called finpics
. When creating this identity I Enabled access to unauthenticated identities. I also created new Unauthenticated and Authenticated AWS IAM Roles. I’ll explain the permissions needed within Unauthenticated role later. I’m currently not using the Authenticated role.
In the client-side JavaScript code I assume the Unathenticated role like this. All subsequent calls to AWS will use this IAM role.
IAM Roles
Cognito Unathenticated Role
Cognito will automatically create the Unathenticated role and setup the Trust Relationship with the Cognito federated identity.
Here are the permissions:
- The first is permission is sync the user with Cognito (Cognito stuff).
- The second permission is to get picture data and metadata from DynamoDB.
- The third permission is to invoke our Lambda function.
Lambda Role
The Lambda role has permissions to interact with the S3 buckets, DynamoDB tables, and the Rekognition collection (which I will create next). Lambda also has permission to CloudWatch logs.
|
|
Rekognition
Amazon Rekognition is a service that makes it easy to add image analysis to your applications. With Rekognition, you can detect objects, scenes, and faces in images. You can also search and compare faces. Rekognition’s API enables you to quickly add sophisticated deep learning-based visual search and image classification to your applications.
To start indexing faces we first need to create a Rekognition collection.
When an image is added to S3 our Lambda function is triggered. The Lambda function indexes the image which adds all the faces within that image to the Rekognition collection.
|
|
When a user clicks on a face we search the Rekognition collection to find matches ordered by match probability.
Lambda Functions
There are two Lambda functions:
- Search - search the Rekognition facial collection given a
faceid
. - Process New Image - Triggered for each new image added to S3.
- Creates a thumbnail
- Indexes the image within the Rekognition collection
- Adds image and metadata to DynamoDB
Search
- Search the Rekognition collection.
- Bulk fetch the images from DynamoDB.
- Normalize the DynamoDB results.12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091search: function(event, context, callback) {//// Search AWS Rekognition given the faceid.//var params = {CollectionId: 'finpics', /* required */FaceId: event.faceid};rekognition.searchFaces(params, function (err, data) {if (err) {var response = {statusCode: 500,err: err,params: params};callback(null, response);} else {//// For each face match. Fetch the image information from DynamoDB.//var keys = [];var imageids = [];_.forEach(data.FaceMatches, function (FaceMatch) {if (!_.includes(imageids, FaceMatch.Face.ImageId)) {keys.push({"image_id": {"S": FaceMatch.Face.ImageId}});imageids.push(FaceMatch.Face.ImageId);}});var params = {"RequestItems": {"pics_by_image_id": {"Keys": _.slice(keys, 0, 100)}}};dynamodb.batchGetItem(params, function (err, results) {if (err) {var response = {statusCode: 500,err: err,params: params};callback(null, response);} else {//// Normalize the images we get back from the DynamoDB bulk get request.//var output = [];var imageids = [];_.forEach(data.FaceMatches, function (FaceMatch) {if (!_.includes(imageids, FaceMatch.Face.ImageId)) {var raw_item = _.find(results.Responses.pics_by_image_id, {image_id: {S: FaceMatch.Face.ImageId}});if (!_.isNil(raw_item)) {var item = {image_id: raw_item.image_id.S,image_path: raw_item.image_path.S};var faces = [];_.forEach(raw_item.data.M.FaceRecords.L, function (FaceRecord) {faces.push({Face: {Confidence: FaceRecord.M.Face.M.Confidence.N,ImageId: FaceRecord.M.Face.M.ImageId.S,BoundingBox: {Top: FaceRecord.M.Face.M.BoundingBox.M.Top.N,Height: FaceRecord.M.Face.M.BoundingBox.M.Height.N,Width: FaceRecord.M.Face.M.BoundingBox.M.Width.N,Left: FaceRecord.M.Face.M.BoundingBox.M.Left.N},FaceId: FaceRecord.M.Face.M.FaceId.S,}});});item.data = {FaceRecords: faces};output.push(item);}imageids.push(FaceMatch.Face.ImageId);}});var response = {statusCode: 200,output: output};callback(null, response);}});}});}
Process New Image
- Download image from S3 (finpics-pics)
- Create thumbnail
- Upload thumbnail to S3 (finpics-thumbs)
- Add feature picture for album, if needed
- Index image using Rekognition
- Add image and metadata to DynamoDB
|
|
Thanks for reading my blog post. Please let me know if you have any questions.
Here is the source code.
Here is the demo (finpics.com).
There are a bunch of untilities here. I had a lot of images already that I had to process using this script.