工事写真撮影:オーバーレイ表示機能付きWebカメラアプリ


参考URL
https://darumasantinoagalog.zapto.org/index.php/camera_test

現場での写真撮影、特に工事写真の撮影に特化したWebカメラアプリを作成しました。
このアプリは、撮影時に物件名や工事内容をリアルタイムで画像に合成できるため、後工程での写真整理の手間を大幅に削減できます。
以下に、その具体的な機能とコードを紹介します。

アプリケーションの特徴
リアルタイムオーバーレイ: 撮影画像に、物件名や工事内容をリアルタイムで合成表示。

豊富な黒板デザイン: 用途に合わせて選べる豊富な黒板デザインをプリセット。

簡単な操作性: 直感的なインターフェースで、誰でも簡単に操作可能。

ファイル名自動生成: テキストフィールドの内容と選択した画像に基づいてファイル名を自動生成。

必要な環境
Webブラウザ (Chrome, Firefox, Safariなど)

Webカメラ

インターネット環境 (画像の読み込みに必要)

画面イメージ
画面イメージ

HTML構造
xml
<div id="text-input" style="text-align: center; margin-bottom: 2px; padding-bottom: 2px;">
  <label for="overlay-text">物件名: </label>
  <input type="text" id="overlay-text" placeholder="物件名称" style="padding: 5px; width: 30%;"><span> 様邸新築工事</span><br>
  <label for="overlay-text2">無　地: </label>
  <input type="text" id="overlay-text2" placeholder="工事内容" style="padding: 5px; width: 30%;"><span> 黒板中央表示</span>
</div>

<div id="overlay-image-selector" style="display: flex; justify-content: flex-start; align-items: flex-start; margin-bottom: 2px; overflow-y: auto; height: 250px; padding-bottom: 2px;">
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image1" class="overlay-button selected">FAG1-21</button>
    <button id="image2" class="overlay-button">FAG1-22</button>
    <button id="image3" class="overlay-button">FAG1-23</button>
    <button id="image4" class="overlay-button">FAG1-24</button>
    <button id="image5" class="overlay-button">FAG1-25</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image6" class="overlay-button">FAG2-1</button>
    <button id="image7" class="overlay-button">FAG2-2</button>
    <button id="image8" class="overlay-button">FAG2-3</button>
    <button id="image9" class="overlay-button">FAG2-4</button>
    <button id="image10" class="overlay-button">FAG2-5</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image11" class="overlay-button">FAS1-11</button>
    <button id="image12" class="overlay-button">FAS1-12</button>
    <button id="image13" class="overlay-button">FAS1-13</button>
    <button id="image14" class="overlay-button">FAS1-14</button>
    <button id="image15" class="overlay-button">FAS1-15</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image16" class="overlay-button">FAT1-21</button>
    <button id="image17" class="overlay-button">FAT1-22</button>
    <button id="image18" class="overlay-button">FAT1-23</button>
    <button id="image19" class="overlay-button">FAT1-24</button>
    <button id="image20" class="overlay-button">FAT1-25</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image21" class="overlay-button">FAW1-21</button>
    <button id="image22" class="overlay-button">FAW1-22</button>
    <button id="image23" class="overlay-button">FAW1-23</button>
    <button id="image24" class="overlay-button">FAW1-24</button>
    <button id="image25" class="overlay-button">FAW1-25</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image26" class="overlay-button">FAW1-21_SL</button>
    <button id="image27" class="overlay-button">FAW1-22_SL</button>
    <button id="image28" class="overlay-button">FAW1-23_SL</button>
    <button id="image29" class="overlay-button">FAW1-24_SL</button>
    <button id="image30" class="overlay-button">FAW1-25_SL</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image31" class="overlay-button">FAW1-21(袖)</button>
    <button id="image32" class="overlay-button">FAW1-22(袖)</button>
    <button id="image33" class="overlay-button">FAW1-23(袖)</button>
    <button id="image34" class="overlay-button">FAW1-24(袖)</button>
    <button id="image35" class="overlay-button">FAW1-25(袖)</button>
  </div>
  <div style="display: flex; flex-direction: column; margin-right: 3px;">
    <button id="image36" class="overlay-button">FAW2-1</button>
    <button id="image37" class="overlay-button">FAW2-2</button>
    <button id="image38" class="overlay-button">FAW2-3</button>
    <button id="image39" class="overlay-button">FAW2-4</button>
    <button id="image40" class="overlay-button">FAW2-5</button>
  </div>
  <div style="display: flex; flex-direction: column;">
    <button id="image41" class="overlay-button">SS1-1</button>
    <button id="image42" class="overlay-button">SS1-2</button>
    <button id="image43" class="overlay-button">SS1-3</button>
    <button id="image44" class="overlay-button">SS1-4</button>
    <button id="image45" class="overlay-button">無地</button>
  </div>
</div>

<div id="button-container" style="text-align: center; margin-bottom: 2px; padding-top: 2px;">
  <button id="take-photo" style="display: inline-block; margin-right: 50px; padding: 20px 100px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">撮影</button>
  <a id="download-link" href="#" download="photo.png" style="display: inline-block; padding: 20px 100px; background-color: #17a2b8; color: white; text-decoration: none; border-radius: 5px;">保存</a>
</div>

<div id="camera-app">
  <!-- カメラ映像の枠 -->
  <div id="camera-container">
    <video id="camera-stream" autoplay playsinline></video>
    <img id="overlay-image" src="[画像URL]" 
         style="position: absolute; top: 20px; left: 20px; height: auto; opacity: 0.5; pointer-events: none;">
  </div>

  <!-- 撮影した写真のプレビュー枠 -->
  <div id="photo-container">
    <img id="photo-preview" alt="撮影した写真がここに表示されます">
  </div>

  <canvas id="photo-canvas" style="display:none;"></canvas>
</div>
CSSスタイル
css
  .overlay-button {
    padding: 10px;
    color: white;
    margin-bottom: 10px;
    background-color: #7e7e7e;
    border: none;
    cursor: pointer;
    border-radius: 5px;
    width: 120px;
    text-align: center;
  }

  .overlay-button.selected {
    background-color: #dc3545;
  }

  .overlay-button:hover {
    opacity: 0.8;
  }

  #download-link {
    padding: 10px 20px;
    background-color: #17a2b8;
    color: white;
    text-decoration: none;
    border-radius: 5px;
    margin-top: 10px;
    display: inline-block;
  }

  #button-container button, #button-container a {
    margin-top: 10px;
  }

  /* camera-stream と photo-preview を横並びにするスタイル */
  #camera-app {
    display: flex;
    justify-content: space-between;
    gap: 0px; /* 要素間のスペース */
    flex-wrap: wrap;
    margin-top: 20px;
  }

  /* カメラの映像用の枠 */
  #camera-container {
    border: 2px solid #007bff;
    padding: 10px;
    width: 48%;
    position: relative;
  }

  /* プレビュー画像用の枠 */
  #photo-container {
    border: 2px solid #28a745;
    padding: 10px;
    width: 48%;
    position: relative;
  }

  #camera-stream {
    width: 100%;
    height: auto;
  }

  #photo-preview {
    width: 100%;
    height: auto;
  }
JavaScript
javascript
  // 画像URL
  const imageUrls = [
  '[画像URL1]',
  '[画像URL2]',
  '[画像URL3]',
  '[画像URL4]',
  '[画像URL5]',
  '[画像URL6]',
  '[画像URL7]',
  '[画像URL8]',
  '[画像URL9]',
  '[画像URL10]',
  '[画像URL11]',
  '[画像URL12]',
  '[画像URL13]',
  '[画像URL14]',
  '[画像URL15]',
  '[画像URL16]',
  '[画像URL17]',
  '[画像URL18]',
  '[画像URL19]',
  '[画像URL20]',
  '[画像URL21]',
  '[画像URL22]',
  '[画像URL23]',
  '[画像URL24]',
  '[画像URL25]',
  '[画像URL26]',
  '[画像URL27]',
  '[画像URL28]',
  '[画像URL29]',
  '[画像URL30]',
  '[画像URL31]',
  '[画像URL32]',
  '[画像URL33]',
  '[画像URL34]',
  '[画像URL35]',
  '[画像URL36]',
  '[画像URL37]',
  '[画像URL38]',
  '[画像URL39]',
  '[画像URL40]',
  '[画像URL41]',
  '[画像URL42]',
  '[画像URL43]',
  '[画像URL44]',
  '[画像URL45]'
  ];

  // 全ボタンを対象にイベントリスナーを追加
  const buttons = document.querySelectorAll('.overlay-button');
  const overlayImage = document.getElementById('overlay-image');

  buttons.forEach((button, index) => {
    button.addEventListener('click', () => {
      overlayImage.src = imageUrls[index];
      
      // ボタンの選択状態を更新
      buttons.forEach(btn => btn.classList.remove('selected'));
      button.classList.add('selected');
    });
  });

  // カメラストリームの幅に基づいてオーバーレイ画像の幅を変更する関数
  function adjustOverlayImageWidth() {
    const cameraStream = document.getElementById('camera-stream');
    const cameraWidth = cameraStream.clientWidth;
    overlayImage.style.width = (cameraWidth * 0.35) + 'px';
  }

  window.onload = adjustOverlayImageWidth;
  window.onresize = adjustOverlayImageWidth;

  const video = document.getElementById('camera-stream');
  const canvas = document.getElementById('photo-canvas');
  const photo = document.getElementById('photo-preview');
  const takePhotoButton = document.getElementById('take-photo');
  const downloadLink = document.getElementById('download-link');
  const overlayText = document.getElementById('overlay-text');

  navigator.mediaDevices.getUserMedia({ 
    video: { width: { ideal: 1500 }, height: { ideal: 1125 }, facingMode: { ideal: "environment" } }
  })
  .then(stream => { video.srcObject = stream; })
  .catch(err => { console.error('カメラを起動できませんでした:', err); });

  takePhotoButton.addEventListener('click', async () => {
    takePhotoButton.style.backgroundColor = "#28a745";
    takePhotoButton.textContent = "待機";

    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext('2d');

    // カメラの映像を描画
    ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

    // オーバーレイ画像を描画
    await drawOverlayImage(ctx);

    // オーバーレイ文字を描画
    drawOverlayText(ctx);

    // 合成した画像をプレビューに表示
    photo.src = canvas.toDataURL('image/png');
    photo.style.display = 'block';

    // 現在の日時を取得（例: 2025-02-13_10-30-45）
    const now = new Date();
    const year = now.getFullYear();
    const month = String(now.getMonth() + 1).padStart(2, '0'); // 月は0から始まるので+1
    const day = String(now.getDate()).padStart(2, '0');
    const hours = String(now.getHours()).padStart(2, '0');
    const minutes = String(now.getMinutes()).padStart(2, '0');
    const seconds = String(now.getSeconds()).padStart(2, '0');
    
    // 日時をフォーマットしてファイル名に追加
    const dateString = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
    
    // オーバーレイテキストを使ってファイル名を作成
    const text1 = overlayText.value ? overlayText.value : "name";
    const text2 = document.getElementById('overlay-text2').value ? document.getElementById('overlay-text2').value : "";
    const selectedImageButton = document.querySelector('.overlay-button.selected');
    const selectedButtonText = selectedImageButton.textContent
    const fileName = `${text1}_${text2}${selectedButtonText}_${dateString}.png`.replace(/[\/:*?"<>|]/g, "_").replace("無地", ""); // "無地"を削除; // ファイル名に使えない文字を置換

    // ダウンロードリンクを作成
    downloadLink.href = canvas.toDataURL('image/png');
    downloadLink.download = fileName; // 動的に作成したファイル名を設定
    downloadLink.style.display = 'inline-block';

    // ボタンの色とテキストを元に戻す（1秒後）
    setTimeout(() => {
      takePhotoButton.style.backgroundColor = "#007bff";
      takePhotoButton.textContent = "撮影";
    }, 1000);
});


  function drawOverlayImage(ctx) {
    return new Promise((resolve, reject) => {
      const overlayImageCanvas = new Image();
      overlayImageCanvas.onload = () => {
        const width = canvas.width * 0.35;
        const height = (overlayImageCanvas.height / overlayImageCanvas.width) * width;
        const x = 20;
        const y = 20;
        ctx.drawImage(overlayImageCanvas, x, y, width, height);
        resolve();
      };
      overlayImageCanvas.onerror = reject;
      overlayImageCanvas.src = overlayImage.src;
    });
  }

  // オーバーレイ文字を描画する関数を修正
  function drawOverlayText(ctx) {
    let text1 = overlayText.value;
    let text2 = document.getElementById('overlay-text2').value; // 新たなテキストフィールドの内容を取得

    if (text1) {
      text1 += " 様邸新築工事"; // 既存のテキストに追加
      const fontSize1 = canvas.width / 70;
      ctx.font = fontSize1 + "px sans-serif";
      ctx.fillStyle = 'rgba(255, 255, 255, 1)';
      const x1 = canvas.width * 0.15;
      const y1 = canvas.height * 0.058;
      ctx.fillText(text1, x1, y1);
    }

    if (text2) {
      const fontSize2 = canvas.width / 20;
      ctx.font = fontSize2 + "px sans-serif";
      ctx.fillStyle = 'rgba(255, 255, 255, 1)';
      const x2 = canvas.width * 0.08;
      const y2 = canvas.height * 0.2; // 少し下に配置
      ctx.fillText(text2, x2, y2);
    }
  }

  downloadLink.addEventListener('click', () => {
    downloadLink.style.backgroundColor = "#28a745";
    downloadLink.textContent = "待機";

    setTimeout(() => {
      downloadLink.style.backgroundColor = "#17a2b8";
      downloadLink.textContent = "保存";
    }, 1000);
  });
JavaScriptのポイント
カメラへのアクセス: navigator.mediaDevices.getUserMedia でカメラにアクセスし、映像を video 要素に表示します。

オーバーレイ画像の描画: 選択されたオーバーレイ画像を canvas に描画します。画像のサイズや位置は、カメラ映像のサイズに合わせて調整します。

テキストの描画: 入力されたテキストを canvas に描画します。テキストの位置やフォントサイズは、画面サイズに合わせて調整します。

撮影と保存: 「撮影」ボタンをクリックすると、カメラ映像、オーバーレイ画像、テキストが合成された画像を canvas からData URLとして取得し、ダウンロードリンクに設定します。

ファイル名の自動生成: 現在の日時、オーバーレイテキスト、選択された画像のボタンテキストを組み合わせてファイル名を生成します。

改善点
UIの改善: CSSを調整して、より使いやすいUIに改善できます。

画像の最適化: 撮影された画像のサイズを最適化することで、保存容量を削減できます。

多言語対応: 現場のニーズに合わせて、多言語対応を追加できます。

まとめ
このWebカメラアプリは、工事写真撮影の効率化に大きく貢献します。
リアルタイムでのオーバーレイ表示により、撮影後の編集作業を削減し、現場での作業時間を短縮できます。
ぜひ、あなたの現場でも試してみてください。

注意: 上記コード内の [画像URL] の箇所には、適切な画像のURLを記述してください。

参考URL
https://darumasantinoagalog.zapto.org/index.php/camera_test