js manipulate image pixels for editing

Get into the habit of writing together! This is the 4th day of my participation in the "Nuggets Daily New Plan·April Update Challenge", click to view the details of the event .

rgba concept

The image is composed of pixels. Each pixel contains four values, which determine the rendered state. These four values ​​are rgba(red, green, blue, alpha).

The first three values ​​are red, green and blue, and the values ​​range in size from 0 to 255, or from 0% to 100%.

The fourth value, alpha, specifies the transparency of the color, which ranges from 0 to 1. 0 means fully transparent and 1 means fully visible.

Red, green and blue are the three primary colors in the color. By setting the proportion of these three colors, all other colors can be changed.

Since each pixel can be expressed by the value of rgba, all pixels contained in a picture can be converted into data. If the rgba value of a certain part of the pixel is modified, the rendered effect of the picture will change. , which enables the editing of the image.

So how do you convert a picture into data composed of pixels?

image to data

A simple html structure is as follows, an original image and a canvas are placed on the page, both width and height are 300;

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

复制代码

First write a getImageData function to convert the original image into data (the code is as follows).

Converting an image to pixel data is performed in the following two steps.

  • Call ctx.drawImage(img, x, y, width, height) to create an ImageData object
  • Call ctx.getImageData(x, y, width, height) to get the ImageData object from the canvas
    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);
            }     
    }) 
}

复制代码

The final printed data result (data) is as follows:

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

data is a one-dimensional array, the first four values ​​of the array [255, 255, 255, 255] are the rgba values ​​of the first pixel of the image (the transparency size returned by ctx.getImageData ranges from 0 to 255), [255 , 61, 61, 255] is the rgba value of the second pixel of the picture, and so on. In this way, the picture is successfully converted into data.

data format conversion

Although the picture has been successfully converted into data, such a data structure is difficult to operate. We expect to be able to keep the expression of the data structure consistent with the display effect of the picture.

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

insert image description here

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

[
   [[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函数的转换,一维数组的图片数据转换成了矩阵形式.有了矩阵,我们就可以更加方便的实现编辑图片的需求.

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

insert image description here

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

实现代码如下,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]]].

insert image description here

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

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

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

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

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

insert image description here

  • 寻找矩阵的高度的中心轴线,上下两侧按照轴线进行数据交换.比如左图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)
复制代码

The pointing error is willing to come from the fact that getImageData can only operate pictures in the same domain as the script. The obtained pictures are in the local folder and have no domain name, so the browser thinks that the operation is cross-domain. So I have to sigh, chrome and ie pay more attention to security issues.

The solution is to build a server environment, put the file in the server directory, and access it through the server, so that no error will be reported.

Now let's talk about the return value canvas.toDataURL() in createRevertPic().

This method returns the url encoded by canvas as image data, which is used to generate images. The default png format is used, and the image format can also be changed by passing parameters, and the quality of the image can also be changed. For example: canvas.toDataURL("images/jpeg",0), the first parameter is to encode the image into jpeg format, the second parameter (0-1) is to specify the image quality, the larger the value, the higher the quality, but for The image/png format does not have to set the image quality orz. In addition, chrome also supports its own image/webp format pictures, and can also set the picture quality.

canvas.toDataURL("images/jpeg",0) The picture is as follows, this code can be typedinsert image description here

Guess you like

Origin juejin.im/post/7082772512479641636