LoginSignup
2

posted at

私の王子さまはどこにいるの?めめに似てる人を教えて~!!(TeachaBleMachineを使った機械学習モデルを作ってみました)

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

せっかくなので、Snow Manの誰に似ているのかも試してみてください!
※Snow Manを知らない方はこちらをクリック

まずは実際にSnow Man判定してみてください!

 注) URL内のボタンを押した後 結果が出るまでしばらくお待ちください!

TeachableMachineの設定

今回は身近なめめを探したかったので、
身近=会社の人をターゲットに作りました。
(最近仕事が忙しくて友達に会えてない!!!!)

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

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

CodePenのエディタ

See the Pen SnowMan誰に似てるか調べてみよう! by NagaharaHitomi (@nagaharahitomi) on CodePen.

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

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

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

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

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

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

友達の感想
絶対しょっぴーには似てないと思う!!
確かに似てないよね~

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

さ、番外編!LINEBotと繋げよう!

注意
Node.jsとLINEBotに繋げるまでは出来ましたが、
まだ正常に動いているとは言えない為、参考にする際は注意ください。

実装するにあたって参考にした記事はこちら
Teachable Machineを使って、ゆでたまごの気持ちをツンデレ風に表現してみた

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

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

実装環境

Node.js (version:17.6.0)
npm (version:8.5.1)
ngrok (version:2.3.40)
LINE Messaging API

コード

実際に作成したコードはこちら
'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サーバーを実行中です…`);



package.jsonの"scripts"の中に"start": "node ファイル名"を入力
{
  "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": ""
}

LineBotに繋げる。(出てきたエラー)

でてきたエラー①
 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
}

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

LINEに文字送って試してみる!(エラー②)

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

LINEに写真送って試してみる!(エラー③)

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

現在

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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2