porter-duff rule for image blend

Back in 1984, Thomas Porter and Tom Duff wrote a paper entitled "Compositing Digital Images" that described 12 rules combining two images. Support for these compositing rules is found in the AlphaComposite class, first introduced in version 1.2 of the Java language. Version 1.4, currently in beta 2, supports all 12 rules.

Support for compositing is necessary for applications like games that include multiple image types, including background images, player-character images, and the like. While it is easy to always draw the player in front of the background, if the player were to jump behind a tree, you would want to show the character image faded out somewhat behind the tree image. This is where compositing comes in handy.

Porter and Duff's 12 rules

The AlphaComposite class has 12 constants, one for each rule. It can be tricky to visualize how the rule is applied, so I've provided an illustration of each rule in action. The actual combinations will vary if the images aren't opaque. The demonstration program, shown in Listing 1 and available for download in the Resources section, allows you to try out different levels of transparency.

Note: Those rules newly added with the Merlin release are indicated with an asterisk (*).

Rule 1.AlphaComposite.CLEAR: Nothing will be drawn in the combined image.


AlphaComposite.CLEAR
AlphaComposite.CLEAR

Rule 2.AlphaComposite.SRC: SRC stands for source; only the source image will be in the combined image.


AlphaComposite.SRC
AlphaComposite.SRC

Rule 3.AlphaComposite.DST*: DST stands for destination; only the destination image will be in the combined image.


AlphaComposite.DST
AlphaComposite.DST

Rule 4.AlphaComposite.SRC_OVER: The source image will be drawn over the destination image.


AlphaComposite.SRC_OVER
AlphaComposite.SRC_OVER

Rule 5.AlphaComposite.DST_OVER: The destination image will be drawn over the source image.


AlphaComposite.DST_OVER
AlphaComposite.DST_OVER

Rule 6.AlphaComposite.SRC_IN: Only the part of the source image that overlaps the destination image will be drawn.


AlphaComposite.SRC_IN
AlphaComposite.SRC_IN

Rule 7.AlphaComposite.DST_IN: Only the part of the destination image that overlaps the source image will be drawn.


AlphaComposite.DST_IN
AlphaComposite.DST_IN

Rule 8.AlphaComposite.SRC_OUT: Only the part of the source image that doesn't overlap the destination image will be drawn.


AlphaComposite.SRC_OUT
AlphaComposite.SRC_OUT

Rule 9.AlphaComposite.DST_OUT: Only the part of the destination image that doesn't overlap the source image will be drawn.


AlphaComposite.DST_OUT
AlphaComposite.DstOut

Rule 10.AlphaComposite.SRC_ATOP*: The part of the source image that overlaps the destination image will be drawn with the part of the destination image that doesn't overlap the source image.


AlphaComposite.SRC_ATOP
AlphaComposite.SRC_ATOP

Rule 11.AlphaComposite.DST_ATOP*: The part of the destination image that overlaps the source image will be drawn with the part of the source image that doesn't overlap the destination image.


AlphaComposite.DST_ATOP
AlphaComposite.DST_ATOP

Rule 12.AlphaComposite.XOR*: The part of the source image that doesn't overlap the destination image will be drawn with the part of the destination image that doesn't overlap the source image.


AlphaComposite.XOR
AlphaComposite.XOR

A complete example

The following program is an interactive demonstration of the alpha compositing rules. Just alter the opacity for each triangle and select a rule to use to see the effect of blending the images.

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.lang.reflect.*;
import javax.swing.*;
import javax.swing.event.*;
import java.util.*;

public class CompositeIt extends JFrame {
  JSlider sourcePercentage = new JSlider();
  JSlider destinationPercentage = new JSlider();
  JComboBox alphaComposites = new JComboBox();
  DrawingPanel drawingPanel = new DrawingPanel();

  public CompositeIt() {
    super("Porter-Duff");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    JPanel contentPane = (JPanel) this.getContentPane();
    Dictionary labels = new Hashtable();
    labels.put(new Integer(0),   new JLabel("0.0"));
    labels.put(new Integer(25),  new JLabel("0.25"));
    labels.put(new Integer(33),  new JLabel("0.33"));
    labels.put(new Integer(50),  new JLabel("0.50"));
    labels.put(new Integer(67),  new JLabel("0.67"));
    labels.put(new Integer(75),  new JLabel("0.75"));
    labels.put(new Integer(100), new JLabel("1.00"));
    sourcePercentage.setOrientation(JSlider.VERTICAL);
    sourcePercentage.setLabelTable(labels);
    sourcePercentage.setPaintTicks(true);
    sourcePercentage.setPaintLabels(true);
    sourcePercentage.setValue(100);
    sourcePercentage.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        int sourceValue = sourcePercentage.getValue();
        drawingPanel.setSourcePercentage(sourceValue/100.0f);
      }
    });
    destinationPercentage.setOrientation(JSlider.VERTICAL);
    destinationPercentage.setLabelTable(labels);
    destinationPercentage.setPaintTicks(true);
    destinationPercentage.setPaintLabels(true);
    destinationPercentage.setValue(100);
    destinationPercentage.addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        int destinationValue = destinationPercentage.getValue();
        drawingPanel.setDestinationPercentage(destinationValue/100.0f);
      }
    });
    String rules[] = {
      "CLEAR",    "DST", 
      "DST_ATOP", "DST_IN", 
      "DST_OUT",  "DST_OVER", 
      "SRC",      "SRC_ATOP", 
      "SRC_IN",   "SRC_OUT", 
      "SRC_OVER", "XOR"};
    ComboBoxModel model = new DefaultComboBoxModel(rules);
    alphaComposites.setModel(model);
    alphaComposites.setSelectedItem("XOR");
    alphaComposites.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        String alphaValue = alphaComposites.getSelectedItem().toString();
        Class alphaClass = AlphaComposite.class;
        try {
          Field field = alphaClass.getDeclaredField(alphaValue);
          int rule = ((Integer)field.get(AlphaComposite.Clear)).intValue();
          drawingPanel.setCompositeRule(rule);
        } catch (Exception exception) {
          System.err.println("Unable to find field");
        }
      }
    });
    contentPane.add(sourcePercentage, BorderLayout.WEST);
    contentPane.add(destinationPercentage, BorderLayout.EAST);
    contentPane.add(alphaComposites, BorderLayout.SOUTH);
    contentPane.add(drawingPanel, BorderLayout.CENTER);
    pack();
  }

  public static void main(String args[]) {
    new CompositeIt().show();
  }

  class DrawingPanel extends JPanel {
    GeneralPath sourcePath, destPath;
    BufferedImage source, dest;
    float sourcePercentage = 1, destinationPercentage = 1;
    int compositeRule = AlphaComposite.XOR;
    Dimension dimension = new Dimension(200, 200);

    public DrawingPanel() {
      sourcePath = new GeneralPath();  
      sourcePath.moveTo(0,   0);   sourcePath.lineTo(150, 0);
      sourcePath.lineTo(0, 200);   sourcePath.closePath();
      source = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
      destPath = new GeneralPath();
      destPath.moveTo(200,  0);    destPath.lineTo(50, 0);
      destPath.lineTo(200, 200);    destPath.closePath();
      dest = new BufferedImage(200, 200, BufferedImage.TYPE_INT_ARGB);
    }

    public void setSourcePercentage(float value) {
      sourcePercentage = value;
       repaint();
    }

    public void setDestinationPercentage(float value) {
      destinationPercentage = value;
       repaint();
    }

    public void setCompositeRule(int value) {
      compositeRule = value;
       repaint();
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);
      Graphics2D g2d = (Graphics2D)g;
      Graphics2D sourceG = source.createGraphics();
      Graphics2D destG = dest.createGraphics();

      destG.setComposite(AlphaComposite.Clear);
      destG.fillRect(0, 0, 200, 200);
      destG.setComposite(AlphaComposite.getInstance(
          AlphaComposite.XOR, destinationPercentage));
      destG.setPaint(Color.magenta);
      destG.fill(destPath);

      sourceG.setComposite(AlphaComposite.Clear);
      sourceG.fillRect(0, 0, 200, 200);
      sourceG.setComposite(AlphaComposite.getInstance(
        AlphaComposite.XOR, sourcePercentage));
      sourceG.setPaint(Color.green);
      sourceG.fill(sourcePath);

      destG.setComposite(AlphaComposite.getInstance(compositeRule));
      destG.drawImage(source, 0, 0, null);
      g2d.drawImage(dest, 0, 0, this);
    }

    public Dimension getPreferredSize() {
      return dimension;
    }
  }
}


稍微接触过一点图形相关应用的人应该都会知道alpha混合,再深入一点就应该会看到porter, duff两位神人的名字,他们是最早在SIGGRAPH上提出图形混合概念的前辈。

对于alpha混合,最简单的说法是Src*alpha + Dst*(1-alpha),至少这是我第一次了解到的alpha算法,但事实上porter, duff提出的混合远比这个复杂。因为每次都会忘记他们提出的那12个混合方式是怎么来的,所以在这里整理一次。

首先,一个大前提,以下porter/duff公式是针对预乘(premultiplied)后的结果的。什么是预乘?假设一个像素点,用RGBA四个分量来表示,记做(R,G,B,A),那预乘后的像素就是(R*A,G*A,B*A, A),这里A的取值范围是[0,1]。所以,预乘就是每个颜色分量都与该像素的alpha分量预先相乘。可以发现,对于一个没有透明度,或者说透明度为1的像素来说,预乘不预乘结果都是一样的。

为什么要用预乘方式来表示像素?主要是这样会使公式更简单。而且实际上,在实际运算过程中,使用预乘像素进行运算在某些情况下效率会更高。但预乘会对运算精度有一定影响。关于预乘的种种内容,这里不讨论,有兴趣可以自行研究。

开始讨论porter/duff混合公式之前,先定义几个符号

C - 表示像素的颜色,即(RGBA)的RGB部分,C是color的缩写

A - 表示像素的透明度,A即alpha

s  - 表示两个混合像素的源像素,s即source

d - 表示两个混合像素的目标像素,d即destination

r - 表示两个像素混合后的结果,r即result

F - 表示作用于C或A上的因子,F即factor

有了这个符号之后,可以开始用它们来描述porter/duff混合公式了,如下,

Cr = Cs*Fs + Cd*Fd

Ar = As*Fs + Ad*Fd

对于12种不同的混合方式,仅仅是Fs与Fd的取值不同,比如最为常见的SRC_OVER混合方式,

Fs = 1, Fd = (1-As)

所以

Cr = Cs + Cd*(1-As)

Ar = As + Ad*(1-Ad)

我们之前提到的Src*alpha + Dst*(1-alpha)其实就是SRC_OVER的一种特殊情况,即目标像素的alpha为1的情况。你可能决定这两个公式长得还有点区别,别忘了porter/duff公式是以预乘方式来讨论的,展开成非预乘之后就是

Cr = Cs*As + Cd*Ad*(1-As)

如果Ad = 1,那就和之前的公式一样了。对于通常的GUI应用,因为最终的背景一定是alpha为1的,所以使用这个公式就足够了。但是,对于OSD与video叠加的情况,由于OSD的最终背景很可能是半透明的,如果再使用这个简化过的公式,可能就会造成结果不正确了。这种时候 就必须按照porter/duff公式严格计算。

下面是12种porter/duff混合的具体定义,

1. CLEAR

Fs = Fd = 0

2. SRC

Fs = 1 Fd = 0

3. DST

Fs = 0 Fd = 1

4. SRC OVER

Fs = 1 Fd = (1-As)

5. DST OVER

Fs = (1-Ad) Fd = 1

6. SRC IN

Fs = Ad Fd = 0

7. DST IN

Fs = 0 Fd = As

8. SRC OUT

Fs = (1-Ad) Fd = 0

9. DST OUT

Fs = 0 Fd = (1-As)

10.SRC ATOP

Fs = Ad Fd = (1-As)

11.DST ATOP

Fs = (1-Ad) Fd = As

12.XOR

Fs = (1-Ad) Fd = (1-As)

 

其实除了第1和第12条,其它都是两两相对的,很好理解。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值