多线程做图片灰度化处理

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

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

思路

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

按照我们的思路,我们需要使用 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>
相关推荐
前端大卫24 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘39 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare40 分钟前
浅浅看一下设计模式
前端
Lee川44 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端