LoginSignup
0
0

More than 3 years have passed since last update.

サーバレスでImage Searchに画像登録を行うアプリを作ってみる

Last updated at Posted at 2020-06-09

構成

以前調査した、Image Searchの画像登録をサーバレス構成で作ってみようと思います。フロントエンドは静的コンテンツ(HTML5)で作成し、OSSの静的 Web サイトホスティング機能を使って公開します。サーバサイドはFunction ComputeのHTTPトリガーを使って実装し、Image Searchに画像を登録します。

01.png

※Image Searchについては以前作成したものを使うので、設定については割愛します。

Function Computeの設定

それでは、まずサーバサイドから作成します。

01.png
02.png
03.png

HTTP関数のテンプレートを使用します

04.png
05.png
06.png

サンプルを実行してみます

07.png
08.png

環境変数の作成

Image Searchへ登録するために環境に依存する設定を環境変数として設定します。

10.png
11.png
12.png
13.png
14.png

Functionsの作成

それでは、Node.jsを使って関数を作成します。

プロジェクトの作成

PS C:\Users\user\Documents\temp> mkdir imgsearch-func


    Directory: C:\Users\user\Documents\temp

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/05/30    16:33                imgsearch-func
PS C:\Users\user\Documents\temp> cd .\imgsearch-func\
PS C:\Users\user\Documents\temp\imgsearch-func> yarn init
yarn init v1.22.4
question name (imgsearch-func):
question version (1.0.0):
question description:
question entry point (index.js):
question repository url:
question author: 
question license (MIT):
question private:
success Saved package.json
Done in 12.97s.

依存関係を追加

PS C:\Users\user\Documents\temp\imgsearch-func> yarn add @alicloud/pop-core
yarn add v1.22.4
info No lockfile found.
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Saved lockfile.
success Saved 10 new dependencies.
info Direct dependencies
└─ @alicloud/pop-core@1.7.9
info All dependencies
├─ @alicloud/pop-core@1.7.9
├─ @types/node@12.12.42
├─ bignumber.js@4.1.0
├─ debug@3.2.6
├─ httpx@2.2.3
├─ json-bigint@0.2.3
├─ kitx@1.3.0
├─ sax@1.2.4
├─ xml2js@0.4.23
└─ xmlbuilder@11.0.1
Done in 1.54s.
PS C:\Users\user\Documents\temp\imgsearch-func> yarn add @alicloud/imagesearch-2019-03-25
yarn add v1.22.4
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ @alicloud/imagesearch-2019-03-25@1.0.0
info All dependencies
└─ @alicloud/imagesearch-2019-03-25@1.0.0
Done in 1.62s.
PS C:\Users\user\Documents\temp\imgsearch-func> yarn add json-to-form-data
yarn add v1.22.4
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ json-to-form-data@1.0.0
info All dependencies
└─ json-to-form-data@1.0.0
Done in 1.17s.

プログラムを作成

index.js
'use strict';

const Client = require('@alicloud/imagesearch-2019-03-25')
const jtfd = require('json-to-form-data')
const getRawBody = require('raw-body')
const body = require('body')

//Function
exports.handler = (req, resp, context) => {
  console.log('invoke functions.')

  //ボディからデータ抽出
  getRawBody(req, (err, body) => {
    const param = JSON.parse(body.toString())
    //console.log("Boyd Param:", param)

    const productId = param.ProductId
    const picName = param.PicName
    const categoryId = param.CategoryId
    const intAttr = param.IntAttr
    const strAttr = param.StrAttr
    const customContent = param.CustomContent
    const picContent = param.PicContent

    //値のチェック
    console.log("ProductID:", productId)
    console.log("PicName:", picName)
    console.log("CategoryId", categoryId)
    console.log("IntAttr", intAttr)
    console.log("StrAttr", strAttr)
    console.log("CustomContent,", customContent)
    //console.log("PicContent", picContent)

    //値を設定
    const instanceName = process.env['INSTANCE_NAME']
    const addRequest = {
      InstanceName: instanceName,
      ProductId: productId,
      PicName: picName,
      PicContent: picContent
    }
    if (categoryId) {
      addRequest.CategoryId = categoryId
    }
    if (intAttr) {
      addRequest.IntAttr = +intAttr
    }
    if (strAttr) {
      addRequest.StrAttr = strAttr
    }
    if (customContent) {
      addRequest.CustomContent = customContent
    }
    //console.log("addRequest:", addRequest)

    //Image Searchに登録
    const accessKey = process.env['ACCESS_KEY']
    const seacretKey = process.env['SEACRET_KEY']
    const endpoint = process.env['ENDPOINT']
    console.log("accessKey:",accessKey)
    console.log("seacretKey:",seacretKey)
    console.log("endpoint:",endpoint)

    const client = new Client({
      accessKeyId: accessKey,
      accessKeySecret: seacretKey,
      endpoint: endpoint,
      apiVersion: "2019-03-25"
    })
    const options = {
      method: 'POST',
      "Content-Type": 'application/x-www-form-urlencoded; charset=UTF-8'
    }
    const addData = jtfd(addRequest)
    client.addImage(addData, options).then(value => {
      const responseJson = JSON.stringify(value)
      console.log("Result:", responseJson)
      resp.send(responseJson)
    }).catch(err => {
      console.log("Error Message: ", err)
      resp.send(JSON.stringify(err, null, 4))
    })
  })

}

ソースコード解説

関数ハンドラに index.handlerを設定しました。index.js のexport.handlerが呼び出されます。

index.js
exports.handler = (req, resp, context) => {
}

今回は、BodyにJSONを設定してリクエストを行うようにするので、getRawBody()を使用します。

index.js
getRawBody(req, (err, body) => {
}

プロジェクトをアップロード(デプロイ)

今回はZIPアーカイブしてアップロードを行います。

プロジェクトをZIPで固める

PS C:\Users\user> cd .\Documents\temp\imgsearch-func\
PS C:\Users\user\Documents\temp\imgsearch-func> ls
Directory: C:\Users\user\Documents\temp\imgsearch-func
 Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/05/30    16:39                node_modules
-a---          2020/06/08    16:16           2545 index.js
-a---          2020/05/30    16:39            278 package.json
-a---          2020/05/30    16:39           4157 yarn.lock
-a---          2020/05/30    16:49           6575 yarn-error.log

PS C:\Users\user\Documents\temp\imgsearch-func> Compress-Archive -Path * -DestinationPath functions.zip -Force
PS C:\Users\user\Documents\temp\imgsearch-func> ls


    Directory: C:\Users\user\Documents\temp\imgsearch-func

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d----          2020/05/30    16:39                node_modules
-a---          2020/06/09    17:16         395892 functions.zip
-a---          2020/06/08    16:16           2545 index.js
-a---          2020/05/30    16:39            278 package.json
-a---          2020/05/30    16:39           4157 yarn.lock
-a---          2020/05/30    16:49           6575 yarn-error.log

PS C:\Users\user\Documents\temp\imgsearch-func>

アップロード

20.png
21.png
22.png
23.png
24.png

以上で、サーバサイドの構築は完了です。

OSSの設定

続いて、フロントエンドのデプロイを行います。フロントエンドはOSSの静的 Web サイトホスティング機能を使います。

30.png
31.png
32.png
33.png
34.png
35.png
36.png
37.png
38.png

フロントエンドの作成

index.html
<!DOCTYPE html>
<html>

<head>
    <title>Image Search Add Sample</title>
    <meta charset="utf-8">
    <style type="text/css" scoped>
        ul li {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            background-color: #EEEEEE;
            margin: 2px;
            padding: 3px;
        }

        ul span:first-of-type {
            flex-basis: 150px;
        }

        ul span:nth-of-type(2) {
            flex-basis: 200px;
            flex-grow: 1;
        }

        ul input[type="text"],
        ul textarea,
        ul select,
        ul input[type="number"] {
            width: 100%;
            box-sizing: border-box;
        }

        #pos {
            border: 3px solid;
            border-color: red;
            position: absolute;
            display: none;
        }
    </style>
</head>

<body>
    <h1>Image Search登録</h1>
    <div>
        <div id="errormsg" style="color:red"></div>
        <ul style="width:800px">
            <li>
                <span><label for="productId">*productId</label></span>
                <span><input type="text" id="productId" maxlength="512" required></span>
            </li>
            <li>
                <span><label for="picName">*picName</label></span>
                <span><input type="text" id="picName" maxlength="512" required></span>
            </li>
            <li>
                <span><label for="categoryId">categoryId</label></span>
                <span><select id="categoryId">
                        <option value="">選択なし</option>
                        <option value="0">トップス</option>
                        <option value="1">ドレス</option>
                        <option value="2">ボトムズ</option>
                        <option value="3">バッグ</option>
                        <option value="4">シューズ</option>
                        <option value="5">アクセサリー</option>
                        <option value="6">スナック</option>
                        <option value="7">メイクアップ</option>
                        <option value="8">ボトルドリンク</option>
                        <option value="9">家具</option>
                        <option value="20">おもちゃ</option>
                        <option value="21">下着</option>
                        <option value="22">デジタル機器</option>
                        <option value="88888888">その他</option>
                    </select></span>
            </li>
            <li>
                <span><label for="intAttr">intAttr</label></span>
                <span><input type="number" id="intAttr" min="0" max="2147483647"></span>
            </li>
            <li>
                <span><label for="strAttr">strAttr</label></span>
                <span><input type="text" id="strAttr" maxlength="128"></span>
            </li>
            <li>
                <span><label for="customContent">customContent</label></span>
                <span><textarea type="text" id="customContent" maxlength="4096" cols="30" rows="5"></textarea></span>
            </li>
            <li>
                <span><label for="picContent">*PicContent : </label></span>
                <span><input type="file" id="picContent" accept="image/jpeg,image/png" required></span>
            </li>
            <li>
                <span>実行</span>
                <span><input type="button" value="登録" id="addImage"><input type="reset" id="clear" value="リセット"></span>
            </li>
            <li>
                <span><label for="postResult">登録結果</label></span>
                <span><textarea id="postResult" cols="100" rows="20" readonly></textarea></span>
            </li>
            <li>
                <span>プレビュー</span>
                <div style="position: relative;">
                    <span><img id="preview"></span>
                    <div id="pos"></div>
                </div>
            </li>
        </ul>
    </div>

    <script>
        //イベントの登録
        document.addEventListener('DOMContentLoaded', () => {
            const productId = document.getElementById('productId')
            const picName = document.getElementById('picName')
            const categoryId = document.getElementById('categoryId')
            const intAttr = document.getElementById('intAttr')
            const strAttr = document.getElementById('strAttr')
            const customContent = document.getElementById('customContent')
            const inputFile = document.getElementById('picContent')
            const preview = document.getElementById('preview')
            const addImage = document.getElementById('addImage')
            const clearbtn = document.getElementById('clear')
            const postResult = document.getElementById('postResult')
            const errormsg = document.getElementById('errormsg')
            const imgpos = document.getElementById('pos')

            // Fileが選択された時のイベントを登録
            inputFile.addEventListener('change', e => {
                console.log(e)
                errormsg.textContent = null
                postResult.value = ""
                imgpos.style.display = "none"

                //プレビューに画像を表示
                const files = e.target.files
                if (files.length) {
                    const file = files[0]
                    const reader = new FileReader()
                    reader.onload = e => {
                        preview.src = e.target.result
                    }
                    reader.readAsDataURL(file)
                }
            }, false)

            //登録ボタンが押された時のイベントを登録
            addImage.addEventListener('click', e => {
                console.log(e)
                errormsg.textContent = null
                postResult.value = ""
                imgpos.style.display = "none"

                const width = preview.naturalWidth
                const height = preview.naturalHeight

                //必須チェック
                if (productId.value === '' || picName.value === '' || picContent.value === '') {
                    errormsg.append('必須入力項目が未入力です。')
                    return false
                }
                if (width > 2000 || height > 2000) {
                    errormsg.append('画像サイズは縦横2,000pixel以内でお願いします。')
                    return false
                }

                console.log('Ajax実行!')
                fetch('https://xxxxxx.ap-northeast-1.fc.aliyuncs.com/2016-08-15/proxy/fn-imgsearch-add/fn-imgsearch-add/', {
                    method: 'POST',
                    headers: {
                        "Content-Type": "application/json; charset=utf-8"
                    },
                    body: JSON.stringify({
                        "ProductId": productId.value,
                        "PicName": picName.value,
                        "CategoryId": categoryId.value,
                        "IntAttr": intAttr.value,
                        "StrAttr": strAttr.value,
                        "CustomContent": customContent.value,
                        "PicContent": preview.src.replace(/^data:image\/jpeg;base64,/, '').replace(/^data:image\/png;base64,/, '')
                    })
                })
                    .then((response) => {
                        if (response.status === 200) {
                            return response.json()
                        } else {
                            console.log('Bad')
                        }
                    })
                    .then((obj) => {
                        console.log(JSON.stringify(obj, null, 4))
                        postResult.value = JSON.stringify(obj, null, 4)
                        marker(obj.PicInfo.Region)
                    })
                    .catch((err) => {
                        console.log('Error Occurred')
                        console.log(err)
                        errormsg.append(err)
                    })
            }, false)

            //クリアボタンを押された時のイベントを登録
            clearbtn.addEventListener('click', e => {
                console.log(e)
                productId.value = ""
                picName.value = ""
                categoryId.value = ""
                intAttr.value = ""
                strAttr.value = ""
                customContent.value = ""
                inputFile.value = ""
                postResult.value = ""
                preview.src = ""
                errormsg.textContent = null
                imgpos.style.display = "none"
            }, false)

        }, false)

        // 被写体認識結果をマーキング
        function marker(region) {
            const [x1, x2, y1, y2]  = region.split(',')
            const imgpos = document.getElementById('pos')
            const width = +x2 - +x1
            const height = +y2 - +y1

            imgpos.style.top = y1 + "px"
            imgpos.style.left = x1 + "px"
            imgpos.style.width = width + "px"
            imgpos.style.height = height + "px"
            imgpos.style.display = "inherit"
        }
    </script>
</body>

</html>

解説

xxx.js
fetch('https://xxxxxx.ap-northeast-1.fc.aliyuncs.com/2016-08-15/proxy/fn-imgsearch-add/fn-imgsearch-add/', {
    method: 'POST',
    headers: {
        "Content-Type": "application/json; charset=utf-8"
    },
    body: JSON.stringify({
        "ProductId": productId.value,
        "PicName": picName.value,
        "CategoryId": categoryId.value,
        "IntAttr": intAttr.value,
        "StrAttr": strAttr.value,
        "CustomContent": customContent.value,
        "PicContent": preview.src.replace(/^data:image\/jpeg;base64,/, '').replace(/^data:image\/png;base64,/, '')
    })
})
    .then((response) => {
        if (response.status === 200) {
            return response.json()
        } else {
            console.log('Bad')
        }
    })
    .then((obj) => {
        console.log(JSON.stringify(obj, null, 4))
        postResult.value = JSON.stringify(obj, null, 4)
        marker(obj.PicInfo.Region)
    })
    .catch((err) => {
        console.log('Error Occurred')
        console.log(err)
        errormsg.append(err)
    })

fetchの部分が、Function Computeに対してリクエストをしている部分です。入力されたパラメータをJSON形式にして、bodyに設定します。他にもFormDataにする方法などいろいろとやり方はありますが、今回はこのようにしました。

戻り値には登録した画像のカテゴリーと、物体検出の結果の座標が返されるのでプレビューに表示してみました。商品以外の製品が乗っている画像を登録する場合、どの部分が物体検出されるかによって精度が変わるので、チューニングの際に見ておきたい部分ですね。

ファイルのアップロード

39.png

実行

index.htmlのパスを確認してアクセスしてみます。
40.png
41.png
42.png
43.png

無事に実行する事ができました!

まとめ

今回は、サーバレス構成でアプリを作ってみました。例外処理やバリデーションなどは割愛しています。。。次回はAPI Gatewayを使ってみようかと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0