大家好我是蜗牛,倾心于用直白的大白话来解释清楚复杂的问题的,让新手也能很好的接受。
或许你还没遇到过让你棘手的图片处理需求,又或许你已经面临着,我们今天要聊的是一个非常正常,但是却让我多掉了十根头发的需求,上传图片时,将图片灰度化处理成黑白色并让用户可预览
(这个需求让我第一时间想到了殡葬行业的定制化墓碑),无论如何,点赞收藏这篇文章,将来你可能用的上(我指的是代码,不是定制化服务,>_<...)
思路
一张五颜六色的照片,如何让其变成黑白色,还要考虑到整体的性能问题,思路大致是这样的
按照我们的思路,我们需要使用 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>
// ...
以上代码中,我们实现了一下功能:
- 使用 new FileReader() 读取文件资源,在load中获取二进制数据 imageData
- new Worker('worker.js'); 会导致浏览器开启新的线程,这样做的好处是让主线程不被占用依然能正常执行后续的逻辑
- 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);
}
上述代码我们实现以下功能:
- 通过监听主线程中的发出的 message,拿到图片数据
- processImage(imageBitmap) 处理图片数据后,再向主线程传递被处理后的数据
- 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>