Image Processing Serverless with AWS Step Functions
Photo by Lindsay Henwood
Image Processing Serverless with AWS Step Functions
As mentioned in the last article, We will follow the AWS workshop https://www.image-processing.serverlessworkshops.io/ to build a real AWS Serverless Project with AWS Step Functions.
This is the structure that we gonna build.
Follow the workshop, The main sam.yaml file located in here: https://gist.githubusercontent.com/khoa-le/98855ac969c8b3c7eb3dfa2cce68de12/raw/ac1373b6b35a678450b47e727d7741a54c5c8ed1/sam_220615.yaml
You can download and take a look on those configuration and to apply successfully you should choose the region us-east-1 because the yaml file for us-east-1 configured.
The Cloudformation created look likes:
The Step functions after finished the workshop
The Step functions definition
Here is the khoale-step-module-resources-FaceDetectionFunction lambda
1const util = require('util')2const AWS = require('aws-sdk')3const rekognition = new AWS.Rekognition()45exports.handler = (event, context, callback) => {6 console.log('Reading input from event:\n', util.inspect(event, {depth: 5}))78 const srcBucket = event.s3Bucket9 // Object key may have spaces or unicode non-ASCII characters.10 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))1112 var params = {13 Image: {14 S3Object: {15 Bucket: srcBucket,16 Name: srcKey,17 },18 },19 Attributes: ['ALL'],20 }2122 rekognition23 .detectFaces(params)24 .promise()25 .then(data => {26 console.log(27 'Detection result from rekognition:\n',28 util.inspect(data, {depth: 5}),29 )30 if (data.FaceDetails.length != 1) {31 callback(32 new PhotoDoesNotMeetRequirementError(33 'Detected ' + data.FaceDetails.length + ' faces in the photo.',34 ),35 )36 }37 if (data.FaceDetails[0].Sunglasses.Value === true) {38 callback(39 new PhotoDoesNotMeetRequirementError('Face is wearing sunglasses'),40 )41 }42 var detectedFaceDetails = data.FaceDetails[0]4344 // remove some fields not used in further processing to de-clutter the output.45 delete detectedFaceDetails['Landmarks']4647 callback(null, detectedFaceDetails)48 })49 .catch(err => {50 console.log(err)51 if (err.code === 'ImageTooLargeException') {52 callback(new PhotoDoesNotMeetRequirementError(err.message))53 }54 if (err.code === 'InvalidImageFormatException') {55 callback(56 new PhotoDoesNotMeetRequirementError(57 'Unsupported image file format. Only JPEG or PNG is supported',58 ),59 )60 }61 callback(err)62 })63}6465function PhotoDoesNotMeetRequirementError(message) {66 this.name = 'PhotoDoesNotMeetRequirementError'67 this.message = message68}69PhotoDoesNotMeetRequirementError.prototype = new Error()
Here is the khoale-step-module-resources-FaceSearchFunction lambda
1const util = require('util')2const AWS = require('aws-sdk')3const rekognition = new AWS.Rekognition()45exports.handler = (event, context, callback) => {6 console.log('Reading input from event:\n', util.inspect(event, {depth: 5}))78 const srcBucket = event.s3Bucket9 // Object key may have spaces or unicode non-ASCII characters.10 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))1112 var params = {13 CollectionId: process.env.REKOGNITION_COLLECTION_ID,14 Image: {15 S3Object: {16 Bucket: srcBucket,17 Name: srcKey,18 },19 },20 FaceMatchThreshold: 70.0,21 MaxFaces: 3,22 }23 rekognition24 .searchFacesByImage(params)25 .promise()26 .then(data => {27 if (data.FaceMatches.length > 0) {28 callback(new FaceAlreadyExistsError())29 } else {30 callback(null, null)31 }32 })33 .catch(err => {34 callback(err)35 })36}3738function FaceAlreadyExistsError() {39 this.name = 'FaceAlreadyExistsError'40 this.message = 'Face in the picture is already in the system. '41}42FaceAlreadyExistsError.prototype = new Error()
Here is the khoale-step-module-resources-ThumbnailFunction lambda
1// dependencies2const AWS = require('aws-sdk')3const gm = require('gm').subClass({imageMagick: true}) // Enable ImageMagick integration.4const util = require('util')5const Promise = require('bluebird')6Promise.promisifyAll(gm.prototype)78// constants9const MAX_WIDTH = process.env.MAX_WIDTH ? process.env.MAX_WIDTH : 25010const MAX_HEIGHT = process.env.MAX_HEIGHT ? process.env.MAX_HEIGHT : 2501112const thumbnailBucket = process.env.THUMBNAIL_BUCKET1314// get reference to S3 client15const s3 = new AWS.S3()1617exports.handler = (event, context, callback) => {18 console.log('Reading input from event:\n', util.inspect(event, {depth: 5}))1920 // get the object from S3 first21 const s3Bucket = event.s3Bucket22 // Object key may have spaces or unicode non-ASCII characters.23 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))24 const getObjectPromise = s325 .getObject({26 Bucket: s3Bucket,27 Key: srcKey,28 })29 .promise()3031 // identify image metadata32 const identifyPromise = new Promise(resolve => {33 getObjectPromise34 .then(getObjectResponse => {35 console.log('success downloading from s3.')36 gm(getObjectResponse.Body)37 .identifyAsync()38 .then(data => {39 console.log(40 'Identified metadata:\n',41 util.inspect(data, {depth: 5}),42 )43 resolve(data)44 })45 .catch(err => {46 callback(err)47 })48 })49 .catch(err => {50 callback(err)51 })52 })5354 // resize the image55 var resizePromise = new Promise(resolve => {56 getObjectPromise57 .then(getObjectResponse => {58 identifyPromise59 .then(identified => {60 const size = identified.size61 const scalingFactor = Math.min(62 MAX_WIDTH / size.width,63 MAX_HEIGHT / size.height,64 )65 const width = scalingFactor * size.width66 const height = scalingFactor * size.height67 gm(getObjectResponse.Body)68 .resize(width, height)69 .toBuffer(identified.format, (err, buffer) => {70 if (err) {71 console.error('failure resizing to ' + width + ' x ' + height)72 callback(err)73 } else {74 console.log('success resizing to ' + width + ' x ' + height)75 resolve(buffer)76 }77 })78 })79 .catch(err => callback(err))80 })81 .catch(function (err) {82 callback(err)83 })84 })8586 // upload the result image back to s387 const destKey = 'resized-' + srcKey8889 resizePromise90 .then(buffer => {91 identifyPromise92 .then(identified => {93 const s3PutParams = {94 Bucket: thumbnailBucket,95 Key: destKey,96 ContentType: 'image/' + identified.format.toLowerCase(),97 }9899 s3PutParams.Body = buffer100 s3.upload(s3PutParams)101 .promise()102 .then(data => {103 delete s3PutParams.Body104 console.log('success uploading to s3:\n ', s3PutParams)105 var thumbnailImage = {}106 thumbnailImage.s3key = destKey107 thumbnailImage.s3bucket = thumbnailBucket108 callback(null, {thumbnail: thumbnailImage})109 })110 .catch(function (err) {111 delete s3PutParams.Body112 console.error('failure uploading to s3:\n ', s3PutParams)113 callback(err)114 })115 })116 .catch(err => {117 callback(err)118 })119 })120 .catch(function (err) {121 callback(err)122 })123}
Here is the khoale-step-module-resources-IndexFaceFunction lambda
1const util = require('util')2const AWS = require('aws-sdk')3const rekognition = new AWS.Rekognition()45exports.handler = (event, context, callback) => {6 const srcBucket = event.s3Bucket7 // Object key may have spaces or unicode non-ASCII characters.8 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))910 var params = {11 CollectionId: process.env.REKOGNITION_COLLECTION_ID,12 DetectionAttributes: [],13 ExternalImageId: event.userId,14 Image: {15 S3Object: {16 Bucket: srcBucket,17 Name: srcKey,18 },19 },20 }21 rekognition22 .indexFaces(params)23 .promise()24 .then(data => {25 callback(null, data['FaceRecords'][0]['Face'])26 })27 .catch(err => {28 callback(err)29 })30}