简易的灰度化图片的工具

gray-image

简易的灰度化图片的工具

引子

今天对于全国同胞来说是个庄严肃穆的日子,是纪念在抗击新冠疫情牺牲的烈士与广大同胞的纪念日,也是中国的四大传统节日「清明节」。

自古以来,清明节就是我们用来纪念先人,缅怀祖先的,20 年这个不平静的年份又赋予了它更伟大的意义。

人类的情感需要寄托,烧纸、扫墓都是与故人交流的方式,国家层面今日降半旗缅怀英烈,对于互联网人员来说,将网页灰度化也是必备的致敬方式之一。

正好,编程群里面大家发了各种灰度化主页的方案,最简单的是采用css


body,

html {

  filter: grayscale(1);

}

这种方案统一的采用 css 方案,我受到其他一个小伙伴js操作canvas的思路的启发,模仿着他的思路把 demo 又重新写了一遍,再次梳理一下这种方案的原理,代码都是基础代码,大同小异,就是很多细节平常不用canvas不会去注意这些细节,再次感谢这个小伙伴乐于分享它的demo,大家可以多关注他的博客

接下来我只是分享我从它的demo里面学到的知识,与一些自己的心得。

具体的实现

主要采用input元素与canvas操作。

input[type=file]

主体html很简单


<input type="file" id="input" accept=".jpeg,.png,jpg" style="display: none;" />

<canvas id="canvas" style="display: none;"></canvas>

<button id="selectImage">选择需要灰度化的图片</button>

采用 button 的点击去调用 input 的点击事件


selectImage.addEventListener("click", function () {

  input.click();

});

input.addEventListener("change", callback);

当选择完要灰度化的图片之后,进入callback回调


function () {

  const file = input.files[0]; // 得到图片项

  const imgName = file.name;

  const reader = new FileReader(); // 创建file blob

  reader.readAsDataURL(file);

  reader.onload = function (*_file*) {

​    const image = new Image();

​    image.src = _file.target.result;

​    image.onload = function () {

​      const a = document.createElement("a");

​      a.href = gray(image); //最核心的算法,后续着重分析

​      a.download = imgName;

​      a.setAttribute("id", "downloadGrayImage");

​      // 防止重复添加

​      if (!document.getElementById("downloadGrayImage")) {

​        document.body.appendChild(a);

​      }

​      a.click();

​    };

  };

}

到此为止,都是基础的js操作本地文件上传,并用a标签模拟点击下载的功能。

接下来重点使用canvas操作以及基础的灰度算法。

canvas

Canvas API主要用来构建2D图像,基本属性只有width height,然后使用HTMLCanvasElement.getContext()获得上下文来进行绘制。


使用canvas的主要目的是:可以来获得图像的像素数据以及最后处理成渲染图像所必备的源



function gray(*imgObj*) {

  const width = imgObj.width;

  const height = imgObj.height;



  const canvas = document.querySelector("canvas");

  if (canvas.getContext) {

​    const context = canvas.getContext("2d");

​    canvas.width = width;

​    canvas.height = height;



​    context.drawImage(imgObj, 0, 0); //画一个图

​    // 获得隐含区域的像素数据, 返回ImageData 对象,其中的data属性包含了所需的数据

​    const imageData = context.getImageData(0, 0, width, height);

​    let pixelData = imageData.data;

​    //重点来了,逐行遍历上述的像素数组

​    //多行代码在下面核心代码分析...



​    // 将重新赋值的像素点重新归位到context中

​    context.putImageData(imageData, 0, 0, 0, 0, imageData.width, imageData.height);



​    // 能变成图片的最重要的一步

​    return canvas.toDataURL(); //把当前构造的canvas对象变成img可使用的uri

  } else {

​    console.error("抱歉,你的浏览器不支持");

  }

}

平均像素值灰度化算法

这个是灰度化最简单、最易操作的算法。核心原理就是把每个像素的值取均值。


//伪代码

everyPixel = (pixelR + pixelG + pixelB) / 3

对于像我一样的没有啥科班知识,并且图像学知识极度匮乏的人来说,先来补一补图像的入门知识。

  • 像素个数与宽高的关系

对于一幅图像我们常说720*480的图像,说的是宽720个像素,高480个像素,每个像素有四个值构成,分别是R G B A,具体代表红 绿 蓝 三原色和透明度。

  • 像素数据从哪来就显得很关键了。上述的例子我们使用context.getImageData(0, 0, width, height);可以得到一个ImageData,这是一个极其重要的接口,它包含了图像的所有像素数据,只不过有了这个数据,到目前为止似乎与颜色仍然没有关系,我们仍然无法直接操作。

  • 如何操作上述的像素呢?ImageData.data是一个只读属性,返回所有像素值转换成的第一步说的四个值后的一维TypedArray数组,按照像素点顺序铺开的数组,更具体的来说是Uint8ClampedArray,不过不需要再深入了。

至此,准备知识结束,对于一幅720*480的图像,我们最终会转换成一个720*480*4=1382400长度的数组,操作数组对于程序员来说就很容易了。


我们完成了一幅具象的图像到抽象的数组的转变


  • 核心的算法

最基础的写法其实就是双重遍历,外层循环先逐行从上至下按照高度遍历,内层循环再逐列从左至右按照宽度遍历。


//重点来了,逐行遍历上述的像素数组

for (let h = 0; h < height; h++) {

  for (let w = 0; w < width; w++) {

​    const i = h * 4 * width + w * 4; // 获得每个像素值的四个点位的值,最核心

​    const avgPixel = (pixelData[i] + pixelData[i + 1] + pixelData[i + 2]) / 3; // 最简单的平均值算法


​    // 使用平均化的像素点去重新赋值RGB三个值

​    pixelData[i] = avgPixel;

​    pixelData[i + 1] = avgPixel;

​    pixelData[i + 2] = avgPixel;

  }

}

上面的双层遍历很容易看懂,就是中间的那句const i = h * 4 * width + w * 4;让我思索了一会,我下面画个草图便于理解这句话。

81586014159_.pic的副本.jpg

上面的示例图是一个4*4的图

  • 其中第一行的第一个像素点0由四个值构成{0, 1, 2, 3},第二个像素点由四个值构成{4, 5, 6, 7},以此类推。上述的公式是首先要确定一个基准点,目前基准点选取的是左边开始的第一个值,也就是R值,也就是红色的值的index

  • 先看公式的第一部分h * 4 * width代表的是每一行的左起点的值的index,那么第几行就是h,走几列数据就是4*width

  • 公式的第二部分是本行的基准点每次移动的步长,很明显是w*4

  • 确定了起始点计算方法后,后续的G Bindex只需要依次累加即可。


至此,把一幅图像用最简单的平均值像素法灰度化的方案就完成了,大家可以手打一遍试试,挺好玩的,也就花费3-4个小时,做一个小demo,看似简单其实知识点也不少的。

关于其他的5种灰度算法可以参考这篇博客

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容