【问题标题】:Image Manipulation - add image with corners in exact positions图像处理 - 添加具有精确位置的角的图像
【发布时间】:2016-07-22 05:25:37
【问题描述】:

我有一个图像,它是一个背景,包含一个像这样的盒装区域:

我知道该形状拐角的确切位置,我想在其中放置另一张图片。 (所以它似乎在盒子里)。

我知道 HTML5 画布的 drawImage 方法,但它似乎只支持 x、y、width、height 参数而不是精确坐标。我如何在特定坐标集的画布上绘制图像,并且理想情况下让浏览器本身处理拉伸图像。

【问题讨论】:

  • 你能展示你的“MCVE”(minimal reproducible example) HTML 和 CSS 吗?展示您在这一点上的习惯,并在理想情况下展示您尝试的解决方案。这样我们就可以给出相关的答案(无需猜测)并解释你的代码哪里出错了,这样你就可以学习了。这个问题及其答案的未来用户也可以这样做。
  • 你考虑过使用 CSS 变换吗?
  • 在 2D 画布中不可能,因为使用的变换只能创建平行四边形(对边始终平行),您将需要使用 WebGL,而不是您几乎可以完全访问 GPU。如果您只需要显示图像,那么建议的 CSS 将是最好的。或者您可以尝试为 2D 画布创建自己的扫描线渲染,但这远非实时。
  • 哦,我忘了说。由于 3D 投影取决于视场(eqiv 相机焦距),因此拥有 4 个角的位置不会给您想要的东西(猜测适合场景的图像)。如果没有这些信息,您会发现很难匹配透视图,而不是点,而是纹理映射。
  • 您可以通过偏移和调整图像垂直切片的大小来“伪造”透视图。这是以前 Stackoverflow Q&A 的示例。这种方法相对于 CSS 转换的优点是,您可以将透视画布绘图转换为图像元素。缺点是您在一定程度上受限于垂直或水平调整图像 - 两者都需要(通常令人失望)多像素插值.

标签: javascript html canvas


【解决方案1】:

您可以使用 CSS 变换使您的图像看起来像那个盒子。例如:

img {
  margin: 50px;
  transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">

阅读更多关于CSS Transforms on MDN的信息。

【讨论】:

  • 第一个答案中的可拖动点正是我所需要的,但使用 CSS 透视图是一种更好的方法,我正在尝试将四边形 X 和 Y 输入组合成 CSS 转换可以理解的东西,有没有人有这方面的经验?就我个人而言,这将是该线程的最终解决方案。
【解决方案2】:

此解决方案依赖于执行合成的浏览器。您将要变形的图像放在一个单独的元素中,使用position: absolute 覆盖背景。

然后使用 CSS transform 属性将任何透视变换应用于覆盖元素。

要找到变换矩阵,您可以使用来自以下位置的答案:How to match 3D perspective of real photo and object in CSS3 3D transforms

【讨论】:

    【解决方案3】:

    四边形变换

    解决此问题的一种方法是使用四边形变换。它们与 3D 变换不同,可让您绘制到画布上,以防您想要导出结果。

    这里显示的示例是简化的,并且在渲染本身上使用了基本的细分和“作弊” - 也就是说,它绘制了一个小正方形而不是细分单元格的形状,但由于尺寸小而且在许多非极端情况下,我们可以避免重叠。

    正确的方法是将形状分成两个三角形,然后在目标位图中逐像素扫描,将目标三角形的点映射到源三角形。如果位置值是小数,您将使用它来确定像素插值(例如双线性 2x2 或双三次 4x4)。

    我不打算在这个答案中涵盖所有这些,因为它很快就会超出 SO 格式的范围,但是该方法可能适合这种情况,除非您需要对其进行动画处理(它的性能不够如果你想要高分辨率)。

    方法

    让我们从一个初始的四边形开始:

    第一步是在每个条 C1-C4 和 C2-C3 上插入 Y 位置。我们需要当前位置和下一个位置。我们将为此使用t 的归一化值使用线性插值(“lerp”):

    y1current = lerp( C1, C4, y / height)
    y2current = lerp( C2, C3, y / height)
    
    y1next = lerp(C1, C4, (y + step) / height)
    y2next = lerp(C2, C3, (y + step) / height)
    

    这为我们在外部垂直条之间和沿线提供了一条新线。

    接下来我们需要该行的 X 位置,包括当前位置和下一个位置。这将为我们提供四个位置,我们将用当前像素填充,或者按原样或对其进行插值(此处未显示):

    p1 = lerp(y1current, y2current, x / width)
    p2 = lerp(y1current, y2current, (x + step) / width)
    p3 = lerp(y1next, y2next, (x + step) / width)
    p4 = lerp(y1next, y2next, x / width)
    

    xy 将是使用整数值的源图像中的位置。

    我们可以在循环中使用此设置,该循环将遍历源位图中的每个像素。

    演示

    可以在答案底部找到演示。移动圆形手柄以变换和播放步长值,以查看其对性能和结果的影响。

    演示有云纹和其他伪影,但如前所述,这将是另一天的话题。

    演示截图:

    替代方法

    您还可以使用 WebGL 或 Three.js 设置 3D 环境并渲染到画布。这是后一种解决方案的链接:

    以及如何使用纹理映射表面的示例:

    使用这种方法,您也可以将结果导出到画布或图像,但为了提高性能,客户端需要 GPU。

    如果您不需要导出或操作结果,我建议您使用简单的 CSS 3D 转换,如其他答案所示。

    /* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
    var img = new Image();  img.onload = go;
    img.src = "https://i.imgur.com/EWoZkZm.jpg";
    
    function go() {
      var me = this,
          stepEl = document.querySelector("input"),
          stepTxt = document.querySelector("span"),
          c = document.querySelector("canvas"),
          ctx = c.getContext("2d"),
          corners = [
            {x: 100, y: 20},           // ul
            {x: 520, y: 20},           // ur
            {x: 520, y: 380},          // br
            {x: 100, y: 380}           // bl
          ],
          radius = 10, cPoint, timer,  // for mouse handling
          step = 4;                    // resolution
    
      update();
    
      // render image to quad using current settings
      function render() {
    		
        var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
            w = img.width - 1,         // -1 to give room for the "next" points
            h = img.height - 1;
    
        ctx.clearRect(0, 0, c.width, c.height);
    
        for(y = 0; y < h; y += step) {
          for(x = 0; x < w; x += step) {
            y1c = lerp(corners[0], corners[3],  y / h);
            y2c = lerp(corners[1], corners[2],  y / h);
            y1n = lerp(corners[0], corners[3], (y + step) / h);
            y2n = lerp(corners[1], corners[2], (y + step) / h);
    
            // corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
            p1 = lerp(y1c, y2c,  x / w);
            p2 = lerp(y1c, y2c, (x + step) / w);
            p3 = lerp(y1n, y2n, (x + step) / w);
            p4 = lerp(y1n, y2n,  x / w);
    
            ctx.drawImage(img, x, y, step, step,  p1.x, p1.y, // get most coverage for w/h:
                Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
                Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
          }
        }
      }
      
      function lerp(p1, p2, t) {
        return {
          x: p1.x + (p2.x - p1.x) * t, 
          y: p1.y + (p2.y - p1.y) * t}
      }
    
      /* Stuff for demo: -----------------*/
      function drawCorners() {
        ctx.strokeStyle = "#09f"; 
        ctx.lineWidth = 2;
        ctx.beginPath();
        // border
        for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
        ctx.closePath();
        // circular handles
        for(i = 0; p = corners[i++];) {
          ctx.moveTo(p.x + radius, p.y); 
          ctx.arc(p.x, p.y, radius, 0, 6.28);
        }
        ctx.stroke()
      }
    	
      function getXY(e) {
        var r = c.getBoundingClientRect();
        return {x: e.clientX - r.left, y: e.clientY - r.top}
      }
    	
      function inCircle(p, pos) {
        var dx = pos.x - p.x,
            dy = pos.y - p.y;
        return dx*dx + dy*dy <= radius * radius
      }
    
      // handle mouse
      c.onmousedown = function(e) {
        var pos = getXY(e);
        for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
      }
      window.onmousemove = function(e) {
        if (cPoint) {
          var pos = getXY(e);
          cPoint.x = pos.x; cPoint.y = pos.y;
          cancelAnimationFrame(timer);
          timer = requestAnimationFrame(update.bind(me))
        }
      }
      window.onmouseup = function() {cPoint = null}
      
      stepEl.oninput = function() {
        stepTxt.innerHTML = (step = Math.pow(2, +this.value));
        update();
      }
      
      function update() {render(); drawCorners()}
    }
    body {margin:20px;font:16px sans-serif}
    canvas {border:1px solid #000;margin-top:10px}
    <label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
    <canvas width=620 height=400></canvas>

    【讨论】:

    • 很酷的方法!你知道使用 lerp 风格的方法是否比向量方法有任何性能优势吗? (我正在做与你类似的事情,但使用向量)
    • 这正是我需要的代码,但不幸的是,预览中看到的图像超出了右侧和底部的蓝线。我不太清楚代码的哪一部分是这样做的,你能修复原始代码吗?当步进分辨率增加时,这种情况更加普遍。
    猜你喜欢
    • 2016-04-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-04
    • 1970-01-01
    • 2011-12-25
    相关资源
    最近更新 更多