js manipula los píxeles de la imagen para editar

¡Acostúmbrate a escribir juntos! Este es el cuarto día de mi participación en el "Nuevo plan diario de los Nuggets·Desafío de actualización de abril", haz clic para ver los detalles del evento .

concepto rgba

La imagen está compuesta de píxeles. Cada píxel contiene cuatro valores, que determinan el estado renderizado. Estos cuatro valores son rgba (rojo, verde, azul, alfa).

Los primeros tres valores son rojo, verde y azul, y los valores varían en tamaño de 0 a 255, o de 0% a 100%.

El cuarto valor, alfa, especifica la transparencia del color, que va de 0 a 1. 0 significa totalmente transparente y 1 significa totalmente visible.

El rojo, el verde y el azul son los tres colores primarios del color. Al establecer la proporción de estos tres colores, se pueden cambiar todos los demás colores.

Dado que cada píxel se puede expresar mediante el valor de rgba, todos los píxeles contenidos en una imagen se pueden convertir en datos. Si se modifica el valor rgba de una determinada parte del píxel, el efecto renderizado de la imagen cambiará, lo que permite la edición de la imagen.

Entonces, ¿cómo se convierte una imagen en datos compuestos de píxeles?

imagen a datos

Una estructura html simple es la siguiente, se coloca una imagen original y un lienzo en la página, tanto el ancho como la altura son 300;

<body>
  <p class="image">
     <img src="./img/rect.png" width="300" height="300" />
  </p>
  <canvas id="myCanvas" width="300" height="300"></canvas>
<body>

复制代码

Primero escriba una función getImageData para convertir la imagen original en datos (el código es el siguiente).

La conversión de una imagen a datos de píxeles se realiza en los siguientes dos pasos.

  • Llame a ctx.drawImage(img, x, y, ancho, alto) para crear un objeto ImageData
  • Llame a ctx.getImageData(x, y, ancho, alto) para obtener el objeto ImageData del lienzo
    const dom = document.getElementById("myCanvas"); // canvas画布

    getImageData(dom,"./img/rect.png").then((data)=>{
      console.log(data); // 打印输出像素数据
    })

    function getImageData(dom,url){
        const ctx = dom.getContext("2d");   // 设置在画布上绘图的环境
        const image = new Image();
        image.src= url;
        //获取画布宽高
        const w = dom.width;
        const h = dom.height;
        return new Promise((resolve)=>{
            image.onload = function(){
                ctx.drawImage(image, 0, 0 ,w,h);                           // 将图片绘制到画布上
                const imgData = ctx.getImageData(0,0,w,h);    // 获取画布上的图像像素
                resolve(imgData.data)  // 获取到的数据为一维数组,包含图像的RGBA四个通道数据
                ctx.clearRect(0,0,w,h);
            }     
    }) 
}

复制代码

El resultado final de datos impresos (datos) es el siguiente:

data = [255, 255, 255, 255, 255, 61, 61, 255, 255, 0, 0, 255, 255,...]
复制代码

data es una matriz unidimensional, los primeros cuatro valores de la matriz [255, 255, 255, 255] son ​​el valor rgba del primer píxel de la imagen (el rango de tamaño de transparencia devuelto por ctx.getImageData es de 0 - 255), [255 , 61, 61, 255] es el valor rgba del segundo píxel de la imagen, y así sucesivamente. De esta manera, la imagen se convierte con éxito en datos.

conversión de formato de datos

Aunque la imagen se ha convertido con éxito en datos, dicha estructura de datos es difícil de operar y esperamos poder mantener la expresión de la estructura de datos consistente con el efecto de visualización de la imagen.

假如存在四个都是黑色的像素点(如下图),总宽高都为2,值为[0, 0, 0, 255,0, 0, 0, 255,0, 0, 0, 255,0, 0, 0, 255].

inserte la descripción de la imagen aquí

通过某个函数转换,数据就变成了下列格式.

[
   [[0, 0, 0, 255],[[0, 0, 0, 255]]], // 第一行
   [[0, 0, 0, 255],[[0, 0, 0, 255]]]  // 第二行
]

复制代码

上列数据格式和图片的展示结构保持了一致,可以很清晰的看出当前图形有多少行,每一行又有多少个像素点, 以及每一个像素点的rgba值.

综合上面描述,可以编写函数normalize(代码如下)实现数据格式的转换.

const dom = document.getElementById("myCanvas"); // canvas画布

getImageData(dom,"./img/rect.png").then((data)=>{
  console.log(normalize(data,dom.width,dom.height)); // 打印格式化后的像素数据
})

function normalize(data,width,height){
  const list = [];
  const result = [];
  const len = Math.ceil(data.length/4);
  // 将每一个像素点的rgba四个值组合在一起
  for(i = 0;i<len;i++){
    const start = i*4;
    list.push([data[start],data[start+1],data[start+2],data[start+3]]);
  }
  //根据图形的宽和高将数据进行分类
  for(hh = 0;hh < height;hh++){
     const tmp = [];
     for(ww = 0; ww < width;ww++){
      tmp.push(list[hh*width + ww]);
     }
     result.push(tmp);
  }
  return result;
}

复制代码

换肤功能实现

通过normalize函数的转换,一维数组的图片数据转换成了矩阵形式.有了矩阵,我们就可以更加方便的实现编辑图片的需求.

首选我们简单实现一个图片换肤的需求,将图片中的黑色全部变成黄色(最终效果图如下).

inserte la descripción de la imagen aquí

上方的原始图片包含红蓝绿黑四种颜色,下方是换肤后生成的新图片.

实现代码如下,peeling函数负责变换图片的颜色.

观察代码,由于黑色的rgb值是(0,0,0).那么只需要判断出像素点是黑色,就重置其rgb值为(255,255,0)便能将图片中所有的黑色换成黄色.

const dom = document.getElementById("myCanvas"); // canvas画布

getImageData(dom,"./img/rect.png").then((data)=>{
  data = peeling(data,dom.width,dom.height); // 换肤
  drawImage(dom,data); // 绘制图像
})

function peeling(data,w,h){
  data = normalize(data,w,h); // 转化成多维数组
  // 将黑色变成黄色 (0,0,0) -> (255,255,0)   
  for(let i = 0;i<data.length;i++){
    for(let j = 0;j<data[i].length;j++){
      //排除透明度的比较
      if(data[i][j].slice(0,3).join("") == "000"){
        data[i][j] = [255,255,0,data[i][j][3]];
      }
    }
  }
  return restoreData(data); // 转化成一维数组
}

复制代码

矩阵的数据操作完了,还需要调用restoreData函数将多维数组再转回一维数组传给浏览器渲染.

 function restoreData(data){
      const result = [];
      for(let i = 0;i<data.length;i++){
        for(let j = 0;j<data[i].length;j++){
          result.push(data[i][j][0],data[i][j][1],data[i][j][2],data[i][j][3]);
        }
      }
      return result;
 }

复制代码

渲染最终图片

数据处理完毕后,还需将处理完的数据data传递给drawImage函数渲染成新图片(代码如下).

渲染图像主要调用以下两个api.

  • ctx.createImageData(width, height) 创建新的空白ImageData对象,通过.data.set重新赋值.
  • ctx.putImageData(imagedata, x, y, dx, dy, width, height) 用于将ImagaData对象的数据填写到canvas中,起到覆盖canvas中原图像的作用,可以只输入前三个参数。参数分别是:用于提供填充图像数据的imagedata对象,imagedata对象左上角相对于canvas左上角的坐标x,y,在canvas上用来填充imagedata区域的左上角相对imagedata对象左上角的坐标x,y(相对于canvas左上角),填充区域的长度和宽度。具体用法效果往下看。
const dom = document.getElementById("myCanvas"); // canvas画布

getImageData(dom,"./img/rect.png").then((data)=>{
  data = peeling(data,dom.width,dom.height); // 换肤
  drawImage(dom,data); // 绘制图像
})

function drawImage(dom,data){
  const ctx = dom.getContext("2d");
  const matrix_obj = ctx.createImageData(dom.width,dom.height);
  matrix_obj.data.set(data);
  ctx.putImageData(matrix_obj,0,0);  
}

复制代码

至此新图片便成功渲染了出来,效果图可自己实践下获得

回顾上述操作,编辑图像主要分解成以下三步.

  • 将原始图片转化成矩阵数据(多维数组)
  • 依据需求操作矩阵
  • 将修改后的矩阵数据渲染成新图片

上述第二步操作是图像编辑的核心,很多复杂的变换效果可以通过编写矩阵算法实现.

为了加深理解,利用上述知识点实现一个图片旋转的需求.

旋转功能实现

假定存在最简单的情况如下图所示,其中左图存在四个像素点.第一行有两个像素点1和2(这里用序号代替rgba值).

第二行也有两个像素点3和4.数据源转换成矩阵data后的值为 [[[1],[2]],[[3],[4]]].

inserte la descripción de la imagen aquí

如何将左图按顺时针旋转90度变成右图?

通过观察图中位置关系,只需要将data中的数据做位置变换,让data = [[[1],[2]],[[3],[4]]]变成data = [[[3],[1]],[[4],[2]]],就可以实现图片变换.

四个像素点可以直接用索引交换数组的值,但一张图片动辄几十万个像素,那该如何进行操作?

这种情况下通常需要编写一个基础算法来实现图片的旋转.

首先从下图中寻找规律,图中有左 - 中 - 右三种图片状态,为了从左图的1-2-3-4变成右图的3-1-4-2,可以通过以下两步实现.

inserte la descripción de la imagen aquí

  • 寻找矩阵的高度的中心轴线,上下两侧按照轴线进行数据交换.比如左图1 - 2和3 - 4之间可以画一条轴线,上下两侧围绕轴线交换数据,第一行变成了3 - 4,第二行变成了1 - 2.通过第一步操作变成了中图的样子.

  • 中图的对角线3 - 2和右图一致,剩下的将对角线两侧的数据对称交换就可以变成右图.比如将中图的1和4进行值交换.操作完后便实现了图片的旋转.值得注意的是4的数组索引是[0][1],而1的索引是[1][0],刚好索引顺序颠倒.

通过以上描述规律便可编写下面函数实现图片的旋转.

const dom = document.getElementById("myCanvas"); // canvas画布

// getImageData 获取像素数据 
getImageData(dom,"./img/rect.png").then((data)=>{
  data = rotate90(data,dom.width,dom.height); // 顺时针旋转90度
  drawImage(dom,data); // 绘制图像
})

function rotate90(data,w,h){
  data = normalize(data,w,h); // 转化成矩阵
  // 围绕中间行上下颠倒
  const mid = h/2; // 找出中间行
  for(hh = 0;hh < mid;hh++){
    const symmetric_hh = h - hh -1; // 对称行的索引
    for(ww = 0;ww<w;ww++){
        let tmp = data[hh][ww];
        data[hh][ww] = data[symmetric_hh][ww];
        data[symmetric_hh][ww] = tmp;
    }
  }
  // 根据对角线进行值交换
  for(hh = 0;hh < h;hh++){
    for(ww = hh+1;ww<w;ww++){
      let tmp = data[hh][ww];
      data[hh][ww] = data[ww][hh];
      data[ww][hh] = tmp;
    }
  }
  return restoreData(data); // 转化成一维数组
}

复制代码

由于我们定义的canvas宽高都为300,上面的旋转算法只适用于正方形(长方形的图片要另外编写).

局部反相效果

实现思路是将图片画到canvas上,获取canvas的ImageData对象,对每个像素的颜色值进行反相处理。

<script type="text/javascript">  /*   * @param {object} img 展示反相的图片   */
  function showRevertPic(img){
    img.color = img.src;    // 给img添加属性指向源文件
    img.revert = createRevertPic(img);   // 给img添加属性指向反相图片
    img.onmouseout = function(){
      this.src = img.revert;
    }
    img.onmouseover = function(){
      this.src = img.color;
    }
    img.onmouseout(); // 默认展示一次图片反相
  }  /*   * @param {object} img 要实现反相的图片   */   
  function createRevertPic(img){
    var canvas = document.createElement("canvas");
    canvas.width = img.width;   
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
    ctx.drawImage(img,0,0);  
    var c = ctx.getImageData(0, 0, img.width, img.height);
    //chrome浏览器报错,ie浏览器报安全错误信息,原因往下看
    for(var i = 0; i < c.height; ++i){
      for(var j = 0; j < c.width; ++j){
        var x = i*4*c.width + 4*j,  //imagedata读取的像素数据存储在data属性里,是从上到下,从左到右的,每个像素需要占用4位数据,分别是r,g,b,alpha透明通道
        r = c.data[x],
        g = c.data[x+1],
        b = c.data[x+2];
        c.data[x+3] = 150;    //透明度设置为150,0表示完全透明        //图片反相:
        c.data[x] = 255-r;
        c.data[x+1] = 255-g;
        c.data[x+2] = 255-b; 
      }
    }
    //ctx.putImageData(c, 40, 40);
    ctx.putImageData(c,0,0,40,40,200,300);    //裁剪效果见图1
    return canvas.toDataURL();          //返回canvas图片数据url
  }
  window.onload=function() { 
    var img = new Image();
    img.src = "boy.png";
    img.isLoad = false;
    document.body.appendChild(img);
    img.onload=function(){
      if(!img.isLoad){
        showRevertPic(img);
        img.isLoad=true;
      }
    }
  }
</script>
复制代码

底片一样的区域就是putImageData放置的区域,鼠标移上去就能看到原来的图片

为什么img的onload函数要设置一个isLoad属性呢,原因你去掉isLoad的判断就知道了,你会发现,我擦咧,图片忽闪忽闪的,这个onload函数居然一直不断的执行下去。

为什么呢,因为showRevertPic(img)默认运行一次mouseout函数,而鼠标移入移出会导致图片的src的改变,每次src改变就会触发onload事件,而onload会导致图片再次反相,于是图片就一直忽闪忽闪的。而查看控制台,img的src一直指向64位编码的png图片数据而没有一次指向原图片地址,原因是当出发了一次mouseout函数img的src就不再指向源文件了,之后的变化是源图片的反相和源图片的反相的反相交替进行。所以给img设置了个isLoad属性是为了只触发一次showRevertPic()函数。

当然去掉showRevertPic()函数中的默认执行一次的mouseout函数也行,但是就不能立马看到图片的反相了。

这里其实存在跨域的问题,当用chrome浏览器或ie浏览器打开(9+)就会报错

chrome:Uncaught SecurityError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.

     ie:SCRIPT5022: DOM Exception: SECURITY_ERR (18)
复制代码

El error de señalización está dispuesto a provenir del hecho de que getImageData solo puede operar imágenes en el mismo dominio que el script.Las imágenes obtenidas están en la carpeta local y no tienen nombre de dominio, por lo que el navegador piensa que la operación es entre dominios. Así que tengo que suspirar, cromar y es decir, prestar más atención a los temas de seguridad.

La solución es crear un entorno de servidor, colocar el archivo en el directorio del servidor y acceder a él a través del servidor, de modo que no se informe ningún error.

Ahora hablemos sobre el valor de retorno canvas.toDataURL() en createRevertPic().

Este método devuelve la URL codificada por canvas como datos de imagen, que se usa para generar imágenes. Se usa el formato png predeterminado, y el formato de imagen también se puede cambiar pasando parámetros, y la calidad de la imagen también se puede cambiar. Por ejemplo: canvas.toDataURL("images/jpeg",0), el primer parámetro es codificar la imagen en formato jpeg, el segundo parámetro (0-1) es especificar la calidad de la imagen, cuanto mayor sea el valor, mayor la calidad, pero para el formato de imagen/png no es necesario establecer la calidad de imagen orz. Además, Chrome también admite sus propias imágenes en formato de imagen/webp y también puede configurar la calidad de la imagen.

canvas.toDataURL("images/jpeg",0) La imagen es la siguiente, este código se puede escribirinserte la descripción de la imagen aquí

Supongo que te gusta

Origin juejin.im/post/7082772512479641636
Recomendado
Clasificación