Jack
Jack

Reputation: 43

Finding transparent pixels on an image and making same pixels transparent on another image

I have two images. The second one is the result of applying some sort of mask to the first one. What I need is to get that mask and be able to apply it to other images. Here are the two images: normal, tattered.

As you can see, the second one has tattered edges, they are transparent, not white. (There's also some kind of blur going on, if there's a way I can find out what blur it is exactly, then great, but it's not really necessary here)

What I need is to be able to create the second image from the first one.

Theoretically, I should create a mask - an image of the same size with any color and each pixel having transparency of either 0 or 255, depending on the transparency value of the same pixel in the second image from above. Then I can just set alpha of pixels of any input image to alpha values from this mask.

However, I have no idea how to do it practically. I tried it in java using BufferedImage, however, it does not work. When I try to getAlpha from the color of the selected pixel, it is always 255, even for pixels which are supposed to be transparent. I did manage to get alpha values in Processing (they are actually not just 0 or 255, lots of values inbetween), however, when I try to apply this value to a new image and save it, it saves as fully opaque image, when I load it, alpha values are all 255.

  PImage mask = loadImage("some/path/1.png");
  PImage img = loadImage("some/path/2.png");

  img.loadPixels();
  for (int x = 0; x < img.width; x++) {
    for (int y = 0; y < img.height; y++) {
      color maskColor = mask.get(x, y);
      if (alpha(maskColor) < 255) {
        color imgColor = img.get(x, y);
        img.pixels[y*img.width + x] = color(red(imgColor), green(imgColor), blue(imgColor), alpha(maskColor));
      }
    }
  }
  img.updatePixels();
  img.save("some/path/3.png"); 

Upvotes: 3

Views: 1471

Answers (4)

George Profenza
George Profenza

Reputation: 51837

I'm not sure why the check is necessary in your particular example. If the destination image doesn't use the alpha channel (it's all opaque), you can simply overwrite the data using the source image's alpha channel.

If you're using pixels[] by the way, a single loop should do:

PImage withAlpha;
PImage noAlpha;

void setup(){
  size(120, 130);
  background(0);
  
  withAlpha = loadImage("8fXFk.png");
  noAlpha = loadImage("AOsi0.png");
  
  copyAlphaChannel(withAlpha, noAlpha);
}

void draw(){
  background(map(sin(frameCount * 0.1), -1.0, 1.0, 0, 192), 0, 0);
  image(withAlpha, 0, 0);
  image(noAlpha, 60, 0);
}

void copyAlphaChannel(PImage src, PImage dst){
  // quick error check
  if(src.width != dst.width || src.height != dst.height){
    println(String.format("error, mismatching dimensions src(%d,%d) != dst(%d,%d)",
            src.width, src.height, dst.width, dst.height));
    return;
  }
  // load pixel data
  src.loadPixels();
  dst.loadPixels();
  
  int numPixels = src.pixels.length;
  // for each pixel
  for(int i = 0 ; i < numPixels; i++){
      // extract source alpha
      int srcAlpha = (src.pixels[i] >> 24) & 0xFF;
      // apply it to the destination image
      //              src alpha      |   dst RGB
      dst.pixels[i] = srcAlpha << 24 | (dst.pixels[i] & 0xFFFFFF);
  }
  
  dst.updatePixels();
}

two images that appear identical: the second one has it's alpha channel (tattered edges) copied from the first

Update Thank you for pointing that out: I've missed this detail.

PImage can have three formats:

  • RGB (=1)
  • ARGB (=2)
  • ALPHA(=4)

One workaround to convert an RGB format PImage into ARGB format is to apply an opaque mask():

PImage withAlpha;
PImage noAlpha;

void setup(){
  size(120, 130);
  background(0);
  
  withAlpha = loadImage("8fXFk.png");
  noAlpha = loadImage("AOsi0.png");
  
  println("before",withAlpha.format, noAlpha.format, ARGB, RGB); // notice noAlpha's format is RGB
  
  forceAlphaChannel(noAlpha);
  
  println("after",withAlpha.format, noAlpha.format, ARGB, RGB); // notice noAlpha's format is ARGB
  
  copyAlphaChannel(withAlpha, noAlpha);
  
  noAlpha.save("test.png");
  
}

void draw(){
  background(map(sin(frameCount * 0.1), -1.0, 1.0, 0, 192), 0, 0);
  image(withAlpha, 0, 0);
  image(noAlpha, 60, 0);
}

void forceAlphaChannel(PImage src){
  // make an opaque mask
  PImage mask = createImage(src.width, src.height, ALPHA);
  java.util.Arrays.fill(mask.pixels, color(255));
  mask.updatePixels();
  // apply the mask force the RGB image into ARGB format
  src.mask(mask);
}

void copyAlphaChannel(PImage src, PImage dst){
  // quick error check
  if(src.width != dst.width || src.height != dst.height){
    println(String.format("error, mismatching dimensions src(%d,%d) != dst(%d,%d)",
            src.width, src.height, dst.width, dst.height));
    return;
  }
  // load pixel data
  src.loadPixels();
  dst.loadPixels();
  
  int numPixels = src.pixels.length;
  // for each pixel
  for(int i = 0 ; i < numPixels; i++){
      // extract source alpha
      int srcAlpha = (src.pixels[i] >> 24) & 0xFF;
      // apply it to the destination image
      //              src alpha      |   dst RGB
      dst.pixels[i] = srcAlpha << 24 | (dst.pixels[i] & 0xFFFFFF);
  }
  
  dst.updatePixels();
}

Since the above loops through pixels a bunch of times (once to create the mask and again to apply it) it may be more efficient to create an ARGB PImage in the first place, then copy RGB data from one PImage and ALPHA from another:

PImage withAlpha;
PImage noAlpha;

void setup(){
  size(120, 130);
  background(0);
  
  withAlpha = loadImage("8fXFk.png");
  noAlpha = loadImage("AOsi0.png");
  
  println("before",withAlpha.format, noAlpha.format, ARGB, RGB); // notice noAlpha's format is RGB
  
  noAlpha = getAlphaChannelCopy(withAlpha, noAlpha);
  
  println("after",withAlpha.format, noAlpha.format, ARGB, RGB); // notice noAlpha's format is ARGB
  
  noAlpha.save("test.png");
  
}

void draw(){
  background(map(sin(frameCount * 0.1), -1.0, 1.0, 0, 192), 0, 0);
  image(withAlpha, 0, 0);
  image(noAlpha, 60, 0);
}

// copy src alpha and dst rgb into new ARGB PImage
PImage getAlphaChannelCopy(PImage src, PImage dst){
  // quick error check
  if(src.width != dst.width || src.height != dst.height){
    println(String.format("error, mismatching dimensions src(%d,%d) != dst(%d,%d)",
            src.width, src.height, dst.width, dst.height));
    return null;
  }
  PImage out = createImage(src.width, src.height, ARGB);
  // load pixel data
  src.loadPixels();
  dst.loadPixels();
  out.loadPixels();
  
  int numPixels = src.pixels.length;
  // for each pixel
  for(int i = 0 ; i < numPixels; i++){
      // extract source alpha
      int srcAlpha = (src.pixels[i] >> 24) & 0xFF;
      // apply it to the destination image
      //              src alpha      |   dst RGB
      out.pixels[i] = srcAlpha << 24 | (dst.pixels[i] & 0xFFFFFF);
  }
  
  out.updatePixels();
  
  return out;
}

(The only minor downside here is you'd loadPixels() thrice: once per image.)

Upvotes: 0

Harald K
Harald K

Reputation: 27054

Using BufferedImage, Graphics2D and AlphaComposite, you can compose your image like this:

BufferedImage image = ImageIO.read(new File("image.png"));
BufferedImage mask = ImageIO.read(new File("mask.png"));

BufferedImage composed = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);

Graphics2D g = composed.createGraphics();
try {
    g.setComposite(AlphaComposite.Src); // Possibly faster than SrcOver
    g.drawImage(image, 0, 0, null);

    // Clear out the transparent parts from mask
    g.setComposite(AlphaComposite.DstIn);
    g.drawImage(mask, 0, 0, null);
}
finally {
    g.dispose();
}

if (!ImageIO.write(composed, "PNG", new File("composed.png"))) {
    throw new IIOException("Could not write image using PNG format: " + composed);
}

PS: If you know that your source image (image in the code above) contains transparency and don't need the original afterwards, you could compose the mask directly onto it. This will be faster and use less memory, as you skip the memory allocation and extra composing.

Upvotes: 1

the Hutt
the Hutt

Reputation: 18398

You can use the tattered image as mask for other images as well. You just need the alpha information from the mask.
Implementation of creating tattered borders using BufferedImage:

public class Test {

    public static void main(String[] args) throws IOException {
        
         BufferedImage mask = loadImage("e:\\mask.png");
         BufferedImage img = loadImage("e:\\1.png");

         int width = mask.getWidth();
         int height = mask.getHeight();
         
         BufferedImage processed = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);

          for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
              int rgb = mask.getRGB(x,y);
              int maskAlpha = alpha(rgb);
              int imgColor = img.getRGB(x, y);
              if (maskAlpha < 255) {
                processed.setRGB(x,y,maskAlpha(imgColor, maskAlpha));
              } else {
                  processed.setRGB(x,y,imgColor);
              }
            }
          }
         
          writeImage(processed, "e:\\2.png");
    }

    static BufferedImage loadImage(String imagePath) {
        File file = new File(imagePath);
        BufferedImage image = null;
        try {
            image = ImageIO.read(file);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return image;
    }
    
    static void writeImage(BufferedImage img,String filePath){
        String format = filePath.substring(filePath.indexOf('.')+1);
        //Get Picture Format
        System.out.println(format);
        try {
            ImageIO.write(img,format,new File(filePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    static int maskAlpha(int rgb, int alpha) {
        //strip alpha from original color
        int color = (0x00ffffff&rgb);
        return color + ((alpha)<<24);
    }
    
    static int alpha(int rgb) {
        return (0xff&(rgb>>24));
    }
    
    static int red(int rgb) {
        return (0xff&rgb);
    }
    static int green(int rgb) {
        return (0xff&(rgb>>8));
    }
    static int blue(int rgb) {
        return (0xff&(rgb>>16));
    }
}

Here BufferedImage.TYPE_4BYTE_ABGR means

Represents an image with 8-bit RGBA color components with the colors Blue, Green, and Red stored in 3 bytes and 1 byte of alpha. The image has a ComponentColorModel with alpha. The color data in this image is considered not to be premultiplied with alpha. The byte data is interleaved in a single byte array in the order A, B, G, R from lower to higher byte addresses within each pixel.

That means color integer is 32 bits, which is stored in java in the order of abgr, i.e., alpha is the first 8 bits, and r is the last 8 bits, so you can get the argb value as follows:

int r = (0xff&rgb);
int g = (0xff&(rgb>>8));
int b = (0xff&(rgb>>16));
int a = (0xff&(rgb>>24));

Upvotes: 1

Mady Daby
Mady Daby

Reputation: 1269

You could try taking the difference between the alpha channels of the original image and the tattered image.

PImage tattered = loadImage("some/path/1.png");
PImage img = loadImage("some/path/2.png");
PImage mask = image.copy();

img.loadPixels();

for (int x = 0; x < img.width; x++) {
   for (int y = 0; y < img.height; y++) {
      mask[x][y] = abs(alpha(img.get(x, y)) - alpha(tattered.get(x, y)));
    }
}

mask.updatePixels();
mask.save("some/path/3.png"); 

Upvotes: 1

Related Questions