Web 1weekに参加してみた話。

だら@Crieit開発者様主催のweb 1weekに参加してみました。web 1weekはお題にそって作ったサービスを作ってみようみたいなイベントで今回のお題は"2"でした。

2と言うことで画像がアップされたらそれを診断して笑顔度(にこにこ度)を判定するといったもので構想しました。実際に作ったものがこちらです。

判定はgoogle cloud vision apiを使用して実装しました。バックエンドはRails フロントはHTML CSS Bootstrap JavaScript jQueryで作っています。

実装にあたってこちらの記事を参考にさせていただきました。

今回初めてアプリのデプロイをしたことで自分の出来なさを再認識出来ました。Bootstrapの表示につまり、apiの使用方法につまり、ajax通信につまる等わからないこと、理解が浅いことが多く、結構時間を使ってしまいました。とはいえわからないことだらけの中わかることがどんどん増えていくことが楽しくもあり、今回web 1weekに参加してみて本当にいい経験になりました。

以下自分用のメモ程度のコードの解説です。

HTML

<div class="container">
 <div class="row">
   <div class="col text-center">
 <%= image_tag 'ai_search.png', id: 'preview', class: 'w-50' %>
 </div>
   </div>
 <div class="row">
   <div class="col">
     <input type="file" name="image" onchange='previewFile()' class='form-control my-1 w-50 mx-auto' accept='image/*' id="js-file-button" >
   </div>
 </div>
 <div class="row">
   <div class="col">
     <div class="w-50 mx-auto my-3" id="sindan_result">
     試しに画像を選択してみるんじゃ。ここに診断結果が表示されるぞい!笑顔度診断はイラストには未対応じゃ
     </div>
   </div>
 </div>
 <div class="row">
   <div class="col text-center">
   <div>                                                                                                               </div>
   </div>
 </div>
   </div>
</div>
<style>
html {
	height: 100%;
	margin: 0 auto;
	padding: 0;
	display: table;
}

body {
	min-height: 100%;
	margin: 0 auto;
	padding: 0;
	display: table-cell;
	vertical-align: middle;
}
</style>​

image_tagの画像のところをJSを使ってonchange='previewFile()で画像が選択されたら発火するようになっています。最後の何も入っていないdiv要素には表示サイズ調節ようにめっちゃ長い全角空白スペースが入っています。(何かいい方法があったら教えてください。)CSSはstyleタグで直書きしましたごめんなさい。

JavaScript

function previewFile() {
 const target = this.event.target;
 const image_file = target.files[0];
 const reader  = new FileReader();
 const api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
 const url = `https://vision.googleapis.com/v1/images:annotate`;
 const sendAPI = (base64string) => {
   let body = {
     requests: [
       {image: {content: base64string}, features: [{type: 'FACE_DETECTION'}, {type: 'SAFE_SEARCH_DETECTION'}, {type: 'WEB_DETECTION'}]}
     ]
   };
   let xhr = new XMLHttpRequest();
   xhr.open('POST', `${url}?key=${api_key}`, true);
   xhr.setRequestHeader('Content-Type', 'application/json');
   const p = new Promise((resolve, reject) => {
     xhr.onreadystatechange = () => {
       if (xhr.readyState != XMLHttpRequest.DONE) return;
       if (xhr.status >= 400) return reject({message: `Failed with ${xhr.status}:${xhr.statusText}`});
       resolve(JSON.parse(xhr.responseText));
     };
   })
   xhr.send(JSON.stringify(body));
   return p;
 }
	const readFile = (file) => {
   let reader = new FileReader();
   const p = new Promise((resolve, reject) => {
     reader.onload = (ev) => {
       document.querySelector('img').setAttribute('src', ev.target.result);
       resolve(ev.target.result.replace(/^data:image\/(png|jpeg);base64,/, ''));
     };
   })
   reader.readAsDataURL(file);
   return p;
 };

   Promise.resolve(image_file)
     .then(readFile)
     .then(sendAPI)
     .then(res => {
       
       var response = res;
       $.ajax({
		    	 url: '/posts',
   		 	 type: "POST",
    			 data: response,
    			 dataType: "json",
          }).done(function(data) {
           document.querySelector('#sindan_result').innerHTML = data.body;
           console.log(data)
           })
           .fail(function() {
           alert("error!");  // 通信に失敗した場合はアラートを表示
           })

       console.log('SUCCESS!', res);
     })
     .catch(err => {
       document.querySelector('#sindan_result').innerHTML = JSON.stringify(err, null, 2);
     });
}

HTMLのonchange属性からこのpreviewFile()発火します。

変数の設定関係。const target = this.event.target;でonchangeで変化したDOM要素を指定して、const image_file = target.files[0];で変数に1番目のファイルを格納します。const reader = new FileReader();でFileReaderオブジェクトを作成。const api_key = 'xxxxxxxxxxx'でgoogle cloud visionで取得したapiキーをセットしています。(apiキーに制限はかけましたが一応xxxxで表現しています。)const url = `https://vision.googleapis.com/v1/images:annotate`;で今回使用するimages:annotateへのurlを定義しています。const sendAPI = (base64string) =>{ }で引数をgoogle cloud visionに送る関数を作っています。const readFile = (file) => { }で選択されたファイルをbase64形式に変換しています。

関数関係

 const sendAPI = (base64string) => {
   let body = {
     requests: [
       {image: {content: base64string}, features: [{type: 'FACE_DETECTION'}, {type: 'SAFE_SEARCH_DETECTION'}, {type: 'WEB_DETECTION'}]}
     ]
   };
   let xhr = new XMLHttpRequest();
   xhr.open('POST', `${url}?key=${api_key}`, true);
   xhr.setRequestHeader('Content-Type', 'application/json');
   const p = new Promise((resolve, reject) => {
     xhr.onreadystatechange = () => {
       if (xhr.readyState != XMLHttpRequest.DONE) return;
       if (xhr.status >= 400) return reject({message: `Failed with ${xhr.status}:${xhr.statusText}`});
       resolve(JSON.parse(xhr.responseText));
     };
   })
   xhr.send(JSON.stringify(body));
   return p;
 }

sendAPIではapiに送るbodyとxhrオブジェクトの作成、urlに接続、HTTP リクエストヘッダーの値を設定、Promiseオブジェクトの作成、リクエストの送信を定義しています。

let bodyでapiに送るjson形式のリクエストを定義しています。今回はFACE_DETECTION(人の顔認識), SAFE_SEARCH_DETECTION(画像の安全度を認識してくれる), WEB_DETECTION(どこのurlで使われているとかの検索結果を返してくれる)の3つを指定しています。let xhr = new XMLHttpRequest();でhttpリクエストを送るためにXMLHttpRequest オブジェクトを作成しています。xhr.openでurlにアクセスします。xhr.setRequestHeaderでHTTP リクエストヘッダーの値を設定しているみたいです。よくわからなかったのでこの記事を読みました。

https://webtan.impress.co.jp/e/2010/01/12/7156

const p = new Promise((resolve, reject) => { }でPromiseオブジェクトを作成しています。その中でXMLHttpRequest.onreadystatechange プロパティを使用。これは、 readystatechange イベントが発生するたびに呼び出されるようです。

if (xhr.readyState != XMLHttpRequest.DONE) return;

で読み込み状態が終わっていなかったらXMLHttpRequest.onreadystatechangeに戻り、

if (xhr.status >= 400) return reject({message: `Failed with ${xhr.status}:${xhr.statusText}`});

でステータス400が出たらPromiseがrejectされたときの処理の入ります。

resolve(JSON.parse(xhr.responseText));

最後までいったらPromiseがresolveした時の処理に入ります。中にはリクエストに対するレスポンスデータが入っています。Promise定義終わり。最後に

xhr.send(JSON.stringify(body));

でリクエストを送って、その後readystatechange イベントが発生するたびxhr.onreadystatechangeが呼び出されます。

const readFile = (file) => {
   let reader = new FileReader();
   const p = new Promise((resolve, reject) => {
     reader.onload = (ev) => {
       document.querySelector('img').setAttribute('src', ev.target.result);
       resolve(ev.target.result.replace(/^data:image\/(png|jpeg);base64,/, ''));
     };
   })
   reader.readAsDataURL(file);
   return p;
 };

const readFile = (file) => { }ではreaderにFileReaderオブジェクトを作成し格納、Promiseオブジェクトを作成、reader.readAsDataURL(file);で引数のファイルを読み込みresultにbase64 エンコーディングされた data: URL の文字列が格納し、loadendイベントが発火しPromise内のreader.onloadが動きます。

document.querySelector('img').setAttribute('src', ev.target.result);

でimgタグのsrcをloadしたbase64データに置き換えています。

resolve(ev.target.result.replace(/^data:image\/(png|jpeg);base64,/, ''));

正規表現でbase64にエンコードされたデータのdata:image/jpeg;base64,を削除してapiで使える形に加工しています。

   Promise.resolve(image_file)
     .then(readFile)
     .then(sendAPI)
     .then(res => {
       
       var response = res;
       $.ajax({
		     url: '/posts',
   		 	 type: "POST",
    		 data: response,
    		 dataType: "json",
          }).done(function(data) {
           document.querySelector('#sindan_result').innerHTML = data.body;
           console.log(data)
           })
           .fail(function() {
           alert("error!");  // 通信に失敗した場合はアラートを表示
           })

       console.log('SUCCESS!', res);
     })
     .catch(err => {
       document.querySelector('#sindan_result').innerHTML = JSON.stringify(err, null, 2);
     });

最後にreadFileで定義した画像データをsendAPIでリクエストを送って、返ってきたresに入っているデータを$.ajaxメソッドでRailsに送って加工したデータを結果表示欄に置き換えます。

class PostsController < ApplicationController
 def create
		responses = params[:responses].permit!.to_hash
		@post_body = {body: post_text(responses)}
		render :json => @post_body 
	end

 private

 def post_text(responses)
		response = responses["0"] if responses
		if response
			safesearch = response["safeSearchAnnotation"]
			faceAnnotations = response["faceAnnotations"]
			bestGuessLabels = response["webDetection"]["bestGuessLabels"]["0"]["label"]
			label = "ふむふむ、#{bestGuessLabels}じゃな <br>"
		end
		
		faceAnnotation = faceAnnotations["0"] if faceAnnotations
		
		if safesearch
			adult_decision = safesearch["adult"]
			violence_decision = safesearch["violence"]
			safesearch = [adult_decision, violence_decision]

			if adult_decision == "VERY_LIKELY"
				adult_level = "これはとてもエチチな画像じゃ <br>"
			elsif adult_decision == "LIKELY"
				adult_level = "これはエッチな画像じゃ!けしからんぞい <br>"
			elsif adult_decision == "POSSIBLE"
				adult_level = "これはエッチな画像の可能性があるぞいこっそり見るんじゃぞ <br>"
			else
				adult_level = ""
			end

			if violence_decision == "VERY_LIKELY"
				violence_level = "暴力はよくないぞい <br>"
			elsif violence_decision == "LIKELY"
				violence_level = "暴力はよくないぞい <br>"
			elsif violence_decision == "POSSIBLE"
				violence_level = "これはバイオレンスな画像の可能性がポッシボウじゃ <br>"
			else
				violence_level = ""
			end
		end

		if faceAnnotation
			#楽しさ
			joyLikelihood = faceAnnotation["joyLikelihood"]
			#悲しみ
			sorrowLikelihood = faceAnnotation["sorrowLikelihood"]
			#怒り
			angerLikelihood  = faceAnnotation["angerLikelihood"]
			#驚き
			surpriseLikelihood  = faceAnnotation["surpriseLikelihood"]
			#"VERY_LIKELY", "LIKELY", "POSSIBLE", "UNLIKELY", "VERY_UNLIKELY"
			if joyLikelihood == "VERY_LIKELY"
				joy = "とってもいい笑顔じゃ喜びボルテージがマックスじゃ <br>"
			elsif joyLikelihood == "LIKELY"
				joy = "うむいい笑顔じゃのやっぱり笑顔が一番じゃ <br>"
			elsif joyLikelihood == "POSSIBLE"
				joy = "いい笑顔じゃの楽しい雰囲気が伝わってくるわい <br>"
			else
				joy = ""
			end

			if sorrowLikelihood == "VERY_LIKELY" || sorrowLikelihood == "LIKELY" || sorrowLikelihood == "POSSIBLE"
				sorrow = "悲しそうな顔をしとるのぅいいことがあるといいのう <br>"
			else 
				sorrow = ""
			end

			if angerLikelihood == "VERY_LIKELY" || angerLikelihood == "LIKELY" || angerLikelihood == "POSSIBLE"
				anger = "怒っておるの激おこぷんぷん丸じゃ <br>"
			else 
				anger = ""
			end
			
			if surpriseLikelihood == "VERY_LIKELY" || surpriseLikelihood == "LIKELY" || surpriseLikelihood == "POSSIBLE"
				surprise = "びっくりしておるのう何があったんじゃ <br>"
			else 
				surprise = ""
			end
			emotion = "#{joy}#{sorrow}#{anger}#{surprise}"
		end

		return post_boby = "#{label}#{adult_level}#{violence_level}#{emotion}"
 end
end
responses = params[:responses].permit!.to_hash

で$.ajaxで送ったapiからのレスポンスを受け取ってハッシュに変換しています。その後post_text(responses)メソッドを呼び出し、診断結果を作成します。

def post_text(responses)
		response = responses["0"] if responses
		if response
			safesearch = response["safeSearchAnnotation"]
			faceAnnotations = response["faceAnnotations"]
			bestGuessLabels = response["webDetection"]["bestGuessLabels"]["0"]["label"]
			label = "ふむふむ、#{bestGuessLabels}じゃな <br>"
		end
		
		faceAnnotation = faceAnnotations["0"] if faceAnnotations
		
		if safesearch
			adult_decision = safesearch["adult"]
			violence_decision = safesearch["violence"]
			safesearch = [adult_decision, violence_decision]

			if adult_decision == "VERY_LIKELY"
				adult_level = "これはとてもエチチな画像じゃ <br>"
			elsif adult_decision == "LIKELY"
				adult_level = "これはエッチな画像じゃ!けしからんぞい <br>"
			elsif adult_decision == "POSSIBLE"
				adult_level = "これはエッチな画像の可能性があるぞいこっそり見るんじゃぞ <br>"
			else
				adult_level = ""
			end

			if violence_decision == "VERY_LIKELY"
				violence_level = "暴力はよくないぞい <br>"
			elsif violence_decision == "LIKELY"
				violence_level = "暴力はよくないぞい <br>"
			elsif violence_decision == "POSSIBLE"
				violence_level = "これはバイオレンスな画像の可能性がポッシボウじゃ <br>"
			else
				violence_level = ""
			end
		end

		if faceAnnotation
			#楽しさ
			joyLikelihood = faceAnnotation["joyLikelihood"]
			#悲しみ
			sorrowLikelihood = faceAnnotation["sorrowLikelihood"]
			#怒り
			angerLikelihood  = faceAnnotation["angerLikelihood"]
			#驚き
			surpriseLikelihood  = faceAnnotation["surpriseLikelihood"]
			#"VERY_LIKELY", "LIKELY", "POSSIBLE", "UNLIKELY", "VERY_UNLIKELY"
			if joyLikelihood == "VERY_LIKELY"
				joy = "とってもいい笑顔じゃ喜びボルテージがマックスじゃ <br>"
			elsif joyLikelihood == "LIKELY"
				joy = "うむいい笑顔じゃのやっぱり笑顔が一番じゃ <br>"
			elsif joyLikelihood == "POSSIBLE"
				joy = "いい笑顔じゃの楽しい雰囲気が伝わってくるわい <br>"
			else
				joy = ""
			end

			if sorrowLikelihood == "VERY_LIKELY" || sorrowLikelihood == "LIKELY" || sorrowLikelihood == "POSSIBLE"
				sorrow = "悲しそうな顔をしとるのぅいいことがあるといいのう <br>"
			else 
				sorrow = ""
			end

			if angerLikelihood == "VERY_LIKELY" || angerLikelihood == "LIKELY" || angerLikelihood == "POSSIBLE"
				anger = "怒っておるの激おこぷんぷん丸じゃ <br>"
			else 
				anger = ""
			end
			
			if surpriseLikelihood == "VERY_LIKELY" || surpriseLikelihood == "LIKELY" || surpriseLikelihood == "POSSIBLE"
				surprise = "びっくりしておるのう何があったんじゃ <br>"
			else 
				surprise = ""
			end
			emotion = "#{joy}#{sorrow}#{anger}#{surprise}"
		end

		return post_boby = "#{label}#{adult_level}#{violence_level}#{emotion}"
 end

喜びや悲しみの度合いを表すレスポンスや画像の安全度を表すsafesearchは"VERY_LIKELY", "LIKELY", "POSSIBLE", "UNLIKELY", "VERY_UNLIKELY"の5種類あるのでそれによってif文で分岐しています。最後にそれぞれの値から作った文を合体して@post_bodyに渡して、json形式にしてrender :json => @post_body で$.ajaxに値を返します。

$.ajax({
		 url: '/posts',
   	   type: "POST",
       data: response,
       dataType: "json",
       }).done(function(data) {
          document.querySelector('#sindan_result').innerHTML = data.body;
          })
           .fail(function() {
           alert("error!");  // 通信に失敗した場合はアラートを表示
           })
.done(function(data) {
  document.querySelector('#sindan_result').innerHTML = data.body;
 })

でdataとして受け取った@post_bodyをinnerHTMLで結果を表示したいところの文章と置き換えれば完成です。

ここまで長々読んでくれたありがとうございます。

解釈や表現、書き方が間違っているところ、アドバイスなどありましたらお願いします。

この記事が気に入ったらサポートをしてみませんか?