田舎と都会論争に審判登場！？

## 1. 田舎ｖｓ都会
田舎に住むのはどうとか、都会に住むのはどうとか
色々と持論を展開している人は多いですね。

田舎の定義や都会の定義は
ご存じでしょうか。

広辞苑によると

![スクリーンショット 2023-12-27 163856.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3554996/36fe7186-a179-0f76-5a50-32f0d05fe5f9.png)

ということみたいです。

都会はまあ皆さんのイメージにお任せします。

皆さんは田舎に住みたいですか？
都会に住みたいですか？
現在は田舎に住んでいますか？
都会に住んでいますか？

わたしは田舎派なのか都会派なのかというと
利便性という点では都会が好きです。
ゴルフ場が多い点では田舎も好きです。

とは言うものの
どこまでが田舎で
どこからが都会なのかの判断は難しいと思います。

私自身も自分が生まれ育った場所が
都会なのか田舎なのかは判断がつきません。

自信をもって
「田舎だ」とか「都会だ」
と判断する人はいますが
その価値基準は個人によって
異なるのではと思います。

こういう判断はロボットにしてもらった方が
すっきりするような気がします。

そこで今回は
都会なのか田舎なのかを機械に判定してもらおうと考え
画像から田舎と都会を判断するLINEBotを作成しました。


## 2. 完成したLINEBot

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">田舎都会審判<br><br>2023年12月27日 <a href="https://t.co/gU780iclji">https://t.co/gU780iclji</a> <a href="https://twitter.com/YouTube?ref_src=twsrc%5Etfw">@YouTube</a>より</p>&mdash; Yokki (@K822Dr) <a href="https://twitter.com/K822Dr/status/1739918032221487320?ref_src=twsrc%5Etfw">December 27, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


<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 || 3000;

const config = {
    channelSecret: '**********',
    channelAccessToken: '*********',
};

// Teachable Machine
let model;

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

// ########################################
//  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
        },{
            type: 'image',
            originalContentUrl: 'https://lh3.googleusercontent.com/pw/ABLVV86g1IHl7v4qFIp4hkO0pmOl2fbKoUszda760FKJNWPA2TIenw1CtmbksDvR2qJ1nskPpYG3fTrxoC5amlAV3UN60-JKQQHyxKs5pvmsocf8-wHcsByXd5hHqHnC1sw0-A_bv3rWr0bDMJobGRYWF6RF=w945-h945-s-no-gm?authuser=0', 
            previewImageUrl: 'https://lh3.googleusercontent.com/pw/ABLVV86g1IHl7v4qFIp4hkO0pmOl2fbKoUszda760FKJNWPA2TIenw1CtmbksDvR2qJ1nskPpYG3fTrxoC5amlAV3UN60-JKQQHyxKs5pvmsocf8-wHcsByXd5hHqHnC1sw0-A_bv3rWr0bDMJobGRYWF6RF=w945-h945-s-no-gm?authuser=0'
        }]);
     }else if(name === '都会'){
        return client.replyMessage(event.replyToken,[{
            type: 'text',
            text: text
        },{
            type: 'image',
            originalContentUrl: 'https://lh3.googleusercontent.com/pw/ABLVV86HE0OzRh9g8DaIxKPXRrhNi9Tod-PbZh-QCtbdmUUTxOvbkVq4gzqQ1V_x4VnOlbhUgMB3Q9MxPmsQENZU0ngXfCxojIN8xDvdHXvNqZbMUzCsJXGOtbPjkJ9CZpirldr-Q3oM16lxHFJQmnYalX49=w945-h945-s-no-gm?authuser=0', 
            previewImageUrl: 'https://lh3.googleusercontent.com/pw/ABLVV86HE0OzRh9g8DaIxKPXRrhNi9Tod-PbZh-QCtbdmUUTxOvbkVq4gzqQ1V_x4VnOlbhUgMB3Q9MxPmsQENZU0ngXfCxojIN8xDvdHXvNqZbMUzCsJXGOtbPjkJ9CZpirldr-Q3oM16lxHFJQmnYalX49=w945-h945-s-no-gm?authuser=0'
        }]);
     }
  }

  // 「テキストメッセージ」であれば、受信したテキストをそのまま返事します
  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>


住んでいる場所の画像データを送ることで
**田舎**か**都会**かを
判断してくれます。

自分の住んでいる場所が
都会なのか田舎なのか迷ったら
これを使えば一発です。


## 3. 作成の手順
* Teachable Machineの画像プロジェクトを利用
* LINE Developersで新規のプロバイダーを作成
* 作成したコードを利用してチャネルシークレットとトークンを埋め込む

### 3-1. Teachable Machineの画像プロジェクト利用

https://teachablemachine.withgoogle.com/

使ってみる⇒画像プロジェクト

から機械学習のトレーニングをして
結果のURLを手に入れることができます。

コードにこのURLを入力すれば画像判定が機能します。

### 3-2. LINE Developersで新規のプロバイダーを作成

https://qiita.com/Yokki01/items/ff070e0792338adb43ed

よろしければ上記の記事に作成方法について書いてあるので
参考にしていただければと思います。

## 4. 機械学習を学んで

Taechable Machineを初めて利用しましたが
とても使いやすいツールでした。
画像プロジェクトだけではなく
音声プロジェクトやポーズプロジェクトもあり
プロダクトの幅が広がる可能性を秘めています。

ぜひ使ってみてください。

## 5. まとめ（長い

機械学習のトレーニングに向けた元データはやはり人間の判断になります。
なので今回の元データは
わたしの価値基準で都会と田舎の画像を用意することになりました。

デジタルテクノロジーでのプロダクト全般に言えることかもしれませんが
最終の結果を伝えるのがロボットというのがとても重要なポイントだと感じています。

意にそぐわないことを言われたり
思いもよらない結果に対して人間は簡単には受け入れられません。
レスポンスをしてくるのが人間であればなおのことです。

そして大抵の人はレスポンスの内容だけではなくて
レスポンスの主の属性によって
受け入れ態勢を変えます。

具体的には
理不尽な内容でも強者の言うことなら受け入れ
正当な内容でも弱者の言うことは受け入れない
などということが起こります。

これは情報に対する受け入れ態勢としては
ノイズがあるということになります。

レスポンスの発信者が人間ではなくて機械だと
このノイズが少なからず改善すると考えます。

今後も人間同士ではトラブルになりかねない内容について
AIなどのロボットで解決できるように
プロダクトをしていければと思います。

お読みいただきありがとうございました。