見出し画像

【AWS AI/MLを触る】Rekognitionを使った画像分析システムを作ってみる

はじめに

先日、Rekognitionを使った画像分析の勉強会を執り行う機会がありまして、その中で作ったシステムを備忘として残したいと思います。
以下の構成をCDKで作りました。

システム動作

画像に写った人物の感情を分析させるシステムになります。
簡単ではありますが、動作は以下のような流れになります。
① 画像(png)をS3にアップロードする
② S3にアップロードされたことをトリガーにLambdaが起動
③ Rekognitionにて画像の分析
④ Rekognitionが画像分析結果を返す
⑤ 画像分析結果(json)をS3に保管


CDK準備

LambdaとS3バケットをCDKで作ります。
CDKのバージョンは2です。cdk bootstrap済みを想定しています。

$ mkdir rekognition && cd rekognition

$ cdk init --language typescript

AWS CDK (Typescript)

/lib/rekognition-stack.ts

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as destinations from 'aws-cdk-lib/aws-s3-notifications';

export class RekognitionStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda用IAMロールの作成
    const sampleRole = new iam.Role(this, 'SampleRole', {
        roleName: 'sample-role',
        assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
        managedPolicies: [
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            'service-role/AWSLambdaBasicExecutionRole'
          ),
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            'AmazonRekognitionFullAccess'
          ),
          iam.ManagedPolicy.fromAwsManagedPolicyName(
            'AmazonS3FullAccess'
          )
        ],
      }
    );
    
    // Lambda関数の作成
    const sampleLambdaFunction = new lambda.Function(this, 'SampleLambdaFunction', {
      functionName: 'sample-lambda-function',
      runtime: lambda.Runtime.PYTHON_3_9,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'sample.handler',
      role: sampleRole,
      timeout: cdk.Duration.seconds(30),      
    });

    // インプット用S3バケット作成
    const bucket = new s3.Bucket(this, 'SampleBucket', {
      bucketName: 'input-bucket-1234567890',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
    
    // S3用のLambdaイベントソースの作成
    bucket.addEventNotification(s3.EventType.OBJECT_CREATED_PUT, 
      new destinations.LambdaDestination(sampleLambdaFunction)
    );

    // アウトプット用S3バケット作成
    const output_bucket = new s3.Bucket(this, 'OutputBucket', {
      bucketName: 'output-bucket-1234567890',
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });
  }
}

CDKで作る主なリソースは、S3バケットとLambda関数だけです。
 (厳密にはLamnda用のiamロールも作りますが。。)
S3バケット名は適宜変更してください。

/bin/rekognition.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { RekognitionStack } from '../lib/rekognition-stack';

const app = new cdk.App();
new RekognitionStack(app, 'RekognitionStack', {});


Lambda関数用コード(Python)

インプット用S3バケットに画像が置かれたことをトリガーに起動します。Rekognitionで画像分析し、分析後、結果をアウトプット用S3バケットに出力(json)させます。
「lambda」フォルダを作成し、sample.pyというファイル名にしてます。

/lambda/sample.py

import json
import boto3
from botocore.exceptions import ClientError

def handler(event, context):
    try:
        # S3イベントからバケット名とオブジェクトキーを取得
        bucket = event['Records'][0]['s3']['bucket']['name']
        key = event['Records'][0]['s3']['object']['key']

        # AWS Rekognitionクライアントの初期化
        rekognition = boto3.client('rekognition')

        # 画像の感情分析を実行
        response = rekognition.detect_faces(
            Image={
                'S3Object': {
                    'Bucket': bucket,
                    'Name': key
                }
            },
            Attributes=['ALL']
        )

        # 分析結果をアウトプット用S3バケットに保存
        output_bucket = 'output-bucket-1234567890'
        output_key = f'{key}_emotion_analysis.json'
        s3 = boto3.client('s3')
        s3.put_object(Bucket=output_bucket, Key=output_key, Body=json.dumps(response, indent=2))

        
        return {
            'statusCode': 200,
            'body': json.dumps('Emotion analysis complete. Result saved to S3.')
        }
    
    except ClientError as e:
        # エラーログを出力
        print(f"Error: {e}")
        # エラーレスポンスを返す
        return {
            'statusCode': 500,
            'body': json.dumps('Error occurred while processing the image.')
        }
    
    except Exception as e:
        # 予期しないエラーの場合の処理
        print(f"Unexpected Error: {e}")
        # エラーレスポンスを返す
        return {
            'statusCode': 500,
            'body': json.dumps('An unexpected error occurred.')
        }

Rekognitionの機能としては、DetectFaces(顔の検出)を使います。
画像に写った人物の感情を分析する機能です。参考リンク

CDKデプロイ

$ cdk synth
$ cdk deploy


用意する画像(分析用のサンプル)

AWSのインフラが出来上がったので、実際に画像をアップロードします。
人物画像を適宜用意いただければよいと思います。
複数人よりも一人で写っている画像がおすすめです。


分析結果

画像をアップしてから、だいたい2〜3秒でアウトプットされます。アウトプット用S3バケット内にjsonが出力されます。
以下のような感じで分析結果が出力されます。「"Emotions"」欄が感情を分析した結果ですね。

{
  "FaceDetails": [
    {
      "BoundingBox": {
        "Width": 0.1388738751411438,
        "Height": 0.2707822322845459,
        "Left": 0.4184369146823883,
        "Top": 0.12376245856285095
      },
      "AgeRange": {
        "Low": 18,
        "High": 24
      },
      "Smile": {
        "Value": true,
        "Confidence": 95.90258026123047
      },
      "Eyeglasses": {
        "Value": false,
        "Confidence": 97.18228912353516
      },
      "Sunglasses": {
        "Value": false,
        "Confidence": 99.99649810791016
      },
      "Gender": {
        "Value": "Male",
        "Confidence": 99.97742462158203
      },
      "Beard": {
        "Value": false,
        "Confidence": 70.18409729003906
      },
      "Mustache": {
        "Value": false,
        "Confidence": 97.23899841308594
      },
      "EyesOpen": {
        "Value": true,
        "Confidence": 97.10236358642578
      },
      "MouthOpen": {
        "Value": true,
        "Confidence": 93.80290985107422
      },
      "Emotions": [
        {
          "Type": "HAPPY",
          "Confidence": 99.21674346923828
        },
        {
          "Type": "SURPRISED",
          "Confidence": 6.298411846160889
        },
        {
          "Type": "FEAR",
          "Confidence": 5.892993450164795
        },
        {
          "Type": "SAD",
          "Confidence": 2.1610665321350098
        },
        {
          "Type": "DISGUSTED",
          "Confidence": 0.21210378408432007
        },
        {
          "Type": "CONFUSED",
          "Confidence": 0.1954614818096161
        },
        {
          "Type": "ANGRY",
          "Confidence": 0.11843512952327728
        },
        {
          "Type": "CALM",
          "Confidence": 0.06870687752962112
        }
      ],
      "Landmarks": [
        {
          "Type": "eyeLeft",
          "X": 0.4593801498413086,
          "Y": 0.22395740449428558
        },
        {
          "Type": "eyeRight",
          "X": 0.5207691192626953,
          "Y": 0.2255735546350479
        },
        {
          "Type": "mouthLeft",
          "X": 0.46423202753067017,
          "Y": 0.31806373596191406
        },
        {
          "Type": "mouthRight",
          "X": 0.5154861807823181,
          "Y": 0.31936246156692505
        },
        {
          "Type": "nose",
          "X": 0.49261409044265747,
          "Y": 0.2715451121330261
        },
        {
          "Type": "leftEyeBrowLeft",
          "X": 0.43555015325546265,
          "Y": 0.20275960862636566
        },
        {
          "Type": "leftEyeBrowRight",
          "X": 0.4731578230857849,
          "Y": 0.19498573243618011
        },
        {
          "Type": "leftEyeBrowUp",
          "X": 0.4549233913421631,
          "Y": 0.19020229578018188
        },
        {
          "Type": "rightEyeBrowLeft",
          "X": 0.5082212686538696,
          "Y": 0.19588841497898102
        },
        {
          "Type": "rightEyeBrowRight",
          "X": 0.5419886112213135,
          "Y": 0.20536167919635773
        },
        {
          "Type": "rightEyeBrowUp",
          "X": 0.5255008339881897,
          "Y": 0.19198313355445862
        },
        {
          "Type": "leftEyeLeft",
          "X": 0.4480724036693573,
          "Y": 0.22369661927223206
        },
        {
          "Type": "leftEyeRight",
          "X": 0.4714200496673584,
          "Y": 0.22514207661151886
        },
        {
          "Type": "leftEyeUp",
          "X": 0.4593241214752197,
          "Y": 0.21902118623256683
        },
        {
          "Type": "leftEyeDown",
          "X": 0.45959344506263733,
          "Y": 0.2281319797039032
        },
        {
          "Type": "rightEyeLeft",
          "X": 0.508468508720398,
          "Y": 0.22611485421657562
        },
        {
          "Type": "rightEyeRight",
          "X": 0.5311418771743774,
          "Y": 0.2257973700761795
        },
        {
          "Type": "rightEyeUp",
          "X": 0.5209113359451294,
          "Y": 0.22061452269554138
        },
        {
          "Type": "rightEyeDown",
          "X": 0.5203281044960022,
          "Y": 0.2296731024980545
        },
        {
          "Type": "noseLeft",
          "X": 0.4791621267795563,
          "Y": 0.28386303782463074
        },
        {
          "Type": "noseRight",
          "X": 0.5017746090888977,
          "Y": 0.2843814492225647
        },
        {
          "Type": "mouthUp",
          "X": 0.4906582534313202,
          "Y": 0.30617186427116394
        },
        {
          "Type": "mouthDown",
          "X": 0.490119606256485,
          "Y": 0.33509960770606995
        },
        {
          "Type": "leftPupil",
          "X": 0.4593801498413086,
          "Y": 0.22395740449428558
        },
        {
          "Type": "rightPupil",
          "X": 0.5207691192626953,
          "Y": 0.2255735546350479
        },
        {
          "Type": "upperJawlineLeft",
          "X": 0.41870230436325073,
          "Y": 0.22970785200595856
        },
        {
          "Type": "midJawlineLeft",
          "X": 0.4314973056316376,
          "Y": 0.33156853914260864
        },
        {
          "Type": "chinBottom",
          "X": 0.48869869112968445,
          "Y": 0.3854958415031433
        },
        {
          "Type": "midJawlineRight",
          "X": 0.5394987463951111,
          "Y": 0.3339669704437256
        },
        {
          "Type": "upperJawlineRight",
          "X": 0.5520328283309937,
          "Y": 0.2328670173883438
        }
      ],
      "Pose": {
        "Roll": 0.9546359777450562,
        "Yaw": 2.386444091796875,
        "Pitch": 11.122673988342285
      },
      "Quality": {
        "Brightness": 92.34352111816406,
        "Sharpness": 94.08262634277344
      },
      "Confidence": 99.99970245361328
    }
  ],
  "ResponseMetadata": {
    "RequestId": "f1234-1234-1234-1234",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "x-amzn-requestid": "f1234-1234-1234-1234",
      "content-type": "application/x-amz-json-1.1",
      "content-length": "3518",
      "date": "Thu, 20 Jul 2023 01:30:23 GMT"
    },
    "RetryAttempts": 0
  }
}

この仕組みをさらに発展させれば、様々な場面で応用ができそうですね。


まとめ

AWS AI/MLの勉強会でRekognitionを使った画像分析システムを講義する機会がありました。講義の中で実際に構築したシステムを簡単ではありますが解説しました。この経験を個人的な備忘として記録しておこうと思います。


おまけ

特定の感情でフィルタリングするPythonコード。
'HAPPY'の感情が90以上検出された場合に、ログとして出力されます。

import json
import boto3
from botocore.exceptions import ClientError

def lambda_handler(event, context):
    try:
        # S3イベントからバケット名とオブジェクトキーを取得
        bucket = event['Records'][0]['s3']['bucket']['name']
        key = event['Records'][0]['s3']['object']['key']

        # AWS Rekognitionクライアントの初期化
        rekognition = boto3.client('rekognition')

        # 画像の感情分析を実行
        response = rekognition.detect_faces(
            Image={
                'S3Object': {
                    'Bucket': bucket,
                    'Name': key
                }
            },
            Attributes=['ALL']
        )

        # 特定の感情を抽出
        target_emotion = 'HAPPY'
        for face_detail in response['FaceDetails']:
            for emotion, confidence in face_detail['Emotions']:
                if emotion['Type'] == target_emotion and emotion['Confidence'] >= 90:
                    print(f"Happy face detected with confidence: {emotion['Confidence']}")

        # 分析結果をS3バケットに保存
        output_bucket = 'output-bucket-1234567890'
        output_key = f'{key}_emotion_analysis.json'
        s3 = boto3.client('s3')
        s3.put_object(Bucket=output_bucket, Key=output_key, Body=json.dumps(response, indent=2))

        return {
            'statusCode': 200,
            'body': json.dumps('Emotion analysis complete. Result saved to S3.')
        }
    
    except ClientError as e:
        # エラーログを出力
        print(f"Error: {e}")
        # エラーレスポンスを返す
        return {
            'statusCode': 500,
            'body': json.dumps('Error occurred while processing the image.')
        }
    
    except Exception as e:
        # 予期しないエラーの場合の処理
        print(f"Unexpected Error: {e}")
        # エラーレスポンスを返す
        return {
            'statusCode': 500,
            'body': json.dumps('An unexpected error occurred.')
        }


参考


この記事が気に入ったらサポートをしてみませんか?