作ったもの
タイトルの通り、写真を投稿すると、どれくらいハッピーなのかAIがハッピー指数を算出します。
写真はパクタソさんより
※「ハッピー指数」としましたが、これって「指数」って言ってもいいものだろうか不安になりましたが、頭の悪そうな語感が気に入ったのでこのまま「ハッピー指数」とします。
そして、同時にハッピー指数のランキングを表示します。
写真はパクタソさんより
おお、眩しすぎる。
キラキラする。
まさにリア充向けシステムだ。
という訳で、
あなたが世界でどれくらい幸せなのか、調べてみましょう。
公開URL
ウェブアプリとして公開していますので、是非お試してください。
ちなみにこのドメインはこちらで取得しました。
freenom
環境
フレームワーク :Nuxt.js
CSSフレームワーク:BULMA
開発プラットフォーム:Firebase
技術面
表情解析
アップロードした写真を、face-api.js というTensorFlow.jsで学習済みの顔認識の機械学習モデルのライブラリに投げて、表情からハッピー度を取得します。
ハッピー度は0から1の間で、大きいほどハッピー度が高いです。
faceapi.resizeResults(detections, displaySize)[0].expressions.happy =
0.999991774559021
ハッピー度はこんな感じで取得されます。
小数点が15桁くらいあるので、1000倍して見やすくし、ハッピー指数ということにしました。
(ご参考)自分の過去記事
【忙しい現代人のために】表情で扇風機を操作するシステムを作ったよ
写真およびデータ保存
ここでハマったことを別記事に書きました。
【Firebase + Nuxt.js】FirebaseStorageへの画像アップロードでハマったところ
ハマったところ
リストの中でのイメージの表示方法
こちらを見て解決しました。ありがとうございました。
【Vue.js】imgタグのsrc要素は指定の仕方によって読み込み方が違う
たった、これだけなんですけど、凄くハマりました。
<img :src="data.picurl"/>
非同期処理のコードの書き方
非同期処理が苦手です。
then(() =>{ とか、Promiseとか async とか await とか・・・
毎回、頭が混乱してしまいます。
じっくりと勉強しなければ。
face-api.js のファイルのままデプロイすると怒られた
[BABEL] Note: The code generator has deoptimised the styling of /pages/face-api.js as it exceeds the max of 500KB.
500KB以上はダメらしいので、
https://github.com/justadudewhohacks/face-api.js/
に書いてある通り、npmでインストールしました。
npm i face-api.js
face-api.jsの機械学習モデルの場所はstaticフォルダへ
同じディレクトリに保存してはいけません。
デプロイされません。
ハマったところは、その他、たくさんあったけど、書ききれないです。
やりたかったけど出来なかったこと
・送信ボタンを付けたかった
(今のは、添付ファイルを選んだ瞬間に送信される)
・画像をリサイズして保存したかった
・ハッピー指数がでた瞬間に順位が出るようにしたかった
コード
<template>
<div class="container">
<div>
<br><br>
<h1 class="title">
ハッピー・ランキング
</h1>
写真を投稿するとハッピー指数を判定して、ランキングします。<br>
ハッピー指数は1000点が最高得点です。<br><br>
※投稿写真は作者が管理しているクラウドサーバーに保存されますので、ご注意ください。
<br><hr><br><br>
<client-only placeholder="Loading...">
<Pic />
</client-only>
</div>
</div>
</template>
<script defer src="face-api.js"></script>
<script defer src="scripts.js"></script>
<script>
import Pic from '~/components/Pic.vue'
export default {
components: {
Pic
}
}
</script>
<style>
.container {
margin: 0 auto;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
background-color: pink;
}
.title {
font-family:
'Quicksand',
'Source Sans Pro',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
'Helvetica Neue',
Arial,
sans-serif;
display: block;
font-weight: bold;
font-size: 30px;
color: #35495e;
letter-spacing: 1px;
}
#happyScore{
font-size: 30px;
font-weight: bold;
}
.list{
padding-top: 50px;
}
</style>
<template>
<div>
投稿者名:<input v-model="name" placeholder="投稿者名">
<br><br>
<input @change="post" type="file" data-label="画像の添付">
<br>
<img id="attachedFile" width=350 v-show="uploadedImage" :src="uploadedImage" />
<div id="happyScore"></div>
<br><hr>
<div class="list">
<h1 class="title">
ハッピー指数 トップ100
</h1>
<br>
<ul v-for="(data, index) in allData" :key="data.id" class="menu-list" >
<li>
順位: {{index + 1}} 位<br>
ハッピー指数 : {{data.happyScore}} 点 <br>
投稿者名:{{data.name}} <br>
<img width=350 :src="data.picurl"/>
</li>
<br><br>
</ul>
</div>
<br>
</div>
</template>
<script>
import firebase from "firebase/app";
import "firebase/firestore";
import 'firebase/storage';
import * as faceapi from 'face-api.js';
import uuid from 'uuid';
export default {
components: {},
data(){
return{
db: {},
allData: [],
name: '',
fileName: '',
picurl: '',
uploadedImage: '',
happyScore: '',
realhappyScore: '',
testId: ''
}
},
methods: {
//初期化、設定
//各人の数値を入れること
init: () => {
const config = {
apiKey: "",
authDomain: "",
databaseURL: "",
projectId: "",
storageBucket: "gs://xxxxxx-99999.appspot.com",
messagingSenderId: "",
appId: "",
measurementId: ""
};
// Initialize Firebase
firebase.initializeApp(config);
},
post(pic){
const file = pic.target.files[0];
if(!file.type.match('image.*')) {
alert("画像ファイルでお願いします");
return;
}
//イメージファイル描画
let reader = new FileReader();
reader.onload = (pic) => {
this.uploadedImage = pic.target.result;
};
let imagefiles = pic.target.files || pic.dataTransfer.files;
reader.readAsDataURL(imagefiles[0]);
let attachedFile = document.getElementById('attachedFile');
const testId = firebase.firestore().collection('pics').doc().id; //ユニークなIDを生成
const docRef = firebase.firestore().collection('pics').doc(testId);
const fileName = uuid(); //ファイル名は他と被らないように uuid ライブラリを使って動的に生成
const storageRef = firebase.storage().ref(fileName);
// 画像をStorageにアップロード
storageRef.put(file).then(() => {
let debug_document = document.getElementById("happyScore");
debug_document.innerHTML = "しばらくお待ちください";
// アップロードした画像のURLを取得
const picurl = firebase.storage().ref(fileName).getDownloadURL().then((url) => {
const happyScoreGet = getFaceData( document.getElementById('attachedFile')).then((happyScore) => {
let debug_document = document.getElementById("happyScore");
let realhappyScore = happyScore;
happyScore = Math.floor(happyScore * 1000); //1000倍
debug_document.innerHTML = "ハッピー指数: "+String(happyScore)+"点";
//firestoreにデータを保存
const setScore = docRef.set({
name: this.name,
happyScore: happyScore,
realhappyScore: realhappyScore ,
fileName: fileName ,
picurl: url
});
//ランキング作成へ
this.get();
});
}).catch((error) => {
console.log(error)
})
})
},
//データ取得
get: function(){
this.allData = [];
//スコアの降順に100個取得
firebase.firestore().collection('pics').orderBy('realhappyScore', 'desc').limit(100).get().then(snapshot => {
snapshot.forEach(doc => {
this.allData.push(doc.data());
})
});
}
},
mounted(){
//ページ読み込み時に実行される
this.init();
},
}
//表情取得
async function getFaceData(img) {
await faceapi.nets.tinyFaceDetector.load("/models") ;//モデル読み込み
await faceapi.nets.faceLandmark68Net.load("/models") ;//モデル読み込み
await faceapi.nets.faceRecognitionNet.load("/models") ;//モデル読み込み
await faceapi.nets.faceExpressionNet.load("/models") ;//モデル読み込み
const detectionsWithLandmarks = await faceapi.detectAllFaces(img,new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks();
if (detectionsWithLandmarks.length == 0){
alert('人間じゃないよ');
return(0)
}else{
const displaySize = { width: attachedFile.width, height: attachedFile.height }
//1つの顔だけなのでfaceapi.detectAllFacesではなくて detectSingleFaceでよいはずが、本件はdetectAllFacesを使った。
const detections = await faceapi.detectAllFaces(attachedFile , new faceapi.TinyFaceDetectorOptions()).withFaceLandmarks().withFaceExpressions()
const resizedDetections = faceapi.resizeResults(detections, displaySize)
return(resizedDetections[0].expressions.happy); //ハッピー指数を返す
}
}
</script>
番外編 (内輪ネタ)
現在、一緒にProtoOut Studioで学んでる受講生が今までQiitaで取り上げた人物で試してみました。