[レジなし無人販売冷蔵庫] 取り出された商品を列挙する

2021.01.15

1 はじめに

CX事業本部の平内(SIN)です。

Amazon Web Services ブログでは、「レジなし無人販売冷蔵庫の構築についての記事」が公開されています。
レジなし無人販売冷蔵庫を構築できる、This is my Smart Cooler プログラムを公開しました

こちらでは、「お客様自らがレジなし無人販売冷蔵庫を迅速に構築し学習や体験ができる This is my Smart Cooler プログラムを発表します。」ということで、そのレシピがGithubで公開されています。
レジ無し無人販売冷蔵庫 構築レシピ

「これを真似てみたい」ということで、ここまで作業を進めています。

今回は、ドアの開閉をトリガーとし、何が取り出されたかを表示してみました。

2 ドアの開閉検出

前回、以下のように、 BCMモードでGPIO18を使用してドアの開閉を検出しました。

import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BCM)
DOOR_SENSOR_PIN = 18

GPIO.setup(DOOR_SENSOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)

Jetson nanoでも、ピンの位置は、RaspberryPiとピン互換なので、そのまま利用しています。

ただし、プルアップが無いと、ちょっとHEIGHTが安定して取得できなかったので、物理的に追加しました。

3 商品データ

レジなし無人販売冷蔵庫は、各所に複数設置される事をイメージし、商品のマスターデータは、DynamoDBに置きました。

クラスID(id NUMBER)をキーとしたテーブル(SmartCoolerProducts)を作成し、下記のデータを入れています。

$ aws dynamodb put-item --table-name SmartCoolerProducts --item '{"id":{"N": "0"},"ShortName":{"S": "jagabee"},"FullName":{"S": "じゃがビー"},"Price":{"N": "110"}}'
$ aws dynamodb put-item --table-name SmartCoolerProducts --item '{"id":{"N": "1"},"ShortName":{"S": "chipstar"},"FullName":{"S": "チップスター うすしお味"},"Price":{"N": "125"}}'
$ aws dynamodb put-item --table-name SmartCoolerProducts --item '{"id":{"N": "2"},"ShortName":{"S": "butamen"},"FullName":{"S": "ブタメン とんこつ味"},"Price":{"N": "90"}}'
$ aws dynamodb put-item --table-name SmartCoolerProducts --item '{"id":{"N": "3"},"ShortName":{"S": "kyo_udon"},"FullName":{"S": "京うどん"},"Price":{"N": "150"}}'
$ aws dynamodb put-item --table-name SmartCoolerProducts --item '{"id":{"N": "4"},"ShortName":{"S": "koara"},"FullName":{"S": "コアラのマーチ"},"Price":{"N": "120"}}'
$ aws dynamodb put-item --table-name SmartCoolerProducts --item '{"id":{"N": "5"},"ShortName":{"S": "curry"},"FullName":{"S": "カップヌードル カレー"},"Price":{"N": "130"}}'

4 在庫管理

DynamoDBのデータをマスターとし、在庫を管理しているコードです。 setStock()で、在庫を変更し、getStock()で、現在の在庫を取得できるようになっています。

# -*- coding: utf-8 -*-
"""
在庫管理クラス
"""

import boto3
from boto3.session import Session

class InventoryManager():
    def __init__(self, session, region, tableName):
        # 商品一覧
        self.__scan_dynamo_db(session, region, tableName)
        # 在庫数初期化
        self.__stock = [0] * len(self.__products)

    # DynamoDBから商品一覧を取得する
    def __scan_dynamo_db(self, session, region, tableName):
        dynamodb = session.resource('dynamodb', region_name=region)
        table = dynamodb.Table(tableName)
        response = table.scan()

        array = []
        for item in response["Items"]:
            array.append({"id":item["id"], "short_name":item["ShortName"], "full_name":item["FullName"], "price":item["Price"]})

        # idでソート    
        self.__products = sorted(array, key=lambda x:x['id'])

    def __getIndex(self, short_name):
        for i, product in enumerate(self.__products):
            if(product["short_name"] == short_name):
                return i
        return -1

    def getStock(self):
        return self.__stock.copy()

    def setStock(self, short_names):
        self.__stock = [0] * len(self.__products)
        for short_name in short_names:
            index = self.__getIndex(short_name)
            self.__stock[index] += 1

    def get_full_name(self, index):
        return self.__products[index]["full_name"]

    def get_short_name(self, index):
        return self.__products[index]["short_name"]

    def get_price(self, index):
        return int(self.__products[index]["price"])

    @property
    def short_names(self):
        result = []
        for item in self.__products:
            result.append(item["short_name"])
        return result

5 メイン

メインとなるコードは、以下のとおりです。 ドアが閉じられた時点のカメラ画像を推論にかけ、在庫管理クラスを経由して、以前の在庫数との差分を確認しています。

なお、MODE.DELAYは、推論結果を数秒間表示することで、視覚的に確認しているだけのものです。

import os
import sys
import json
import time
import cv2
import numpy as np
from enum import Enum
from cognito import createSession # CognitoのPoolIdでSessionを取得する
from model import Model # DLRによる物体検出モデルの推論クラス
from inventoryManager import InventoryManager # 在庫管理クラス
from video import Video # OpenCVでWebカメラの画像を取得・表示するクラス
from doorSensor import DoorSensor, DOOR_STATUS # センサーをGPIO18とGNDに接続して、ドアの開閉を検出するクラス

# Cognito identity Id
poolId = 'ap-northeast-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# 商品データベース
tableName = "SmartCoolerProducts"
region = 'ap-northeast-1'
# Webカメラのデバイス番号及び、解像度
deviceId = 0 
width = 800
height = 600

# 動作モード
class MODE(Enum):
    IDLE = 0 # 待機中
    INFERNCE = 1 # 推論
    PROCESSING = 2 # 処理
    DELAY = 3 # 

class Main():
    def __init__(self):
        # 動作モード
        self.__mode = MODE.IDLE
        # Session取得
        session = createSession(poolId, region)
        # 在庫管理
        self.__inventoryManager = InventoryManager(session, region, tableName)
        # 商品検出モデル
        self.__model = Model(self.__inventoryManager.short_names, height, width)
        # カメラ映像
        self.__video = Video(deviceId, width, height)
        # ドアの開閉検出
        DoorSensor(self.__on_change_door_status)

        self.__run()

    # 動作モードの変更
    def __setMode(self, mode):
        self.__mode = mode
        print("---------------------------------------")
        print("{}".format(self.__mode))
        print("---------------------------------------")

    # 推論処理
    def __inference(self):
        print("model.inference frame:{}".format(self.__frame.shape))
        (result, self.__frame) = self.__model.inference(self.__frame)
        print("冷蔵庫内の商品  {}".format(result))
        return result

    # ドアの開閉の変化
    def __on_change_door_status(self, status):
        str = "ドアが、開けられました" if status == DOOR_STATUS.OPEN else "ドアが、閉じられました"
        print(str)

        # ドアが閉じられた時
        if(status == DOOR_STATUS.CLOSED):
            # 推論処理へ
            self.__setMode(MODE.INFERNCE)


    # メインループ
    def __run(self):
        while(True):
            # MODE.DELAY中だけ、画像を更新しない
            if(self.__mode != MODE.DELAY):
                self.__frame = self.__video.read()
                if(self.__frame is None):
                    continue

            # 推論処理
            if(self.__mode == MODE.INFERNCE):
                result = self.__inference()
                self.__setMode(MODE.PROCESSING)

            # 購入判定
            if(self.__mode == MODE.PROCESSING):

                # 変更前の在庫
                before_stock = self.__inventoryManager.getStock()
                # 在庫数変更
                self.__inventoryManager.setStock(result)
                # 変更後の在庫
                after_stock = self.__inventoryManager.getStock()

                # 減少分のカウント
                diff = []
                for index in range(len(before_stock)):
                    diff.append(before_stock[index] - after_stock[index])

                anount = 0
                items = []
                for index in range(len(diff)):
                    if(diff[index] != 0):
                        full_name = self.__inventoryManager.get_full_name(index)
                        price = self.__inventoryManager.get_price(index)
                        items.append(" >> {} {}円 {}個".format(full_name, price, diff[index]))
                        anount += price * diff[index]
                if(anount != 0):
                    print("\n >> 取り出された商品は、以下のとおりです\n")
                    for item in items:
                        print(item)
                    print("\n >> 合計{}円\n".format(anount))

                # MODE.DELAYで10秒間待機
                self.__setMode(MODE.DELAY)
                self.__delay_counter = 10


            if(self.__mode == MODE.DELAY):
                print("DELAY {}".format(self.__delay_counter))
                self.__delay_counter -= 1
                time.sleep(1)
                if(self.__delay_counter <= 0):
                    print("待機中")
                    self.__setMode(MODE.IDLE)

            # 画像表示 終了キー'q'が押された場合Falseが返される
            if(self.__video.show(self.__frame) == False):
                break

Main()

6 最後に

今回は、冷蔵庫内の商品の在庫管理をしてみました。取り出した商品が列挙できて、いよいよ無人販売っぽくなってきました。

本稿に関係ないのですが・・・Jetsonとかリレーとか、色々機材が増えてきてカオスになってきたので、台を簡単に作ってみました。ちょっとだけスッキリしました。