私の王子さまはどこにいるの？めめに似てる人を教えて～！！（TeachaBleMachineを使った機械学習モデルを作ってみました）

こんにちは。
今回はコロナ自粛期間中にドハマりしたSnow Manの目黒蓮くんこと【通称めめ】を
身近に発見したい！というただ、それだけの気持ちで画像認証できるものを作りました。

せっかくなので、Snow Manの誰に似ているのかも試してみてください！
※Snow Manを知らない方は[こちらをクリック](https://www.johnnys-net.jp/page?id=profile&artist=43)





## まずは実際にSnow Man判定してみてください！
　注)　URL内のボタンを押した後　結果が出るまでしばらくお待ちください！



https://codepen.io/nagaharahitomi/full/ZEvmPXQ

## TeachableMachineの設定
今回は身近なめめを探したかったので、
身近＝会社の人をターゲットに作りました。
（最近仕事が忙しくて友達に会えてない！！！！）


:::note info
会社で会う人はスーツ姿！！
てことで、スーツ姿のSnow Manの写真をTeachableMachineに設定！！
:::


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1811930/1437874c-8c48-d68e-bb63-99a20f72f081.png" width="400" height="200">

写真に引用したのはブラザービートでのパケ写真だけど
こんなスーツが似合う方たちいないよね～♡

## CodePenのエディタ

<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="ZEvmPXQ" data-user="nagaharahitomi" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/nagaharahitomi/pen/ZEvmPXQ">
  SnowMan誰に似てるか調べてみよう！</a> by NagaharaHitomi (<a href="https://codepen.io/nagaharahitomi">@nagaharahitomi</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

## 部長とSnow Manファンに試してもらう！(感想あり)

まずは部長！！いざ検証！！！！
（とりあえずSnow Manに似てないことは確かなので王子さま判定でないことを祈る）


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1811930/0a9118f2-2e62-04e3-dfc1-26dc9e867c9e.png" width="400" height="200">

セーフ！！！！(笑)
でもこの判定合ってる！
正直私の中で部長はSnow Manでいうとふっかさんだと思っていた(笑)

**部長の感想**
「**くだらないな～**」と言いつつ、なんか楽しそうだった(笑)
「**俺はSnow Manわからないから吉本新喜劇で作ってほしいわ！**」とのこと。


**はい、次！！**
女の子だったらどう判定されるんだ？と疑問になったので
生粋のSnow Manファンに依頼して試してもらうことに！


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1811930/b7391a38-8616-8432-ad3f-51c5cd29653d.png" width="400" height="200">


うん、大体しょっぴーが出てくるよね。
私もしょっぴー出てきた！

**友達の感想**
「**絶対しょっぴーには似てないと思う！！**」
確かに似てないよね～

今回あまり明確なアドバイスは出てきませんでしたが、
二人とも割と楽しんで使ってくれました！

## さ、番外編！LINEBotと繋げよう！
:::note warn
注意
Node.jsとLINEBotに繋げるまでは出来ましたが、
まだ正常に動いているとは言えない為、参考にする際は注意ください。
:::

実装するにあたって参考にした記事はこちら
[Teachable Machineを使って、ゆでたまごの気持ちをツンデレ風に表現してみた](https://qiita.com/tanakahiroki/items/9124b89a1e9c61572be1)

↑上記記事を見つけて読んだときに、凄く面白くて工夫されていると思ったので
割と早い段階から参考にしてました。

LINEBotと繋げた方がCodePenを開くときに出てくる、
「私はロボットではありません」の確認しなくて済むので今回もLINEBotに繋げるに挑戦！



## 実装環境
Node.js （version:17.6.0）
npm （version:8.5.1）
ngrok　（version:2.3.40）
LINE Messaging API

## コード

<details><summary>実際に作成したコードはこちら</summary>

```js
'use strict'; 

const { JSDOM } = require('jsdom');
var dom = new JSDOM('');
global.document = dom.window.document;
global.HTMLVideoElement = dom.window.HTMLVideoElement;
const canvas = require('canvas');
global.fetch = require('node-fetch');
const tmImage = require('@teachablemachine/image');

const express = require('express');
const line = require('@line/bot-sdk');
const fs = require('fs');

const PORT = process.env.PORT || 3001;

const config = {
    channelSecret: '**********', //忘れず変更
    channelAccessToken: '**********', //忘れず変更
};

// Teachable Machine
let model;

// https://teachablemachine.withgoogle.com/
// ここでエクスポート、クラウドにモデルをアップロードした後に取得できる
const URL = 'https://teachablemachine.withgoogle.com/models/iWfSsrVVh/';

// ########################################
//  Teachable Machineを使って画像分類をする部分
// ########################################

// 初期化が時間かかるので、node立ち上げ時に行うようにする
async function initTeachableMachine() {
  const modelURL = URL + 'model.json';
  const metadataURL = URL + 'metadata.json';
  // モデルデータのロード
  model = await tmImage.load(modelURL, metadataURL);

  // クラスのリストを取得
  // const classes = model.getClassLabels();
  // console.log(classes);
}
initTeachableMachine();

async function predict(imgPath) {
  // canvasに画像をロードする
  const image = await canvas.loadImage(imgPath);

  // 判定する
  const predictions = await model.predict(image);

  // 一番近いもの順でソート
  predictions.sort((a, b) => {
    return b.probability - a.probability;
  });

  return predictions;
}

// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
  // 受信したWebhookが「画像以外」であればnullを返すことで無視します
  if (event.message.type === 'image') {
    console.log("あなたは私の王子様なの？");

    // 画像を保存
    const downloadPath = './01.png'; //左記の名前で使用中のフォルダ内に送信された画像が保存される
    const getContent = await downloadContent(event.message.id, downloadPath);

    const result = await predict(downloadPath);

    // AIメーカーAPIの結果から、返信するメッセージを組み立てる
    let text = '';
    let name = '';
    name = result[0].className
    // 判定結果をテキストに代入
    text = name;
    // これまでの結果を確認するためにコンソールに表示
    console.log(result);
    console.log(name);
    console.log(text);
    // 判定結果に応じた画像を送信
    if(name === 'あなたはひーくん似'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === 'あなたはふっかさん似'){
        return client.replyMessage(event.replyToken,[{
            type: 'text',
            text: text
        }]);
     }else if(name === 'ラウールに似ているなんて素敵'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === '声もしょっぴーに似ているのかしら？'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === 'こーじー！！'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
    }else if(name === 'あなたは阿部ちゃん似'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === 'ここに居たのね私の王子様'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
      }else if(name === '舘さま～'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
     }else if(name === 'あなたは元気いっぱいさっくん'){
        return client.replyMessage(event.replyToken, [{
            type: 'text',
            text: text
        }]);
    }
  }

  // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
  if (event.message.type === 'text') {
    return client.replyMessage(event.replyToken, {
      type: 'text',
      text: event.message.text // ← ここに入れた言葉が実際に返信されます
    });
  }
}

// ########################################
//          LINEで送られた画像を保存する部分
// ########################################

function downloadContent(messageId, downloadPath) {
  const data = [];
  return client.getMessageContent(messageId)
    .then((stream) => new Promise((resolve, reject) => {
      const writable = fs.createWriteStream(downloadPath);
      stream.on('data', (chunk) => data.push(Buffer.from(chunk)));
      stream.pipe(writable);
      stream.on('end', () => resolve(Buffer.concat(data)));
      stream.on('error', reject);
    }));
}

// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP GETによって '/' のパスにアクセスがあったときに 'Hello LINE BOT! (HTTP GET)' と返事します
// これはMessaging APIとは関係のない確認用のものです
app.get('/', (req, res) => res.send('<h1>Hello LINE BOT! (HTTP GET)</h1>'));

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
  // Webhookの中身を確認用にターミナルに表示します
  console.log(req.body.events);

  // 空っぽの場合、検証ボタンをクリックしたときに飛んできた"接続確認"用
  // 削除しても問題ありません
  if (req.body.events.length == 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します
    console.log('検証イベントを受信しました！'); // ターミナルに表示します
    return; // これより下は実行されません
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);



```
</details>




<details><summary>package.jsonの"scripts"の中に"start": "node ファイル名"を入力</summary>

```js
{
  "name": "snowman",
  "version": "1.0.0",
  "main": "meme.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node meme.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/bot-sdk": "^7.5.0",
    "@teachablemachine/image": "^0.8.5",
    "@tensorflow/tfjs": "^1.3.1",
    "canvas": "^2.9.1",
    "express": "^4.17.3",
    "jimp": "^0.16.1",
    "jsdom": "^19.0.0"
  },
  "description": ""
}

```

</details>

## LineBotに繋げる。（出てきたエラー）


<details><summary>でてきたエラー①</summary>

```js
 node meme.js
ポート3000番でExpressサーバーを実行中です…
node:events:505
      throw er; // Unhandled 'error' event
      ^

me.js:195:5)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1151:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)    at Function.Module._load (node:internal/modules/cjs/loader:822:12)    
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12)
Emitted 'error' event on Server instance at:
    at emitErrorNT (node:net:1358:8)    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  code: 'EADDRINUSE',
  errno: -4091,  syscall: 'listen',
  address: '::',
  port: 3000
}
```
</details>

3000番はどこかで使われているのからエラーが起きるらしい・・・
てことで、コードの15行目を3000番から3001番に変更したら
正常に動いた！！


**LINEに文字送って試してみる！（エラー②）**

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1811930/3c951dce-1134-9989-7022-6998dbd9be45.png" width="400" height="200">


同じメッセージが返ってくる・・・
LINEBotの応答設定→詳細設定→応答メッセージをオフに設定！
よし、解決した！


**LINEに写真送って試してみる！（エラー③）**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1811930/b2c2053d-e3cb-aef0-662c-1b4cadbda502.png" width="400" height="200">

写真を送ってみたが、なんの反応もない。
きっとNode.jsのコードか何かがおかしいんだろうな～
（全然まだ理解できていないし、気づきが遅いかもだけどこれが私の実力なので、潔く認めることにした。）


## 現在
まだまだコードが読めない私はLINEBotにつなぐだけで精一杯だったけど、
もう少し頑張りたいので、コード分析中です。

それでもいつもはLINEBotにさえも繋げられてもないので、今回は少し進んだ気がしてます。
少しずつ、前進したいと思います！！

最後まで読んでいただきありがとうございました。