Skip to content

(feat): static image example with Teblid tracker #30

Description

@kalwalt

Add a static image example with Teblid tracker

Description

Add a new example that uses a static image (pinball-demo.jpg) as input
instead of a webcam video stream. The example uses the Teblid feature
detector and follows the same structure as the existing threejs_ES6_example.html.

Motivation

  • Enable testing without webcam access or camera permissions
  • Provide a reproducible test case for CI/CD pipelines
  • Simplify development and debugging of the Teblid tracker

Files to Add

  • examples/threejs_teblid_static_image_ES6_example.html
  • examples/threejs_static_image_worker_ES6.js
  • examples/data/pinball-demo.jpg (test image, to be provided)

worker_threejs.js and data/pinball.jpg are reused without any changes.

Implementation Details

threejs_teblid_static_image_ES6_example.html follows the structure of
threejs_ES6_example.html with two differences:

  • the <video> element is replaced by a hidden <img id="static-image">
    pointing to data/pinball-demo.jpg (the test image to process)
  • index.js (initCamera) is not needed; the load event on the <img>
    element triggers start() directly

threejs_static_image_worker_ES6.js mirrors threejs_worker_ES6.js exactly,
with one change in the process() function: drawImage reads from the
HTMLImageElement instead of the HTMLVideoElement.

- context_process.drawImage(video, 0, 0, vw, vh, ox, oy, w, h);
+ context_process.drawImage(image, 0, 0, vw, vh, ox, oy, w, h);

Notes

  • data/pinball.jpg (1637x2048) is the marker reference, already in the repo
  • data/pinball-demo.jpg is the test image containing the marker, to be added
  • setTrackerType() returns 'teblid'
  • The GrayScale class is not used

Code snippets

threejs_teblid_static_image_ES6_example.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>WebARKit Teblid - Static Image ES6 example</title>
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=0.5, maximum-scale=1">
    <link rel="stylesheet" href="css/nft-style.css">
</head>
<body>

<div id="loading">
    <img src="data/aframe-k.png"/>
    <span class="loading-text">Loading, please wait</span>
</div>

<div id="stats" class="ui stats">
    <div id="stats1" class="stats-item">
        <p class="stats-item-title">Main</p>
    </div>
    <div id="stats2" class="stats-item">
        <p class="stats-item-title">Worker</p>
    </div>
</div>

<div id="app">
    <!--
        Niente <video>: l'input è un'immagine statica.
        L'elemento img è nascosto; viene usato solo come sorgente
        per context_process.drawImage() in threejs_static_image_worker_ES6.js
    -->
    <img id="static-image" src="data/pinball-demo.jpg" style="display:none;">
    <canvas id="canvas"></canvas>
</div>

<script src="js/stats.min.js"></script>
<script src="js/three.min.js"></script>

<script>
    function setTrackerType() {
        return 'teblid';
    }
</script>

<script src="threejs_static_image_worker_ES6.js"></script>

<script>
    var statsMain = new Stats();
    statsMain.showPanel(0);
    document.getElementById('stats1').appendChild(statsMain.dom);

    var statsWorker = new Stats();
    statsWorker.showPanel(0);
    document.getElementById('stats2').appendChild(statsWorker.dom);

    window.addEventListener('load', () => {
        console.log('init WebARKit Teblid Static Image...');

        const image = document.getElementById('static-image');

        // Aspetta che l'immagine sia caricata prima di avviare il tracker
        const ready = () => {
            initTargetCanvas(image.naturalWidth, image.naturalHeight);
            start(
                './data/pinball-demo.jpg',
                image,
                image.naturalWidth,
                image.naturalHeight,
                function () { statsMain.update(); },
                function () { statsWorker.update(); }
            );
        };

        if (image.complete) {
            ready();
        } else {
            image.addEventListener('load', ready);
        }
    });

    function initTargetCanvas(width, height) {
        const canvas = document.querySelector('#canvas');
        canvas.width = width;
        canvas.height = height;
    }
</script>

</body>
</html>

threejs_static_image_worker_ES6.js

function isMobile() {
    return /Android|mobile|iPad|iPhone/i.test(navigator.userAgent);
}

const setMatrix = function (matrix, value) {
    const array = [];
    for (const key in value) {
        array[key] = value[key];
    }
    if (typeof matrix.elements.set === 'function') {
        matrix.elements.set(array);
    } else {
        matrix.elements = [].slice.call(array);
    }
};

// Stessa firma di threejs_worker_ES6.js, ma `video` è un HTMLImageElement
function start(markerUrl, image, input_width, input_height, render_update, track_update) {
    let vw, vh;
    let sw, sh;
    let pscale, sscale;
    let w, h;
    let pw, ph;
    let ox, oy;
    let worker;

    const canvas_process = document.createElement('canvas');
    const context_process = canvas_process.getContext('2d', { willReadFrequently: true });
    const targetCanvas = document.querySelector('#canvas');

    const renderer = new THREE.WebGLRenderer({ canvas: targetCanvas, alpha: true, antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);

    const scene = new THREE.Scene();

    const fov = (0.8 * 180) / Math.PI;
    const ratio = input_width / input_height;

    const cameraConfig = {
        fov: fov,
        aspect: ratio,
        near: 0.01,
        far: 1000,
    };

    const camera = new THREE.PerspectiveCamera(cameraConfig);
    camera.matrixAutoUpdate = false;
    scene.add(camera);

    const sphere = new THREE.Mesh(
        new THREE.SphereGeometry(0.5, 8, 8),
        new THREE.MeshNormalMaterial()
    );

    const root = new THREE.Object3D();
    scene.add(root);

    sphere.material.flatShading;
    sphere.scale.set(0.5, 0.5, 0.5);

    root.matrixAutoUpdate = false;
    root.add(sphere);

    const load = function () {
        vw = input_width;
        vh = input_height;

        pscale = 320 / Math.max(vw, vh / 3 * 4);
        sscale = isMobile() ? window.outerWidth / input_width : 1;

        sw = vw * sscale;
        sh = vh * sscale;

        w = vw * pscale;
        h = vh * pscale;
        pw = Math.max(w, h / 3 * 4);
        ph = Math.max(h, w / 4 * 3);
        ox = (pw - w) / 2;
        oy = (ph - h) / 2;

        canvas_process.style.clientWidth = pw + 'px';
        canvas_process.style.clientHeight = ph + 'px';
        canvas_process.width = pw;
        canvas_process.height = ph;

        renderer.setSize(sw, sh);

        worker = new Worker('./worker_threejs.js');

        const type = setTrackerType();

        const loadImage = (URL) => {
            fetch(URL)
                .then(response => response.arrayBuffer())
                .then(buff => {
                    let buffer = new Uint8Array(buff);
                    worker.postMessage({
                        type: 'initTracker',
                        trackerType: type,
                        imageData: buffer,
                        imgWidth: 1637,
                        imgHeight: 2048,
                        videoWidth: vw,
                        videoHeight: vh,
                    });
                    return buffer;
                });
        };

        loadImage(markerUrl);

        worker.onmessage = function (ev) {
            const msg = ev.data;
            switch (msg.type) {
                case 'loadedTracker': {
                    const proj = JSON.parse(msg.cameraProjMat);
                    const ratioW = pw / w;
                    const ratioH = ph / h;
                    proj[0] *= ratioW;
                    proj[4] *= ratioW;
                    proj[8] *= ratioW;
                    proj[12] *= ratioW;
                    proj[1] *= ratioH;
                    proj[5] *= ratioH;
                    proj[9] *= ratioH;
                    proj[13] *= ratioH;
                    setMatrix(camera.projectionMatrix, proj);
                    process();
                    break;
                }
                case 'endLoading': {
                    if (msg.end === true) {
                        const loader = document.getElementById('loading');
                        if (loader) {
                            loader.querySelector('.loading-text').innerText = 'Start the tracking!';
                            setTimeout(function () {
                                loader.parentElement.removeChild(loader);
                            }, 2000);
                        }
                    }
                    break;
                }
                case 'found': {
                    found(msg);
                    break;
                }
                case 'not found': {
                    found(null);
                    break;
                }
            }
            track_update();
        };
    };

    let world;

    const found = function (msg) {
        if (!msg) {
            world = null;
        } else {
            world = JSON.parse(msg.pose);
        }
    };

    var lasttime = Date.now();
    var time = 0;

    const draw = function () {
        render_update();
        var now = Date.now();
        var dt = now - lasttime;
        time += dt;
        lasttime = now;

        if (!world) {
            sphere.visible = false;
        } else {
            sphere.visible = true;
            setMatrix(root.matrix, world);
        }
        renderer.render(scene, camera);
    };

    const process = function () {
        context_process.fillStyle = 'black';
        context_process.fillRect(0, 0, pw, ph);

        // Unica differenza rispetto a threejs_worker_ES6.js:
        // `image` (HTMLImageElement statico) al posto di `video` (HTMLVideoElement)
        context_process.drawImage(image, 0, 0, vw, vh, ox, oy, w, h);

        const imageData = context_process.getImageData(0, 0, pw, ph);
        worker.postMessage({ type: 'process', imagedata: imageData }, [imageData.data.buffer]);
    };

    const tick = function () {
        draw();
        process();
        requestAnimationFrame(tick);
    };

    load();
    tick();
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions