6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

シフト間違いはもうやめて!顔認識して、その人のシフトを画面に表示させるアプリを作ってみた。

Last updated at Posted at 2025-07-10

みなさんこんにちは。
北海道の某小売店で朝から働くおじさん(主任)です。
今回で4回目の投稿になります。前回はVBAを使って、商品の売上を自動で取得させるアプリを作成しました。前回の記事はこちら。
https://qiita.com/horigome2245/items/86781d7f933c4ae644f6

今回は 機械学習 × デジタルツール を使って
「顔認識でその人のシフトを表示するアプリ」を作っていきます!

Teachable Machine は、Google が提供している無料の機械学習ツール。画像・音声・ポーズなどを学習させて、AIモデルを直感的に作れます。プログラミング知識不要なのがポイント!

勤務シフト間違いを防ぎたい

うちの会社は「変形労働時間制」。
つまり、人や日によってシフトがバラバラなんです。

そのせいで…

間違って早く来てしまう

残業禁止の日にうっかり残ってしまう

理由書を書かなきゃいけない

といった「不本意な残業」が多発しています。

「カードをスキャンする前に自分のシフトが見えたら防げるのでは?」
と思い、顔認識でシフトを表示するアプリを作ってみました!

完成品

完成品 (1).gif

作成開始

使用ツール

シフト.png

実装の流れ
1.Teachable Machineで認識したい人の顔を用意
2.4パターンの画像をTeachable Machineで機械学習させる
3.CodePenを利用してカメラで顔を撮影し、どのパターンを指しているかを判別
4.判別した人の本日のシフトをCodePen画面上に表示させる。

このような形で実装していきます。

Teachable Machineで機械学習

Teachable Machineで4パターンを機械学習させます。今回はプライバシーの都合で人ではなく「ビール缶」を使用(笑)

ビール.jpeg

銘柄の違うビール缶を4つ用意しました。ここで言うビール缶は人の顔の画像と同じ意味で捉えてください。こちらをTeachable Machineのサイトにで機械学習させていきます。
まずサイトにアクセスし、画像プロジェクト→標準画像モデルを選択。

画面のClass1、Class2と書かれたボックスがありますので、それぞれに名前を入れますが、「Aさん」「Bさん」などとGoogleスプレッドシート一致させてください。画面の「クラスを追加」をクリックすれば判別する数を増やすことができます。今回は4パターンなのでクラスを4つまで追加します。

次にクラスごとに画像サンプルを追加します。ウェブカメラをクリックするとカメラ画面が出てきますので、判別したいビール缶をカメラに移し、「長押しして録画」を押している間だけ、カメラに写っているものがサンプルとして追加されていきます。1つのクラスに対し最低でも100枚ほどあればOKです。

そしてトレーニングをクリックすると機械学習が始まります(サンプルが多ければ多いほど時間がかかります)。学習が終了したら右側にプレビュー画面が表示されるので、実際にビール缶をカメラに映してみて、正しく判別できているか確認してみましょう。うまくできていれば、右上のモデルをエクスポートをクリックしましょう。

画面の赤枠の箇所に「モデルをアップロード」とあるので、アップロードすると共有可能なリンクにURLが表示されます。このURLは後に使用するので、どこかに控えておきましょう。
その後、左上のメニューから、「ドライブにプロジェクトを保存」もしくは「プロジェクトをファイルとしてダウンロード」して、プロジェクトの保存もしておきましょう。ドライブに保存する場合は、Googleドライブから「アプリからのアクセスを許可するか」のダイアログが出てきますので許可しておいてください。
Teachable Machineの処理はここで終了になります。

CodePenでカメラ判定し、結果のシフトを表示

こちらの処理では、

  1. CodePen上でカメラを起動し、撮影と判別を行い、判別したデータをGoogleスプレッドシートへ送る
  2. GASを使って、判別したデータとシフト表を照合
  3. 照合した結果、正しいシフトをCodePen上に返す

という処理となっています。

CodePenにコードを記入。

CodePenの構成図は以下の通りです。

codepen.png

HTML、CSS、JavaScript(JS)の3つに分かれていることがわかります。CSSに関してはユーザーインターフェースのデザインなので、実際にはなくても動作しますので割愛します。HTMLとJSは一つにまとめて書くこともできますが、分けたほうが見やすいため今回は分けて記載しています。それぞれのコードはこちら。

HTML
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>カメラ判別</title>
  <style>
    #webcam-container canvas {
      border: 1px solid black;
    }
    #snapshotResult {
      margin-top: 10px;
      font-size: 18px;
    }
    button {
      margin: 5px;
    }
  </style>

  <!-- ライブラリ -->
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@3.9.0"></script>
  <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
</head>
<body>
  <h2>カメラ映像&撮影判別</h2>

  <div id="webcam-container"></div>

  <button id="startButton">カメラ起動</button>
  <button id="stopButton">カメラ停止</button>
  <button id="predictButton">撮影&判別</button>

  <div id="snapshotResult">カメラ待機中</div>
</body>
</html>

JS

JS
const URL = "先ほどTeachable Machineで控えたURL"; //ここ重要!!!!
const snapshotResult = document.getElementById("snapshotResult");
let model, webcam, isPredicting = false;

async function fetchShiftTime(name) {
  const today = formatDateSlash(); // "2025/7/9を、2025/07/09(ゼロ埋め)にする関数を呼び出す。
  const webhookURL = "MakeのWebhookモジュールのURL"; //Makeと連携する必要があります。//ここも重要

  try {
    const response = await fetch(webhookURL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ 名前: name, 日付: today })
    });

    const data = await response.json();
    return data.shift || "データが見つかりません";
  } catch (error) {
    console.error("勤務時間取得エラー:", error);
    return "エラー";
  }
}

// 2025/7/9→2025/07/09にする関数
function formatDateSlash(date = new Date()) {
  const y = date.getFullYear();
  const m = String(date.getMonth() + 1).padStart(2, '0'); // 月(0始まり)
  const d = String(date.getDate()).padStart(2, '0');
  return `${y}/${m}/${d}`; // 例: "2025/07/09"
}

document.getElementById("startButton").onclick = async () => {
  if (isPredicting) return;
  snapshotResult.textContent = "カメラ起動中...";
  model = await tmImage.load(URL + "model.json", URL + "metadata.json");
  webcam = new tmImage.Webcam(320, 240, false);
  await webcam.setup();
  await webcam.play();
  document.getElementById("webcam-container").appendChild(webcam.canvas);
  window.requestAnimationFrame(loop);
  isPredicting = true;
  snapshotResult.textContent = "撮影をしてください"; // 起動直後の表示
};

document.getElementById("stopButton").onclick = () => {
  if (!isPredicting) return;
  webcam.stop();
  webcam.canvas.remove();
  snapshotResult.textContent = "カメラ待機中";
  isPredicting = false;
};

async function loop() {
  if (!isPredicting) return;
  webcam.update();
  await new Promise(resolve => setTimeout(resolve, 100));
  window.requestAnimationFrame(loop);
}

document.getElementById("predictButton").onclick = async () => {
  if (!isPredicting) {
    snapshotResult.textContent = "カメラ待機中";
    return;
  }

  webcam.update();
  const prediction = await model.predict(webcam.canvas);
  prediction.sort((a, b) => b.probability - a.probability);
  const top = prediction[0];
  const label = top.className;
  const confidence = (top.probability * 100).toFixed(1);
  snapshotResult.textContent = `📸 判別結果:${label}${confidence}%)`;

  // ここで勤務時間取得
  const shift = await fetchShiftTime(label);
  snapshotResult.textContent += `\n 勤務時間:${shift}`;
};

CSSコードを見たい方は開いてください。
css
body {
    font-family: Arial, sans-serif;
    text-align: center;
    margin: 0;
    padding: 20px;
    background-color: #f0f0f5;
    color: #333;
}
h1 {
    font-size: 24px;
    margin-bottom: 15px;
}
button {
    padding: 8px 16px;
    margin: 5px;
    font-size: 16px;
    cursor: pointer;
    background-color: #007bff;
    color: #fff;
    border: none;
    border-radius: 5px;
    transition: background 0.3s;
}
button:hover {
    background-color: #0056b3;
}

#webcam-container {
    margin-top: 15px;
}
#status-label {
    margin-top: 10px;
    font-size: 18px;
    color: #444;
}
#result-label {
    margin-top: 15px;
    font-size: 18px;
    font-weight: bold;
    color: #333;
}
#snapshot-container {
    margin-top: 15px;
}
この通りのコードを貼れば先ほどのCodePen構成図のような画面になります。

画面のカメラ起動をクリックすると、カメラが起動します。その後、撮影&判別をクリックした時点で判別した結果が画面下に表示されると同時に、Googleスプレッドシートに結果が送られます。

GASを利用しGoogleスプレッドシート上でデータの照合

CodePenから送られてきたデータをGASを活用して、Googleスプレッドシートのシフトと照合します。Googleスプレッドシート(シフト表)を開き、画面上の拡張機能→Apps Scriptをクリック。

コードを書く画面になるので、以下のコードを書いて、保存してください。

function doPost(e) {
  const params = JSON.parse(e.postData.contents);
  const targetDate = params.日付; // 例: "2025/07/10"
  const targetName = params.名前; // 例: "Bさん"

  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("シート1");

  const data = sheet.getDataRange().getValues(); // 全データ取得
  const headers = data[0]; // 1行目:列名(Aさん、Bさん…)
  const nameIndex = headers.indexOf(targetName); // 「Bさん」などの列番号を取得

  if (nameIndex === -1) {
    return ContentService.createTextOutput(
      JSON.stringify({ shift: "名前が見つかりません" })
    ).setMimeType(ContentService.MimeType.JSON);
  }

  for (let i = 1; i < data.length; i++) {
    const rowDate = Utilities.formatDate(new Date(data[i][0]), "Asia/Tokyo", "yyyy/MM/dd");
    if (rowDate === targetDate) {
      const shift = data[i][nameIndex];
      return ContentService.createTextOutput(
        JSON.stringify({ shift: shift })
      ).setMimeType(ContentService.MimeType.JSON);
    }
  }

  return ContentService.createTextOutput(
    JSON.stringify({ shift: "該当する日付が見つかりません" })
  ).setMimeType(ContentService.MimeType.JSON);
}

次にAPIキーを取得します。画面左の歯車をクリック→プロジェクトの設定をクリック。
画面を下にスクロールして、スクリプトプロパティを追加をクリックし、下画像のように設定してください。
値には任意の文字列でOKです。この値は後に使用するので控えておいてください。

APIキー取得できたら、右上のデプロイをクリック→新しいデプロイをクリック
デプロイのタイプを選択してくださいとあるので、画面左上の歯車をクリック→ウェブアプリをクリック
その後、下画像のように設定してください。
設定してデプロイした後にウェブアプリURLが記載されますので、コピーして控えておいてください。

これでGASの設定は終了になります。

CodePenとGASを直接連携できれば早いのですが、CORS制限(クロスドメイン制限) に引っかかってしまいうまくいきませんでした。よって、一度Makeを介してGASと連携を行っていきます。

CORS制限とは、ブラウザがセキュリティのために、他のドメインへ勝手にアクセスするのを制限する仕組みです。正式にはCORS(Cross-Origin Resource Sharing:クロスオリジン リソース共有)と呼ばれます。

Makeを介して、GASと連携し、CodePenに結果を出力

Makeの構造は以下のようになります。

make1.png

  1. Wenhooksモジュールで、CodePenからのデータを受け取ります。ここのWebhookURLをCodePenに貼り付けてください

  2. HTTP→Make a requestモジュールでデータをGASに送ります。下図のように設定してください(縦長なので2分割しています)。URLは先ほどGASでデプロイした後に得たURLを貼り付けます。Request contentには、下記のように入れます

make2.png

{
  "apyKey":"先ほど控えたAPIキー",
  "名前": "{{1.名前}}",
  "日付": "{{1.日付}}"
}

この段階で一度Run once(1度実行) してください。CodePenに戻り、カメラ起動→撮影&判別を押すことでエラーは出ますが起動します。

3. HTTP→Make a requestモジュールをもう一つ。下図のように設定してください。URLには候補で出てくる Headers[]→value をクリックすると、間に文字打ち込めるので、7と入れます。

ここでもう一度先ほどと同じ手順でRun onceしてください。まだデータは返ってきません。

4. Webhooks→Webhook responseモジュールで、Bodyに下記のように入れてください。先ほどRun onceしていないと、4.data:shift が出てきません。

{
"shift": "{{4.data.shift}}" 
}

以上でアプリの設定が完了になります。それぞれビール缶を読み取ると正しくシフトを出してくれます。

最後に

今回はTeachable MachineやGAS、Makeを活用して、機械学習による画像判別を用いた業務改善に取り組みました。実際に社内PCで行うことを想定していますが、従業員全員の顔のサンプルを取ることや、社内PCではTeachable Machineのウェブサイトを始めインターネットへの接続ができないことなど、実装にはいろいろな障害があると考えられます。しかし、無料のデジタルツールだけでもここまでできるという自信がつきました。今後も様々なデジタルツールに触って、いろいろできるようになりたいなーと思います。

ここまで見ていただき。ありがとうございました!

6
1
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
6
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?