This site runs best with JavaScript enabled.

Image Processing Serverless with AWS Step Functions


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.

Image Processing Serverless with AWS Step Functions

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:

cloudformation stack

The Step functions after finished the workshop

Steps functions

The Step functions definition

Steps 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()
4
5exports.handler = (event, context, callback) => {
6 console.log('Reading input from event:\n', util.inspect(event, {depth: 5}))
7
8 const srcBucket = event.s3Bucket
9 // Object key may have spaces or unicode non-ASCII characters.
10 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))
11
12 var params = {
13 Image: {
14 S3Object: {
15 Bucket: srcBucket,
16 Name: srcKey,
17 },
18 },
19 Attributes: ['ALL'],
20 }
21
22 rekognition
23 .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]
43
44 // remove some fields not used in further processing to de-clutter the output.
45 delete detectedFaceDetails['Landmarks']
46
47 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}
64
65function PhotoDoesNotMeetRequirementError(message) {
66 this.name = 'PhotoDoesNotMeetRequirementError'
67 this.message = message
68}
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()
4
5exports.handler = (event, context, callback) => {
6 console.log('Reading input from event:\n', util.inspect(event, {depth: 5}))
7
8 const srcBucket = event.s3Bucket
9 // Object key may have spaces or unicode non-ASCII characters.
10 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))
11
12 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 rekognition
24 .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}
37
38function 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// dependencies
2const 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)
7
8// constants
9const MAX_WIDTH = process.env.MAX_WIDTH ? process.env.MAX_WIDTH : 250
10const MAX_HEIGHT = process.env.MAX_HEIGHT ? process.env.MAX_HEIGHT : 250
11
12const thumbnailBucket = process.env.THUMBNAIL_BUCKET
13
14// get reference to S3 client
15const s3 = new AWS.S3()
16
17exports.handler = (event, context, callback) => {
18 console.log('Reading input from event:\n', util.inspect(event, {depth: 5}))
19
20 // get the object from S3 first
21 const s3Bucket = event.s3Bucket
22 // Object key may have spaces or unicode non-ASCII characters.
23 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))
24 const getObjectPromise = s3
25 .getObject({
26 Bucket: s3Bucket,
27 Key: srcKey,
28 })
29 .promise()
30
31 // identify image metadata
32 const identifyPromise = new Promise(resolve => {
33 getObjectPromise
34 .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 })
53
54 // resize the image
55 var resizePromise = new Promise(resolve => {
56 getObjectPromise
57 .then(getObjectResponse => {
58 identifyPromise
59 .then(identified => {
60 const size = identified.size
61 const scalingFactor = Math.min(
62 MAX_WIDTH / size.width,
63 MAX_HEIGHT / size.height,
64 )
65 const width = scalingFactor * size.width
66 const height = scalingFactor * size.height
67 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 })
85
86 // upload the result image back to s3
87 const destKey = 'resized-' + srcKey
88
89 resizePromise
90 .then(buffer => {
91 identifyPromise
92 .then(identified => {
93 const s3PutParams = {
94 Bucket: thumbnailBucket,
95 Key: destKey,
96 ContentType: 'image/' + identified.format.toLowerCase(),
97 }
98
99 s3PutParams.Body = buffer
100 s3.upload(s3PutParams)
101 .promise()
102 .then(data => {
103 delete s3PutParams.Body
104 console.log('success uploading to s3:\n ', s3PutParams)
105 var thumbnailImage = {}
106 thumbnailImage.s3key = destKey
107 thumbnailImage.s3bucket = thumbnailBucket
108 callback(null, {thumbnail: thumbnailImage})
109 })
110 .catch(function (err) {
111 delete s3PutParams.Body
112 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()
4
5exports.handler = (event, context, callback) => {
6 const srcBucket = event.s3Bucket
7 // Object key may have spaces or unicode non-ASCII characters.
8 const srcKey = decodeURIComponent(event.s3Key.replace(/\+/g, ' '))
9
10 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 rekognition
22 .indexFaces(params)
23 .promise()
24 .then(data => {
25 callback(null, data['FaceRecords'][0]['Face'])
26 })
27 .catch(err => {
28 callback(err)
29 })
30}

Discuss on TwitterEdit post on GitHub

Share article
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.