'Remove image background with HTML Canvas given the alpha matte

How to remove the background of an image using HTML Canvas, given an alpha matte as a matrix?
More context about the problem:

  • Alpha matte is a matrix of floats between 0 and 1, inclusive. It corresponds to the alpha channel in PNG images.
  • 0 corresponds to a fully transparent, and 1 corresponds to a fully opaque pixel. Values between 0 and 1 represent mixed pixels.
  • Alpha is always a square matrix; however, the image could be rectangular.
  • The image is an RGB image.
  • The output should be an image with transparent background.

Example image:
enter image description here
Alpha channel visualization:
enter image description here

Expected output:
enter image description here

A minimal, reproducible example:

<!DOCTYPE html>
<html>
<body>

<p>Image to use:</p>
<img id="scream" width="220" height="277" src="https://www.w3schools.com/tags/img_the_scream.jpg" alt="The Scream">

<p>Image without background:</p>
<canvas id="canvas" width="220" height="277" style="border:1px solid #d3d3d3;">
</canvas>

<script>
var alpha = [
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 1.0, 0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 0.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.9, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7, 1.0, 1.0, 0.8, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 0.8, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6, 1.0, 0.9, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6, 1.0, 0.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7, 1.0, 0.9, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
]

var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var img = document.getElementById("scream");
ctx.drawImage(img, 0, 0);

</script>

</body>
</html>

I solved the problem when I keep alpha channel as a gray-scale image. However, I couldn't achieve the same result when the alpha channel is a matrix, as represented in the minimal example I provided.



Solution 1:[1]

See the MDN canvas documentation.. It is the same principle, here you want to change the alpha channel of the pixels so you may have something that look like this :

ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
    data[i + 3] = flatAlpha[i / 4];
}
ctx.putImageData(imageData, 0, 0);

The ImageData.data element is an array containing all the values of the pixels flattened with the red color, green, blue and alpha. Here i will correspond to the index of the red color, so i + 3 will correspond to the alpha channel. On another hand, you will need the flattened alpha variable and you get the value which you divide by four because on for each pixel on the ImageData.data you get four values when on the alpha you got one.

I hope I have been enough clear on my explanations, if you have questions, feel free to ask.

Edit : After re-reading your problem, the alpha matrix isn't always the same size as the image, so as I guessed of what I saw, it aligns on the bottom of the image (and I guess the width is the same). In this case, you should make a function that fills that blank space on the alpha array, or either shift the index use in the flatAlpha array by the amount of pixels missing.

Answer

Edit 2 : After some reflection, I made an other answer for your question. The main idea is to map through all pixel of the image and get its value.
The problem is that the alpha matrix doesn't match the size of the image. This is still a problem on my answer because the alpha matrix is absolutely not accurate.
So the principle is to map through all the pixels on the array with x and y coordinates, then get the index in the ImageData.data array from the x and y values.
The ImageData.data array, is basically the concatenation of the red, blue, green and alpha values for each pixel. (see the reference I mentioned before). For the scale function, it takes a value within a range and an other range to map. Example: You have the value 0.5 which is on the range [0;1] (you you the value is greater than 0 and lower than 1), and you want that value on the range [0;100], so you apply the function and get 50.
Applying this function on the coordinates of each pixel's coordinates get the coordinates of the pixels on the alpha matrix. Then, you update the fourth value of the pixel on the ImageData.data and get the result.

You can see on the example that the opacity is absolutely not accurate, because of the size of the alpha matrix.

const alpha = [ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 1.0, 0.6, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 0.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3, 0.9, 1.0, 0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7, 1.0, 1.0, 0.8, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.8, 1.0, 1.0, 0.8, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6, 1.0, 0.9, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.6, 1.0, 0.7, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7, 1.0, 0.9, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]];


// Get the size of the alpha matrix
const alphaWidth = alpha[0].length;
const alphaHeight = alpha.length;

// Retrieve the elements from the DOM
const canvas = document.getElementById('canvas');
const img = document.getElementById('scream');

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

/**
 * Clamp the value (returns a value in the interval)
 * @param {number} value The value to be clamped
 * @param {number} min The minimum value of the interval
 * @param {number} max The maximum value of the intervak
 * @returns A number in the given interval
 */
const clamp = (value, min, max) => {
  return Math.min(max, Math.max(min, value));
};

/**
 * Scale a number from a range to another
 * @param {number} value The value to be scaled
 * @param {number} min The value interval min
 * @param {number} max The value interval max
 * @param {number} newMin The new interval min
 * @param {number} newMax The new interval max
 *
 * @example scale(15, 10, 20, 0, 100) = 50 // Because 15 in range [10; 20] gives 50 in range [0; 100]
 *
 * @returns A number
 */
const scale = (value, min, max, newMin, newMax) => {
  const scaledTo0Value = value - min;
  const newDiff = newMax - newMin;
  const diff = max - min;

  const mapped = newMin + (scaledTo0Value * newDiff) / diff;
  return clamp(mapped, newMin, newMax);
};

/**
 * Draw the image with the alpha matrix
 */
const draw = () => {
  // Draw the default image on the canvas
  ctx.drawImage(img, 0, 0);

  // Retrieve the ImageData data, width and height from the loaded image.
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const { data, width, height } = imageData;

  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      // Get index of array in a flattened array
      const index = y * width + x;

      // Scale the x and y if the image pixel to the alpha matrix size
      const alphaX = Math.round(scale(x, 0, width - 1, 0, alphaWidth - 1));
      const alphaY = Math.round(scale(y, 0, height - 1, 0, alphaHeight - 1));

      // Retrieve the alpha value from the alpha matrix
      const alphaValue = alpha[alphaY][alphaX];

      // Apply the alpha on the ImageData alpha channel (alpha values need to be map over 255)
      data[index * 4 + 3] = Math.round(scale(alphaValue, 0, 1, 0, 255));
    }
  }

  // Put the image data on the canvas
  ctx.putImageData(imageData, 0, 0);
};

// Needed otherwise, it tries to load the ImageData before the image gets loaded
img.addEventListener('load', draw);
  <p>Image to use:</p>
  <img id="scream" width="220" height="277"
    src="https://i.imgur.com/p05yC3N.jpeg" alt="The Scream"
    crossorigin="anonymous">

  <p>Image without background:</p>
  <canvas id="canvas" width="220" height="277"
    style="border:1px solid #d3d3d3;">
  </canvas>

PS: Sorry for all the english mistakes.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1