pose-alert / index.html
soiz1's picture
Update index.html
2135c8f verified
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>複合認識システム</title>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.3.1/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@teachablemachine/pose@0.8/dist/teachablemachine-pose.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.camera-container {
position: relative;
margin-bottom: 20px;
border: 2px solid #333;
border-radius: 8px;
overflow: hidden;
}
.camera-info {
display: flex;
justify-content: space-between;
background-color: #333;
color: white;
padding: 10px;
}
.detection-strength {
display: flex;
gap: 20px;
}
.strength-bar {
height: 20px;
width: 100px;
background-color: #ddd;
border-radius: 4px;
overflow: hidden;
}
.strength-fill {
height: 100%;
background-color: #4CAF50;
width: 0%;
}
.tile-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
}
.tile {
background-color: white;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
width: calc(33% - 20px);
position: relative;
}
.tile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.tile-title {
font-weight: bold;
}
.delete-btn {
background-color: #ff4444;
color: white;
border: none;
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
}
.chart-container {
height: 150px;
margin-bottom: 10px;
display: flex;
align-items: flex-end;
gap: 5px;
}
.chart-bar {
flex-grow: 1;
background-color: #4CAF50;
transition: height 0.3s;
}
.add-tile {
display: flex;
justify-content: center;
align-items: center;
background-color: #ddd;
cursor: pointer;
font-size: 24px;
}
.add-tile:hover {
background-color: #ccc;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background-color: white;
padding: 20px;
border-radius: 8px;
width: 400px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.close-btn {
font-size: 24px;
cursor: pointer;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input, select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.submit-btn {
background-color: #4CAF50;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
}
.alarm {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255,0,0,0.3);
z-index: 999;
display: none;
}
.eye-status {
display: flex;
gap: 10px;
align-items: center;
}
.eye-status-label {
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>複合認識システム</h1>
</div>
<div class="camera-container">
<video id="webcam" width="640" height="480" autoplay playsinline></video>
<canvas id="output" width="640" height="480"></canvas>
<div class="camera-info">
<div class="eye-status">
<span class="eye-status-label">目の状態:</span>
<span id="eye-status-text">検出中...</span>
</div>
<div class="detection-strength">
<div>
<div>認識強度</div>
<div class="strength-bar">
<div class="strength-fill" id="strength-fill"></div>
</div>
</div>
</div>
</div>
</div>
<h2>認識モデル設定</h2>
<div class="tile-container" id="tile-container">
<!-- タイルはここに動的に追加されます -->
</div>
<div class="tile add-tile" id="add-tile">
</div>
</div>
<div class="alarm" id="alarm"></div>
<div class="modal" id="modal">
<div class="modal-content">
<div class="modal-header">
<h3>新しいモデルを追加</h3>
<span class="close-btn" id="close-modal">&times;</span>
</div>
<div class="form-group">
<label for="model-type">モデルタイプ</label>
<select id="model-type">
<option value="image">画像認識</option>
<option value="pose">ポーズ認識</option>
</select>
</div>
<div class="form-group">
<label for="model-id">モデルID (TeachableMachineのURLの最後の部分)</label>
<input type="text" id="model-id" placeholder="例: E7pp4SoMG">
</div>
<div class="form-group">
<label for="alarm-class">警報を鳴らすクラス名 (カンマ区切り)</label>
<input type="text" id="alarm-class" placeholder="例: 危険,警告">
</div>
<button class="submit-btn" id="submit-model">追加</button>
</div>
</div>
<audio id="alarm-sound" src="https://assets.mixkit.co/sfx/preview/mixkit-alarm-digital-clock-beep-989.mp3" preload="auto"></audio>
<script>
// グローバル変数
let faceMesh = null;
let eyeStatus = "検出しない";
let eyeDetectionCount = 0;
let eyeClosedCount = 0;
let eyeOpenCount = 0;
let alarmSound = document.getElementById("alarm-sound");
let alarmElement = document.getElementById("alarm");
let modelTiles = [];
let activeModels = [];
let detectionHistory = [];
const HISTORY_SIZE = 50;
const ALARM_THRESHOLD = 40;
// 目の状態を更新する関数
function updateEyeStatus(newStatus) {
const eyeStatusText = document.getElementById("eye-status-text");
if (newStatus !== eyeStatus) {
eyeStatus = newStatus;
eyeStatusText.textContent = eyeStatus;
// 警報条件チェック
checkEyeAlarmCondition();
}
}
// 目の警報条件チェック
function checkEyeAlarmCondition() {
if (eyeStatus === "検出しない") {
// 警報を止める
stopAlarm();
return;
}
// 50回の検出で40回以上条件を満たしたら警報を鳴らす
if (eyeDetectionCount >= HISTORY_SIZE) {
if (eyeStatus === "目をつぶっている場合" && eyeClosedCount >= ALARM_THRESHOLD) {
startAlarm();
} else if (eyeStatus === "目を開けている場合" && eyeOpenCount >= ALARM_THRESHOLD) {
startAlarm();
} else {
stopAlarm();
}
}
}
// 警報を開始
function startAlarm() {
alarmElement.style.display = "block";
alarmSound.currentTime = 0;
alarmSound.play().catch(e => console.log("Audio play failed:", e));
}
// 警報を停止
function stopAlarm() {
alarmElement.style.display = "none";
alarmSound.pause();
}
// 目の検出を初期化
async function initEyeDetection() {
const videoElement = document.getElementById('webcam');
const canvasElement = document.getElementById('output');
const canvasCtx = canvasElement.getContext('2d');
faceMesh = new FaceMesh({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`;
}
});
faceMesh.setOptions({
maxNumFaces: 1,
refineLandmarks: true,
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5
});
faceMesh.onResults((results) => {
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
if (results.multiFaceLandmarks) {
for (const landmarks of results.multiFaceLandmarks) {
// 左目のEAR (Eye Aspect Ratio) を計算
const leftEAR = getEAR(
landmarks,
159, 145, 133, 33 // 左目のlandmarkインデックス
);
// 右目のEARを計算
const rightEAR = getEAR(
landmarks,
386, 374, 362, 263 // 右目のlandmarkインデックス
);
// 両目の平均EAR
const ear = (leftEAR + rightEAR) / 2;
// EARに基づいて目の状態を判定
const EAR_THRESHOLD = 0.25;
const isBlinking = ear < EAR_THRESHOLD;
// 検出強度を更新
updateDetectionStrength(ear);
// 目の状態を更新
if (eyeStatus !== "検出しない") {
eyeDetectionCount = (eyeDetectionCount + 1) % HISTORY_SIZE;
if (isBlinking) {
eyeClosedCount = Math.min(eyeClosedCount + 1, HISTORY_SIZE);
if (eyeOpenCount > 0) eyeOpenCount--;
if (eyeStatus === "目をつぶっている場合") {
updateEyeStatus("目をつぶっている場合");
}
} else {
eyeOpenCount = Math.min(eyeOpenCount + 1, HISTORY_SIZE);
if (eyeClosedCount > 0) eyeClosedCount--;
if (eyeStatus === "目を開けている場合") {
updateEyeStatus("目を開けている場合");
}
}
}
}
}
canvasCtx.restore();
});
const camera = new Camera(videoElement, {
onFrame: async () => {
await faceMesh.send({image: videoElement});
},
width: 640,
height: 480
});
camera.start();
}
// EAR (Eye Aspect Ratio) を計算する関数
function getEAR(landmarks, topIdx, bottomIdx, leftIdx, rightIdx) {
const vertical = Math.hypot(
landmarks[topIdx].x - landmarks[bottomIdx].x,
landmarks[topIdx].y - landmarks[bottomIdx].y
);
const horizontal = Math.hypot(
landmarks[leftIdx].x - landmarks[rightIdx].x,
landmarks[leftIdx].y - landmarks[rightIdx].y
);
return vertical / horizontal;
}
// 検出強度を更新
function updateDetectionStrength(ear) {
const strengthFill = document.getElementById("strength-fill");
// EARが0.15-0.45の範囲で0-100%にマッピング
const strength = Math.min(Math.max((ear - 0.15) / (0.45 - 0.15) * 100, 0), 100);
strengthFill.style.width = `${strength}%`;
// 色を変更 (緑→黄→赤)
if (strength > 50) {
strengthFill.style.backgroundColor = "#4CAF50"; // 緑
} else if (strength > 20) {
strengthFill.style.backgroundColor = "#FFC107"; // 黄
} else {
strengthFill.style.backgroundColor = "#F44336"; // 赤
}
}
// モデルをロードしてタイルに追加
async function addModelTile(modelType, modelId, alarmClasses) {
const tileContainer = document.getElementById("tile-container");
// タイル要素を作成
const tile = document.createElement("div");
tile.className = "tile";
tile.dataset.modelType = modelType;
tile.dataset.modelId = modelId;
tile.dataset.alarmClasses = alarmClasses;
// タイルヘッダー
const tileHeader = document.createElement("div");
tileHeader.className = "tile-header";
const tileTitle = document.createElement("div");
tileTitle.className = "tile-title";
tileTitle.textContent = `${modelType === 'image' ? '画像認識' : 'ポーズ認識'}: ${modelId}`;
const deleteBtn = document.createElement("button");
deleteBtn.className = "delete-btn";
deleteBtn.textContent = "×";
deleteBtn.onclick = () => {
tileContainer.removeChild(tile);
modelTiles = modelTiles.filter(t => t !== tile);
activeModels = activeModels.filter(m => m.tile !== tile);
};
tileHeader.appendChild(tileTitle);
tileHeader.appendChild(deleteBtn);
tile.appendChild(tileHeader);
// チャートコンテナ
const chartContainer = document.createElement("div");
chartContainer.className = "chart-container";
tile.appendChild(chartContainer);
// モデル情報
const modelInfo = document.createElement("div");
modelInfo.textContent = `警報クラス: ${alarmClasses}`;
tile.appendChild(modelInfo);
// タイルを追加
tileContainer.appendChild(tile);
modelTiles.push(tile);
// モデルをロード
const modelUrl = `https://storage.googleapis.com/tm-model/${modelId}/`;
try {
let model;
let maxPredictions;
if (modelType === 'image') {
model = await tmImage.load(`${modelUrl}model.json`, `${modelUrl}metadata.json`);
maxPredictions = model.getTotalClasses();
// チャート用のバーを作成
for (let i = 0; i < maxPredictions; i++) {
const bar = document.createElement("div");
bar.className = "chart-bar";
bar.dataset.classIndex = i;
chartContainer.appendChild(bar);
}
// Webcamをセットアップ
const webcam = new tmImage.Webcam(200, 200, true);
await webcam.setup();
await webcam.play();
// 予測ループ
const predictLoop = async () => {
const prediction = await model.predict(webcam.canvas);
// 警報条件チェック
let shouldAlarm = false;
// チャートを更新
for (let i = 0; i < maxPredictions; i++) {
const probability = prediction[i].probability;
const className = prediction[i].className;
// バーの高さを更新
const bars = chartContainer.querySelectorAll(".chart-bar");
if (bars[i]) {
bars[i].style.height = `${probability * 100}%`;
// 警報クラスの場合は色を赤に
if (alarmClasses.split(',').includes(className)) {
bars[i].style.backgroundColor = probability > 0.5 ? "#F44336" : "#4CAF50";
if (probability > 0.5) {
shouldAlarm = true;
}
} else {
bars[i].style.backgroundColor = "#4CAF50";
}
}
}
// 警報条件を履歴に追加
detectionHistory.push(shouldAlarm);
if (detectionHistory.length > HISTORY_SIZE) {
detectionHistory.shift();
}
// 警報条件チェック
if (detectionHistory.length === HISTORY_SIZE) {
const alarmCount = detectionHistory.filter(Boolean).length;
if (alarmCount >= ALARM_THRESHOLD) {
startAlarm();
} else {
stopAlarm();
}
}
requestAnimationFrame(predictLoop);
};
predictLoop();
activeModels.push({
tile,
model,
webcam,
predictLoop
});
} else if (modelType === 'pose') {
model = await tmPose.load(`${modelUrl}model.json`, `${modelUrl}metadata.json`);
maxPredictions = model.getTotalClasses();
// チャート用のバーを作成
for (let i = 0; i < maxPredictions; i++) {
const bar = document.createElement("div");
bar.className = "chart-bar";
bar.dataset.classIndex = i;
chartContainer.appendChild(bar);
}
// Webcamをセットアップ
const webcam = new tmPose.Webcam(200, 200, true);
await webcam.setup();
await webcam.play();
// 予測ループ
const predictLoop = async () => {
const { pose, posenetOutput } = await model.estimatePose(webcam.canvas);
const prediction = await model.predict(posenetOutput);
// 警報条件チェック
let shouldAlarm = false;
// チャートを更新
for (let i = 0; i < maxPredictions; i++) {
const probability = prediction[i].probability;
const className = prediction[i].className;
// バーの高さを更新
const bars = chartContainer.querySelectorAll(".chart-bar");
if (bars[i]) {
bars[i].style.height = `${probability * 100}%`;
// 警報クラスの場合は色を赤に
if (alarmClasses.split(',').includes(className)) {
bars[i].style.backgroundColor = probability > 0.5 ? "#F44336" : "#4CAF50";
if (probability > 0.5) {
shouldAlarm = true;
}
} else {
bars[i].style.backgroundColor = "#4CAF50";
}
}
}
// 警報条件を履歴に追加
detectionHistory.push(shouldAlarm);
if (detectionHistory.length > HISTORY_SIZE) {
detectionHistory.shift();
}
// 警報条件チェック
if (detectionHistory.length === HISTORY_SIZE) {
const alarmCount = detectionHistory.filter(Boolean).length;
if (alarmCount >= ALARM_THRESHOLD) {
startAlarm();
} else {
stopAlarm();
}
}
requestAnimationFrame(predictLoop);
};
predictLoop();
activeModels.push({
tile,
model,
webcam,
predictLoop
});
}
} catch (error) {
console.error("モデルのロードに失敗しました:", error);
tileTitle.textContent += " (ロード失敗)";
}
}
// モーダルを開く
document.getElementById("add-tile").addEventListener("click", () => {
document.getElementById("modal").style.display = "flex";
});
// モーダルを閉じる
document.getElementById("close-modal").addEventListener("click", () => {
document.getElementById("modal").style.display = "none";
});
// モデルを追加
document.getElementById("submit-model").addEventListener("click", () => {
const modelType = document.getElementById("model-type").value;
const modelId = document.getElementById("model-id").value.trim();
const alarmClasses = document.getElementById("alarm-class").value.trim();
if (modelId && alarmClasses) {
addModelTile(modelType, modelId, alarmClasses);
document.getElementById("modal").style.display = "none";
// フォームをリセット
document.getElementById("model-id").value = "";
document.getElementById("alarm-class").value = "";
} else {
alert("モデルIDと警報クラスを入力してください");
}
});
// 目の状態選択 (デモ用)
function setupEyeStatusSelector() {
const eyeStatusText = document.getElementById("eye-status-text");
eyeStatusText.addEventListener("click", () => {
const currentStatus = eyeStatusText.textContent;
let newStatus;
if (currentStatus === "検出しない") {
newStatus = "目をつぶっている場合";
} else if (currentStatus === "目をつぶっている場合") {
newStatus = "目を開けている場合";
} else {
newStatus = "検出しない";
}
updateEyeStatus(newStatus);
eyeDetectionCount = 0;
eyeClosedCount = 0;
eyeOpenCount = 0;
});
}
// 初期化
async function init() {
await initEyeDetection();
setupEyeStatusSelector();
// デフォルトで目の状態を「検出しない」に設定
updateEyeStatus("検出しない");
}
// ページ読み込み時に初期化
window.onload = init;
</script>
</body>
</html>