在专用网络工作者中进行图像像素操作

问题描述 投票:0回答:1

我需要使用 JavaScript 在浏览器中进行计算密集型图像像素操作。像素操作包括直方图均衡、使用亮度(CIE Lab 色彩空间的 L*)转换为灰度。

为了不阻塞 UI 线程,我想在专用的 Web Worker 中执行此像素操作。

处理后的图像不会仅一次直接绘制到画布上。图像可以缩放和平移,一些其他形状将绘制在其上。因此,我无法使用

canvas.transferControlToOffscreen()

我想出了一个解决方案,使用构造函数创建

OffsceenCanvas
,绘制图像并获取
ImageData
。然后将
ImageData
作为可传输对象发送给工作人员
worker.postMessage(imageData, [imageData.data.buffer])
,并在处理后将其从工作人员发送回主线程
self.postMessage(imageData, [imageData.data.buffer])

这是一个最优的解决方案还是还有其他更有效的模式?

main.js

const canvas = document.getElementById('canvas');
const zoom = 1, offsetX = 0, offsetY = 0;
const offscreen, offscreenCtx;

const draw = () => {
  if (!offscreen) {
    return;
  }
  const ctx = canvas.getContext('2d');
  ctx.save();
  // simplified zooming and panning
  ctx.scale(zoom, zoom);
  ctx.translate(offsetX, offsetY);
  ctx.drawImage(offscreen, 0, 0);
  ctx.restore();
};

canvas.addEventListener('wheel', e => {
  e.preventDefault();
  if (e.deltaY > 0) {
    zoom *= 1.1;
  } else {
    zoom /= 1.1;
  }
  draw();
});

const worker = new Worker(new URL('./worker.js', import.meta.url), {
  type: 'module',
});
worker.addEventListener('message', message => {
  const imageData = message.data;
  offscreenCtx.putImageData(imageData, 0, 0);
  draw();
});

const image = new Image();
image.addEventListener('load', () => {
  offscreen = new OffscreenCanvas(image.width, image.height);
  offscreenCtx = canvas.getContext('2d');
  offscreenCtx.drawImage(image, 0, 0);
  const imageData = offscreenCtx.getImageData(0, 0, image.width, image.height);
  // clear the offscreen canvas to make sure the unprocessed image can't be sh
  offscreenCtx.clearRect(0, 0, image.width, image.height);
  worker.postMessage(imageData, [imageData.data.buffer]);
});

document.getElementById('image-select').addEventListener('change', e) => {
  const files = e.target.files;
  if (files && files.length > 0) {
    const file = files[0];
    if (/image\/.*/.test(file.type)) {
      image.src = URL.createObjectURL(file);
    }
  }
});

worker.js

self.addEventListener('message', message => {
  const imageData = message.data;
  pixelManipulation(imageData);
  self.postMessage(imageData, [imageData.data.buffer]);
  self.close();
});
javascript performance canvas html5-canvas web-worker
1个回答
0
投票

图像可以缩放和平移,一些其他形状将绘制在其上。因此,我无法使用 canvas.transferControlToOffscreen()。

不知道你是如何得出这个结论的。您可以很好地在工作人员中的

OffcreenCanvas
上绘制缩放或平移图像,并在占位符画布上渲染。
唯一的问题是处理事件,但您仍然可以从主线程执行此操作,并仅将绘图参数发送给您的工作人员,或者我不久前编写了一个
EventPort
原型
,可以帮助解决这种情况。


现在,如果您确实不希望所有代码都位于工作线程中,您仍然可以将大部分正在执行的操作移到那里。

不要使用

HTMLImageElement
来加载和解码您的图像,而是直接使用工作线程中的
createImageBitmap(Blob)
,这意味着您必须自己
fetch()
您的图像,但您将节省处理时间。

[注意] 不幸的是,我们仍然没有

ImageBitmap#getImageData()
方法,但是比通过
OffscreenCanvas
执行性能更好的路径是使用
VideoFrame
然后将其数据复制到缓冲区中,尽管它仍然是实验性的(仅在Chromium 浏览器)并且我们仍然没有办法强制执行 RGBA 格式,所以我暂时将其隐藏,但它可能仍然有用。

async function readImageData(url) {
  const resp = await fetch(url);
  if (!resp.ok) { throw "Network Error"; }
  const blob = await resp.blob();
  const bmp = await createImageBitmap(blob);
  const { width, height } = bmp;
  if ("VideoFrame" in window) {
    const frame = new VideoFrame(bmp, { timestamp: 0 })
    bmp.close();
    const { format } = frame;
    if (format !== "RGBA" || format !== "RGBX") {
      console.warn("This image would require a special parsing logic for: ", format);
    }
    const arr = new Uint8Array(frame.allocationSize());
    frame.copyTo(arr);
    frame.close();
    return { width, height, data: arr, format: frame.format };
  }
  console.warn("using legacy canvas2d.getImageData()");
  const canvas = new OffscreenCanvas(bmp.width, bmp.height);
  const ctx = canvas.getContext("2d", { willReadFrequently: true });
  ctx.drawImage(bmp, 0, 0);
  bmp.close();
  return ctx.getImageData(0, 0, width, height);
}

readImageData("https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Elakha.jpg/320px-Elakha.jpg")
  .then(console.log)
  .catch(console.error);


因此,我们在工作程序中对图像进行解码,提取像素数据,进行处理,现在仍然是将

ImageData
转换为位图,这是您在调用
putImageData()
时执行的操作。
缩放和平移
ImageData
意味着两步光栅化,首先对未变换的光栅化,然后变换该位图。
因此,不要将
ImageData
传递到主线程,而是再次使用
createImageBitmap()
传递
ImageData
对象,并将生成的
ImageBitmap
发送到渲染线程。您将能够直接从那里进行转换:

const workerContent = document.querySelector("[type=worker]").textContent;
const workerURL = URL.createObjectURL(new Blob([workerContent], { type: "text/javascript" }));
const worker = new Worker(workerURL);
worker.onmessage = ({data: bmp}) => {
  const canvas = document.querySelector("canvas");
  // Rendering rotated 90deg
  canvas.width = bmp.height;
  canvas.height = bmp.width;
  const ctx = canvas.getContext("2d");
  ctx.translate(canvas.width/2, canvas.height/2);
  ctx.rotate(Math.PI/2);
  ctx.drawImage(bmp, -bmp.width/2, -bmp.height/2);
};
worker.postMessage("https://upload.wikimedia.org/wikipedia/commons/thumb/9/9d/Elakha.jpg/320px-Elakha.jpg");
<script type="worker">
  function process({ data }) {
    for (let i = 0; i<data.length; i+=4) {
      const r = data[i];
      data[i] = data[i+1];
      data[i+1] = r;
    }
  };
  onmessage = async ({ data: url }) => {
    const resp = await fetch(url);
    if (!resp.ok) { throw "Network Error"; }
    const blob = await resp.blob();
    const source = await createImageBitmap(blob);
    const { width, height } = source;
    // Using  legacy context2d.getImageData() for now.
    const canvas = new OffscreenCanvas(width, height);
    const ctx = canvas.getContext("2d", { willReadFrequently: true });
    ctx.drawImage(source, 0, 0);
    source.close(); // free memory, we don't need it anymore
    const imageData = ctx.getImageData(0, 0, width, height);
    process(imageData);
    const bmp = await createImageBitmap(imageData);
    // Transferring the ImageBitmap auto closes it.
    self.postMessage(bmp, [bmp]);
  }
</script>
<canvas></canvas>


最后,不要创建一次性工作人员,以可重用的方式编写代码。启动一个worker是一项繁重的操作,相比之下保持它的存活就很好了。
因此,如果您需要处理多个图像,或者即使您需要在另一个线程中执行其他工作,请保持该工作线程处于活动状态并使其处理所有这些工作。

© www.soinside.com 2019 - 2024. All rights reserved.