Teachable Machineで画像認識！ 棚卸時の注意点を表示するアプリっぽいものをつくってみた！

## 半年に１度じゃ忘れるよなぁ・・・。
どうも、アラ還サラリーマンです。
とある地方の某小売業の会社で、店舗運営全般を統括する部署の管理部門の担当をしています。
管理部門では、店舗事務方のサポートやデータ管理などなど、多岐にわたる業務を担当しています。

わが社では年に２回の決算期に、各店舗で商品の棚卸を実施しています。
棚卸は会社の利益の確定のための重要業務です。
ですが、半年に１度のことということもあり、社内で決めた実施手順や監査手順を忘れてしまう人も結構います。
よし！　棚卸の時の注意点を表示するアプリっぽいものをつくってみよう！

というわけで、最近 Teachable Machine を少し触った機会があったので、今回はこれを試したいと思います。

## 実際にできた動画がこちら！

https://x.com/hiromae123/status/1838294883943223348

**操作方法**
* 今回はサンプルとして、２種類の画像を認識させています。
* スマホから【確認票】と【明細リスト】をカメラで認識させると、棚卸時の監査員の注意点をそれぞれ表示します。

## 今回使用したツール
* **Teachable Machine**　機械学習モデルを簡単につくれます。
* **CodePen**　WEBアプリっぽいものがつくれます。
* **ChatGPT**　兄さん───！　今回もお願いします！

#### Teachable Machine
* 以下のリンク先から画像認識モデルを作成しました。

https://teachablemachine.withgoogle.com/


* 【画像プロジェクト】を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/4f6d6340-84de-8c33-a5ad-25afe8b47236.png)
　
* 【確認票】と【明細リスト】の２種類の画像ファイルをそれぞれ100枚ずつアップロード。
* モデルをトレーニング後、モデルをエキスポートします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/edc9d154-d371-f9bf-dd40-3bf29ffa19a5.png)
　
* 【モデルをアップロード】を押してしばらく待つと、共有可能なリンク欄にリンクが表示されます。
* 表示されたリンクと、その下にある【モデルを使用するコード スニペット】の **p5.js** をコピーしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/4d20e78b-9cc5-a6e6-3029-34a99f694df0.png)

#### CodePen
* 以下のリンク先からGoogleアカウントなどでログインします。（ログインしなくても使えますが、つくったものを保存できません）

https://codepen.io/

* Teachable Machine の【モデルをアップロード】時にコピーしておいた **p5.js** を HTML 欄に貼り付けます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/6f8cb4cb-127b-ce16-e0a3-8479e3c85367.png)
あれっ？　**p5.js** のコードを貼っても動かない！
カメラは動いてるみたいだけどなぁ。

#### ChatGPT
兄さん───！　さっそく助けてください！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/2f753b98-bf1b-5655-46d9-6a9d4069917b.png)
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/8040c667-07d2-c882-756e-c7d02fff6771.png)
```javascript
let model, webcam, labelContainer, maxPredictions;

// モデルのURL
const URL = "your_model_url_here";

// モデルをロードして初期化
async function preload() {
  const modelURL = URL + "model.json";
  const metadataURL = URL + "metadata.json";
  model = await tmImage.load(modelURL, metadataURL);
  maxPredictions = model.getTotalClasses();
}

// ウェブカメラのセットアップと再生
function setup() {
  createCanvas(320, 260);
  webcam = createCapture(VIDEO);
  webcam.size(320, 240);
  webcam.hide();
  labelContainer = createDiv(''); // ラベルを表示する要素を作成
  labelContainer.position(10, 250); // ラベルの表示位置を調整
}

// フレームごとに予測を実行して描画
function draw() {
  background(220);
  image(webcam, 0, 0); // カメラ映像を描画
  predict(); // 予測を実行
}

async function predict() {
  const prediction = await model.predict(webcam.elt); // カメラ映像に対して予測を実行
  let resultText = '';
  for (let i = 0; i < maxPredictions; i++) {
    resultText += `${prediction[i].className}: ${prediction[i].probability.toFixed(2)}<br/>`;
  }
  labelContainer.html(resultText); // 予測結果を表示
}
```
なるほど！　さすが、兄さん───！
さっそく素直に貼り付け実施！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/ee04f54e-5064-977a-ace6-a20b7d709cd7.png)
おおっ！　カメラもちゃんと写ってる！

おや？　気になることが書いてあるよ、兄さん───。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3842559/24a7f8fb-78fa-1a2c-873b-b2b9f1c5daa9.png)
ウェブカメラの設定！　さすが、兄さん───！
スマホで使用したいからインカメラじゃダメですよね！

あと追加したい点です、兄さん───！
* 使うデバイスでパソコンの場合はWEBカメラ、スマホの場合はリアカメラに切り替える処理を加えたい。
* Teachable Machineモデルの予測確率が90%以上の時だけ結果を反映させたい。それ以外の場合は非表示にしたい。

兄さんから教えてもらったコードに、【確認票】と【明細リスト】でそれぞれ表示させたい注意点を記載して出来上がり！
#### そしてできたものがこちら！（実は失敗例！）

https://x.com/hiromae123/status/1840294865348772010

あれれ？　注意点がチカチカするぞ！
あとこれも追加したいです、兄さん───！
* 画面が落ち着かないので、3秒ごとに予測を行わせたい。

こんなお願いにも、当然応えてくれるわれらが兄さん───！
#### そして完成した **最終のコード** がこちら！
```javascript
let model, webcam, labelContainer, maxPredictions;
let lastPredictionTime = 0; // 最後に予測を行った時間を保存
const msg1 = [
  '【確認票 - 監査員の注意点】',
  '・担当者印の有無を確認！',
  '・監査印は２ヵ所に押印！',
  '・監査後、半券は回収表に貼付！'
].join('</br>');
const msg2 = [
  '【明細リスト - 監査員の注意点】',
  '・担当者印の有無を確認！',
  '・商品コードのエラーの有無を確認！',
  '・明細リストの監査行にレ点チェック！',
  '・監査後、監査印を忘れずに押印！'
].join('</br>');

// モデルのURL
const URL = "Teachable MachineでエクスポートされたモデルのURLを指定";

// モデルをロードして初期化
async function preload() {
  const modelURL = URL + "model.json";
  const metadataURL = URL + "metadata.json";
  model = await tmImage.load(modelURL, metadataURL);
  maxPredictions = model.getTotalClasses();
}

// デバイスがモバイルかどうかを判定
function isMobile() {
  const userAgent = navigator.userAgent || navigator.vendor || window.opera;
  return /android|iPad|iPhone|iPod/i.test(userAgent); // モバイルデバイスならtrueを返す
}

// ウェブカメラのセットアップと再生
function setup() {
  createCanvas(320, 260);

  // デバイスによってカメラの設定を変更
  let videoConstraints = {};
  if (isMobile()) {
    // モバイルデバイスの場合、リアカメラ（環境カメラ）を使用
    videoConstraints = {
      video: {
        facingMode: { exact: "environment" } // リアカメラを指定
      }
    };
  } else {
    // パソコンの場合、Webカメラ（インカメラ）を使用
    videoConstraints = {
      video: true // デフォルトのカメラ設定（Webカメラ）
    };
  }

  // カメラのキャプチャを作成
  webcam = createCapture(videoConstraints);
  webcam.size(320, 240);
  webcam.hide();

  // ラベルを表示するためのコンテナ
  labelContainer = createDiv('');
  labelContainer.style("background-color:#ffff00; font-weight:bold; width:320px;");
  labelContainer.hide(); // 初期状態では非表示
}

// フレームごとに描画し、3秒ごとに予測を行う
function draw() {
  background(220);
  image(webcam, 0, 0); // ウェブカメラ映像をキャンバスに描画

  // 3秒（3000ms）ごとに予測を実行
  if (millis() - lastPredictionTime >= 3000) {
    predict(); // 予測を実行
    lastPredictionTime = millis(); // 最後の予測時間を更新
  }
}

// 予測を行い、確率が90%以上の場合のみ「確認票」か「明細リスト」で表示
async function predict() {
  const prediction = await model.predict(webcam.elt); // カメラ映像に対して予測を実行

  // 予測確率の高い結果を取得
  let highestPrediction = prediction[0];
  for (let i = 1; i < maxPredictions; i++) {
    if (prediction[i].probability > highestPrediction.probability) {
      highestPrediction = prediction[i];
    }
  }

  // 確率が90%以上の場合にのみ結果を表示
  if (highestPrediction.probability >= 0.9) {
    if (highestPrediction.className === "確認票") {
      labelContainer.html(msg1);
    } else if (highestPrediction.className === "明細リスト") {
      labelContainer.html(msg2);
    }
    labelContainer.show(); // 結果を表示
  } else {
    labelContainer.hide(); // 確率が90%未満の場合は非表示
  }
}
```
## 簡単にスマホアプリっぽいものができた！
スマホを使用することが前提だったので、インカメラとリアカメラの切り替えの仕方を ChatGPT に教わりながら実装していきました。
また注意点を表示する時間も、３秒ごとに判定を行うことで、画面がチカチカしないようにしました。
今回は２種類だけの画像認識でしたが、種類を増やすことで、もっと充実したマニュアルがつくれそうです。
とにかく簡単なサンプルだけど、できた───！　兄さん───に感謝！！

ではでは、今日はこのへんで！

**2024/09/29 下記を追記しました。**
* 画面に表示される注意点がチカチカして、落ち着かない失敗例の動画を追加。
* 上記の動画の前後に説明文を若干追加。