soiz1 commited on
Commit
2135c8f
·
verified ·
1 Parent(s): 7a283eb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +556 -720
index.html CHANGED
@@ -5,8 +5,8 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>複合認識システム</title>
7
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.3.1/dist/tf.min.js"></script>
8
- <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/pose@0.8/dist/teachablemachine-pose.min.js"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
 
10
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>
11
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
12
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
@@ -16,62 +16,60 @@
16
  margin: 0;
17
  padding: 20px;
18
  background-color: #f5f5f5;
19
- transition: background-color 0.5s;
20
  }
21
- .alarm-active {
22
- background-color: rgba(255, 0, 0, 0.3);
23
- animation: blink 1s infinite;
24
  }
25
- @keyframes blink {
26
- 0% { background-color: rgba(255, 0, 0, 0.3); }
27
- 50% { background-color: rgba(255, 0, 0, 0.1); }
28
- 100% { background-color: rgba(255, 0, 0, 0.3); }
29
- }
30
- .camera-container {
31
  display: flex;
32
- flex-direction: column;
33
  align-items: center;
34
  margin-bottom: 20px;
35
  }
36
- .detection-info {
 
 
 
 
 
 
 
37
  display: flex;
38
- justify-content: space-around;
39
- width: 100%;
40
- margin-top: 10px;
41
  padding: 10px;
42
- background-color: #fff;
43
- border-radius: 5px;
44
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
45
  }
46
- .info-box {
47
  display: flex;
48
- flex-direction: column;
49
- align-items: center;
50
  }
51
- .bar-chart {
52
- display: flex;
53
- height: 100px;
54
- align-items: flex-end;
55
- margin-top: 10px;
 
56
  }
57
- .bar {
58
- width: 30px;
59
- margin: 0 5px;
60
  background-color: #4CAF50;
61
- transition: height 0.3s;
62
  }
63
  .tile-container {
64
  display: flex;
65
  flex-wrap: wrap;
66
- gap: 15px;
67
- margin-top: 20px;
68
  }
69
  .tile {
70
  background-color: white;
71
  border-radius: 8px;
72
  padding: 15px;
73
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
74
- width: 300px;
 
75
  }
76
  .tile-header {
77
  display: flex;
@@ -81,242 +79,318 @@
81
  }
82
  .tile-title {
83
  font-weight: bold;
84
- font-size: 16px;
85
  }
86
  .delete-btn {
87
  background-color: #ff4444;
88
  color: white;
89
  border: none;
90
  border-radius: 4px;
91
- padding: 5px 10px;
92
  cursor: pointer;
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
94
  .add-tile {
95
  display: flex;
96
- flex-direction: column;
97
- align-items: center;
98
  justify-content: center;
 
 
99
  cursor: pointer;
100
- background-color: #e9e9e9;
101
- }
102
- .add-btn {
103
  font-size: 24px;
104
- margin-bottom: 10px;
105
  }
106
- .url-input {
 
 
 
 
 
 
 
107
  width: 100%;
108
- padding: 8px;
109
- margin-bottom: 10px;
110
- border: 1px solid #ddd;
111
- border-radius: 4px;
 
112
  }
113
- .alert-options {
114
- margin-top: 10px;
 
 
 
115
  }
116
- .alert-option {
117
- margin-bottom: 8px;
 
 
 
118
  }
119
- canvas {
120
- border-radius: 8px;
 
121
  }
122
- select, button {
123
- padding: 8px 12px;
124
- border-radius: 4px;
 
 
 
 
 
 
 
125
  border: 1px solid #ddd;
 
126
  }
127
- button {
128
- background-color: #4285F4;
129
  color: white;
130
  border: none;
 
 
131
  cursor: pointer;
132
  }
 
 
 
 
 
 
 
 
 
 
133
  .eye-status {
 
 
 
 
 
134
  font-weight: bold;
135
- margin-top: 5px;
136
  }
137
- .open-eye { color: #4CAF50; }
138
- .closed-eye { color: #F44336; }
139
  </style>
140
  </head>
141
  <body>
142
- <div class="camera-container">
143
- <div style="display: flex; gap: 20px;">
144
- <canvas id="pose-canvas" width="200" height="200"></canvas>
145
- <canvas id="image-canvas" width="200" height="200"></canvas>
146
- <canvas id="face-canvas" width="200" height="200"></canvas>
147
  </div>
148
- <div class="detection-info">
149
- <div class="info-box">
150
- <div>ポーズ認識</div>
151
- <div class="bar-chart" id="pose-bars"></div>
152
- <div id="pose-strength">強度: 0%</div>
 
 
 
 
 
 
 
 
 
 
 
 
153
  </div>
154
- <div class="info-box">
155
- <div>画像認識</div>
156
- <div class="bar-chart" id="image-bars"></div>
157
- <div id="image-strength">強度: 0%</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  </div>
159
- <div class="info-box">
160
- <div>目つぶり検出</div>
161
- <div id="eye-status" class="eye-status">検出なし</div>
162
- <div id="eye-ear">EAR: 0.00</div>
163
- <select id="eye-alert-mode">
164
- <option value="none">検出しない</option>
165
- <option value="open">目を開いている場合</option>
166
- <option value="closed">目をつぶっている場合</option>
167
  </select>
168
  </div>
 
 
 
 
 
 
 
 
 
169
  </div>
170
  </div>
171
-
172
- <div class="tile-container" id="tile-container">
173
- <div class="tile add-tile" id="add-tile">
174
- <div class="add-btn">+</div>
175
- <div>新しいモデルを追加</div>
176
- </div>
177
- </div>
178
-
179
- <audio id="alarm-sound" loop>
180
- <source src="https://assets.mixkit.co/active_storage/sfx/2593/2593-preview.mp3" type="audio/mpeg">
181
- </audio>
182
-
183
  <script>
184
  // グローバル変数
185
- let poseModel, imageModel, poseWebcam, imageWebcam, faceWebcam;
186
- let poseCtx, imageCtx, faceCtx;
187
- let poseMaxPredictions, imageMaxPredictions;
188
  let faceMesh = null;
189
- let alarmSound = document.getElementById('alarm-sound');
190
- let alarmActive = false;
191
- let eyeAlertMode = 'none';
192
- let eyeEAR = 0;
193
- let eyeStatus = 'none';
194
- let poseDetectionCount = 0;
195
- let imageDetectionCount = 0;
196
  let eyeDetectionCount = 0;
197
- let poseAlarmConditions = [];
198
- let imageAlarmConditions = [];
199
- let eyeAlarmCondition = false;
200
- let poseAlarmTriggered = false;
201
- let imageAlarmTriggered = false;
202
- let eyeAlarmTriggered = false;
203
- let poseAlarmCount = 0;
204
- let imageAlarmCount = 0;
205
- let eyeAlarmCount = 0;
206
- let noPoseAlarmCount = 0;
207
- let noImageAlarmCount = 0;
208
- let noEyeAlarmCount = 0;
209
- const DETECTION_THRESHOLD = 40; // 50回中40回以上
210
- const RESET_THRESHOLD = 30; // リセット閾値
211
-
212
- // 目つぶり検出用の定数
213
- const EYE_CLOSED_THRESHOLD = 0.2;
214
- const EYE_OPEN_THRESHOLD = 0.3;
215
- const LEFT_EYE_TOP = 159;
216
- const LEFT_EYE_BOTTOM = 145;
217
- const LEFT_EYE_LEFT = 33;
218
- const LEFT_EYE_RIGHT = 133;
219
- const RIGHT_EYE_TOP = 386;
220
- const RIGHT_EYE_BOTTOM = 374;
221
- const RIGHT_EYE_LEFT = 362;
222
- const RIGHT_EYE_RIGHT = 263;
223
-
224
- // 初期化関数
225
- async function init() {
226
- // 目つぶり検出モードの変更を監視
227
- document.getElementById('eye-alert-mode').addEventListener('change', function() {
228
- eyeAlertMode = this.value;
229
- resetEyeAlarm();
230
- });
231
-
232
- // タイル追加ボタン
233
- document.getElementById('add-tile').addEventListener('click', addNewTile);
234
-
235
- // Face Meshの初期化
236
- initFaceMesh();
237
- }
238
-
239
- // Face Meshの初期化
240
- function initFaceMesh() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  faceMesh = new FaceMesh({
242
  locateFile: (file) => {
243
  return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`;
244
  }
245
  });
246
-
247
  faceMesh.setOptions({
248
  maxNumFaces: 1,
249
  refineLandmarks: true,
250
  minDetectionConfidence: 0.5,
251
  minTrackingConfidence: 0.5
252
  });
253
-
254
- faceMesh.onResults(onFaceResults);
255
-
256
- faceWebcam = new Camera(document.getElementById('face-canvas'), {
257
- onFrame: async () => {
258
- await faceMesh.send({image: faceWebcam.video});
259
- },
260
- width: 200,
261
- height: 200
262
- });
263
- faceWebcam.start();
264
- }
265
-
266
- // Face Meshの結果処理
267
- function onFaceResults(results) {
268
- faceCtx = document.getElementById('face-canvas').getContext('2d');
269
- faceCtx.clearRect(0, 0, faceWebcam.video.width, faceWebcam.video.height);
270
- faceCtx.save();
271
- faceCtx.drawImage(results.image, 0, 0, faceWebcam.video.width, faceWebcam.video.height);
272
 
273
- if (results.multiFaceLandmarks) {
274
- for (const landmarks of results.multiFaceLandmarks) {
275
- drawConnectors(faceCtx, landmarks, FACEMESH_TESSELATION, {color: '#C0C0C070', lineWidth: 1});
276
- drawConnectors(faceCtx, landmarks, FACEMESH_RIGHT_EYE, {color: '#FF3030'});
277
- drawConnectors(faceCtx, landmarks, FACEMESH_RIGHT_EYEBROW, {color: '#FF3030'});
278
- drawConnectors(faceCtx, landmarks, FACEMESH_LEFT_EYE, {color: '#30FF30'});
279
- drawConnectors(faceCtx, landmarks, FACEMESH_LEFT_EYEBROW, {color: '#30FF30'});
280
- drawConnectors(faceCtx, landmarks, FACEMESH_FACE_OVAL, {color: '#E0E0E0'});
281
- drawConnectors(faceCtx, landmarks, FACEMESH_LIPS, {color: '#E0E0E0'});
282
-
283
- // 目つぶり検出
284
- const leftEAR = getEAR(
285
- landmarks,
286
- LEFT_EYE_TOP, LEFT_EYE_BOTTOM,
287
- LEFT_EYE_LEFT, LEFT_EYE_RIGHT
288
- );
289
-
290
- const rightEAR = getEAR(
291
- landmarks,
292
- RIGHT_EYE_TOP, RIGHT_EYE_BOTTOM,
293
- RIGHT_EYE_LEFT, RIGHT_EYE_RIGHT
294
- );
295
-
296
- eyeEAR = ((leftEAR + rightEAR) / 2).toFixed(2);
297
- document.getElementById('eye-ear').textContent = `EAR: ${eyeEAR}`;
298
-
299
- let newEyeStatus = 'none';
300
- if (eyeEAR < EYE_CLOSED_THRESHOLD) {
301
- newEyeStatus = 'closed';
302
- } else if (eyeEAR > EYE_OPEN_THRESHOLD) {
303
- newEyeStatus = 'open';
304
- }
305
-
306
- if (newEyeStatus !== eyeStatus) {
307
- eyeStatus = newEyeStatus;
308
- updateEyeStatusDisplay();
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  }
310
-
311
- // 目つぶり警報のチェック
312
- checkEyeAlarm();
313
  }
314
- }
 
 
315
 
316
- faceCtx.restore();
 
 
 
 
 
 
 
 
317
  }
318
-
319
- // EAR (Eye Aspect Ratio) 計算
320
  function getEAR(landmarks, topIdx, bottomIdx, leftIdx, rightIdx) {
321
  const vertical = Math.hypot(
322
  landmarks[topIdx].x - landmarks[bottomIdx].x,
@@ -328,533 +402,295 @@
328
  );
329
  return vertical / horizontal;
330
  }
331
-
332
- // 目の状態表示を更新
333
- function updateEyeStatusDisplay() {
334
- const eyeStatusElement = document.getElementById('eye-status');
335
- eyeStatusElement.textContent = eyeStatus === 'closed' ? '目をつぶっています' :
336
- eyeStatus === 'open' ? '目を開いています' : '検出なし';
337
-
338
- eyeStatusElement.className = 'eye-status ' +
339
- (eyeStatus === 'closed' ? 'closed-eye' :
340
- eyeStatus === 'open' ? 'open-eye' : '');
341
- }
342
-
343
- // 目つぶり警報をチェック
344
- function checkEyeAlarm() {
345
- if (eyeAlertMode === 'none') {
346
- eyeDetectionCount = 0;
347
- eyeAlarmCount = 0;
348
- noEyeAlarmCount = 0;
349
- return;
350
- }
351
-
352
- eyeDetectionCount++;
353
-
354
- const shouldAlarm =
355
- (eyeAlertMode === 'closed' && eyeStatus === 'closed') ||
356
- (eyeAlertMode === 'open' && eyeStatus === 'open');
357
-
358
- if (shouldAlarm) {
359
- eyeAlarmCount++;
360
- noEyeAlarmCount = 0;
361
  } else {
362
- noEyeAlarmCount++;
363
- }
364
-
365
- // 警報をトリガーするかチェック
366
- if (eyeAlarmCount >= DETECTION_THRESHOLD && !eyeAlarmTriggered) {
367
- eyeAlarmTriggered = true;
368
- triggerAlarm('eye');
369
- }
370
- // 警報を停止するかチェック
371
- else if (noEyeAlarmCount >= RESET_THRESHOLD && eyeAlarmTriggered) {
372
- resetEyeAlarm();
373
- }
374
- }
375
-
376
- // 目つぶり警報をリセット
377
- function resetEyeAlarm() {
378
- eyeAlarmTriggered = false;
379
- eyeAlarmCount = 0;
380
- noEyeAlarmCount = 0;
381
- eyeDetectionCount = 0;
382
- checkOverallAlarm();
383
- }
384
-
385
- // ポーズモデルの初期化
386
- async function initPoseModel(modelURL, metadataURL) {
387
- poseModel = await tmPose.load(modelURL, metadataURL);
388
- poseMaxPredictions = poseModel.getTotalClasses();
389
-
390
- poseWebcam = new tmPose.Webcam(200, 200, true);
391
- await poseWebcam.setup();
392
- await poseWebcam.play();
393
-
394
- poseCtx = document.getElementById('pose-canvas').getContext('2d');
395
- window.requestAnimationFrame(poseLoop);
396
- }
397
-
398
- // ポーズ認識ループ
399
- async function poseLoop() {
400
- poseWebcam.update();
401
- await predictPose();
402
- window.requestAnimationFrame(poseLoop);
403
- }
404
-
405
- // ポーズ予測
406
- async function predictPose() {
407
- const { pose, posenetOutput } = await poseModel.estimatePose(poseWebcam.canvas);
408
- const prediction = await poseModel.predict(posenetOutput);
409
-
410
- // バーチャートの更新
411
- updateBarChart('pose-bars', prediction);
412
-
413
- // 警報条件のチェック
414
- checkPoseAlarmConditions(prediction);
415
-
416
- // ポーズを描画
417
- drawPose(pose);
418
- }
419
-
420
- // ポーズを描画
421
- function drawPose(pose) {
422
- if (poseWebcam.canvas) {
423
- poseCtx.drawImage(poseWebcam.canvas, 0, 0);
424
- if (pose) {
425
- const minPartConfidence = 0.5;
426
- tmPose.drawKeypoints(pose.keypoints, minPartConfidence, poseCtx);
427
- tmPose.drawSkeleton(pose.keypoints, minPartConfidence, poseCtx);
428
- }
429
  }
430
  }
431
-
432
- // 画像モデルの初期化
433
- async function initImageModel(modelURL, metadataURL) {
434
- imageModel = await tmImage.load(modelURL, metadataURL);
435
- imageMaxPredictions = imageModel.getTotalClasses();
436
-
437
- imageWebcam = new tmImage.Webcam(200, 200, true);
438
- await imageWebcam.setup();
439
- await imageWebcam.play();
440
-
441
- imageCtx = document.getElementById('image-canvas').getContext('2d');
442
- window.requestAnimationFrame(imageLoop);
443
- }
444
-
445
- // 画像認識ループ
446
- async function imageLoop() {
447
- imageWebcam.update();
448
- await predictImage();
449
- window.requestAnimationFrame(imageLoop);
450
- }
451
-
452
- // 画像予測
453
- async function predictImage() {
454
- const prediction = await imageModel.predict(imageWebcam.canvas);
455
-
456
- // バーチャートの更新
457
- updateBarChart('image-bars', prediction);
458
-
459
- // 警報条件のチェック
460
- checkImageAlarmConditions(prediction);
461
- }
462
-
463
- // バーチャートを更新
464
- function updateBarChart(chartId, predictions) {
465
- const chart = document.getElementById(chartId);
466
- chart.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
467
 
468
- let maxProbability = 0;
469
- predictions.forEach(pred => {
470
- if (pred.probability > maxProbability) {
471
- maxProbability = pred.probability;
472
- }
473
 
474
- const bar = document.createElement('div');
475
- bar.className = 'bar';
476
- bar.style.height = `${pred.probability * 100}%`;
477
- bar.title = `${pred.className}: ${(pred.probability * 100).toFixed(1)}%`;
478
- chart.appendChild(bar);
479
- });
480
-
481
- // 強度表示を更新
482
- if (chartId === 'pose-bars') {
483
- document.getElementById('pose-strength').textContent = `強度: ${(maxProbability * 100).toFixed(0)}%`;
484
- } else {
485
- document.getElementById('image-strength').textContent = `強度: ${(maxProbability * 100).toFixed(0)}%`;
486
- }
487
- }
488
-
489
- // ポーズ警報条件をチェック
490
- function checkPoseAlarmConditions(predictions) {
491
- if (poseAlarmConditions.length === 0) return;
492
-
493
- poseDetectionCount++;
494
-
495
- let shouldAlarm = false;
496
- poseAlarmConditions.forEach(condition => {
497
- predictions.forEach(pred => {
498
- if (pred.className === condition.className && pred.probability >= condition.threshold) {
499
- shouldAlarm = true;
500
  }
501
- });
502
- });
503
-
504
- if (shouldAlarm) {
505
- poseAlarmCount++;
506
- noPoseAlarmCount = 0;
507
- } else {
508
- noPoseAlarmCount++;
509
- }
510
-
511
- // 警報をトリガーするかチェック
512
- if (poseAlarmCount >= DETECTION_THRESHOLD && !poseAlarmTriggered) {
513
- poseAlarmTriggered = true;
514
- triggerAlarm('pose');
515
- }
516
- // 警報を停止するかチェック
517
- else if (noPoseAlarmCount >= RESET_THRESHOLD && poseAlarmTriggered) {
518
- resetPoseAlarm();
519
- }
520
- }
521
-
522
- // 画像警報条件をチェック
523
- function checkImageAlarmConditions(predictions) {
524
- if (imageAlarmConditions.length === 0) return;
525
-
526
- imageDetectionCount++;
527
-
528
- let shouldAlarm = false;
529
- imageAlarmConditions.forEach(condition => {
530
- predictions.forEach(pred => {
531
- if (pred.className === condition.className && pred.probability >= condition.threshold) {
532
- shouldAlarm = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  }
534
- });
535
- });
536
-
537
- if (shouldAlarm) {
538
- imageAlarmCount++;
539
- noImageAlarmCount = 0;
540
- } else {
541
- noImageAlarmCount++;
542
- }
543
-
544
- // 警報をトリガーするかチェック
545
- if (imageAlarmCount >= DETECTION_THRESHOLD && !imageAlarmTriggered) {
546
- imageAlarmTriggered = true;
547
- triggerAlarm('image');
548
- }
549
- // 警報を停止するかチェック
550
- else if (noImageAlarmCount >= RESET_THRESHOLD && imageAlarmTriggered) {
551
- resetImageAlarm();
552
- }
553
- }
554
-
555
- // ポーズ警報をリセット
556
- function resetPoseAlarm() {
557
- poseAlarmTriggered = false;
558
- poseAlarmCount = 0;
559
- noPoseAlarmCount = 0;
560
- poseDetectionCount = 0;
561
- checkOverallAlarm();
562
- }
563
-
564
- // 画像警報をリセット
565
- function resetImageAlarm() {
566
- imageAlarmTriggered = false;
567
- imageAlarmCount = 0;
568
- noImageAlarmCount = 0;
569
- imageDetectionCount = 0;
570
- checkOverallAlarm();
571
- }
572
-
573
- // 警報をトリガー
574
- function triggerAlarm(source) {
575
- if (!alarmActive) {
576
- alarmActive = true;
577
- alarmSound.play();
578
- document.body.classList.add('alarm-active');
579
- console.log(`警報が鳴っています (${source})`);
580
- }
581
- }
582
-
583
- // 全体の警報状態をチェック
584
- function checkOverallAlarm() {
585
- if (!poseAlarmTriggered && !imageAlarmTriggered && !eyeAlarmTriggered) {
586
- if (alarmActive) {
587
- alarmActive = false;
588
- alarmSound.pause();
589
- alarmSound.currentTime = 0;
590
- document.body.classList.remove('alarm-active');
591
- console.log('警報が停止しました');
592
- }
593
- }
594
- }
595
-
596
- // 新しいタイルを追加
597
- function addNewTile() {
598
- const tileContainer = document.getElementById('tile-container');
599
- const tileId = 'tile-' + Date.now();
600
-
601
- const tile = document.createElement('div');
602
- tile.className = 'tile';
603
- tile.id = tileId;
604
-
605
- tile.innerHTML = `
606
- <div class="tile-header">
607
- <div class="tile-title">新しいモデル</div>
608
- <button class="delete-btn" data-tile="${tileId}">削除</button>
609
- </div>
610
- <input type="text" class="url-input" placeholder="モデルURL (model.json)" data-tile="${tileId}">
611
- <input type="text" class="url-input" placeholder="メタデータURL (metadata.json)" data-tile="${tileId}">
612
- <button class="load-btn" data-tile="${tileId}">モデルを読み込む</button>
613
- <div class="bar-chart" id="${tileId}-bars" style="height: 100px; margin-top: 10px;"></div>
614
- <div class="alert-options" id="${tileId}-options"></div>
615
- `;
616
-
617
- // 追加ボタンの前に挿入
618
- tileContainer.insertBefore(tile, document.getElementById('add-tile'));
619
-
620
- // イベントリスナーを追加
621
- document.querySelector(`.load-btn[data-tile="${tileId}"]`).addEventListener('click', function() {
622
- loadModelForTile(tileId);
623
- });
624
-
625
- document.querySelector(`.delete-btn[data-tile="${tileId}"]`).addEventListener('click', function() {
626
- tileContainer.removeChild(document.getElementById(tileId));
627
- });
628
- }
629
-
630
- // タイル用のモデルを読み込み
631
- async function loadModelForTile(tileId) {
632
- const modelURL = document.querySelector(`.url-input[data-tile="${tileId}"]:first-of-type`).value;
633
- const metadataURL = document.querySelector(`.url-input[data-tile="${tileId}"]:last-of-type`).value;
634
-
635
- if (!modelURL || !metadataURL) {
636
- alert('モデルURLとメタデータURLを入力してください');
637
- return;
638
- }
639
-
640
- try {
641
- // モデルタイプを判別 (URLに'/pose/'が含まれているかで判断)
642
- const isPoseModel = modelURL.includes('/pose/');
643
-
644
- if (isPoseModel) {
645
- await loadPoseModelForTile(tileId, modelURL, metadataURL);
646
- } else {
647
- await loadImageModelForTile(tileId, modelURL, metadataURL);
648
  }
649
  } catch (error) {
650
- console.error('モデルの読み込みに失敗しました:', error);
651
- alert('モデルの読み込みに失敗しました: ' + error.message);
652
  }
653
  }
654
-
655
- // ポーズルをタイル用に読み込み
656
- async function loadPoseModelForTile(tileId, modelURL, metadataURL) {
657
- const model = await tmPose.load(modelURL, metadataURL);
658
- const maxPredictions = model.getTotalClasses();
659
-
660
- // 警報オプション追加
661
- const optionsContainer = document.getElementById(`${tileId}-options`);
662
- optionsContainer.innerHTML = '';
663
-
664
- // タイトルを更新
665
- document.querySelector(`#${tileId} .tile-title`).textContent = 'ポーズ認識モデル';
666
-
667
- // クラスごとに警報オプションを追加
668
- for (let i = 0; i < maxPredictions; i++) {
669
- const className = model.getClassLabels()[i];
670
-
671
- const optionDiv = document.createElement('div');
672
- optionDiv.className = 'alert-option';
673
- optionDiv.innerHTML = `
674
- <label>
675
- <input type="checkbox" data-class="${className}" data-tile="${tileId}">
676
- ${className} で警報
677
- </label>
678
- <select data-class="${className}" data-tile="${tileId}">
679
- <option value="0.7">70%以上</option>
680
- <option value="0.8">80%以上</option>
681
- <option value="0.9">90%以上</option>
682
- </select>
683
- `;
684
-
685
- optionsContainer.appendChild(optionDiv);
686
-
687
- // チェックボックスの変更を監視
688
- optionDiv.querySelector('input').addEventListener('change', function() {
689
- updatePoseAlarmConditions(tileId);
690
- });
691
-
692
- // セレクトボックスの変更を監視
693
- optionDiv.querySelector('select').addEventListener('change', function() {
694
- updatePoseAlarmConditions(tileId);
695
- });
696
- }
697
-
698
- // 予測ループを開始
699
- startPosePredictionForTile(tileId, model);
700
- }
701
-
702
- // 画像モデルをタイル用に読み込み
703
- async function loadImageModelForTile(tileId, modelURL, metadataURL) {
704
- const model = await tmImage.load(modelURL, metadataURL);
705
- const maxPredictions = model.getTotalClasses();
706
-
707
- // 警報オプションを追加
708
- const optionsContainer = document.getElementById(`${tileId}-options`);
709
- optionsContainer.innerHTML = '';
710
-
711
- // タイトルを更新
712
- document.querySelector(`#${tileId} .tile-title`).textContent = '画像認識モデル';
713
-
714
- // クラスごとに警報オプションを追加
715
- for (let i = 0; i < maxPredictions; i++) {
716
- const className = model.getClassLabels()[i];
717
 
718
- const optionDiv = document.createElement('div');
719
- optionDiv.className = 'alert-option';
720
- optionDiv.innerHTML = `
721
- <label>
722
- <input type="checkbox" data-class="${className}" data-tile="${tileId}">
723
- ${className} で警報
724
- </label>
725
- <select data-class="${className}" data-tile="${tileId}">
726
- <option value="0.7">70%以上</option>
727
- <option value="0.8">80%以上</option>
728
- <option value="0.9">90%以上</option>
729
- </select>
730
- `;
731
-
732
- optionsContainer.appendChild(optionDiv);
733
-
734
- // チェックボックスの変更を監視
735
- optionDiv.querySelector('input').addEventListener('change', function() {
736
- updateImageAlarmConditions(tileId);
737
- });
738
-
739
- // セレクトボックスの変更を監視
740
- optionDiv.querySelector('select').addEventListener('change', function() {
741
- updateImageAlarmConditions(tileId);
742
- });
743
  }
744
-
745
- // 予測ループを開始
746
- startImagePredictionForTile(tileId, model);
747
- }
748
-
749
- // ポーズ予測をタイル用に開始
750
- async function startPosePredictionForTile(tileId, model) {
751
- const canvas = document.createElement('canvas');
752
- canvas.width = 200;
753
- canvas.height = 200;
754
- const ctx = canvas.getContext('2d');
755
-
756
- async function predict() {
757
- const { pose, posenetOutput } = await model.estimatePose(poseWebcam.canvas);
758
- const prediction = await model.predict(posenetOutput);
759
-
760
- // バーチャートを更新
761
- updateTileBarChart(tileId, prediction);
762
 
763
- // ポーズを描画
764
- if (poseWebcam.canvas) {
765
- ctx.drawImage(poseWebcam.canvas, 0, 0);
766
- if (pose) {
767
- const minPartConfidence = 0.5;
768
- tmPose.drawKeypoints(pose.keypoints, minPartConfidence, ctx);
769
- tmPose.drawSkeleton(pose.keypoints, minPartConfidence, ctx);
770
- }
771
  }
772
 
773
- requestAnimationFrame(predict);
774
- }
775
-
776
- predict();
777
- }
778
-
779
- // 画像予測をタイル用に開始
780
- async function startImagePredictionForTile(tileId, model) {
781
- const canvas = document.createElement('canvas');
782
- canvas.width = 200;
783
- canvas.height = 200;
784
- const ctx = canvas.getContext('2d');
785
-
786
- async function predict() {
787
- ctx.drawImage(imageWebcam.canvas, 0, 0);
788
- const prediction = await model.predict(imageWebcam.canvas);
789
-
790
- // バーチャートを更新
791
- updateTileBarChart(tileId, prediction);
792
-
793
- requestAnimationFrame(predict);
794
- }
795
-
796
- predict();
797
- }
798
-
799
- // タイルのバーチャートを更新
800
- function updateTileBarChart(tileId, predictions) {
801
- const chart = document.getElementById(`${tileId}-bars`);
802
- chart.innerHTML = '';
803
-
804
- predictions.forEach(pred => {
805
- const bar = document.createElement('div');
806
- bar.className = 'bar';
807
- bar.style.height = `${pred.probability * 100}%`;
808
- bar.title = `${pred.className}: ${(pred.probability * 100).toFixed(1)}%`;
809
- chart.appendChild(bar);
810
  });
811
  }
812
-
813
- // ポーズ警報条件を更新
814
- function updatePoseAlarmConditions(tileId) {
815
- // 既存の条件を削除 (同じタイルからの条件)
816
- poseAlarmConditions = poseAlarmConditions.filter(cond => !cond.tileId === tileId);
817
-
818
- // 新しい条件を追加
819
- const checkboxes = document.querySelectorAll(`input[type="checkbox"][data-tile="${tileId}"]:checked`);
820
- checkboxes.forEach(checkbox => {
821
- const className = checkbox.dataset.class;
822
- const threshold = parseFloat(document.querySelector(`select[data-class="${className}"][data-tile="${tileId}"]`).value);
823
-
824
- poseAlarmConditions.push({
825
- tileId,
826
- className,
827
- threshold
828
- });
829
- });
830
-
831
- // 警報状態をリセット
832
- resetPoseAlarm();
833
- }
834
-
835
- // 画像警報条件を更新
836
- function updateImageAlarmConditions(tileId) {
837
- // 既存の条件を削除 (同じタイルからの条件)
838
- imageAlarmConditions = imageAlarmConditions.filter(cond => !cond.tileId === tileId);
839
-
840
- // 新しい条件を追加
841
- const checkboxes = document.querySelectorAll(`input[type="checkbox"][data-tile="${tileId}"]:checked`);
842
- checkboxes.forEach(checkbox => {
843
- const className = checkbox.dataset.class;
844
- const threshold = parseFloat(document.querySelector(`select[data-class="${className}"][data-tile="${tileId}"]`).value);
845
-
846
- imageAlarmConditions.push({
847
- tileId,
848
- className,
849
- threshold
850
- });
851
- });
852
 
853
- // 警報状態をリセット
854
- resetImageAlarm();
855
  }
856
-
857
- // 初期化を開始
858
  window.onload = init;
859
  </script>
860
  </body>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>複合認識システム</title>
7
  <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.3.1/dist/tf.min.js"></script>
 
8
  <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/image@0.8/dist/teachablemachine-image.min.js"></script>
9
+ <script src="https://cdn.jsdelivr.net/npm/@teachablemachine/pose@0.8/dist/teachablemachine-pose.min.js"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>
11
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js"></script>
12
  <script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>
 
16
  margin: 0;
17
  padding: 20px;
18
  background-color: #f5f5f5;
 
19
  }
20
+ .container {
21
+ max-width: 1200px;
22
+ margin: 0 auto;
23
  }
24
+ .header {
 
 
 
 
 
25
  display: flex;
26
+ justify-content: space-between;
27
  align-items: center;
28
  margin-bottom: 20px;
29
  }
30
+ .camera-container {
31
+ position: relative;
32
+ margin-bottom: 20px;
33
+ border: 2px solid #333;
34
+ border-radius: 8px;
35
+ overflow: hidden;
36
+ }
37
+ .camera-info {
38
  display: flex;
39
+ justify-content: space-between;
40
+ background-color: #333;
41
+ color: white;
42
  padding: 10px;
 
 
 
43
  }
44
+ .detection-strength {
45
  display: flex;
46
+ gap: 20px;
 
47
  }
48
+ .strength-bar {
49
+ height: 20px;
50
+ width: 100px;
51
+ background-color: #ddd;
52
+ border-radius: 4px;
53
+ overflow: hidden;
54
  }
55
+ .strength-fill {
56
+ height: 100%;
 
57
  background-color: #4CAF50;
58
+ width: 0%;
59
  }
60
  .tile-container {
61
  display: flex;
62
  flex-wrap: wrap;
63
+ gap: 20px;
64
+ margin-bottom: 20px;
65
  }
66
  .tile {
67
  background-color: white;
68
  border-radius: 8px;
69
  padding: 15px;
70
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
71
+ width: calc(33% - 20px);
72
+ position: relative;
73
  }
74
  .tile-header {
75
  display: flex;
 
79
  }
80
  .tile-title {
81
  font-weight: bold;
 
82
  }
83
  .delete-btn {
84
  background-color: #ff4444;
85
  color: white;
86
  border: none;
87
  border-radius: 4px;
88
+ padding: 2px 8px;
89
  cursor: pointer;
90
  }
91
+ .chart-container {
92
+ height: 150px;
93
+ margin-bottom: 10px;
94
+ display: flex;
95
+ align-items: flex-end;
96
+ gap: 5px;
97
+ }
98
+ .chart-bar {
99
+ flex-grow: 1;
100
+ background-color: #4CAF50;
101
+ transition: height 0.3s;
102
+ }
103
  .add-tile {
104
  display: flex;
 
 
105
  justify-content: center;
106
+ align-items: center;
107
+ background-color: #ddd;
108
  cursor: pointer;
 
 
 
109
  font-size: 24px;
 
110
  }
111
+ .add-tile:hover {
112
+ background-color: #ccc;
113
+ }
114
+ .modal {
115
+ display: none;
116
+ position: fixed;
117
+ top: 0;
118
+ left: 0;
119
  width: 100%;
120
+ height: 100%;
121
+ background-color: rgba(0,0,0,0.5);
122
+ justify-content: center;
123
+ align-items: center;
124
+ z-index: 1000;
125
  }
126
+ .modal-content {
127
+ background-color: white;
128
+ padding: 20px;
129
+ border-radius: 8px;
130
+ width: 400px;
131
  }
132
+ .modal-header {
133
+ display: flex;
134
+ justify-content: space-between;
135
+ align-items: center;
136
+ margin-bottom: 15px;
137
  }
138
+ .close-btn {
139
+ font-size: 24px;
140
+ cursor: pointer;
141
  }
142
+ .form-group {
143
+ margin-bottom: 15px;
144
+ }
145
+ label {
146
+ display: block;
147
+ margin-bottom: 5px;
148
+ }
149
+ input, select {
150
+ width: 100%;
151
+ padding: 8px;
152
  border: 1px solid #ddd;
153
+ border-radius: 4px;
154
  }
155
+ .submit-btn {
156
+ background-color: #4CAF50;
157
  color: white;
158
  border: none;
159
+ padding: 10px 15px;
160
+ border-radius: 4px;
161
  cursor: pointer;
162
  }
163
+ .alarm {
164
+ position: fixed;
165
+ top: 0;
166
+ left: 0;
167
+ width: 100%;
168
+ height: 100%;
169
+ background-color: rgba(255,0,0,0.3);
170
+ z-index: 999;
171
+ display: none;
172
+ }
173
  .eye-status {
174
+ display: flex;
175
+ gap: 10px;
176
+ align-items: center;
177
+ }
178
+ .eye-status-label {
179
  font-weight: bold;
 
180
  }
 
 
181
  </style>
182
  </head>
183
  <body>
184
+ <div class="container">
185
+ <div class="header">
186
+ <h1>複合認識システム</h1>
 
 
187
  </div>
188
+
189
+ <div class="camera-container">
190
+ <video id="webcam" width="640" height="480" autoplay playsinline></video>
191
+ <canvas id="output" width="640" height="480"></canvas>
192
+ <div class="camera-info">
193
+ <div class="eye-status">
194
+ <span class="eye-status-label">目の状態:</span>
195
+ <span id="eye-status-text">検出中...</span>
196
+ </div>
197
+ <div class="detection-strength">
198
+ <div>
199
+ <div>認識強度</div>
200
+ <div class="strength-bar">
201
+ <div class="strength-fill" id="strength-fill"></div>
202
+ </div>
203
+ </div>
204
+ </div>
205
  </div>
206
+ </div>
207
+
208
+ <h2>認識モデル設定</h2>
209
+ <div class="tile-container" id="tile-container">
210
+ <!-- タイルはここに動的に追加されます -->
211
+ </div>
212
+
213
+ <div class="tile add-tile" id="add-tile">
214
+
215
+ </div>
216
+ </div>
217
+
218
+ <div class="alarm" id="alarm"></div>
219
+
220
+ <div class="modal" id="modal">
221
+ <div class="modal-content">
222
+ <div class="modal-header">
223
+ <h3>新しいモデルを追加</h3>
224
+ <span class="close-btn" id="close-modal">&times;</span>
225
  </div>
226
+ <div class="form-group">
227
+ <label for="model-type">モデルタイプ</label>
228
+ <select id="model-type">
229
+ <option value="image">画像認識</option>
230
+ <option value="pose">ポーズ認識</option>
 
 
 
231
  </select>
232
  </div>
233
+ <div class="form-group">
234
+ <label for="model-id">モデルID (TeachableMachineのURLの最後の部分)</label>
235
+ <input type="text" id="model-id" placeholder="例: E7pp4SoMG">
236
+ </div>
237
+ <div class="form-group">
238
+ <label for="alarm-class">警報を鳴らすクラス名 (カンマ区切り)</label>
239
+ <input type="text" id="alarm-class" placeholder="例: 危険,警告">
240
+ </div>
241
+ <button class="submit-btn" id="submit-model">追加</button>
242
  </div>
243
  </div>
244
+
245
+ <audio id="alarm-sound" src="https://assets.mixkit.co/sfx/preview/mixkit-alarm-digital-clock-beep-989.mp3" preload="auto"></audio>
246
+
 
 
 
 
 
 
 
 
 
247
  <script>
248
  // グローバル変数
 
 
 
249
  let faceMesh = null;
250
+ let eyeStatus = "検出しない";
 
 
 
 
 
 
251
  let eyeDetectionCount = 0;
252
+ let eyeClosedCount = 0;
253
+ let eyeOpenCount = 0;
254
+ let alarmSound = document.getElementById("alarm-sound");
255
+ let alarmElement = document.getElementById("alarm");
256
+ let modelTiles = [];
257
+ let activeModels = [];
258
+ let detectionHistory = [];
259
+ const HISTORY_SIZE = 50;
260
+ const ALARM_THRESHOLD = 40;
261
+
262
+ // 目の状態を更新する関数
263
+ function updateEyeStatus(newStatus) {
264
+ const eyeStatusText = document.getElementById("eye-status-text");
265
+
266
+ if (newStatus !== eyeStatus) {
267
+ eyeStatus = newStatus;
268
+ eyeStatusText.textContent = eyeStatus;
269
+
270
+ // 警報条件チェック
271
+ checkEyeAlarmCondition();
272
+ }
273
+ }
274
+
275
+ // 目の警報条件チェック
276
+ function checkEyeAlarmCondition() {
277
+ if (eyeStatus === "検出しない") {
278
+ // 警報を止める
279
+ stopAlarm();
280
+ return;
281
+ }
282
+
283
+ // 50回の検出で40回以上条件を満たしたら警報を鳴らす
284
+ if (eyeDetectionCount >= HISTORY_SIZE) {
285
+ if (eyeStatus === "目をつぶっている場合" && eyeClosedCount >= ALARM_THRESHOLD) {
286
+ startAlarm();
287
+ } else if (eyeStatus === "目を開けている場合" && eyeOpenCount >= ALARM_THRESHOLD) {
288
+ startAlarm();
289
+ } else {
290
+ stopAlarm();
291
+ }
292
+ }
293
+ }
294
+
295
+ // 警報を開始
296
+ function startAlarm() {
297
+ alarmElement.style.display = "block";
298
+ alarmSound.currentTime = 0;
299
+ alarmSound.play().catch(e => console.log("Audio play failed:", e));
300
+ }
301
+
302
+ // 警報を停止
303
+ function stopAlarm() {
304
+ alarmElement.style.display = "none";
305
+ alarmSound.pause();
306
+ }
307
+
308
+ // 目の検出を初期化
309
+ async function initEyeDetection() {
310
+ const videoElement = document.getElementById('webcam');
311
+ const canvasElement = document.getElementById('output');
312
+ const canvasCtx = canvasElement.getContext('2d');
313
+
314
  faceMesh = new FaceMesh({
315
  locateFile: (file) => {
316
  return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/${file}`;
317
  }
318
  });
319
+
320
  faceMesh.setOptions({
321
  maxNumFaces: 1,
322
  refineLandmarks: true,
323
  minDetectionConfidence: 0.5,
324
  minTrackingConfidence: 0.5
325
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
327
+ faceMesh.onResults((results) => {
328
+ canvasCtx.save();
329
+ canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
330
+ canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
331
+
332
+ if (results.multiFaceLandmarks) {
333
+ for (const landmarks of results.multiFaceLandmarks) {
334
+ // 左目のEAR (Eye Aspect Ratio) を計算
335
+ const leftEAR = getEAR(
336
+ landmarks,
337
+ 159, 145, 133, 33 // のlandmarkインデックス
338
+ );
339
+
340
+ // 右目のEARを計算
341
+ const rightEAR = getEAR(
342
+ landmarks,
343
+ 386, 374, 362, 263 // 右目のlandmarkインデックス
344
+ );
345
+
346
+ // 両目の平均EAR
347
+ const ear = (leftEAR + rightEAR) / 2;
348
+
349
+ // EARに基づいて目の状��を判定
350
+ const EAR_THRESHOLD = 0.25;
351
+ const isBlinking = ear < EAR_THRESHOLD;
352
+
353
+ // 検出強度を更新
354
+ updateDetectionStrength(ear);
355
+
356
+ // 目の状態を更新
357
+ if (eyeStatus !== "検出しない") {
358
+ eyeDetectionCount = (eyeDetectionCount + 1) % HISTORY_SIZE;
359
+
360
+ if (isBlinking) {
361
+ eyeClosedCount = Math.min(eyeClosedCount + 1, HISTORY_SIZE);
362
+ if (eyeOpenCount > 0) eyeOpenCount--;
363
+
364
+ if (eyeStatus === "目をつぶっている場合") {
365
+ updateEyeStatus("目をつぶっている場合");
366
+ }
367
+ } else {
368
+ eyeOpenCount = Math.min(eyeOpenCount + 1, HISTORY_SIZE);
369
+ if (eyeClosedCount > 0) eyeClosedCount--;
370
+
371
+ if (eyeStatus === "目を開けている場合") {
372
+ updateEyeStatus("目を開けている場合");
373
+ }
374
+ }
375
+ }
376
  }
 
 
 
377
  }
378
+
379
+ canvasCtx.restore();
380
+ });
381
 
382
+ const camera = new Camera(videoElement, {
383
+ onFrame: async () => {
384
+ await faceMesh.send({image: videoElement});
385
+ },
386
+ width: 640,
387
+ height: 480
388
+ });
389
+
390
+ camera.start();
391
  }
392
+
393
+ // EAR (Eye Aspect Ratio) 計算する関数
394
  function getEAR(landmarks, topIdx, bottomIdx, leftIdx, rightIdx) {
395
  const vertical = Math.hypot(
396
  landmarks[topIdx].x - landmarks[bottomIdx].x,
 
402
  );
403
  return vertical / horizontal;
404
  }
405
+
406
+ // 検出強度を更新
407
+ function updateDetectionStrength(ear) {
408
+ const strengthFill = document.getElementById("strength-fill");
409
+ // EARが0.15-0.45の範囲で0-100%にマッピング
410
+ const strength = Math.min(Math.max((ear - 0.15) / (0.45 - 0.15) * 100, 0), 100);
411
+ strengthFill.style.width = `${strength}%`;
412
+
413
+ // 色を変更 (緑→黄→赤)
414
+ if (strength > 50) {
415
+ strengthFill.style.backgroundColor = "#4CAF50"; // 緑
416
+ } else if (strength > 20) {
417
+ strengthFill.style.backgroundColor = "#FFC107"; //
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
  } else {
419
+ strengthFill.style.backgroundColor = "#F44336"; // 赤
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  }
421
  }
422
+
423
+ // モデルをロードしてタイルに追加
424
+ async function addModelTile(modelType, modelId, alarmClasses) {
425
+ const tileContainer = document.getElementById("tile-container");
426
+
427
+ // タイル要素を作成
428
+ const tile = document.createElement("div");
429
+ tile.className = "tile";
430
+ tile.dataset.modelType = modelType;
431
+ tile.dataset.modelId = modelId;
432
+ tile.dataset.alarmClasses = alarmClasses;
433
+
434
+ // タイルヘッダー
435
+ const tileHeader = document.createElement("div");
436
+ tileHeader.className = "tile-header";
437
+
438
+ const tileTitle = document.createElement("div");
439
+ tileTitle.className = "tile-title";
440
+ tileTitle.textContent = `${modelType === 'image' ? '画像認識' : 'ポーズ認識'}: ${modelId}`;
441
+
442
+ const deleteBtn = document.createElement("button");
443
+ deleteBtn.className = "delete-btn";
444
+ deleteBtn.textContent = "×";
445
+ deleteBtn.onclick = () => {
446
+ tileContainer.removeChild(tile);
447
+ modelTiles = modelTiles.filter(t => t !== tile);
448
+ activeModels = activeModels.filter(m => m.tile !== tile);
449
+ };
450
+
451
+ tileHeader.appendChild(tileTitle);
452
+ tileHeader.appendChild(deleteBtn);
453
+ tile.appendChild(tileHeader);
454
+
455
+ // チャートコンテナ
456
+ const chartContainer = document.createElement("div");
457
+ chartContainer.className = "chart-container";
458
+ tile.appendChild(chartContainer);
459
+
460
+ // モデル情報
461
+ const modelInfo = document.createElement("div");
462
+ modelInfo.textContent = `警報クラス: ${alarmClasses}`;
463
+ tile.appendChild(modelInfo);
464
+
465
+ // タイルを追加
466
+ tileContainer.appendChild(tile);
467
+ modelTiles.push(tile);
468
+
469
+ // モデルをロード
470
+ const modelUrl = `https://storage.googleapis.com/tm-model/${modelId}/`;
471
 
472
+ try {
473
+ let model;
474
+ let maxPredictions;
 
 
475
 
476
+ if (modelType === 'image') {
477
+ model = await tmImage.load(`${modelUrl}model.json`, `${modelUrl}metadata.json`);
478
+ maxPredictions = model.getTotalClasses();
479
+
480
+ // チャート用のバーを作成
481
+ for (let i = 0; i < maxPredictions; i++) {
482
+ const bar = document.createElement("div");
483
+ bar.className = "chart-bar";
484
+ bar.dataset.classIndex = i;
485
+ chartContainer.appendChild(bar);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  }
487
+
488
+ // Webcamをセットアップ
489
+ const webcam = new tmImage.Webcam(200, 200, true);
490
+ await webcam.setup();
491
+ await webcam.play();
492
+
493
+ // 予測ループ
494
+ const predictLoop = async () => {
495
+ const prediction = await model.predict(webcam.canvas);
496
+
497
+ // 警報条件チェック
498
+ let shouldAlarm = false;
499
+
500
+ // チャートを更新
501
+ for (let i = 0; i < maxPredictions; i++) {
502
+ const probability = prediction[i].probability;
503
+ const className = prediction[i].className;
504
+
505
+ // バーの高さを更新
506
+ const bars = chartContainer.querySelectorAll(".chart-bar");
507
+ if (bars[i]) {
508
+ bars[i].style.height = `${probability * 100}%`;
509
+
510
+ // 警報クラスの場合は色を赤に
511
+ if (alarmClasses.split(',').includes(className)) {
512
+ bars[i].style.backgroundColor = probability > 0.5 ? "#F44336" : "#4CAF50";
513
+
514
+ if (probability > 0.5) {
515
+ shouldAlarm = true;
516
+ }
517
+ } else {
518
+ bars[i].style.backgroundColor = "#4CAF50";
519
+ }
520
+ }
521
+ }
522
+
523
+ // 警報条件を履歴に追加
524
+ detectionHistory.push(shouldAlarm);
525
+ if (detectionHistory.length > HISTORY_SIZE) {
526
+ detectionHistory.shift();
527
+ }
528
+
529
+ // 警報条件チェック
530
+ if (detectionHistory.length === HISTORY_SIZE) {
531
+ const alarmCount = detectionHistory.filter(Boolean).length;
532
+ if (alarmCount >= ALARM_THRESHOLD) {
533
+ startAlarm();
534
+ } else {
535
+ stopAlarm();
536
+ }
537
+ }
538
+
539
+ requestAnimationFrame(predictLoop);
540
+ };
541
+
542
+ predictLoop();
543
+
544
+ activeModels.push({
545
+ tile,
546
+ model,
547
+ webcam,
548
+ predictLoop
549
+ });
550
+
551
+ } else if (modelType === 'pose') {
552
+ model = await tmPose.load(`${modelUrl}model.json`, `${modelUrl}metadata.json`);
553
+ maxPredictions = model.getTotalClasses();
554
+
555
+ // チャート用のバーを作成
556
+ for (let i = 0; i < maxPredictions; i++) {
557
+ const bar = document.createElement("div");
558
+ bar.className = "chart-bar";
559
+ bar.dataset.classIndex = i;
560
+ chartContainer.appendChild(bar);
561
  }
562
+
563
+ // Webcamをセットアップ
564
+ const webcam = new tmPose.Webcam(200, 200, true);
565
+ await webcam.setup();
566
+ await webcam.play();
567
+
568
+ // 予測ループ
569
+ const predictLoop = async () => {
570
+ const { pose, posenetOutput } = await model.estimatePose(webcam.canvas);
571
+ const prediction = await model.predict(posenetOutput);
572
+
573
+ // 警報条件チェック
574
+ let shouldAlarm = false;
575
+
576
+ // チャートを更新
577
+ for (let i = 0; i < maxPredictions; i++) {
578
+ const probability = prediction[i].probability;
579
+ const className = prediction[i].className;
580
+
581
+ // バーの高さを更新
582
+ const bars = chartContainer.querySelectorAll(".chart-bar");
583
+ if (bars[i]) {
584
+ bars[i].style.height = `${probability * 100}%`;
585
+
586
+ // 警報クラスの場合は色を赤に
587
+ if (alarmClasses.split(',').includes(className)) {
588
+ bars[i].style.backgroundColor = probability > 0.5 ? "#F44336" : "#4CAF50";
589
+
590
+ if (probability > 0.5) {
591
+ shouldAlarm = true;
592
+ }
593
+ } else {
594
+ bars[i].style.backgroundColor = "#4CAF50";
595
+ }
596
+ }
597
+ }
598
+
599
+ // 警報条件を履歴に追加
600
+ detectionHistory.push(shouldAlarm);
601
+ if (detectionHistory.length > HISTORY_SIZE) {
602
+ detectionHistory.shift();
603
+ }
604
+
605
+ // 警報条件チェック
606
+ if (detectionHistory.length === HISTORY_SIZE) {
607
+ const alarmCount = detectionHistory.filter(Boolean).length;
608
+ if (alarmCount >= ALARM_THRESHOLD) {
609
+ startAlarm();
610
+ } else {
611
+ stopAlarm();
612
+ }
613
+ }
614
+
615
+ requestAnimationFrame(predictLoop);
616
+ };
617
+
618
+ predictLoop();
619
+
620
+ activeModels.push({
621
+ tile,
622
+ model,
623
+ webcam,
624
+ predictLoop
625
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  }
627
  } catch (error) {
628
+ console.error("モデルのロードに失敗しました:", error);
629
+ tileTitle.textContent += " (ロード失敗)";
630
  }
631
  }
632
+
633
+ // モーダルを開く
634
+ document.getElementById("add-tile").addEventListener("click", () => {
635
+ document.getElementById("modal").style.display = "flex";
636
+ });
637
+
638
+ // モーダル閉じる
639
+ document.getElementById("close-modal").addEventListener("click", () => {
640
+ document.getElementById("modal").style.display = "none";
641
+ });
642
+
643
+ // モデルを追加
644
+ document.getElementById("submit-model").addEventListener("click", () => {
645
+ const modelType = document.getElementById("model-type").value;
646
+ const modelId = document.getElementById("model-id").value.trim();
647
+ const alarmClasses = document.getElementById("alarm-class").value.trim();
648
+
649
+ if (modelId && alarmClasses) {
650
+ addModelTile(modelType, modelId, alarmClasses);
651
+ document.getElementById("modal").style.display = "none";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
 
653
+ // フォームをリセット
654
+ document.getElementById("model-id").value = "";
655
+ document.getElementById("alarm-class").value = "";
656
+ } else {
657
+ alert("モデルIDと警報クラスを入力してください");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  }
659
+ });
660
+
661
+ // 目の状態選択 (デモ用)
662
+ function setupEyeStatusSelector() {
663
+ const eyeStatusText = document.getElementById("eye-status-text");
664
+
665
+ eyeStatusText.addEventListener("click", () => {
666
+ const currentStatus = eyeStatusText.textContent;
667
+ let newStatus;
 
 
 
 
 
 
 
 
 
668
 
669
+ if (currentStatus === "検出しない") {
670
+ newStatus = "目をつぶっている場合";
671
+ } else if (currentStatus === "目をつぶっている場合") {
672
+ newStatus = "目を開けている場合";
673
+ } else {
674
+ newStatus = "検出しない";
 
 
675
  }
676
 
677
+ updateEyeStatus(newStatus);
678
+ eyeDetectionCount = 0;
679
+ eyeClosedCount = 0;
680
+ eyeOpenCount = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
  });
682
  }
683
+
684
+ // 初期化
685
+ async function init() {
686
+ await initEyeDetection();
687
+ setupEyeStatusSelector();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
+ // デフォルトで目の状態を「検出しない」に設定
690
+ updateEyeStatus("検出しない");
691
  }
692
+
693
+ // ページ読み込み時に初期化
694
  window.onload = init;
695
  </script>
696
  </body>