How to create a clipping mask that preserves transparency?

I’ve been trying to implement clipping masks for a project and have come up against a roadblock. I want to be able to mask over a PGraphics object that has some transparent pixels while retaining that transparency. The clip() function only works for rectangle clipping areas, which is not sufficient for me, and the mask() function implements an alpha mask, not a clipping mask, so it doesn’t preserve transparency. The result of using mask() is that areas that I want to keep transparent become black, like in this image, where it “should” just show a red circle against a yellow background:

with_mask

As I understand from some experimenting, mask() takes the blue color data in the masking image, and sets the alpha value of the image being masked to that value, regardless of what it was before. What I need is a binary clipping function, that either turns pixels transparent or leaves them untouched. Is there a good, built-in way to do this, or will I need to write my own function? Any advice is appreciated.

2 Likes

I found a workable answer, so I’m posting my findings for completeness. It remains to be seen if this solution will scale well with my program, but it seems promising.

I am going with an ‘alpha subtraction’ method. I made that term up, but I think you can do something like this in Photoshop with the right brush and settings. I grab the blue value of the mask and subtract that from the image’s current alpha value. This would produce the opposite effect of a regular alpha mask, so I also subtract the blue value from 255. Subtracting alpha from the image maintains transparency in the way that a regular alpha mask cannot, since it just overrides the value.

Below is demonstrative code. Comment out lines 32 or 33 or both to see the different options.

Here’s what it looks like with the alpha subtraction mask (line 32 commented out):
new_mask

PGraphics pg, mask;

void setup() {
  size(400, 400);
  
  //draw a yellow background
  background(#ffd700);
  
  pg = createGraphics(width, height);
  mask = createGraphics(width, height);
  
  //draw 4 red circles over a transparent background
  pg.beginDraw();
  pg.clear();
  pg.noStroke();
  pg.fill(#D3191C);
  pg.ellipse(width/2,height/2,50,50);
  pg.ellipse(150,150,50,50);
  pg.ellipse(100,100,50,50);
  pg.ellipse(50,50,50,50);
  pg.endDraw();
  
  //create a square clipping mask
  mask.beginDraw();
  mask.background(0);
  mask.noStroke();
  mask.fill(255);
  mask.rect(100,100,200,200);
  mask.endDraw();
  
  //mask the circles
  //pg.mask(mask); // <- std function
  alphaSubtract(pg, mask);  // <- new function
  
  //show the result
  image(pg, 0, 0);
}

void draw() {
}

void alphaSubtract(PGraphics img, PGraphics cm){
  img.loadPixels();
  cm.loadPixels();
  if(img.pixels.length != cm.pixels.length){
    return;
  }
  for(int j = 0; j<img.height; j++){
    for(int i = 0; i<img.width; i++){
      // get argb values
      color argb = img.pixels[(j*img.width) + i];
      int a = argb >> 24 & 0xFF;
      int r = argb >> 16 & 0xFF;
      int g = argb >> 8 & 0xFF;
      int b = argb & 0xFF;
      
      color maskPixel = cm.pixels[(j*img.width) + i];
      int alphaShift = 0xFF - (maskPixel & 0xFF);  //grab blue value from mask pixel
      
      // subtract alphaShift from pixel's alpha value;
      img.pixels[(j*img.width) + i] = color(r,g,b,a-alphaShift);
    }
  }
}

void keyPressed(){
  if(key == 's'){
    save("with_mask.png");
  }
}
3 Likes

Thanks for sharing this solution.

If I’m understanding right, you want to create a mask that is is transparent outside your clipping area AND transparent inside your clipping area if that was the original pixel value. As a related approach, did you look at the alpha data option on the reference page for mask()?

In addition to using a mask image, an integer array containing the alpha channel data can be specified directly. This method is useful for creating dynamically generated alpha masks. This array must be of the same length as the target image’s pixels array and should contain only grayscale data of values between 0-255.

Because PImage.mask() takes an int array, just extract the minimum alpha channel value from the two source images into an array:

int[] minAlphas(PImage img, PImage img2) {
  img.loadPixels();
  img2.loadPixels();
  int[] a = new int[img.pixels.length];
  for (int i =0; i<img.pixels.length; i++) {
    a[i] = min(img.pixels[i] >> 24 & 0xFF, img2.pixels[i] >> 24 & 0xFF);
  }
  return a;
}

You can then call this in a single line of code:

img.mask(minAlphas(img, mask));

Here is an example:

/**
 * MaskAlphaImage
 * 2019-12 Processing 3.4
 * combine a tranparent image with mask alpha, and use both to mask the image
 * https://discourse.processing.org/t/how-to-create-a-clipping-mask-that-preserves-transparency/16093/2
 */
 
PImage img;
PGraphics mask;

void setup() {
  size(480, 480);
  // load source image
  img = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Processing_3_logo.png/480px-Processing_3_logo.png");
  // create drawable mask image (PGraphics)
  mask = createGraphics(480, 480);
  // and configure draw properties
  mask.beginDraw();
  mask.noStroke();
  mask.rect(60, 60, 260, 260);
  mask.endDraw();
  // apply existing alpha and mask --
  // e.g. both the rectangular mask AND the existing
  // circular cutout around the Processing logo are used
  img.mask(minAlphas(img, mask));
}

void draw() {
  background(255, 0, 0);
  image(img, 0, 0);
}

/**
 * return a minimum alpha channel from two images -- 
 * useful combining a transparent image with a second mask
 * and passing it to mask()
 */
int[] minAlphas(PImage img, PImage img2) {
  img.loadPixels();
  img2.loadPixels();
  int[] a = new int[img.pixels.length];
  for (int i =0; i<img.pixels.length; i++) {
    a[i] = min(img.pixels[i] >> 24 & 0xFF, img2.pixels[i] >> 24 & 0xFF);
  }
  return a;
}

MaskAlphaImage--screenshot

5 Likes

Nice, that’s a really good approach! More condensed too. I can see the two approaches being used for slightly different purposes, as they handle semi-transparent values differently. Like, if a pixel with an alpha value of 128 was masked by a value of 128, they have different results. alphaSubtract() would push the value to 0, while minAlphas() would keep it at 128. Either works fine for what I’m doing now.

2 Likes

I always come back to this post :smiley:
Today I wanted to prepare an example for a student, so I made my Python mode version based on your brilliant solution).

def setup():
    global offscreen
    size(500, 500)
    offscreen = createGraphics(width, height)
    offscreen.beginDraw()
    offscreen.clear() # this makes the background transparent
    offscreen.fill(255, 0, 0)
    for _ in range(100):
        offscreen.rect(random(width), random(height), 50, 50)
    offscreen.endDraw()
 
    clip_mask = createGraphics(width, height)
    clip_mask.beginDraw()   
    clip_mask.fill(255)
    clip_mask.circle(250, 250, 500)    
    clip_mask.endDraw()
 
    offscreen.mask(min_alphas(offscreen, clip_mask))
                                         
def draw():
    background(0, 0, 200)
    image(offscreen, 0, 0)  # draws the offscreen buffer
 
def min_alphas(img, img2):
    img.loadPixels()
    img2.loadPixels()
    return [min(pix >> 24 & 0xFF, pix2 >> 24 & 0xFF)       # dark magic, don't ask
            for pix, pix2 in zip(img.pixels, img2.pixels)]

3 Likes

Thanks – and thanks for sharing a Python mode solution, @villares!

:laughing: :rofl:

I know you were joking, but I realize that I didn’t explain this well, so I’ll leave a short explanation here.

  • a color int contains an alpha channel – we want that part
  • the int is arranged with 32 bits in 4 groups of 8bits like this:
    AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB
  • We want to get the alpha out of each color as a number – the AAAAAAAA part – which we can do with alpha().
  • HOWEVER, we are going to do this a lot, with every pixel, so we want to do it fast.

We’ll get the alpha out of each pixel FAST by shifting the AA bits to the right >> and chopping off everything on the left with &

  1. we start with:
    AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB
  2. shift the AAs 24 places to the right with >> 24. Now we have:
    xxxxxxxxxxxxxxxxxxxxxxxxAAAAAAAA
  3. make anything but the AAs into a zero. we’ll use 0xFF, which is eight 1s: 11111111
    xxxxxxxxxxxxxxxxxxxxxxxxAAAAAAAA
    &
    00000000000000000000000011111111
    =
    000000000000000000000000AAAAAAAA

Only the AAs are left over, so we have our alpha value as a normal integer, and we got it super fast with >> and & instead of using alpha().

For a good visual explanation, see:

from:

2 Likes