2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像の中で生活するVR体験。Webカメラ映像からリアルタイムで人物を抽出し、背景シーンに重ねて描画するインタラクティブなアプリケーション。

Last updated at Posted at 2024-10-17

ここで生活してます。

image.png

image.png

スクリーンショット 2024-10-18 051154.png

Webカメラ映像からリアルタイムで人物を抽出し、背景画像シーンに描画するインタラクティブなアプリケーションです。ブラウザでHTMLファイルを開くことで、すぐに体験できます。

コードをメモ帳などのテキストエディタに貼り付け、ファイル名を「index.html」として保存します。その後、保存したファイルをブラウザで開けば、コードが実行されます。

ユーザーが任意の画像をアップロードして、シーンの背景として使用できます。人物のスケール調整ができます。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D空間に人物画像を描画</title>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
    <script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
        canvas {
            display: block;
        }
        #video {
            display: none; /* カメラの映像は隠しますが、内部で使用します */
        }
        #scale-slider-container {
            position: absolute;
            top: 10px;
            left: 10px;
            z-index: 10;
        }
    </style>
</head>
<body>
    <video id="video" width="640" height="480" autoplay></video>
    <canvas id="output-canvas" style="display:none;"></canvas>
    <input type="file" id="background-upload" accept="image/*">
    <div id="scale-slider-container">
        <label for="scale-slider">人物スケール調整: </label>
        <input type="range" id="scale-slider" min="0.1" max="3" step="0.1" value="1">
    </div>

    <script>
        let scene, camera, renderer;
        let personTexture;
        let net;
        let personPlane = null;
        let isDragging = false;
        let mouse = new THREE.Vector2();
        let planeOffset = new THREE.Vector3();
        let scale = 1;  // 初期スケール

        // Three.jsのシーンを初期化
        function initThreeJS() {
            scene = new THREE.Scene();

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.z = 5;

            renderer = new THREE.WebGLRenderer();
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.setClearColor(0xffffff); // 背景を白でクリア
            document.body.appendChild(renderer.domElement);

            // ウィンドウリサイズ時にレンダラーとカメラのアスペクト比を調整
            window.addEventListener('resize', () => {
                const width = window.innerWidth;
                const height = window.innerHeight;
                renderer.setSize(width, height);
                camera.aspect = width / height;
                camera.updateProjectionMatrix();
            });

            // マウスイベントを追加
            document.addEventListener('mousedown', onMouseDown, false);
            document.addEventListener('mousemove', onMouseMove, false);
            document.addEventListener('mouseup', onMouseUp, false);

            animate();
        }

        // マウスダウンイベント処理
        function onMouseDown(event) {
            if (personPlane) {
                isDragging = true;
                const intersect = getIntersect(event);
                if (intersect) {
                    planeOffset.copy(intersect.point).sub(personPlane.position);
                }
            }
        }

        // マウスムーブイベント処理
        function onMouseMove(event) {
            if (isDragging && personPlane) {
                const intersect = getIntersect(event);
                if (intersect) {
                    personPlane.position.copy(intersect.point.sub(planeOffset));
                }
            }
        }

        // マウスアップイベント処理
        function onMouseUp() {
            isDragging = false;
        }

        // レイキャスターで平面との交差を計算
        function getIntersect(event) {
            const rect = renderer.domElement.getBoundingClientRect();
            mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
            mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

            const raycaster = new THREE.Raycaster();
            raycaster.setFromCamera(mouse, camera);

            if (personPlane) {
                const intersects = raycaster.intersectObject(personPlane);
                return intersects.length > 0 ? intersects[0] : null;
            }
            return null;
        }

        // アニメーションループ
        function animate() {
            requestAnimationFrame(animate);

            // 人物のスケールを更新
            if (personPlane) {
                personPlane.scale.set(scale, scale, 1);
            }

            // レンダリング前に背景をクリア
            renderer.clear();
            renderer.render(scene, camera);
        }

        // 人物の画像を平面に描画
        function addPersonToScene(texture) {
            const geometry = new THREE.PlaneGeometry(3, 4);
            const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
            personPlane = new THREE.Mesh(geometry, material);
            scene.add(personPlane);
        }

        // BodyPixのモデルをロード
        async function loadBodyPix() {
            net = await bodyPix.load();
            console.log('BodyPixモデルがロードされました');
        }
        loadBodyPix();

        // Webカメラの映像を取得
        async function setupCamera() {
            const video = document.getElementById('video');
            const stream = await navigator.mediaDevices.getUserMedia({ video: true });
            video.srcObject = stream;
            return new Promise(resolve => {
                video.onloadedmetadata = () => {
                    resolve();
                };
            });
        }

        // 画像を処理して人物のみを抽出し、背景をカスタム画像に変更
        async function processImage() {
            const video = document.getElementById('video');
            const outputCanvas = document.getElementById('output-canvas');
            const ctx = outputCanvas.getContext('2d', { willReadFrequently: true });

            outputCanvas.width = video.videoWidth;
            outputCanvas.height = video.videoHeight;

            ctx.clearRect(0, 0, outputCanvas.width, outputCanvas.height); // 背景クリア
            ctx.drawImage(video, 0, 0);

            // セグメンテーションを実行
            const segmentation = await net.segmentPerson(outputCanvas);

            // 画像のピクセルデータを取得
            const imageData = ctx.getImageData(0, 0, outputCanvas.width, outputCanvas.height);
            const data = imageData.data;

            // セグメンテーション結果に基づいて人物部分を描画し、背景を透明に設定
            for (let i = 0; i < data.length; i += 4) {
                const n = i / 4;
                if (segmentation.data[n] === 0) {
                    // 背景部分は透明に設定
                    data[i + 3] = 0; // アルファ値(透明度)
                }
            }
            ctx.putImageData(imageData, 0, 0);

            // Three.js用のテクスチャとして設定
            personTexture = new THREE.Texture(outputCanvas);
            personTexture.needsUpdate = true;

            // 人物をシーンに追加
            if (personPlane) {
                scene.remove(personPlane); // 以前の人物を削除
            }
            addPersonToScene(personTexture);
        }

        // 背景画像をアップロードし、Three.jsシーンに適用
        document.getElementById('background-upload').addEventListener('change', function (event) {
            const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = function (e) {
                const textureLoader = new THREE.TextureLoader();
                textureLoader.load(e.target.result, function (texture) {
                    scene.background = texture;  // アップロードされた画像を背景に設定
                });
            };
            if (file) {
                reader.readAsDataURL(file);
            }
        });

        // スケールスライダーのイベント処理
        document.getElementById('scale-slider').addEventListener('input', function (event) {
            scale = parseFloat(event.target.value);
        });

        // リアルタイム処理を実行
        async function renderLoop() {
            await processImage();
            requestAnimationFrame(renderLoop); // 次のフレームを処理
        }

        // Three.jsの初期化とカメラのセットアップ
        initThreeJS();
        setupCamera().then(() => {
            renderLoop(); // 画像処理を開始
        });
    </script>
</body>
</html>

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?