在 JavaScript 和 WebGL 中实现大量点 Beier 和 Neely 的图像变形方法

问题描述 投票:0回答:1

我正在尝试使用 JavaScript 和 WebGL 实现 Beier 和 Neely 的图像扭曲方法(例如 this 作为示例,并在 here 详细描述)。目标是通过允许用户交互式地在画布上移动点来扭曲面部图像。每个点都会根据所描述的算法操纵图像。

到目前为止,我已经成功创建了一个WebGL程序,允许用户通过单击并拖动图像上的点来与图像进行交互,并且我已经成功设置了基本的图像扭曲效果。

此时,我已经实现了该算法的基本版本,一次允许最多 32 个“点”(计算为线),尽管最终我希望能够使用它来与其他面部跟踪信息进行匹配,所以我想允许数百个/动态数量。

创建了一个带有可移动点的交互式画布。 应用了基本的扭曲效果来操纵这些点周围的图像。

解决了失真效果偏离鼠标光标的问题。

使用适合 WebGL 统一的有限少量点实现了 Beier 和 Neely 的基本变形方法。

剩下的挑战: 创建具有数百个点的效果,可能在 GLSL 纹理中,尽管我正在努力实现它,考虑到制服一次只允许少量的点,而且我还需要允许动态他们的数量。

还请记住,最终目标是能够跟踪一张脸上的点(在不同的项目中完成,使用张量流获取地标)以便能够影响图片的脸部,所以如果有人有更好的建议除了这个之外,对于更好的失真算法,也可以接受。

这是我的代码的当前状态(如果您尝试大图像,请全屏查看):

<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>

<button id="sl">Select Image</button>
<br>
<button id="editMode">Enter point edit mode</button>
<button id="point" disabled>Add warp point</button>
<br>
<h4>Add at least 3 points</h4><br>
<canvas id="canvas"></canvas>
<style>
    .warpPoint {
        display:inline-block;
        border-radius:50%;
        border:1px solid black;
        background:gold;
        position:absolute;
        width: 10px;
        height: 10px;
    }

    .warpPoint:hover {
        cursor:pointer;
    }
</style>

<script>
let warpPoints = [];
let warpPointsFlat = [];
let warpPointCount = 0;
let isAddingPoints = false;
let img = new Image();
let resolution = vec2.create();
let canvas = document.getElementById('canvas');
let warpPointCountLoc;
let sl = document.getElementById('sl');
let editMode = document.getElementById('editMode');
let point = document.getElementById('point');
let program, gl, imageLoc, resolutionLoc, texture;
let warpPointsLocs = [];

let imageSamplerLoc;


// Initial resolution values
resolution[0] = canvas.width;
resolution[1] = canvas.height;

// Warp point
let warpPoint = vec2.create();

// Functions
function mouseMoveHandler(e, warpPoint, origX, origY, startX, startY) {
    let newX = origX + e.clientX - startX;
    let newY = origY + e.clientY - startY;

    // Account for the canvas' offset from the top-left of the viewport
    newX -= canvas.offsetLeft;
    newY -= canvas.offsetTop;

    warpPoint.setPosition(newX, newY);

    var newX_gl;
    var newY_gl;
    newX_gl = newX / resolution[0]; // Convert to UV coordinates
    newY_gl = 1 - newY / resolution[1]; // Convert to UV coordinates

    warpPoints[warpPoint.index] = vec4.fromValues(newX_gl, newY_gl, 0.46, 0.2); // Use vec4 and include strength and radius

    warpPointsFlat = updateWarpPointsFlat(warpPoints);
    render();
}



function WarpPoint(index) {
    this.index = index;
    this.el = document.createElement("div");
    this.el.className = "warpPoint";
    // In the WarpPoint setPosition function:
    // In the WarpPoint setPosition function:
    this.setPosition = function(x, y) {
        this.el.style.left = `${x + canvas.offsetLeft}px`;
        this.el.style.top = `${y + canvas.offsetTop}px`;

        this.x = x / resolution[0]; // Convert to UV coordinates
        this.y = 1 - y / resolution[1];
        warpPoints[this.index] = vec4.fromValues(this.x, this.y, 0.1, 0.1); // Use vec4 to include z (strength) and w (radius)

        warpPointsFlat = updateWarpPointsFlat(warpPoints);
        render(); // Make sure to rerender when a warp point is moved
    };

    document.body.appendChild(this.el);

    this.el.onmousedown = (e) => {
        var startX = e.clientX;
        var startY = e.clientY;
        var origX = this.el.offsetLeft;
        var origY = this.el.offsetTop;

        var mouseMoveHandlerBind = (e) => {
            mouseMoveHandler(e, this, origX, origY, startX, startY);
        };
        var mouseUpHandler = () => {
            document.removeEventListener('mousemove', mouseMoveHandlerBind);
            document.removeEventListener('mouseup', mouseUpHandler);
        };

        document.addEventListener('mousemove', mouseMoveHandlerBind);
        document.addEventListener('mouseup', mouseUpHandler);
    };
}


editMode.onclick = () =>{
    if(isAddingPoints) {
        point.disabled = true;
        isAddingPoints = false;
        editMode.innerHTML = "entered WARP mode. Click again to edit"
    } else {
        point.disabled = false;
        isAddingPoints = true;
        editMode.innerHTML = "entered EDIT mode. Click again to warp"
    }
}


function updateWarpPointsFlat(warpPoints) {
    let warpPointsFlat = [];
    for(let i = 0; i < warpPoints.length; i++) {
        warpPointsFlat.push(warpPoints[i][0]);
        warpPointsFlat.push(warpPoints[i][1]);
        warpPointsFlat.push(warpPoints[i][2]); // Z
        warpPointsFlat.push(warpPoints[i][3]); // W
    }
    return new Float32Array(warpPointsFlat);
}
point.onclick=addWarpPoint;
function addWarpPoint() {
    warpPoints.push(vec4.fromValues(0, 0, 0.1, 0.1));
    var warpPoint = new WarpPoint(warpPoints.length - 1); // pass index
    let x = (canvas.width / 2);
    let y = (canvas.height / 2);

    warpPoint.setPosition(x, y);

    warpPointCount = warpPoints.length;
    warpPointsFlat = updateWarpPointsFlat(warpPoints);

    console.log("Warp point added. Current warp points: ", warpPoints);
}

function loadImage(url, callback) {
    let image = new Image();
    image.src = url;
    image.onload = () => callback(image);
}

function createShader(type, source) {
    let shader = gl.createShader(type);
    gl.shaderSource(shader, source);
    gl.compileShader(shader);
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        console.error('Error compiling shader:', gl.getShaderInfoLog(shader));
        gl.deleteShader(shader);
        return null;
    }
    return shader;
}

function createProgram(vertexShader, fragmentShader) {
    let program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error('Error linking program:', gl.getProgramInfoLog(program));
        gl.deleteProgram(program);
        return null;
    }
    return program;
}

function createTexture(image) {
    let texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    return texture;
}

function resizeCanvas() {
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
    resolution[0] = canvas.width;
    resolution[1] = canvas.height;
    gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}

// In the render function:
function render() {
    // Clear the canvas
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    gl.useProgram(program);

    // Update uniforms
    gl.uniform2fv(resolutionLoc, resolution);

    // Set up the viewport
    gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

    // Update uniforms
    let warpPointCountUniformLoc = gl.getUniformLocation(program, 'warpPointCount');
    let warpPointsUniformLoc = [];
    for(let i = 0; i < 32; i++) {
        warpPointsUniformLoc[i] = gl.getUniformLocation(program, `warpPoints[${i}]`);
    }

    gl.uniform1i(warpPointCountUniformLoc, warpPointCount);

    // If there are warp points, pass them to the shader
    if(warpPointsFlat.length > 0) {
        for(let i = 0; i < warpPointCount; i++) {
            gl.uniform2fv(warpPointsUniformLoc[i], [warpPointsFlat[i * 4], warpPointsFlat[i * 4 + 1]]);
        }
    }

    // Always bind texture and draw image, regardless of warp point count
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.uniform1i(imageSamplerLoc, 0);

    // Draw primitives
    gl.drawArrays(gl.TRIANGLES, 0, 6);

}


window.onload = function() {
    console.log("hi!")
    let canvas = document.getElementById('canvas');
    gl = canvas.getContext("webgl")
    let vertexShader = createShader(gl.VERTEX_SHADER, `
        attribute vec2 position;
        varying vec2 uv;
        void main() {
            uv = position * 0.5 + 0.5;
            gl_Position = vec4(position, 0.0, 1.0);
        }
    `);
    let fragmentShader = createShader(gl.FRAGMENT_SHADER, `
    precision mediump float;

    uniform vec2 resolution;
    uniform sampler2D u_image;
    uniform vec2 warpPoints[32];  
    uniform int warpPointCount;  
    uniform float alpha;

    void main() {
    vec2 uv = gl_FragCoord.xy / resolution.xy;
    vec2 pos = uv;

    vec2 displacement = vec2(0.0);
    float weightSum = 0.0;

    for (int i = 0; i < 32; i += 2) {
        if(i >= warpPointCount) break;

        vec2 P = warpPoints[i].xy;
        vec2 Q = warpPoints[i+1].xy;

        float dist_PQ = distance(P, Q);
        vec2 dir_PQ = normalize(Q - P);

        vec2 X_P = uv - P;
        float u = dot(X_P, dir_PQ);
        float v = length(cross(vec3(dir_PQ, 0.0), vec3(X_P, 0.0)));

        vec2 P_prime = P + u * dir_PQ;
        vec2 Q_prime = Q + v * dir_PQ;

        vec2 displacement_this_line = P_prime - uv;
        float weight_this_line = pow(dist_PQ, alpha) / (0.001 + dot(X_P, X_P));

        displacement += weight_this_line * displacement_this_line;
        weightSum += weight_this_line;
    }

    if (weightSum != 0.0) {
        displacement /= weightSum;
    }

    pos += displacement;

    vec4 color = texture2D(u_image, clamp(pos, 0.0, 1.0));
    gl_FragColor = color;
    }

    `);

    program = createProgram(vertexShader, fragmentShader);
    gl.useProgram(program);
    gl.uniform2fv(resolutionLoc, resolution);

    imageSamplerLoc = gl.getUniformLocation(program, 'u_image');

    // Fetch warp point uniform locations
    for(let i = 0; i < 10; i++) {
        warpPointsLocs[i] = gl.getUniformLocation(program, `warpPoints[${i}]`);
    }

    

    let positionLoc = gl.getAttribLocation(program, 'position');
    resolutionLoc = gl.getUniformLocation(program, 'resolution');
    warpPointCountLoc = gl.getUniformLocation(program, 'warpPointCount');
    imageLoc = gl.getUniformLocation(program, 'image');

    
    let positionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        -1.0, -1.0,
        1.0, -1.0,
        -1.0, 1.0,
        -1.0, 1.0,
        1.0, -1.0,
        1.0, 1.0
    ]), gl.STATIC_DRAW);


    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
    
};

window.addEventListener('resize', function() {
    resizeCanvas();
    render();
});

sl.onclick = () => {
    
    var inp = document.createElement("input")
    inp.type="file"
    inp.onchange=()=>{
        var url = URL.createObjectURL(
            inp.files[0]
        )
        img.src=url;
        img.onload = () => {
            texture = createTexture(img);
            canvas.width=img.width;
            canvas.height=img.height
            resizeCanvas();
            render();
        };
    }
    inp.click()
}
</script>

javascript html image-processing glsl webgl
1个回答
0
投票

坐标是关键!

在您的代码中,您在 screen 和 GL 之间进行了多种约定,但它们并不完全正确。

由于您的

canvas
存在于 HTML 页面上,因此它具有相对于页面的位置。翘曲点的情况相同。因此,要将坐标从页面转换为 GL 坐标,您不仅要考虑变形点的位置,还要考虑它与
canvas
角的距离。

事情变得更加棘手,因为在着色器中,纹理从左下角开始(即在着色器中为 0:0),但对于屏幕上的矩形,它是

offsetLeft:offsetTop + offsetHeight

最后一件事是,扭曲公式对我来说看起来不太正确,但我会把它留给你,因为主要要求是让着色器对扭曲点做出正确的反应。

如果这有帮助,请告诉我。

        // moved to constants for testing
        const STRENGTH = 0.2;
        const RADIUS = 50.0 / 300.0;
        
        let warpPoints = [];
        let warpPointsFlat = [];
        let warpPointCount = 0;
        let isEditing = false;
        let img = new Image();
        let resolution = vec2.create();
        let canvas = document.getElementById('canvas');
        let warpPointCountLoc;
        let sl = document.getElementById('sl');
        let editMode = document.getElementById('editMode');
        let point = document.getElementById('point');
        let program, gl, imageLoc, resolutionLoc, texture;
        console.log(888)

        // Initial resolution values
        resolution[0] = canvas.clientWidth;
        resolution[1] = canvas.clientHeight;

        // Warp point
        let warpPoint = vec2.create();

        // Functions
        // Functions
        function mouseMoveHandler(e, warpPoint, origX, origY, startX, startY) {
            let newX = origX + e.clientX - startX;
            let newY = origY + e.clientY - startY;
            warpPoint.setPosition(newX, newY);
            var newX_gl;
            var newY_gl;
            if (!isEditing) {
                let newX_gl = (newX - canvas.offsetLeft) / resolution[0];
                let newY_gl = ((canvas.offsetTop + canvas.offsetHeight) - newY) / resolution[1];
                warpPoints[warpPoint.index] = vec4.fromValues(newX_gl, newY_gl, STRENGTH, RADIUS); // Use vec4 and include strength and radius

                warpPointsFlat = updateWarpPointsFlat(warpPoints);
                render();
            }
        }

        function WarpPoint(index) {
            this.index = index;
            this.el = document.createElement("div");
            this.el.className = "warpPoint";

            this.setPosition = function (x, y) {
                this.el.style.left = `${x}px`;
                this.el.style.top = `${y}px`;

                let x_gl = (x - canvas.offsetLeft) / resolution[0];
                let y_gl = ((canvas.offsetTop + canvas.offsetHeight) - y) / resolution[1];
                warpPoints[this.index] = vec4.fromValues(x_gl, y_gl, STRENGTH, RADIUS); // Use vec4 to include z (strength) and w (radius)

                warpPointsFlat = updateWarpPointsFlat(warpPoints);
            };

            document.body.appendChild(this.el);

            this.el.onmousedown = (e) => {
                var startX = e.clientX;
                var startY = e.clientY;
                var origX = this.el.offsetLeft;
                var origY = this.el.offsetTop;

                var mouseMoveHandlerBind = (e) => {
                    mouseMoveHandler(e, this, origX, origY, startX, startY);
                };
                var mouseUpHandler = () => {
                    document.removeEventListener('mousemove', mouseMoveHandlerBind);
                    document.removeEventListener('mouseup', mouseUpHandler);
                };

                document.addEventListener('mousemove', mouseMoveHandlerBind);
                document.addEventListener('mouseup', mouseUpHandler);
            };
        }

        editMode.onclick = () => {
            if (isEditing) {
                point.disabled = true;
                isEditing = false;
                editMode.innerHTML = "entered WARP mode. Click again to edit"
            } else {
                point.disabled = false;
                isEditing = true;
                editMode.innerHTML = "entered EDIT mode. Click again to warp"
            }
        }


        function updateWarpPointsFlat(warpPoints) {
            let warpPointsFlat = [];
            for (let i = 0; i < warpPoints.length; i++) {
                warpPointsFlat.push(warpPoints[i][0]);
                warpPointsFlat.push(warpPoints[i][1]);
                warpPointsFlat.push(STRENGTH); // strength
                warpPointsFlat.push(RADIUS); // radius
            }
            return new Float32Array(warpPointsFlat);
        }
        point.onclick = addWarpPoint;
        function addWarpPoint() {
            warpPoints.push(vec4.fromValues(0, 0, STRENGTH, RADIUS));
            var warpPoint = new WarpPoint(warpPoints.length - 1); // pass index
            let x = canvas.width / 2;
            let y = canvas.height / 2;
            warpPoint.setPosition(x, y);

            warpPointCount = warpPoints.length;
            warpPointsFlat = updateWarpPointsFlat(warpPoints);
            console.log("Warp point added. Current warp points: ", warpPoints);
        }

        function loadImage(url, callback) {
            let image = new Image();
            image.src = url;
            image.onload = () => callback(image);
        }

        function createShader(type, source) {
            let shader = gl.createShader(type);
            gl.shaderSource(shader, source);
            gl.compileShader(shader);
            if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
                console.error('Error compiling shader:', gl.getShaderInfoLog(shader));
                gl.deleteShader(shader);
                return null;
            }
            return shader;
        }

        function createProgram(vertexShader, fragmentShader) {
            let program = gl.createProgram();
            gl.attachShader(program, vertexShader);
            gl.attachShader(program, fragmentShader);
            gl.linkProgram(program);
            if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
                console.error('Error linking program:', gl.getProgramInfoLog(program));
                gl.deleteProgram(program);
                return null;
            }
            return program;
        }

        function createTexture(image) {
            let texture = gl.createTexture();
            gl.bindTexture(gl.TEXTURE_2D, texture);
            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
            return texture;
        }

        function resizeCanvas() {
            canvas.width = canvas.clientWidth;
            canvas.height = canvas.clientHeight;
            resolution[0] = canvas.width;
            resolution[1] = canvas.height;
            gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
        }

        function render() {
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.useProgram(program);

            gl.uniform2fv(resolutionLoc, resolution);

            // If warp points are present, update uniform data
            if (warpPointsFlat.length > 0) {
                for (let i = 0; i < warpPointCount; i++) {
                    let offset = i * 4;
                    let warpPoint_i_Location = gl.getUniformLocation(program, 'warpPoints[' + i + ']');
                    gl.uniform4fv(warpPoint_i_Location, warpPointsFlat.subarray(offset, offset + 4));
                }
            }

            gl.activeTexture(gl.TEXTURE0);
            gl.bindTexture(gl.TEXTURE_2D, texture);

            gl.uniform1i(imageLoc, 0);
            gl.uniform1i(warpPointCountLoc, warpPointCount);

            gl.drawArrays(gl.TRIANGLES, 0, 6);

            console.log("(at end of render..)Rendering with resolution: ", resolution);
            console.log("Rendering with warp points: ", warpPointsFlat);
            console.log("Rendering with warp point count (end of render..): ", warpPointCount);
        }
        console.log("ok")
        window.onload = function () {
            console.log("hi!")
            let canvas = document.getElementById('canvas');
            gl = canvas.getContext("webgl")
            console.log(gl, 22)
            let vertexShader = createShader(gl.VERTEX_SHADER, `
            attribute vec2 position;
            varying vec2 uv;
            void main() {
                uv = position * 0.5 + 0.5;
                gl_Position = vec4(position, 0.0, 1.0);
            }
        `);

            let fragmentShader = createShader(gl.FRAGMENT_SHADER, `
            precision mediump float;
            const int MAX_WARP_POINTS = 10;
            uniform vec2 resolution;
            uniform vec4 warpPoints[MAX_WARP_POINTS];
            uniform int warpPointCount;
            uniform sampler2D image;
            varying vec2 uv;

            void main() {
                vec2 pos = uv;
                vec2 newPos = pos;
                for(int i = 0; i < MAX_WARP_POINTS; i++) {
                    if(i >= warpPointCount) {
                        break;
                    }
                    vec2 warpPoint = warpPoints[i].xy;
                    vec2 offset = warpPoint - pos;
                    float dist = length(offset);
                    vec2 direction = offset / dist;
                    float strength = warpPoints[i].z;
                    float radius = warpPoints[i].w;
                    if (dist < radius) {
                        newPos -= strength * direction * (1.0 - smoothstep(0.0, radius, dist));
                    }
                }
                gl_FragColor = texture2D(image, newPos);
            }
        `);

            program = createProgram(vertexShader, fragmentShader);

            let positionLoc = gl.getAttribLocation(program, 'position');
            resolutionLoc = gl.getUniformLocation(program, 'resolution');
            warpPointCountLoc = gl.getUniformLocation(program, 'warpPointCount');
            imageLoc = gl.getUniformLocation(program, 'image');

            let positionBuffer = gl.createBuffer();
            gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
                -1.0, -1.0,
                1.0, -1.0,
                -1.0, 1.0,
                -1.0, 1.0,
                1.0, -1.0,
                1.0, 1.0
            ]), gl.STATIC_DRAW);

            gl.enableVertexAttribArray(positionLoc);
            gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);

        };

        window.addEventListener('resize', function () {
            resizeCanvas();
            render();
        });

        sl.onclick = () => {
            console.log(2)
            var inp = document.createElement("input")
            inp.type = "file"
            inp.onchange = () => {
                var url = URL.createObjectURL(
                    inp.files[0]
                )
                img.src = url;
                img.onload = () => {
                    texture = createTexture(img);
                    resizeCanvas();
                    render();
                };
            }
            inp.click()
        }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js"></script>

    <button id="sl">Select Image</button>
    <br>
    <button id="editMode">Enter point edit mode</button>
    <button id="point" disabled>Add warp point</button>
    <br>
    <canvas id="canvas"></canvas>
    <style>
        .warpPoint {
            display: inline-block;
            border-radius: 50%;
            border: 1px solid black;
            background: gold;
            position: absolute;
            width: 10px;
            height: 10px;
        }

        .warpPoint:hover {
            cursor: pointer;
        }
    </style>

© www.soinside.com 2019 - 2024. All rights reserved.