Skip to content

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

@kalwalt

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();
}

Metadata

Metadata

Assignees

No fields configured for Feature.

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions