Black and white (not grayscale) filter in svg, ideally snap.svg

4.2k views Asked by At

Edit 1: Rewrote question so it is clearer to understand.

This one really split my mind.

I would like to apply a filter to an image such that values over a certain threshold are displayed as completely white, and all the values that are equal or less than the threshold are completely black.

here's a map representing RGB values of a 3x3 pixel image:

|(255,255,255)|(220,220,220)|(100,100,100)|  
|(254,254,254)|(12 ,12 ,12 )|(38 ,38 ,38 )|  
|(201,201,201)|(105,105,105)|(60 ,60 ,60 )|

After applying my filter I wish to receive an image where all values greater than 200 are converted to (255,255,255) and all values equal or less than 200 are converted to (0,0,0) such:

|(255,255,255)|(255,255,255)|(0  ,0  ,0  )|
|(255,255,255)|(0  ,0  ,0  )|(0  ,0  ,0  )|
|(255,255,255)|(0  ,0  ,0  )|(0  ,0  ,0  )|

Any ideas for where I should start? Is that even possible in svg? I know I can create matrix multiplications in filters in svg but I don't know how to approach this issue.

thank you!

Edit 1: Related question I posted in math exchange: https://math.stackexchange.com/questions/2087482/creating-binary-matrix-for-threshold-as-a-result-of-matrix-multiplcation

Edit 2:: Extracted from the Snap.svg code: this matrix will transform a color image to a grayscale:

<feColorMatrix type="matrix" values="0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0.2126 0.7152 0.0722 0 0 0 0 0 1 0"/>

Instead of getting grayscale, I would like to modify the values here to get Black and White. And I would like to choose the threshold above which values are returned as white, and below are returned as black

Additional Info: According to MSDN, this is how the multiplication occurs:

Resulting vector   my coveted matrix     original vector
    | R' |     | a00 a01 a02 a03 a04 |   | R |
    | G' |     | a10 a11 a12 a13 a14 |   | G |
    | B' |  =  | a20 a21 a22 a23 a24 | * | B |
    | A' |     | a30 a31 a32 a33 a34 |   | A |
    | 1  |     |  0   0   0   0   1  |   | 1 |

This in turn is applied to every pixel of the inputed image.

3

There are 3 answers

3
Paul LeBeau On BEST ANSWER

You can achieve thresholding with the <feComponentTransfer type="discrete"> filter primitive.

Here's an example.

<svg width="300" height="550" style="background-color: linen">
  <defs>
    <linearGradient id="gradient">
      <stop offset="0" stop-color="white"/>
      <stop offset="1" stop-color="black"/>
    </linearGradient>
    
    <filter id="threshold" color-interpolation-filters="sRGB">
      <feComponentTransfer>
        <feFuncR type="discrete" tableValues="0 1"/>
        <feFuncG type="discrete" tableValues="0 1"/>
        <feFuncB type="discrete" tableValues="0 1"/>
      </feComponentTransfer>
    </filter>
  </defs>

  <rect x="50" y="50" width="200" height="200" fill="url(#gradient)"/>
  <rect x="50" y="300" width="200" height="200" fill="url(#gradient)" filter="url(#threshold)"/>
</svg>

How this primitive works is that you create a table of bands for each colour component. However the number of bands is related to the input, not the output. Think of it as the input is divided up into a number of equal sized bands. Then you assign an output value to each band. So if you want the split to happen at 50% (R, G, or B = 0.5 (128)) then you create two bands, "0 1". Values in the first band (0 -> 0.5) are assigned the value 0, and values in the second band (0.5 -> 1) get assigned the value 1.

So for example, if you wanted to threshold at 20% (0.2 or 51), you would need to create five bands. And the output table values would be "0 1 1 1 1".

<svg width="300" height="550" style="background-color: linen">
  <defs>
    <linearGradient id="gradient">
      <stop offset="0" stop-color="white"/>
      <stop offset="1" stop-color="black"/>
    </linearGradient>
    
    <filter id="threshold" color-interpolation-filters="sRGB">
      <feComponentTransfer>
        <feFuncR type="discrete" tableValues="0 1 1 1 1"/>
        <feFuncG type="discrete" tableValues="0 1 1 1 1"/>
        <feFuncB type="discrete" tableValues="0 1 1 1 1"/>
      </feComponentTransfer>
    </filter>
  </defs>

  <rect x="50" y="50" width="200" height="200" fill="url(#gradient)"/>
  <rect x="50" y="300" width="200" height="200" fill="url(#gradient)" filter="url(#threshold)"/>
</svg>

The drawback of how this works is that it does mean that if you want to threshold at an arbitrary component value, you may need to have up to 256 values in the table.

To demonstrate this, in this final example, I use some javascript to update the filter table based on a value set by a range slider.

var selector = document.getElementById("selector");
var funcR = document.getElementById("funcR");
var funcG = document.getElementById("funcG");
var funcB = document.getElementById("funcB");

function updateFilter() {
  // Input value
  var threshold = selector.value;
  // Create the table
  var table = "0 ".repeat(threshold) + "1 ".repeat(256-threshold);
  // Update the filter components
  funcR.setAttribute("tableValues", table);
  funcG.setAttribute("tableValues", table);
  funcB.setAttribute("tableValues", table);
}

selector.addEventListener('input', updateFilter);

// Call the filter updater once at the start to initialise the filter table
updateFilter();
<svg width="300" height="300" style="background-color: linen">
  <defs>
    <filter id="threshold" color-interpolation-filters="sRGB">
      <feComponentTransfer>
        <feFuncR id="funcR" type="discrete" tableValues="0 1 1 1"/>
        <feFuncG id="funcG" type="discrete" tableValues="0 1 1 1"/>
        <feFuncB id="funcB" type="discrete" tableValues="0 1 1 1"/>
      </feComponentTransfer>
    </filter>
  </defs>

  <image xlink:href="https://placekitten.com/g/300/300"
         x="50" y="50"width="200" height="200"
         filter="url(#threshold)"/>
</svg>

<form>
  <input id="selector" type="range" min="0" max="256"/>
</form>

1
Michael Mullany On

This is what the feComponentTransfer primitive for SVG is used for, with a type="discrete". Please consult the webplatform documentation to understand how to use the element.

0
Piglet On

The RGB to grayscale conversion you refer to is a linear process. It can be done through a matrix multiplication. Every output value is a weighted sum of R,G and B values. If you choose the same weights for every output channel the resulting RGB image will appear gray.

Binarization is a non-linear operation. Therefor it is not possible to modify that conversion matrix so it will do black and white instead of grayscale.

I don't know if there is a flag or something you can add to the svg file that will make it look black and white but you could just write a litte program that replaces the colour values appropriately.