2016-01-09 284 views
5

使用Graphics2d,我試圖在背景圖像上繪製一個BufferedImage。在這幅圖像的任意一點上,我想在繪製的圖像中「切出一個圓形孔」,讓背景顯示出來。Java Graphics2D - 使用漸變不透明度繪製圖像

我想這個洞不是一個堅實的形狀,而是一個漸變。換句話說,BufferedImage中的每個像素應該具有與距離孔中心的距離成比例的α/不透明度。

我有點熟悉Graphics2d梯度和AlphaComposite,但是有沒有辦法將這些結合起來?

有沒有(不是非常昂貴)的方式來實現這種效果?

回答

6

這可以用RadialGradientPaint和適當AlphaComposite來解決。

以下是MCVE顯示如何可以做到這一點。它使用相同的圖片,user1803551 used in his answer,所以截圖看起來(幾乎)相同。但是這樣一來增加了MouseMotionListener,使您可以四處移動孔,通過將當前鼠標位置到updateGradientAt方法,其中所需圖像的實際創作發生:

  • 它先用填充圖像原始圖像
  • 然後它會創建一個RadialGradientPaint,它的中心顏色完全不透明,邊緣(!)的顏色完全透明。這可能看起來違反直覺,但其目的是從現有圖像中「挖出」洞,這是通過下一步完成的:
  • AlphaComposite.DstOut被分配給Graphics2D。這一個導致一個α值的「反轉」,如式

    Ar = Ad*(1-As) 
    Cr = Cd*(1-As) 
    

    其中r代表「結果」,s代表「源」和d代表「目的地」

結果是在所需位置具有徑向漸變透明度的圖像,在中心處完全透明且在邊界(!)完全不透明。然後使用PaintComposite的組合來填充具有孔的尺寸和座標的橢圓。 (也可以撥打fillRect電話,填寫整個圖像 - 它不會改變結果)。

import java.awt.AlphaComposite; 
import java.awt.Color; 
import java.awt.Graphics; 
import java.awt.Graphics2D; 
import java.awt.Point; 
import java.awt.RadialGradientPaint; 
import java.awt.event.MouseAdapter; 
import java.awt.event.MouseEvent; 
import java.awt.image.BufferedImage; 
import java.io.File; 
import java.io.IOException; 

import javax.imageio.ImageIO; 
import javax.swing.JFrame; 
import javax.swing.JPanel; 
import javax.swing.SwingUtilities; 

public class TransparentGradientInImage 
{ 
    public static void main(String[] args) 
    { 
     SwingUtilities.invokeLater(new Runnable() 
     { 
      @Override 
      public void run() 
      { 
       createAndShowGUI(); 
      } 
     }); 
    } 

    private static void createAndShowGUI() 
    { 
     JFrame f = new JFrame(); 
     f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 

     TransparentGradientInImagePanel p = 
      new TransparentGradientInImagePanel(); 
     f.getContentPane().add(p); 
     f.setSize(800, 600); 
     f.setLocationRelativeTo(null); 
     f.setVisible(true); 
    } 

} 

class TransparentGradientInImagePanel extends JPanel 
{ 
    private BufferedImage background; 
    private BufferedImage originalImage; 
    private BufferedImage imageWithGradient; 

    TransparentGradientInImagePanel() 
    { 
     try 
     { 
      background = ImageIO.read(
       new File("night-sky-astrophotography-1.jpg")); 
      originalImage = convertToARGB(ImageIO.read(new File("7bI1Y.jpg"))); 
      imageWithGradient = convertToARGB(originalImage); 
     } 
     catch (IOException e) 
     { 
      e.printStackTrace(); 
     } 

     addMouseMotionListener(new MouseAdapter() 
     { 
      @Override 
      public void mouseMoved(MouseEvent e) 
      { 
       updateGradientAt(e.getPoint()); 
      } 
     }); 
    } 


    private void updateGradientAt(Point point) 
    { 
     Graphics2D g = imageWithGradient.createGraphics(); 
     g.drawImage(originalImage, 0, 0, null); 

     int radius = 100; 
     float fractions[] = { 0.0f, 1.0f }; 
     Color colors[] = { new Color(0,0,0,255), new Color(0,0,0,0) }; 
     RadialGradientPaint paint = 
      new RadialGradientPaint(point, radius, fractions, colors); 
     g.setPaint(paint); 

     g.setComposite(AlphaComposite.DstOut); 
     g.fillOval(point.x - radius, point.y - radius, radius * 2, radius * 2); 
     g.dispose(); 
     repaint(); 
    } 

    private static BufferedImage convertToARGB(BufferedImage image) 
    { 
     BufferedImage newImage = 
      new BufferedImage(image.getWidth(), image.getHeight(), 
       BufferedImage.TYPE_INT_ARGB); 
     Graphics2D g = newImage.createGraphics(); 
     g.drawImage(image, 0, 0, null); 
     g.dispose(); 
     return newImage; 
    } 

    @Override 
    protected void paintComponent(Graphics g) 
    { 
     super.paintComponent(g); 
     g.drawImage(background, 0, 0, null); 
     g.drawImage(imageWithGradient, 0, 0, null); 
    } 
} 

您可以與fractionsRadialGradientPaintcolors播放,以實現不同的效果。例如,這些值...

float fractions[] = { 0.0f, 0.1f, 1.0f }; 
Color colors[] = { 
    new Color(0,0,0,255), 
    new Color(0,0,0,255), 
    new Color(0,0,0,0) 
}; 

引起小,透明孔,具有大的,柔軟的 「電暈」:

TransparentGradientInImage02.png

而這些值

float fractions[] = { 0.0f, 0.9f, 1.0f }; 
Color colors[] = { 
    new Color(0,0,0,255), 
    new Color(0,0,0,255), 
    new Color(0,0,0,0) 
}; 

導致一個大而透明的中心,帶有一個小的「電暈」:

TransparentGradientInImage01.png

RadialGradientPaint JavaDocs有一些可能有助於找到所需值的示例。


,我發佈了一些相關的問題(類似的)答案:


編輯迴應有關性能的疑問,在紀念中被問到NTS

的如何Paint/Composite方法的性能進行比較的getRGB/setRGB方法的問題確實有趣。從我之前的經驗來看,我的直覺是第一個比第二個快得多,因爲一般來說,getRGB/setRGB往往很慢,並且內置機制高度優化(並且在某些情況下,甚至可能會硬件加速)。

事實上,Paint/Composite方法getRGB/setRGB方法快,但並不如我預期的多。下面當然不是一個真正深刻的「基準」(我沒有使用卡尺或江鈴控股此),但應提供有關實際性能的良好估計:

// NOTE: This is not really a sophisticated "Benchmark", 
// but gives a rough estimate about the performance 

import java.awt.AlphaComposite; 
import java.awt.Color; 
import java.awt.Graphics2D; 
import java.awt.Point; 
import java.awt.RadialGradientPaint; 
import java.awt.image.BufferedImage; 

public class TransparentGradientInImagePerformance 
{ 
    public static void main(String[] args) 
    { 
     int w = 1000; 
     int h = 1000; 
     BufferedImage image0 = new BufferedImage(w, h, 
      BufferedImage.TYPE_INT_ARGB); 
     BufferedImage image1 = new BufferedImage(w, h, 
      BufferedImage.TYPE_INT_ARGB); 

     long before = 0; 
     long after = 0; 
     int runs = 100; 
     for (int radius = 100; radius <=400; radius += 10) 
     { 
      before = System.nanoTime(); 
      for (int i=0; i<runs; i++) 
      { 
       transparitize(image0, w/2, h/2, radius); 
      } 
      after = System.nanoTime(); 
      System.out.println(
       "Radius "+radius+" with getRGB/setRGB: "+(after-before)/1e6); 

      before = System.nanoTime(); 
      for (int i=0; i<runs; i++) 
      { 
       updateGradientAt(image0, image1, new Point(w/2, h/2), radius); 
      } 
      after = System.nanoTime(); 
      System.out.println(
       "Radius "+radius+" with paint   "+(after-before)/1e6); 
     } 
    } 

    private static void transparitize(
     BufferedImage imgA, int centerX, int centerY, int r) 
    { 

     for (int x = centerX - r; x < centerX + r; x++) 
     { 
      for (int y = centerY - r; y < centerY + r; y++) 
      { 
       double distance = Math.sqrt(
        Math.pow(Math.abs(centerX - x), 2) + 
        Math.pow(Math.abs(centerY - y), 2)); 
       if (distance > r) 
        continue; 
       int argb = imgA.getRGB(x, y); 
       int a = (argb >> 24) & 255; 
       double factor = distance/r; 
       argb = (argb - (a << 24) + ((int) (a * factor) << 24)); 
       imgA.setRGB(x, y, argb); 
      } 
     } 
    } 

    private static void updateGradientAt(BufferedImage originalImage, 
     BufferedImage imageWithGradient, Point point, int radius) 
    { 
     Graphics2D g = imageWithGradient.createGraphics(); 
     g.drawImage(originalImage, 0, 0, null); 

     float fractions[] = { 0.0f, 1.0f }; 
     Color colors[] = { new Color(0, 0, 0, 255), new Color(0, 0, 0, 0) }; 
     RadialGradientPaint paint = new RadialGradientPaint(point, radius, 
      fractions, colors); 
     g.setPaint(paint); 

     g.setComposite(AlphaComposite.DstOut); 
     g.fillOval(point.x - radius, point.y - radius, radius * 2, radius * 2); 
     g.dispose(); 
    } 
} 

我的電腦上的計時沿的

... 
Radius 390 with getRGB/setRGB: 1518.224404 
Radius 390 with paint   764.11017 
Radius 400 with getRGB/setRGB: 1612.854049 
Radius 400 with paint   794.695199 

線示出的是,Paint/Composite方法大致快兩倍的getRGB/setRGB方法。除了性能之外,Paint/Composite還有其他一些優勢,主要是上述RadialGradientPaint的可能參數化,這是我更喜歡這種解決方案的原因。

+0

非常好,這是否會給我的解決方案帶來更好的性能呢? – user1803551

+1

@ user1803551它確實給了更好的性能(爲此添加了EDIT),但並不像我預期的那樣多(這可能是由於' RadialGradientPaint' - 你可以通過這種方式獲得一些漂亮的效果,而簡單地「剪下一個洞」就是這個類最簡單的例子之一) – Marco13

2

,如果你打算創建這個透明的「洞」動態或者如果它是一個一次性的事情我不知道。我確信有幾種方法可以完成你想要的功能,並且我將其中一種方法直接改變了像素,其中可能不是性能最好的(我只是沒有比較其他方式我認爲這將取決於你做什麼)。

我在這裏描繪的臭氧層破洞澳超:

enter image description here

public class Paint extends JPanel { 

    BufferedImage imgA; 
    BufferedImage bck; 

    Paint() { 

     BufferedImage img = null; 
     try { 
      img = ImageIO.read(getClass().getResource("img.jpg")); // images linked below 
      bck = ImageIO.read(getClass().getResource("bck.jpg")); 
     } catch (IOException e) { 
      e.printStackTrace(); 
     } 
     imgA = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); 
     Graphics2D g2d = imgA.createGraphics(); 
     g2d.drawImage(img, 0, 0, null); 
     g2d.dispose(); 

     transparitize(200, 100, 80); 
    } 

    private void transparitize(int centerX, int centerY, int r) { 

     for (int x = centerX - r; x < centerX + r; x++) { 
      for (int y = centerY - r; y < centerY + r; y++) { 
       double distance = Math.sqrt(Math.pow(Math.abs(centerX - x), 2) 
              + Math.pow(Math.abs(centerY - y), 2)); 
       if (distance > r) 
        continue; 
       int argb = imgA.getRGB(x, y); 
       int a = (argb >> 24) & 255; 
       double factor = distance/r; 
       argb = (argb - (a << 24) + ((int) (a * factor) << 24)); 
       imgA.setRGB(x, y, argb); 
      } 
     } 
    } 

    @Override 
    protected void paintComponent(Graphics g) { 

     super.paintComponent(g); 
     g.drawImage(bck, 0, 0, null); 
     g.drawImage(imgA, 0, 0, null); 
    } 

    @Override 
    public Dimension getPreferredSize() { 

     return new Dimension(bck.getWidth(), bck.getHeight()); // because bck is larger than imgA, otherwise use Math.max 
    } 
} 

的想法是與getRGB拿到像素的ARGB值,改變α(或其他任何東西),和將其設置爲setRGB。我創建了一個給定中心和半徑的徑向漸變方法。它當然可以提高,我會留給你(提示:centerX - r可以出界;與distance > r像素可以從迭代被完全移除)。

注:

  • 我畫的背景圖像和在它上面的小過的圖像只是爲了清楚地顯示的背景是什麼樣子。
  • 有相當讀取和改變int的Alpha值,搜索這個網站的一些方法,你會發現至少2-3個更多的途徑。
  • 添加到您最喜愛的頂級容器並運行。

來源:

+0

謝謝,這或多或少是我打算做的,如果我找不到其他解決方案。我只是想知道是否有一些內置的,優化的功能來做到這一點。 – B1CL0PS

+1

一些邊界檢查也可能是必要的,例如對於在大小爲(100,100)的圖像上使用參數(99,99,10)調用該方法的情況。 – Marco13

+0

@ Marco13我在談論方法時提到了邊界檢查(「*它當然可以改進,我會把它留給你(提示:'centerX - r'可以超出邊界*」) – user1803551