alex
alex

Reputation: 61

Is it possible to make dilation or erosion morphology for a Uint8ClampedArray from ImageData canvas?

I've got an Uint8ClampedArray from ImageData(ctx.getImageData()). I grayscaled it by 0.21 * R + 0.72 * G + 0.07 * B formula. So now I have a Uint8ClampedArray, where each 4 is a rgba channels. I need to make dilation with that array, then with the result of dilation make an erosion. I was searching over the internet and found many different solutions with python and OpenCV, but nothing with JS. I know that SVG allows to use feMorphology filters with dilation and erosion, but is there something like that in canvas? Or is that possible to use Uint8ClampedArray | ImageData with svg filters? Is it possible to make own algorithm, which will make these morph operations? Thanks

Upvotes: 3

Views: 437

Answers (2)

Ateş Göral
Ateş Göral

Reputation: 140050

Here's a general way to compose a stack of SVG filters and apply it directly on a canvas:

const DILATION_DISTANCE = 4;

const DILATION_SVG_FILTER_NAME = 'dilation';
const DILATION_SVG = `<svg xmlns="http://www.w3.org/2000/svg">
  <filter id="${DILATION_SVG_FILTER_NAME}">
    <feMorphology operator="dilate" radius="${DILATION_DISTANCE}" />
  </filter>
</svg>`;
const DILATION_SVG_URL = `data:image/svg+xml;base64,${btoa(DILATION_SVG)}`;
const DILATION_CTX_FILTER = `url(${DILATION_SVG_URL}#${DILATION_SVG_FILTER_NAME})`;

// ...

const ctx = canvas.getContext('2d');

ctx.filter = DILATION_CTX_FILTER;

ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
  1. Using a data: URL to compose the SVG instead of fetching it from elsewhere. You don't necessarily have to base-64 encode it via btoa(). encodeURIComponent() would also do.
  2. You need to apply the filter on the rendering context before drawing the image.

In the OP's particular case, you can include a <feColorMatrix> filter before <feMorphology> to turn the image into grayscale first:

 <feColorMatrix type="saturate" values="0"/>

Assuming you're starting off with an ImageData, you'd need to put that on an off-screen canvas first and then the draw this canvas on your target canvas:

const ofsCanvas = new OffscreenCanvas(imageData.width, imageData.height);
const ofsCtx = ofsCanvas.getContext('2d');
ofsCtx.putImageData(imageData, 0, 0);

ctx.filter = DILATION_CTX_FILTER;
ctx.drawImage(ofsCanvas, 0, 0); // You can draw a canvas on a canvas

You can also create the offscreen canvas using a DOM canvas, if browser support is an issue:

const ofsCanvas = document.createElement('canvas');
ofsCanvas.width = imageData.width;
ofsCanvas.height = imageData.height;

When done with all this, grab the dilated image's ImageData with:

const dilatedImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

Upvotes: 0

alex
alex

Reputation: 61

Well, own algo: Erosion:

function erosionFilter(frame, width, height) {
const erodedFrame = new Uint8ClampedArray(frame.length);

for (let y = 1; y < height - 1; y++) {
  for (let x = 1; x < width - 1; x++) {
    let min = 255;

    for (let j = -1; j <= 1; j++) {
      for (let i = -1; i <= 1; i++) {
        const value = frame[(y + j) * width + (x + i)];
        if (value < min) {
          min = value;
        }
      }
    }

    erodedFrame[y * width + x] = min;
  }
}

return erodedFrame;
}

Dilate:

function dilationFilter(frame, width, height) {

const dilatedFrame = new Uint8ClampedArray(frame.length);

for (let y = 1; y < height - 1; y++) {
  for (let x = 1; x < width - 1; x++) {
    let max = 0;

    for (let j = -1; j <= 1; j++) {
      for (let i = -1; i <= 1; i++) {
        const value = frame[(y + j) * width + (x + i)];
        if (value > max) {
          max = value;
        }
      }
    }

    dilatedFrame[y * width + x] = max;
  }
}

return dilatedFrame;
}

frame - Uint8ClampedArray to filter, returns filtered Uint8ClampedArray

Upvotes: 2

Related Questions