3

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.

Community
  • 1
  • 1
Michael Seltenreich
  • 3,013
  • 2
  • 29
  • 55
  • You could look at svg filters. It's difficult to see quite what you are after. Is there an initial image or svg that will be being wiped, or what ? – Ian Jan 07 '17 at 13:47
  • No specific svg or image. Basically I need (to create) a filter that takes an image and returns a monochromatic image instead (not grayscale, but strickly black and white). Have all values larger then x appear as white, and all values small or equal to x to appear as black. – Michael Seltenreich Jan 07 '17 at 13:57
  • If you know of a built-in svg filter or such, that would be great. If not, I'll have to figure this mathy problem. http://math.stackexchange.com/questions/2087482/creating-binary-matrix-for-threshold-as-a-result-of-matrix-multiplcation – Michael Seltenreich Jan 07 '17 at 14:07
  • I get that you want to convert an RGB image into a B&W one. As you later said, probably you want an application that applied to every single pixel, return (0,0,0) or (255,255,255) (the A stands for opacity). My question is: if you get a pixel (203,97,150), what would be the output? – Exodd Jan 07 '17 at 15:24
  • I don't have a pixel (203,97,150). In my input all three fields are equal, that is always of the format (x,x,x) – Michael Seltenreich Jan 07 '17 at 15:44

3 Answers3

10

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>
Paul LeBeau
  • 97,474
  • 9
  • 154
  • 181
2

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.

Michael Mullany
  • 30,283
  • 6
  • 81
  • 105
0

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.

Piglet
  • 27,501
  • 3
  • 20
  • 43