Teachable Machineでお寿司の識別

どうも！皆さまの冷蔵庫（スーパー）で働くちょもです。

デジタルについてカリカリと囓り始めて2ヶ月が経ち、「これはデジタルで良いでしょ！」とか、「あれはデジタルで解決できるっぽいな」と思う反面、まだまだ自分には技術も説得力も無くてもどかしさを感じる日々です。
今まで気付かなかった事・気付いても見て見ぬふりしていたものに対し、「こうすれば解決するんじゃないかな？」と考える小さな一歩を踏み出せてると思っています。
気付けるようになったきっかけ（研修）に、自ら手を挙げた自分と仲間を褒めよう！と鼓舞しております。

# 寿司の製造間違いを防ぐ:sushi:
これは私が寿司屋に応援に入った際の実体験です。
* 最後に乗せようと思った本マグロを乗せ忘れて売場に出しそうなった
* いつも作ってるから大丈夫と思い込みでレシピの確認を疎かにし、海老の種類を間違えた
* 穴子にタレ付け忘れる
* 太巻きの断面で商品を判断するのは至難の業（節分）

出来上がった商品を確認する態勢が整っているので、売場に出る前に未然に防げましたが、その確認も人間がやることですので、Wチェックしたとしてもいつか間違いが起きてしまうのではないかとヒヤヒヤです。
<B>アレルゲンの問題（人の命が関わる問題）でもあるので、間違いは許されないのです。</B>
今回はその間違いの判別にデジタルを取り入れられないかチャレンジします。
***
## 用意した物
 | 使用した物 | 用途 |
|:-:|:-:|
| [Teachable Machine](https://teachablemachine.withgoogle.com/)  |基本の画像を読み込んで保存し、判別してもらう大切なやつ |
|[CodePen](https://codepen.io/)|実際に出来るか確認するためのやつ|
|[ChatGPT](https://chatgpt.com/)|困り事はここに尋ねる。貴方なしじゃいられない|
|  にぎり寿司 | ネタ。判別材料  |
| スマートフォン  | 確認用デバイス(iphone & Android）  |
| タブレット  | 確認用デバイス（Android）  |

![スクリーンショット 2025-02-26 233102.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951623/91d14ae6-1735-438d-b4ee-65d0c071dc27.png)


***

### 手順① Teachable Machineを使う
[Teachable Machine](https://teachablemachine.withgoogle.com/)で以下の手順で寿司の画像をアップします。
今回はテストなので商品は3つに絞り、クラスに商品名を入力。それぞれに実際の出来上がりの画像をアップします。
パソコンだとその場でカメラ撮影が出来ますが、スマホからだと出来ませんでした。なので、今回はスマホで撮影した画像をパソコンで読み込みます。

![スクリーンショット 2025-02-26 232514.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951623/8d53a8ea-4f5f-4a6e-a12d-29c174e252a8.png)


***

### 手順② CodePenで自分で試す
出来上がったコードを[CodePen](https://codepen.io/)に貼り付ける。
この時、CodePenはHTMLとcssとJSに分かれた窓を持っていますので、コピーしたコードをどこに貼り付けることが正しいのか迷います。
ここでChatGPTに聞いてみました。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951623/140c86bd-cf24-408c-9a63-8faefb697228.png"><BR>


| HTML | CSS | JS |
|:-:|:-:|:-:|
| 家の骨組み（設計図）| デザイン  | 中身の動き |
| 玄関の位置・水回りの場所など何をどこに配置するか決めるところ |  屋根の色・壁の色・表札の文字の大きさなど見た目のデザインを整えるところ |床暖房・スイッチ押したらお風呂のお湯張りするなどの何か指示出したら動くところ  |

こんなイメージです。<B>あくまで私のイメージです。</B>

<details><summary>本当に思ったのはこっち</summary>


| HTML | CSS | JS |
|:-:|:-:|:-:|
| 人間のかたち　| 見た目  | 脳みそ |
| 筋肉とか、四肢とかの形を決めたり、物理的な刺激を受けるところ |  目や髪の色・ふくよかとか、細め、標準から服装などの見た目を選択するところ |刺激から指令を出すとこ（腕上げるとか、寝るとか） |


</details>

![スクリーンショット 2025-02-26 232819.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951623/374a2b77-5447-4faf-a45e-9313e80f5ecb.png)



Teachable Machineで出来たコードをそのままHTMLにペースとしても良かったのですが、細かく弄るときにこの３つに分かれている方が分かりやすいのかな？と思い出来たコードをChatGPTにお願いして３つに分けてもらい、こんなんができあがります。

https://x.com/Cyomo092/status/1894743962273095791

![スクリーンショット 2025-03-03 203623.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951623/782831cc-98f5-486c-af0d-e88e3800299c.png)



手元に寿司がないので、試作品第一号は身近なビタミン剤を使って識別してもらっています。

:::note
ピュアバランスホワイトBB→華雅
命の母ホワイト→大吉寿司
その他→塩とレモン
``ビタミン剤は同じ容器なので色を識別させないとはっきり出ませんでした。``
:::
***

### 手順③　改善点を出す
* 実際の寿司は似てるから判別できるのか不安
* 0.00～表示を％表示にしたい

似てるものの判別は、画像の量で勝負するしかないと思いました。ビタミン剤は瓶の形が同じなのでかなり迷っているようでした。しかし、色の違いがはっきりしているので、色で識別してくれます。
そして、これは元々は内カメしか使えませんでした。
商品は目の前にあるのに、外カメにならなくて何度ChatGPTと相談しても結局出来ず、諦めていたときにGoogleで検索してみたら、成功している方がいて、しかもコードを公開してくれてました:star2:
その方の記事はこちらです:grinning:

https://qiita.com/M8nEl0s0v8/items/738d175390d4630bfb06

この記事を読むまでは内カメしかできないなら音声で伝えるしかないとChatGPTと相談をしてはズーーーっと「○○寿司、○○寿司、○○寿司」とエンドレス寿司聞いてました。これも解決しませんでしたが、判別結果を音声で読み上げていく性質上、繰り返すのは必然なのかな？とも思っています。
パーセント表示はChatGPTでパパパッと解決しました。

こちらが今回のコードです。HTML・CSS・JSと別々で表示します。


<details><summary>HTML</summary>

```rb
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Teachable Machine 画像認識</title>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@latest/dist/tf.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@latest/dist/teachablemachine-image.min.js"></script>
    <script defer src="script.js"></script>
    <style>
        body { text-align: center; font-family: Arial, sans-serif; }
        #main-container { display: flex; justify-content: center; align-items: center; gap: 20px; }
        #camera-container { position: relative; display: inline-block; }
        #webcam { width: 100%; height: auto; max-width: 400px; }
        #overlay { 
            position: absolute; 
            top: 0; left: 0;
            width: 100%; height: 100%;
            pointer-events: none;
        }
        #label-container {
            text-align: left;
            padding: 10px;
            border: 2px solid #333;
            background: #f9f9f9;
            max-width: 200px;
        }
        .label-item { font-size: 13px; margin-bottom: 5px; }
    </style>
</head>
<body>
    <h1>画像判別テスト</h1>
    <div id="main-container">
        <div id="camera-container">
            <video id="webcam" autoplay playsinline></video>
            <canvas id="overlay"></canvas>
        </div>
        <div id="label-container">モデルをロード中...</div>
    </div>
</body>
</html>

```
</details>


<details><summary>CSS</summary>

```rb
body { 
  background-color: #B60081;  /* 背景色をピンクに設定 */
  font-family: Arial, sans-serif;  /* フォントをArialに設定 */
  color: #000000;  /* 文字色を白に設定 */
  padding: 20px;  /* 画面の端に20pxの余白を設定 */
}

table {
  width: 100%;  /* テーブルの幅を100%に設定 */
  border-collapse: collapse;  /* テーブルの枠線が重ならないように設定 */
  margin-top: 20px;  /* テーブルの上に20pxの余白を設定 */
}

table, th, td {
  border: 1px solid #000000;  /* テーブル、ヘッダー、セルに黒い枠線を設定 */
}

th, td {
  padding: 10px;  /* セル内の文字と枠線の間に10pxの余白を設定 */
  text-align: left;  /* テキストを左寄せに設定 */
}

th {
  background-color: #ffffff;  /* ヘッダーの背景色を白に設定 */
  color: #000000;  /* ヘッダーの文字色を黒に設定 */
}

td {
  background-color: #ffffff;  /* セルの背景色を白に設定 */
  color: #000000;  /* セルの文字色を黒に設定 */
}

.class-name {
  color: #000000;  /* クラス名の文字色を黒に設定 */
}

#webcam-container {
  border: 2px solid #000000;  /* 外枠を白色の2pxの枠線で囲む */
  border-radius: 10px;  /* 角を10pxの半径で丸める */
  padding: 10px;  /* 内部の余白を10pxに設定 */
  margin: 20px 0;  /* 上下に20pxの余白を設定 */
  position: relative;  /* 子要素を相対的に配置できるようにする */
  background-color: #e6e6fa;  /* 背景色を薄紫色に設定 */
  display: flex;  /* フレックスボックスで要素を配置 */
  justify-content: center;  /* 水平方向に中央に配置 */
  align-items: center;  /* 垂直方向に中央に配置 */
  height: 400px;  /* 高さを400pxに設定 */
}

#result-text {
  position: absolute;  /* 結果テキストを絶対位置で配置 */
  top: 0px;  /* 上から0pxの位置に配置 */
  left: 0px;  /* 左から0pxの位置に配置 */
  font-size: 10px;  /* 文字の大きさを10pxに設定（変更） */
  font-weight: bold;  /* フォントを太字に設定 */
  color: #000000;  /* 文字色を黒に設定 */
  background-color: rgba(0, 0, 0, 0.2);  /* 背景をもっと薄くした透明の黒に設定（変更） */
  padding: 5px;  /* 内部の余白を5pxに設定 */
  border-radius: 5px;  /* 角を5pxの半径で丸める */
}

button {
  padding: 10px 20px;  /* ボタンの内部の余白を10px上下、20px左右に設定 */
  margin: 10px;  /* ボタン周囲に10pxの余白を設定 */
  background-color: #4CAF50;  /* ボタンの背景色を緑色に設定 */
  color: white;  /* ボタンの文字色を白に設定 */
  border: none;  /* 枠線をなしに設定 */
  cursor: pointer;  /* ボタンにカーソルを置いたときに手の形に変更 */
}

button:hover {
  background-color: #45a049;  /* ホバー時に背景色を少し濃い緑に変更 */
}
```
</details>



<details><summary>JS</summary>

```rb
    const URL = "https://○○○";
let model, video, labelContainer, maxPredictions;
let canvas, ctx;

// モデルのロードとカメラセットアップ
async function init() {
    console.log("モデルをロード中...");
    const modelURL = URL + "model.json";
    const metadataURL = URL + "metadata.json";
    
    model = await tmImage.load(modelURL, metadataURL);
    maxPredictions = model.getTotalClasses();
    console.log("モデルロード成功");

    // カメラをセットアップ
    await setupWebcam();
    setupCanvas();

    // ラベル表示用のコンテナを初期化（カメラ横に表示）
    labelContainer = document.getElementById("label-container");
    labelContainer.innerHTML = "";
    for (let i = 0; i < maxPredictions; i++) {
        const labelDiv = document.createElement("div");
        labelDiv.className = "label-item";
        labelContainer.appendChild(labelDiv);
    }

    // 画像分類を開始
    predictLoop();
}

// カメラをセットアップ
async function setupWebcam() {
    video = document.getElementById("webcam");

    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    const constraints = {
        video: {
            facingMode: isMobile ? { exact: "environment" } : "user"
        }
    };

    try {
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        video.srcObject = stream;
        await video.play();
        console.log("カメラ起動成功");
    } catch (err) {
        console.error("カメラ起動エラー:", err);
    }
}

// キャンバスを設定（判定結果を重ねる用）
function setupCanvas() {
    canvas = document.getElementById("overlay");
    ctx = canvas.getContext("2d");

    video.addEventListener("loadedmetadata", () => {
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
    });
}

// 画像分類のループ
async function predictLoop() {
    if (!model) {
        console.error("モデルがロードされていません");
        return;
    }

    const predictions = await model.predict(video);
    drawResults(predictions);

    requestAnimationFrame(predictLoop);
}

// 判定結果を描画
function drawResults(predictions) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.font = "18px Arial";
    ctx.fillStyle = "#ffffff";

    // 確率が一番高いものを取得
    predictions.sort((a, b) => b.probability - a.probability);
    const topPrediction = predictions[0];

    // 画像の中央に表示
    // 画像の左上に表示
ctx.fillText(`${topPrediction.className}`, 10, 30);

    // カメラ横のラベル一覧を更新
    for (let i = 0; i < maxPredictions; i++) {
        const text = `${predictions[i].className}: ${(predictions[i].probability * 100).toFixed(1)}%`;
        labelContainer.childNodes[i].innerHTML = text;
    }
}

// ページ読み込み時に実行
window.onload = init;
```
</details>

***
## 部門の人に使ってもらった感想

https://x.com/Cyomo092/status/1894742577162993866


![IMG_2754.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951623/be0b6090-d64a-40b2-8c44-51a403f701d7.jpeg)


* この画面じゃ分かりづらい（CodePenの画面）
* いちいちタブレットとかで見てらんないよ。
　→<B>でも値付け機と一緒になって判別とパックしてくれたら良いな。</B>
* 画像認識したら間違いが格段に減るね！
* テレビで見る世界じゃん！こんなことできるんだね！ 



## わたしの正直な感想


* <B>判別精度が増せば間違いは確実に減る</B>
人の目には限界があるので、デジタルの力を使って助けになる。
画像認識が手軽にチャレンジできるのであれば、今回のお寿司の判別だけでなく、別のことに応用できるのではないかと閃きがあった。
* <B>実用化するには壁 :yen: がある</B>
できない理由があることも重々承知しているので、こんなことが実は出来るんだという経験が価値になりました。

画像の識別を実際にやってみて、その精度の良さに驚きました。
個人で無料で出来るものなので精度は無難な物かな、と勝手に思っていたけれど、そんなことなかった。
私たちのようにかじった程度の人間でも、これから実際に出来るかもしれないデジタルツールのかけらになれたら本望だなぁと思う今回の作成でした:blush: