You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在p5.js中不使用WEBGL实现图像到任意四边形的映射

在p5.js中实现非矩形四边形的图像映射(无需WEBGL)

核心思路:用透视变换替代WEBGL

WEBGL确实能快速实现纹理映射,但如果只想用2D画布,**透视变换(Perspective Transformation)**是核心解决方案——它可以把矩形图像的四个顶点精准映射到任意四边形的四个顶点,实现带透视效果的图像变形。

实现步骤

  • 计算透视变换矩阵:根据原始图像的四个顶点(矩形)和目标四边形的四个顶点,求解出能完成坐标映射的变换矩阵
  • 优化像素渲染:放弃暴力遍历所有像素,只针对四边形的边界范围计算,结合离屏画布缓存原始图像,用像素数组直接写入的方式提升渲染效率

示例代码

let img;
// 目标四边形的四个顶点(顺时针:左上、右上、右下、左下)
let quadPoints = [];
let offscreenCanvas;

function preload() {
  img = loadImage('your-image.jpg'); // 替换为你的图像路径
}

function setup() {
  createCanvas(800, 600);
  // 初始化自定义四边形坐标,可根据需求修改
  quadPoints = [
    createVector(100, 100),
    createVector(700, 150),
    createVector(650, 500),
    createVector(150, 450)
  ];
  // 创建离屏画布缓存原始图像,避免重复加载
  offscreenCanvas = createGraphics(img.width, img.height);
  offscreenCanvas.image(img, 0, 0);
  offscreenCanvas.loadPixels();
}

function draw() {
  background(220);
  // 计算透视变换矩阵
  let matrix = calculatePerspectiveMatrix(
    // 原始图像的四个顶点
    [createVector(0,0), createVector(img.width,0), createVector(img.width,img.height), createVector(0,img.height)],
    quadPoints
  );
  
  loadPixels();
  // 只遍历四边形的边界范围,减少无效计算
  let minX = Math.min(...quadPoints.map(p => p.x));
  let maxX = Math.max(...quadPoints.map(p => p.x));
  let minY = Math.min(...quadPoints.map(p => p.y));
  let maxY = Math.max(...quadPoints.map(p => p.y));
  
  for (let x = minX; x < maxX; x++) {
    for (let y = minY; y < maxY; y++) {
      let currentPoint = createVector(x, y);
      // 检查当前点是否在目标四边形内
      if (pointInQuad(currentPoint, quadPoints)) {
        // 逆变换:将画布点映射回原始图像坐标
        let imgCoord = applyInversePerspectiveMatrix(currentPoint, matrix);
        // 确保坐标在图像范围内
        if (imgCoord.x >= 0 && imgCoord.x < img.width && imgCoord.y >=0 && imgCoord.y < img.height) {
          // 读取离屏画布的像素颜色
          let pixelIdx = (floor(imgCoord.y) * offscreenCanvas.width + floor(imgCoord.x)) * 4;
          let r = offscreenCanvas.pixels[pixelIdx];
          let g = offscreenCanvas.pixels[pixelIdx+1];
          let b = offscreenCanvas.pixels[pixelIdx+2];
          let a = offscreenCanvas.pixels[pixelIdx+3];
          // 写入主画布像素数组
          let mainPixelIdx = (y * width + x) * 4;
          pixels[mainPixelIdx] = r;
          pixels[mainPixelIdx+1] = g;
          pixels[mainPixelIdx+2] = b;
          pixels[mainPixelIdx+3] = a;
        }
      }
    }
  }
  updatePixels();
  
  // 可选:绘制四边形边框用于调试
  stroke(255, 0, 0);
  noFill();
  beginShape();
  quadPoints.forEach(p => vertex(p.x, p.y));
  endShape(CLOSE);
}

// 计算透视变换矩阵(基于四点映射的线性方程组求解)
function calculatePerspectiveMatrix(srcPoints, dstPoints) {
  let A = [];
  let B = [];
  
  // 为每个点添加约束方程
  function addConstraint(src, dst) {
    A.push([src.x, src.y, 1, 0, 0, 0, -src.x*dst.x, -src.y*dst.x]);
    B.push(dst.x);
    A.push([0, 0, 0, src.x, src.y, 1, -src.x*dst.y, -src.y*dst.y]);
    B.push(dst.y);
  }
  
  srcPoints.forEach((s, i) => addConstraint(s, dstPoints[i]));
  return solveLinearSystem(A, B);
}

// 简化的高斯消元法求解线性方程组
function solveLinearSystem(A, B) {
  let n = A.length;
  let aug = A.map((row, i) => [...row, B[i]]);
  
  // 前向消元
  for (let col = 0; col < n; col++) {
    // 寻找主元行
    let pivotRow = col;
    for (let row = col; row < n; row++) {
      if (Math.abs(aug[row][col]) > Math.abs(aug[pivotRow][col])) pivotRow = row;
    }
    // 交换主元行与当前行
    [aug[col], aug[pivotRow]] = [aug[pivotRow], aug[col]];
    
    // 归一化主元行
    let div = aug[col][col];
    for (let j = col; j <= n; j++) aug[col][j] /= div;
    
    // 消去其他行的当前列
    for (let row = 0; row < n; row++) {
      if (row !== col && aug[row][col] !== 0) {
        let factor = aug[row][col];
        for (let j = col; j <= n; j++) aug[row][j] -= factor * aug[col][j];
      }
    }
  }
  
  return aug.map(row => row[n]);
}

// 应用逆透视变换,将画布坐标映射回图像坐标
function applyInversePerspectiveMatrix(p, matrix) {
  let [a,b,c,d,e,f,g,h] = matrix;
  let denom = g * p.x + h * p.y + 1;
  let u = (a * p.x + b * p.y + c) / denom;
  let v = (d * p.x + e * p.y + f) / denom;
  return createVector(u, v);
}

// 射线法判断点是否在四边形内
function pointInQuad(p, quad) {
  let inside = false;
  for (let i = 0, j = quad.length - 1; i < quad.length; j = i++) {
    let [xi, yi] = [quad[i].x, quad[i].y];
    let [xj, yj] = [quad[j].x, quad[j].y];
    let intersect = ((yi > p.y) !== (yj > p.y)) && (p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi);
    if (intersect) inside = !inside;
  }
  return inside;
}

性能优化建议

  • 缓存变换矩阵:如果四边形顶点不实时变化,只在初始化或顶点修改时计算一次矩阵,无需每帧重复计算
  • 缩小遍历范围:只遍历四边形的外接矩形区域,避免整个画布的无效计算
  • 整数坐标取整:对映射后的图像坐标取整,减少浮点运算的性能损耗

内容的提问来源于stack exchange,提问作者Game_Freeze

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 13:50:33