JavaScriptで体温計の数字を画像認識してGoogleスプレッドシートに書き込ませたかった。

#作りたいもの

体温計の液晶に表示された体温を、PCのWEBカメラで読み取って、Googleスプレッドシートに書き込みます。
WEBカメラが撮影するのは「画面に体温計が映ったら」をトリガーにして自動化します。
![0606-2248.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1289676/a030a6e7-cc95-5fc7-2ccc-e6b02553b452.png)


#なぜ作ろうと思ったのか

最近は会社指示等で毎朝検温される方も多いかと思いますが、私は以前から起き抜けの体温を測ってその日の体調とともにGoogleスプレッドシートに記入し続けていました。**この手動による記入をなくせないかなぁ**、というのが今回の課題です。

ちなみに普通に「Bluetooth対応の体温計」なんて当たり前にあるだろうと思っていたのですが、価格.comで調べても（婦人用の口で咥えて測るものを除くと）「[BLE（Bluetooth Low Energy) 内蔵体温計 UT-201BLE](https://www.aandd.co.jp/products/medical/hhc/hhc-thermometer/ut201ble/)」という1機種しか出てこず、エー・アンド・デイという私の知らない会社だったのと、接続が不安定という口コミがあったので購入は見合わせました。

パルスオキシメーターですらBluetooth接続製品がわりとあるのにもっと身近な体温計にないのは不思議です。

さて、通信方式はBluetoothでなくてもいいから、信頼できるメーカーから出てないのかな、と調べていくとオムロンが2021年3月25日に「[音波通信体温計 MC-6800B　けんおんくん](https://www.healthcare.omron.co.jp/product/mc/mc-6800b.html)」というものを発売していました。が、**専用アプリを立ち上げるという手間が増えている**気がします。

そもそも現状はGoogleスプレッドシートに**体温の他に寝起きの体調や、気温も記入している**ので専用アプリに入力するというのはデータの使い道が狭まります。

また、ここまで気にされる方は少数派であろうという自覚はあるのですが、今まで小数点以下2位で取得してきたデータが3年分あるのに、これからは小数点以下1位になるのは**なんかいやです**。（理系っぽいことをまったく理系っぽくない理由で言う理系）

ところで「けんおんくん」というネーミングは<S>一昔前のシステムっぽくて</S>良いので勝手に拝借して今回のシステムは**「けんおんくんGS」**としましょう。

#処理の概要
以下のような処理でいきます。

1.Teachable Machineで体温計の数値を読み取れる写真を100枚以上撮影し、学習データを作成する。（写真データAとする）
2.WEBページを表示して、動画撮影を開始する。
3.ml5.jsを使って、写真データAと合致したときに画像をcanvasに取得する。（画像Bとする）
4.画像BをTesseract.jsに投入して、数字を読み取る。（数字Cとする）
5.数字CをGoogleスプレッドシートに書き込む。

今回、「[【勝手企画】Twilioオンラインコンテストに応募しちゃおうハッカソン](https://mashupawards.connpass.com/event/202313/)」に参加するためにTwilioの必要バージョンであるnode.jsのバージョンを切り替えできるように変更したら開発環境が壊れました( ﾉД`)ｼｸｼｸ…  なので今回できているのは3.までです。。。

追記）環境直りました(/・ω・)/

#つくりかた

Teachable Machineに「体温計の液晶面を見せている画像」と「体温計がない背景」の2パターンで学習させます。一致する場合の返り値は「Thermometer」とし、それ以外は「None」とします。

![0606-2226.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1289676/62513d6a-16be-880b-fd8a-e27fbb62f7a6.png)

ここでトラブル。「体温計」として認識されるのは以下の赤で囲んだ画像でした。**アイ・アム・体温計マン。**
それどころか**電灯をつけるだけで体温計と認識されてしまいます。** 対比先の画像「None」をまったく同じ画像にしすぎました。

![0606-2225.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1289676/f38628bd-fc88-ab5b-9400-f50e980983bf.png)

なので「None」の画像には人間が映ったり、電灯をつけたり消したりしてデータを追加しました。
これで以下のように割と正確に認識するようになりました。以下はConsoleに画像認識結果を吐いてるところです。

![0606-2235.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1289676/84abe93e-c6e5-b46c-eb36-2108f9018024.png)

#課題
「処理の概要」の4.と5.がまだです。

また、以下のように体温計をかざしたあとに移動しようとした自分が静止画として写っています。ある程度認識するとはいえ、体温計だけを認識するのは厳しそう。となると、「最初に体温計を検知してから3秒後に1回だけ静止画を撮影する」（3秒は人間が体温計をかざしてカウントする運用）などが良さそうでしょうか。

あと、体温計の数字が反転して映っているので、読み取れるように再反転させる必要もありますね。。。**あれ、結構大変そうだな。。。**

![0606-2236.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1289676/2711228e-26cc-eedd-b603-ed53003640e5.png)

#後日談

上記を作ってから、オムロンの「[音波通信体温計 MC-6800B　けんおんくん](https://www.healthcare.omron.co.jp/product/mc/mc-6800b.html)」についてもっと調べてみました。
専用アプリ「OMRON connect」はcsvを書き出し＆メール添付とiPhoneの「ヘルスケア App」に連携できるそうです。

すでに「ヘルスケア App」にはタニタのアプリから体重、AppleWatchから心拍数、歩数、酸素飽和度が、SleepCycleから睡眠時間が集約されていることと思うと、体温も集約させたい気になってきますね…。小数点以下2桁と自分の体調コメント入力を諦めるか、もうちょっと考えます。

#開発中に書いた記事

https://qiita.com/ShinsukeSutou/items/704276b306a4daeec188

https://qiita.com/ShinsukeSutou/items/f8895070c25c67996fb5



#環境
ml5.js
Teachable Machine
Tesseract.js

#コード

```thermometer.html

<!DOCTYPE html>
<html>
<head>
	<meta charset="UTF-8">
	<meta content="width=device-width, initial-scale=1.0" name="viewport">
	<title>Thermometer</title>
</head>
<body>
	<h1>Temperature to GoogleSpreadSheet</h1>
	<div id="app">
		<p>1.体温計を検知したら静止画として撮影します。</p>
		<p>2.撮影した画像から体温を読み取ります。</p>
		<p>3.読み取った体温をGoogleスプレッドシートに入力します。</p><video autoplay="" height="240" id="myvideo" style="border: 3px solid deepskyblue;" width="320"></video>
		<canvas height="240" id="mycanvas" style="border: 3px solid pink;" width="320"></canvas><img height="240" id="myimg" src="" style="border: 3px solid green;" width="320"><br>
		<p>4.体温計の自動判別がうまく動かないときは手動で撮影もできます。</p><button onclick="video2canvas2img()">撮影</button>
	</div>
	<script src="https://unpkg.com/ml5@latest/dist/ml5.min.js">
	</script> 
	<script>
	     // 作成したモデルのURL
	     const imageModelURL = 'https://teachablemachine.withgoogle.com/models/KMTRCuFmI/';
	     let classifier;
	     

	       // 撮影ボタンクリックでvideoタグ→canvasタグ→imgタグへとその瞬間のフレーム（静止画）データを移してゆく
	       function video2canvas2img() {
	           // videoタグ、canvasタグを取得
	           const video = document.getElementById('myvideo');
	           const canvas = document.getElementById('mycanvas');
	           // canvasへ描画するための「コンテキスト」を取得
	           const context = canvas.getContext('2d');
	           // コンテキストに対してvideoのデータを書き込むことで、canvasへ反映
	           context.drawImage(video, 0, 0);
	           // imgタグを取得
	           const img = document.getElementById('myimg');
	           // imgのsrc属性に、canvasの中身から変換したデータURL（URLっぽいが実体は生データ）をセット
	           img.src = canvas.toDataURL();
	       }

	        async function main() {       
	            // カメラからの映像取得
	           // 映像や音声が使えるデバイスが確定するまで時間がかかるためawaitを使う
	           const stream = await navigator.mediaDevices.getUserMedia({
	              audio: false,
	              video: { width: 320, height: 240 },
	            });

	            // IDが"myvideo"であるDOMを取得
	            const video = document.getElementById('myvideo');

	            // videoにカメラ映像をセット
	            // myvideoなvideo要素のsrcObject（映像オブジェクトを入れるところ）にデータ（メディアストリーム）をセットする
	            video.srcObject = stream;

	           // 自作モデルのロード
	           classifier = ml5.imageClassifier(imageModelURL + 'model.json', video, () => {
	              // ロード完了
	               console.log('Model Loaded!');
	           });

	           // 分類処理を連続的に行います（YOLOと同じ）。
	           function onDetect(err, results) {

	           if (results[0].label==='thermometer'){
	               console.log('体温計を検知しました！');
	               video2canvas2img();
	           }else{
	               console.log('serching...');             
	           }
	           classifier.classify(onDetect);
	           }
	           classifier.classify(onDetect);
	       }


	       // カメラ映像をビデオタグに表示する
	       main();


	</script>
</body>
</html>

```