こんにちは!
忘年会エンジニアのたかぱい(@takapy)です。
コネヒトの忘年会は例年気合いが入っており、有志メンバーが運営となり1年の締めくくりを最高のものにすべく、尽力しています。
今回は2019年の忘年会を技術で盛り上げるべく、同じく忘年会エンジニアのあぼ氏(@abo)と奮闘したことをご紹介できればと思います。
目次
前回(2018年)忘年会時に、弊社エンジニアにより「パシャりんピック」という歓談タイムを盛り上げるコンテンツが爆誕しました。 ざっくり説明すると、 というものです。(詳しくは下記参照) このコンテンツの評判がとても良かったので、今回はこれをアップデートさせる形で忘年会を盛り上げてやろうと画策しました。 そもそものコンセプトが明確だったので、今回はこれに「笑顔」要素と順位と写真の見える化をプラスすることで、より盛り上がるコンテンツに仕上げようと決めました。 大別して下記2点です。 それぞれについて、全体の構成図も交えながら説明していきます。 以下のようなアーキテクチャを構築しました。 使用した技術は下記です。 今回は"人"と"写真"の2つに対して点数がつくので、点数上位の人と写真がリアルタイムで見られるようにリーダーボード(スコアボード)を作りました。 使用技術ですが、今回はFlutterをweb上で動かしました。Flutter on the webは現状beta版ではありますが、 という理由で採用しました。 基本的にリーダーボード側はFirestoreから購読した値を画面に表示するだけなので、そこまでやることは多くありません。Firestoreの購読にはStreamBuilderを使って、UIはRowとColumnを駆使して組み立てていきました。 また、リーダーボードのメインのUIとは別に、当日の忘年会進行スライドとの統一感を出すために、画面右上に忘年会のロゴを表示させました。今回はStackという、Widgetを重ねられるWidgetを使って実現しました。Stackを使うことで"ロゴ"表示とメインコンテンツの"スコア"表示部分のレイアウトが分割されるので、個人的にはこっちのほうが好みです。 それから、Firebaseの機能を使うのに一工夫必要だったので紹介します。まず、Flutter on the webはFirebase Hostingで動かしました。設定はfirebaseコマンドを使ってサクサクと行えます。firebase.jsonではpublicにbuild/webを指定します。これは 次に、Dart packagesからFirebaseパッケージをインストールするのに加えてweb/index.htmlのscriptタグでjsファイルを読み込ませるようにしました。この辺はFirebaseをJavaScriptアプリケーションにインストールするドキュメントを参考にしました。これでFirebaseの機能が使えるようになりました。 pubspec.yaml web/index.html 忘年会エンジニア兼・機械学習エンジニアなので、スコア算出に機械学習の要素を取り入れました。 「笑顔」という時点で察している方もいらっしゃると思いますが、写真に写る人物の笑顔度合いを数値化してスコア算出を行いました。 本来であれば、複数の写真(学習データ)を用意して、笑顔度のアノテーションをして、学習させて・・・というフローが必要なところですが、今のご時世APIを1つ叩くだけでやりたいことができます。 という訳で、笑顔の数値化にはAzure Face APIを使用しました。 Azure Face APIには、Python SDKがあり、普段Pythonを触っている人であれば比較的スムーズに使用できるのではないかと思います。 例えば、画像をpostしてレスポンスを受け取る場合は下記のように記述します。 ここから笑顔の値を取得したい場合は下記のように記述します。 これで笑顔度(0〜1)を取得することができます。 試しに何枚か写真を撮って笑顔度を算出してみると、ある問題に気付きました。。。 「割と簡単に最高値が取得できてしまう🤔」 これでは盛り上がるコンテンツにならないのでは?と思い、とあるロジックを追加しました。 それは「写っている人物の鼻端が近ければ近いほど、高得点になる」というもので、これを「仲の良さ」と銘打ってスコア計算に組み込みました。 鼻端のx座標、y座標に関しては上記のAPIを使用すれば取得できるので、「近さ」の定義だけ考えました。 通常であればユークリッド距離を計算すれば良いのですが、ハックできる要素を含ませた方がコンテンツとして面白いのでは?と思い、x座標の差分の絶対値を「近さ」の定義として計算することにしました。 どうすればハックできるかはもちろん伝えていなかったのですが、開始10分くらいで気付かれはじめ、最終的には高得点の写真ばかりになっていました・・・笑(詳細は後述) 以下が「近さ」を考慮したスコア計算例です。(スコアの幅は0点〜1000点にしています) このようにすることで、隣り合う2人の距離(鼻端のx座標)が近ければ近いほど高得点が出るようにしました。 「笑顔」という条件を提示することで、いろんな人の様々な表情を垣間見ることができました! 想定外だったことといえば、先にも述べた通り開始10分くらいでハックされたことです(汗) これにより、忘年会後半はほぼ全員が縦並びで写真を撮るという見慣れない光景が広がっていました(笑)が、結果的に仲良さそうに写真を撮っていたので運営側としても大成功&嬉しかったです! 告知です! 2月6日にMeetUpを開催します! ママ向けNo1アプリ「ママリ」を運営している社員とざっくばらんにお話ししてみませか? 当日はCEO、CTO、デザイナー、新規事業開発責任者、人事責任者など様々な職種の社員が参加します!ちょっと話聞きたいなくらいの温度感でOKなのでご参加お待ちしてます!
パシャりんピック(既存構成)の紹介
今回のコンセプト
アップデート内容
アーキテクチャ
リーダーボードについて(byあぼ)
class BoardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
Positioned(
top: -20,
right: -50,
child: Image.asset(
'logo.png',
),
),
// メインコンテンツのスコア表示UIを組み立てる
Column(
~~~
),
],
),
);
}
}
Firebaseの機能を使えるようにする
flutter build web
でweb用のリリースビルドが生成される場所です。{
"hosting": {
"site": "year-end-party-2019-board",
"public": "build/web",
"ignore": [
"firebase.json"
]
}
}
dependencies:
flutter:
sdk: flutter
firebase: ^7.1.0
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>year-end-party-2019-board</title>
</head>
<body>
<script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-auth.js"></script>
<script src="https://www.gstatic.com/firebasejs/6.2.0/firebase-firestore.js"></script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
</html>
スコア計算について (byたかぱい)
Azure Face APIの使用例
FACE_API_URL = 'https://japaneast.api.cognitive.microsoft.com/face/v1.0/detect'
KEY = 'hogehoge'
HEADER = {'Ocp-Apim-Subscription-Key': KEY}
def call_api(image_url):
params = {
'returnFaceId': 'false',
'returnFaceLandmarks': 'true',
'returnFaceAttributes': 'smile,emotion'
}
response = requests.post(
FACE_API_URL,
params=params,
headers=HEADER,
json={"url": image_url}
)
return response
# image_urlには画像のURLを設定
response = call_api(image_url)
smile = response.json()[0]['faceAttributes']['smile']
Azure Face APIが笑顔に寛大すぎる問題
(近い位置で写真撮ってる方が仲良さそうですよね?そうでしょ?)「近さ」の定義
「近さ」を考慮したスコア計算
def calc_score(response, width):
score = 0
nosetip = 0
count = 0
if len(response.json()) > 2:
count = 2
else:
count = len(response.json())
for i in range(count):
smile = response.json()[i]['faceAttributes']['smile'] # 笑顔度 0 ~ 1
sadness = response.json()[i]['faceAttributes']['emotion']['sadness'] # 悲しみ 0 ~ 1
score += (smile - sadness) * 100
# 鼻端の近さ
if nosetip == 0:
nosetip = response.json()[i]['faceLandmarks']['noseTip']['x']
else:
nosetip -= response.json()[i]['faceLandmarks']['noseTip']['x']
# 鼻端の距離スコアを計算
nosetip = 1 - (abs(nosetip) / width)
if score == 0: return 0
return max(0, int(round(((score / 2) * nosetip), 1)*10))
# responseは前項で取得したFace APIのレスポンス、widthは写真の横幅
score = calc_score(response, width)
やってみてどうだったか
と同時に、今回はリーダーボードがほぼリアルタイム*1で更新されたので、リーダーボードに映っている、スコアの高い写真を参考にどのように写真を取れば高得点が叩き出せるか試行錯誤するという一味違った体験を提供することができ、大盛況に終わることができたのではないかと思っています!
今回はx座標のみスコアに寄与するため、横並びではなく、縦並びで写真を撮るとかなり高いスコアを叩き出すことができます。*2最後に
(忘年会の様子とか聞いてみたいな〜という方も気軽にお越しください(笑))