はじめに
LINE DC Advent Calendar 2023 の7日目の記事です。
柿ピーの画像を見て「柿の種」と「ピーナッツ」の比率を当てるクイズゲームチャットボットを作ったので、その紹介をします。
これは #ヒーローズリーグ 2023 【決勝進出権有】 ハイブリット・ハッカソン にて作ったものを改良したものです。
作品ページ : https://protopedia.net/prototype/4715
亀田製菓の柿の種の「柿の種:ピーナッツ」比率は、以前は6:4でしたが、現在は7:3だそうです (参考)
数値を知ったら「そうなんだー」って感じですが、食べる時に何対何なのか考えたことはありますか?
これをゲームにしてみたら面白そうだと思い、この比率当てゲームを考えました。
作ったもの
こんな感じです。
ソースコード
構成
バックエンドはAWS Lambda、DynamoDB、S3で作ってます。
デプロイにはServerless Frameworkを使いました。
OpenCVを使っているので、コンテナイメージでLambda 関数をデプロイしてます。
解説
- 言語: Python
- フレームワーク: Powertools for AWS Lambda
柿ピー画像生成部
このゲームの核となる、柿ピー画像生成部についてです。
まず、柿の種とピーナッツ画像を用意します。実際にそれぞれ写真を撮って切り抜きました。
これらの画像を使用し、OpenCVで任意の比率の柿ピー画像を作ります。
1000✕1000の白画像の上に、埋め尽くすように、かつ、場所・角度をランダムさせて配置します。
import cv2
import numpy as np
import random
# 画像にシャドウを追加する関数
def add_shadow(image):
alpha = image[:, :, 3]/255
alpha = alpha[:, :, np.newaxis] # alphaを3次元配列に変換
shadow = cv2.GaussianBlur(image[:, :, :3], (15, 15), 0)
image[:, :, :3] = alpha * image[:, :, :3] + (1 - alpha) * shadow
return image
# 画像をランダムに回転させる関数
def rotate_image(image):
angle = random.randint(0,360)
angle_rad = angle/180.0*np.pi
h, w = image.shape[:2]
# 回転後の画像サイズを計算
w_rot = int(np.round(h*np.absolute(np.sin(angle_rad))+w*np.absolute(np.cos(angle_rad))))
h_rot = int(np.round(h*np.absolute(np.cos(angle_rad))+w*np.absolute(np.sin(angle_rad))))
size_rot = (w_rot, h_rot)
# 元画像の中心を軸に回転する
center = (w/2, h/2)
scale = 1.0
rotation_matrix = cv2.getRotationMatrix2D(center, angle, scale)
# 平行移動を加える (rotation + translation)
affine_matrix = rotation_matrix.copy()
affine_matrix[0][2] = affine_matrix[0][2] -w/2 + w_rot/2
affine_matrix[1][2] = affine_matrix[1][2] -h/2 + h_rot/2
rotated_image = cv2.warpAffine(image, affine_matrix, size_rot)
return rotated_image
# 透過画像を背景に配置する関数
def place_image(background, img, x, y):
#img = rotate_image(img) # 画像回転
h, w, _ = img.shape
alpha = img[:, :, 3]/255
for c in range(3):
background[y:y+h, x:x+w, c] = alpha * img[:, :, c] + (1 - alpha) * background[y:y+h, x:x+w, c]
return background
# 画像をランダムに配置する関数
def place_images_randomly(background, img, n):
bh, bw, _ = background.shape
for _ in range(n):
img_rot = rotate_image(img)
h_rot, w_rot, _ = img_rot.shape
x = random.randint(0, bw - w_rot)
y = random.randint(0, bh - h_rot)
background = place_image(background, img_rot, x, y)
return background
def create_kakinotane_image(ratio: float,
output_file_name: str = "output.jpg",
kakinotane_img_file_name: str = "assets/kakinotane_photo.png",
peanut_img_file_name: str = "assets/peanut_photo.png",
):
# 画像の読み込み
img_a = cv2.imread(kakinotane_img_file_name, cv2.IMREAD_UNCHANGED)
img_b = cv2.imread(peanut_img_file_name, cv2.IMREAD_UNCHANGED)
# 画像のリサイズ
img_a = cv2.resize(img_a, (img_a.shape[1] // 4, img_a.shape[0] // 4))
img_b = cv2.resize(img_b, (img_b.shape[1] // 4, img_b.shape[0] // 4))
# 白背景画像の作成
background = np.ones((1000, 1000, 3), np.uint8) * 255
# 配置する回数を計算
n_total = (background.shape[0]//img_a.shape[0]) * (background.shape[1]//img_a.shape[1])
# 画像aとbの配置比率を設定
ratio_a = ratio
ratio_b = 1 - ratio_a
# 配置する回数を計算
n_a = round(n_total * ratio_a)
n_b = n_total - n_a
# 画像をランダムに配置
for i in range(3):
# N回する。
background = place_images_randomly(background, img_a, n_a)
background = place_images_randomly(background, img_b, n_b)
cv2.imwrite(output_file_name, background)
柿の種 : ピーナッツ = 7 : 3で生成したものがこちら。
生成した画像を、パブリックアクセスを許可しているS3バケットにアップロードし、そのURLをLINEに返します。
選択肢の作成
難易度ごとに、3択をランダムに作ります。
パラメータ
-
step
: 数値のスケール (10%ごと・5%ごとなど) -
select_range
: 数値のレンジ (中央の値からどれだけ離れた値をあと2つ作るか)
def create_selections(step: int, select_range: int = 20) -> tuple:
percent_1 = random.randrange(MIN_PERCENT + step * 2, MAX_PERCENT - step * 2, step)
if percent_1 + select_range >= MAX_PERCENT:
percent_2 = percent_1 + random.randrange(0, MAX_PERCENT - percent_1, step) + step
else:
percent_2 = percent_1 + random.randrange(0, select_range, step) + step
if percent_1 - select_range <= MIN_PERCENT:
percent_3 = percent_1 - random.randrange(0, percent_1 - MIN_PERCENT, step) - step
else:
percent_3 = percent_1 - random.randrange(0, select_range, step) - step
return percent_1, percent_2, percent_3
クイックリプライにて難易度 (初級・中級・上級) を選んで、その難易度に応じた選択肢を3つ作ります。
そのうち、正解とする選択肢をランダムで選んで、その比率に応じた柿ピー画像を生成します。
# select level
if message_text == "初級":
step = 10
sct_range = 20
elif message_text == "中級":
step = 5
sct_range = 15
elif message_text == "上級":
step = 1
sct_range = 10
else:
step = 5
sct_range = 15
# create selections
percent_1, percent_2, percent_3 = create_selections(step, sct_range)
correct_percent = random.choice([percent_1, percent_2, percent_3])
正解の選択肢と画像はユーザーごとに持っており、正解はDynamoDBに格納して答え合わせします。
def put_items(table_name: str, items: List[dict]):
"""
Put items from DynamoDB table
"""
table = boto3.resource("dynamodb").Table(table_name)
with table.batch_writer() as writer:
try:
for item in items:
_params = dict()
_params["Item"] = json.loads(json.dumps(item), parse_float=Decimal)
table.put_item(**_params)
except botocore.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
# 条件NG
pass
else:
raise
# put database
data = {
"user_id": event.source.user_id,
"correct_ans": correct_ans,
}
dynamodb.put_item(table_name, data)
その他のコードはリポジトリをご覧ください。
まとめ
画像生成AIが大流行りしている中、世界初の柿ピー画像生成AI(?)を作ってクイズゲームにしてみました。
ボットはこちらです。ぜひ遊んでみてください。