2016-01-09 23 views
5

Utilizzando Graphics2d, sto cercando di disegnare un BufferedImage sopra un'immagine di sfondo. In un punto arbitrario in questa immagine, vorrei "tagliare un buco circolare" nell'immagine disegnata per far sì che lo sfondo venga visualizzato.Java Graphics2D - disegna un'immagine con opacità sfumata

Vorrei che il foro non fosse una forma solida, ma piuttosto un gradiente. In altre parole, ogni pixel nello BufferedImage dovrebbe avere un'alpha/opacità proporzionale alla sua distanza dal centro del foro.

Ho una certa familiarità con le sfumature Graphics2d e con AlphaComposite, ma c'è un modo per combinarle?

Esiste un modo (non follemente costoso) per ottenere questo effetto?

risposta

6

Questo può essere risolto con un RadialGradientPaint e l'appropriato AlphaComposite.

Quello che segue è un MCVE che mostra come questo può essere fatto. Usa le stesse immagini di user1803551 used in his answer, quindi uno screenshot sembrerebbe (quasi) lo stesso.Ma questo aggiunge un MouseMotionListener che consente di spostare il foro intorno, facendo passare la posizione corrente del mouse al metodo updateGradientAt, dove la creazione effettiva dell'immagine desiderata avviene:

  • Si riempie prima l'immagine con la immagine originale
  • Quindi crea un RadialGradientPaint, che ha un colore completamente opaco al centro e un colore completamente trasparente al bordo (!). Questo può sembrare controintuitivo, ma l'intenzione è di "ritagliare" il buco da un'immagine esistente, che viene eseguita con il passaggio successivo:
  • Uno AlphaComposite.DstOut è assegnato allo Graphics2D. Questo causa un "inversione" dei valori alfa, come nella formula

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

    dove r sta per "risultato", s sta per "source" e d sta per "destinazione"

Il risultato è un'immagine con la trasparenza del gradiente radiale nella posizione desiderata, completamente trasparente al centro e completamente opaca al bordo (!). Questa combinazione di Paint e Composite viene quindi utilizzata per riempire un ovale con le dimensioni e le coordinate del foro. (Si potrebbe anche fare una chiamata fillRect, riempiendo l'intera immagine - non cambierebbe il risultato).

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); 
    } 
} 

Si può giocare con il fractions e colors del RadialGradientPaint per ottenere effetti diversi. Ad esempio, questi valori ...

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) 
}; 

causa un piccolo foro trasparente, con un grande, morbido "corona":

TransparentGradientInImage02.png

che tali valori

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) 
}; 

causare un centro grande, nitidamente trasparente, con una piccola "corona":

TransparentGradientInImage01.png

Gli RadialGradientPaint JavaDocs hanno alcuni esempi che possono aiutare a trovare i valori desiderati.


alcune domande relative dove ho postato (simili) risponde:


EDIT In risposta alla domanda circa le prestazioni che è stato chiesto in comme nti

La questione di come le prestazioni dell'approccio Paint/Composite paragona l'approccio getRGB/setRGB è davvero interessante. Dalla mia esperienza precedente, il mio istinto sarebbe stato che il primo fosse molto più veloce del secondo, perché, in generale, getRGB/setRGB tende a essere lento, e i meccanismi integrati sono altamente ottimizzati (e, in alcuni casi , potrebbe persino essere accelerato dall'hardware).

In realtà, l'approccio Paint/Compositeè più veloce rispetto al metodo getRGB/setRGB, ma non tanto quanto mi aspettavo. Quanto segue è, naturalmente, non è una realtà profonda "benchmark" (non ho impiegano Pinza o JMH per questo), ma dovrebbe dare una buona stima circa l'effettiva performance:

// 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(); 
    } 
} 

I tempi sul mio PC si trovano lungo le linee di

... 
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 

dimostrano che il metodo Paint/Composite è approssimativamente due volte più veloce del metodo getRGB/setRGB. Oltre alle prestazioni, lo Paint/Composite presenta altri vantaggi, principalmente le possibili parametrizzazioni dello RadialGradientPaint descritte sopra, che sono motivi per cui preferirei questa soluzione.

+0

Molto bello, questo offre prestazioni migliori della mia soluzione? – user1803551

+1

@ user1803551 Sembra dare prestazioni migliori (ha aggiunto un EDIT per questo), ma non tanto quanto mi aspettavo, ammettiamolo. (Ciò potrebbe essere dovuto alla complessità/flessibilità di ' RadialGradientPaint' - con questo potresti ottenere alcuni effetti eleganti e semplicemente "tagliare un buco" è uno dei casi più semplici possibili per questa classe) – Marco13

2

Non so se si intende creare questo "foro" trasparente in modo dinamico o se si tratta di una cosa da fare una sola volta. Sono sicuro che ci sono diversi metodi per realizzare quello che vuoi e ne sto mostrando uno con la modifica diretta dei pixel, che potrebbe non essere il delle migliori prestazioni dal punto di vista delle prestazioni (non so come si confronta con altri modi e penso che dipenderà da ciò che fai esattamente).

Qui ho raffigurano il buco nello strato di ozono sopra l'Australia:

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 
    } 
} 

L'idea è quella di ottenere il valore ARGB del pixel con getRGB, cambiare l'alfa (o qualsiasi altra cosa), e impostalo con setRGB. Ho creato un metodo che rende un gradiente radiale dato un centro e un raggio. Può certamente essere migliorato, ve lo lascio (suggerimento: centerX - r può essere fuori limite, i pixel con distance > r possono essere rimossi completamente dall'iterazione).

Note:

  • ho dipinto l'immagine di sfondo e su di esso il più piccolo su immagine solo per mostrare chiaramente ciò che lo sfondo assomiglia.
  • Ci sono diversi modi per leggere e modificare il valore alfa dello int, cercare in questo sito e troverete almeno 2-3 modi in più.
  • Aggiungi al contenitore di livello superiore preferito ed esegui.

Fonti:

+0

Grazie, questo è più o meno quello che stavo progettando di fare se non riuscivo a trovare un'altra soluzione. Mi chiedevo solo se esistesse una funzione integrata e ottimizzata per farlo. – B1CL0PS

+1

Alcuni controlli sui limiti potrebbero anche essere necessari, ad es. per il caso che il metodo è chiamato con i parametri (99,99,10) su un'immagine con dimensione (100.100). – Marco13

+0

@ Marco13 Ho menzionato il controllo dei limiti quando si parla del metodo ("* Può certamente essere migliorato, lascerò questo a voi (suggerimenti:' centerX - r' può essere fuori dai limiti * "). – user1803551