多线程做图片灰度化处理

大家好我是蜗牛,倾心于用直白的大白话来解释清楚复杂的问题的,让新手也能很好的接受。

或许你还没遇到过让你棘手的图片处理需求,又或许你已经面临着,我们今天要聊的是一个非常正常,但是却让我多掉了十根头发的需求,上传图片时,将图片灰度化处理成黑白色并让用户可预览(这个需求让我第一时间想到了殡葬行业的定制化墓碑),无论如何,点赞收藏这篇文章,将来你可能用的上(我指的是代码,不是定制化服务,>_<...)

思路

一张五颜六色的照片,如何让其变成黑白色,还要考虑到整体的性能问题,思路大致是这样的

按照我们的思路,我们需要使用 Web Worker 帮我们开辟一个新的浏览器线程,以及 FileReader方法来帮助我们将文件读取成二进制数据

实现

整体的思路有了,创建一个index.html文件

xml 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Image Processing</title>
  <style>
    #previewImage{
      width: 300px;
    }
  </style>
</head>

<body>
  <input type="file" id="imageInput">
  <div id="previewContainer">
    <h2>Preview</h2>
    <img id="previewImage" alt="Preview">
  </div>

  <script>
    
  </script>
</body>

</html>

在用户点击图片上传的按钮并选择图片之后

ini 复制代码
// ...
  <script>
    // 获取元素
    const imageInput = document.getElementById('imageInput');
    const previewImage = document.getElementById('previewImage');

    // 监听文件上传事件
    imageInput.addEventListener('change', handleImageUpload);


    // 处理图像上传事件
    function handleImageUpload(event) {
      const file = event.target.files[0];

      if (file) {
        // 使用FileReader来读取上传的图像文件
        const reader = new FileReader();
       
        // 二进制流读取完成
        reader.onload = function (e) {
          const imageData = e.target.result;

          // 创建一个新的Web Worker
          const worker = new Worker('worker.js');

          // 使用 createImageBitmap 来转换数据
          createImageBitmap(new Blob([imageData])).then(imageBitmap => {
            // 向Web Worker发送图像数据
            worker.postMessage({ imageBitmap }, [imageBitmap]);

            // 监听Web Worker的消息
            worker.onmessage = function (e) {
              
            };
            
          });
        };

        // 读取图像文件,readAsArrayBuffer将文件读取成二进制流
        reader.readAsArrayBuffer(file);
      }
    }

  </script>
// ...

以上代码中,我们实现了一下功能:

  1. 使用 new FileReader() 读取文件资源,在load中获取二进制数据 imageData
  2. new Worker('worker.js'); 会导致浏览器开启新的线程,这样做的好处是让主线程不被占用依然能正常执行后续的逻辑
  3. createImageBitmap:它接受各种不同的图像来源,new Blob([imageData])先将二进制转换成Blob类型的数据,再利用createImageBitmap,转换将Blob类型转化为 ImageBitmap对象的格式

解释一下第三步为什么要这样转化,我曾经尝试过,直接将二进制数据交给 worker 线程,但是结果始终不尽人意,所以我借助createImageBitmap变更了数据类型,又因为createImageBitmap它需要接受图片源,所以就只能先new Blob([imageData])转成 Blob类型

同级下创建 worker.js 文件

ini 复制代码
// 添加事件监听器以接收主线程传递的消息
self.addEventListener('message', function (e) {
  const imageBitmap = e.data.imageBitmap;  // 获取到主线程中的图片资源

  // 将图像数据转换为灰度图像
  createImageBitmap(processImage(imageBitmap)).then(processedImageBitmap => {
      // 发送处理后的图像数据给主线程
      self.postMessage({ processedImageBitmap }, [processedImageBitmap]);
  });
});

// 在Web Worker中处理图像数据
function processImage(inputImageBitmap) {
  const canvas = new OffscreenCanvas(inputImageBitmap.width,    inputImageBitmap.height);
  const ctx = canvas.getContext('2d');
  ctx.drawImage(inputImageBitmap, 0, 0);

  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const inputData = new Uint8Array(imageData.data.buffer);
  const outputData = new Uint8Array(inputData.length); // 创建长度一行的empty数组

  // 将图像数据转换为灰度
  for (let i = 0; i < inputData.length; i += 4) {
      const avg = (inputData[i] + inputData[i + 1] + inputData[i + 2]) / 3;
      outputData[i] = avg;
      outputData[i + 1] = avg;
      outputData[i + 2] = avg;
      outputData[i + 3] = inputData[i + 3]; // 保留 alpha 值
  }

  // 创建并返回处理后的 ImageData 对象
  return new ImageData(new Uint8ClampedArray(outputData.buffer), canvas.width, canvas.height);
}

上述代码我们实现以下功能:

  1. 通过监听主线程中的发出的 message,拿到图片数据
  2. processImage(imageBitmap) 处理图片数据后,再向主线程传递被处理后的数据
  3. processImage函数中,主要利用canvas画布的drawImage将图片绘制出来,再借助canvas的getImageData读取到绘制的图片的详细信息,读取到的数据又为二进制,我为了方便接下来对其做灰度处理,所以再将二进制通过Uint8Array将其转换为数组

二进制是计算机语言,我们没法操作二进制数据,所以要将其转化为我们能操作的的了的数据

我们看一下灰度化之前和之后的数据对比

现在灰度化处理做好了,也成功将数据返回给了主线程,那么我们再回到主线程中添加代码

ini 复制代码
// ...

// 监听Web Worker的消息
worker.onmessage = function (e) {
  const processedImageBitmap = e.data.processedImageBitmap;

  // 在预览元素中显示处理后的图像
  const previewCanvas = document.createElement('canvas');
  previewCanvas.width = processedImageBitmap.width;
  previewCanvas.height = processedImageBitmap.height;
  const previewCtx = previewCanvas.getContext('2d');
  previewCtx.drawImage(processedImageBitmap, 0, 0);
  previewImage.src = previewCanvas.toDataURL();
};

// ...

你可能会问为什么这里又用到了canvas?这里我们拿到处理好的数据,再次借助canvas只是因为canvas绘制好的图片再被toDataURL() 会默认读取成base64格式的地址,方便前端预览而已啦~~~

效果展示

代码写好了给各位看官们展示最终的效果

效果达到,并且可以看到代码执行时间很短,当然这里的执行时间只是主线程得执行时间。最后附上完整的主线程中的代码,worker.js 代码文中已是完整的。

index.html 主线程代码:

xml 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Image Processing</title>
  <style>
    #previewImage{
      width: 300px;
    }
  </style>
</head>

<body>
  <input type="file" id="imageInput">
  <div id="previewContainer">
    <h2>Preview</h2>
    <img id="previewImage" alt="Preview">
  </div>

  <script>
    // 获取元素
    const imageInput = document.getElementById('imageInput');
    const previewImage = document.getElementById('previewImage');

    // 监听文件上传事件
    imageInput.addEventListener('change', handleImageUpload);


    // 处理图像上传事件
    function handleImageUpload(event) {
      // console.time()

      const file = event.target.files[0];

      if (file) {
        // 使用FileReader来读取上传的图像文件
        const reader = new FileReader();
        reader.onload = function (e) {
          const imageData = e.target.result;
          // console.log(imageData);

          // 创建一个新的Web Worker
          const worker = new Worker('worker.js');

          // 使用 createImageBitmap 来转换数据
          createImageBitmap(new Blob([imageData])).then(imageBitmap => {
            // console.log(imageBitmap);
            // 向Web Worker发送图像数据
            worker.postMessage({ imageBitmap }, [imageBitmap]);

            // 监听Web Worker的消息
            worker.onmessage = function (e) {
              const processedImageBitmap = e.data.processedImageBitmap;

              // 在预览元素中显示处理后的图像
              const previewCanvas = document.createElement('canvas');
              previewCanvas.width = processedImageBitmap.width;
              previewCanvas.height = processedImageBitmap.height;
              const previewCtx = previewCanvas.getContext('2d');
              previewCtx.drawImage(processedImageBitmap, 0, 0);
              previewImage.src = previewCanvas.toDataURL();
            };
          });
        };

        // 读取图像文件
        reader.readAsArrayBuffer(file);
      }
    
      // console.timeEnd()
    }

  </script>
</body>

</html>
相关推荐
太阳花ˉ3 分钟前
html+css+js实现step进度条效果
javascript·css·html
小白学习日记44 分钟前
【复习】HTML常用标签<table>
前端·html
john_hjy1 小时前
11. 异步编程
运维·服务器·javascript
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo2 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE2 小时前
VSCode终端:提升命令行使用体验
前端
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试