4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

こんにちは!
今回は、GlitchTeachable Machine による機械学習を使って業務に役立つアプリの作成にチャレンジしてみました。

過去の記事はこちら↓

お客さま「この商品どこですか?」 私「少々お待ちください!(わからん!)」

売場に立っていると、「この商品、どこにありますか?」 とお客様に聞かれることがよくあります。
自分の担当部門の商品であればすぐにご案内できますが、他部門の商品となると、どこにあるのかすぐにはわからず、担当者に確認したり、実際に探しに行ったりすることになります。

しかし、そういうときに限って担当者が不在だったり、連絡がつかなかったり…。
内心「あれ、どこだ?」と焦りながら対応する場面も少なくありません。

そこで今回、商品画像を送るだけで売場の場所が返ってくるアプリを試作してみました。

使用画面

image.png

使用ツール

  • Teachable Machine
  • Googleスプレッドシート
  • Glitch(当初はCodepenで挑戦していましたがカメラの実装がうまくいかずこちらを選択。)
  • ChatGPT

開発プロセス

✅ Teachable Machine でモデル作成

まずは、アプリで認識させたい商品(今回はツールを制作しているときに近くにあったウインナー、お茶、ポテトチップス、食器洗剤)の画像を Teachable Machine にアップロードし、それぞれの画像を 「クラス」 として学習させました。
これにより、カメラで商品を映した際に、どのクラスに属するかを判定できるようになります。(大体120~180枚くらいのサンプルがあれば判定結果が安定します。)

→そのモデルをエクスポートしURLを取得。

image.png

ここで気づき
当初、実物の商品をカメラで映して学習させていましたが、お客様から商品を聞かれる際は手元に現物はありません。 そこで、インターネット上の商品画像をスマートフォンで表示し、それをカメラで映す想定で学習サンプルを追加しました。

✅ Googleスプレッドシートで売場データ管理

「商品名」と「売り場」を入力し、CSV形式でWeb公開。
→ アプリから読み取り可能に。
image.png

✅ GlitchでWebアプリ構築

Glitch とは、ウェブアプリケーションを簡単に作成、共有、公開できるオンライン開発環境です。
image.png

以下の3ファイルを作成

  • index.html:UI構成
index.html
<!DOCTYPE html>
<html>
  <head>
    <title>商品認識売場検索</title>

    <!-- ✅ 1. TensorFlow.js を先に読み込む -->
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.21.0/dist/tf.min.js"></script>

    <!-- ✅ 2. Teachable Machine の画像モデルライブラリ -->
    <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>

    <!-- ✅ 3. defer付きで script.js を読み込む(これ超重要) -->
    <script src="script.js" defer></script>

    <!-- ✅ CSSがあればここで -->
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h2>商品認識+売場検索</h2>
    <button onclick="init()">カメラ起動</button>
    <div id="webcam-container"></div>
    <div id="prediction">商品判定中...</div>
    <pre id="output">ここに売場情報が表示されます</pre>
  </body>
</html>
  • script.js:ロジック
script.js
const modelURL = "https://teachablemachine.withgoogle.com/models/JlYbs6PCz/";
let model, webcam, maxPredictions;

let isInitialized = false;         // カメラ多重起動防止用
let lastPredictedClass = "";       // 直近の認識結果を保持して重複処理を防ぐ

async function init() {
  if (isInitialized) return;       // すでにカメラが起動していればスキップ
  isInitialized = true;

  const modelURLFull = modelURL + "model.json";
  const metadataURL = modelURL + "metadata.json";

  model = await tmImage.load(modelURLFull, metadataURL);
  maxPredictions = model.getTotalClasses();

  webcam = new tmImage.Webcam(300, 300, true); // 映像サイズ変更可
  await webcam.setup();
  await webcam.play();
  window.requestAnimationFrame(loop);

  document.getElementById("webcam-container").appendChild(webcam.canvas);
}

async function loop() {
  webcam.update();
  await predict();
  window.requestAnimationFrame(loop);
}

async function predict() {
  const prediction = await model.predict(webcam.canvas);
  prediction.sort((a, b) => b.probability - a.probability);
  const topPrediction = prediction[0];

  document.getElementById("prediction").innerText =
    `判定: ${topPrediction.className} (${(topPrediction.probability * 100).toFixed(1)}%)`;

  if (topPrediction.probability > 0.85 && topPrediction.className !== lastPredictedClass) {
    lastPredictedClass = topPrediction.className;
    fetchCSVAndSearch(topPrediction.className);
  }
}

async function fetchCSVAndSearch(className) {
  const csvURL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vTkxyM7kcbvDxlFNLt3geOcAAOXoBz12BhBYfp_4hVN7TurszQpjrBZqOJnMR7pmNisiWNAlvgqodBo/pub?output=csv";
  const response = await fetch(csvURL);
  const csvText = await response.text();

  const rows = csvText.split("\n").map(row => row.split(","));
  const headers = rows[0];
  const data = rows.slice(1);

  const match = data.find(row => row[0].trim() === className.trim());

  if (match) {
    const info = headers.map((h, i) => `${h}: ${match[i]}`).join("\n");
    document.getElementById("output").innerText = info;
  } else {
    document.getElementById("output").innerText = "該当する商品が見つかりませんでした。";
  }
}
  • style.css:デザイン
style.css
/* 全体のベースデザイン */
body {
  background-color: #f0f0f0;
  color: #333333;
  font-family: 'Segoe UI', sans-serif;
  padding: 20px;
  line-height: 1.6;
}

/* タイトル */
h2 {
  font-size: 24px;
  margin-bottom: 16px;
}

/* ボタン */
button {
  background-color: #007bff;
  color: white;
  border: none;
  padding: 10px 16px;
  font-size: 16px;
  cursor: pointer;
  border-radius: 5px;
  transition: background-color 0.2s ease-in-out;
}
button:hover {
  background-color: #0056b3;
}

/* 映像コンテナ(video + canvas) */
#webcam-container {
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
  margin-top: 20px;
}

/* 映像表示 */
video, canvas {
  width: 300px;
  height: auto;
  border: 2px solid #444;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* 判定結果テキスト */
#prediction {
  background-color: #ffffff;
  border: 1px solid #cccccc;
  padding: 12px;
  border-radius: 5px;
  margin-top: 20px;
  font-size: 16px;
}

/* 売場情報の出力欄 */
#output {
  background-color: #ffffff;
  border: 1px solid #cccccc;
  padding: 12px;
  border-radius: 5px;
  margin-top: 10px;
  font-size: 15px;
  white-space: pre-wrap;
}

✅ ChatGPTと二人三脚で開発

私は未だにHTML/JS/CSSについて完全に理解できていません。
そこで 「カメラが映らない」「fetchの使い方が分からない」 など、すべてChatGPTに相談して進行!

詰まったポイントと解決方法は以下の通りです。

詰まったこと 解決策
tmImage is not defined <script defer>で読み込み順を調整
カメラ映像が増殖する isInitializedフラグで初回のみ起動
判定結果が更新されない 前回結果と比較して変化時のみ更新
videoとcanvas両方出る CSSとJSでvideoだけ表示に

今のアプリでできること

現在のプロトタイプでできることは以下の通りです。

・カメラ起動と商品分類 : カメラを起動し、学習済みの商品を映すと、Teachable Machine が自動で商品を分類します。

・売場情報の表示 : 認識された商品名をもとに、Googleスプレッドシートから該当する売場情報を検索し、画面に表示します(例: お茶「2番柱 ペットボトル飲料コーナー 1段目」)。

・シンプルなUI : 明るめのグレー背景、見やすいフォントを採用しています。

今後の展望

今後は以下のような機能拡張と課題解決に取り組んでいきたいと考えています。

「未知の商品」の判定と拒否

現在の大きな課題は、登録していない商品が映った際に誤認識してしまうことです。Teachable Machineで「その他」クラスを追加してみたもののほかの何らかのクラスに分類されてしまいます。

私がカメラに映るとポテトチップスになります。(?)
スクリーンショット 2025-07-08 103232.png

対策としては判定の 確信度(confidence) が低い場合に
「商品が見つかりません」と表示する などを実装し、より賢い未知商品への対応を強化していきたいです。

画像アップロード対応

現在はリアルタイムカメラのみ対応ですが、 これではお客さまに画像を表示してもらう、もしくは自分でこのアプリを起動する機器と画像を表示する機器を別々で用意する必要があります。正直使い勝手が悪すぎる。
そこで保存された画像をアップロードして判定する機能を追加すれば、この問題を解決できます。

実装間に合わず…。
これら機能の追加に挑戦してみました。しかし、一度画像をアップロードすると、その判定結果が画面に残り続け、その後のカメラ起動がうまくいかなかったり、正しく判定されなかったりといったエラーが解消できず、現在も試行錯誤中です…。


商品補充タイミング判定ツール(制作中に浮かんだアイデア)

今回のツール作成を通じて、画像認識や機械学習を応用した別の活用アイデアも思いつきました。

店舗で事務作業に集中していると、
売場の状況確認が後回しになり、気づいたら棚がスカスカ… ということも少なくありません。

そんな時にこの技術を活用し、

・ 棚の状態をカメラで定期的にチェック
・ 商品の補充が必要な場合に通知を送る

といった売場監視&補充支援アプリがあれば、
一人勤務の時間帯でも棚の空きを見逃さず対応できるのではないかと思いました。
今後、この構想も実現に向けて検討していきたいです。

最後に

今回、初めて画像認識と機械学習の分野に本格的に触れることができました。
正直今回のアプリの完成度には満足できていませんが、これらの技術を活用したツールを、ChatGPTのサポートやノーコードツールの力を借りて、形にすることができたというポイントだけは大きな収穫だったかなと思っています。

まだまだこの分野に関する知識は浅いですが、
今後も学びを続けながら、より現場の役に立つツールづくりに挑戦していきたいと思います。

4
0
0

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
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?