Bildverarbeitung mit Java 2D

Bildverarbeitung ist die Kunst und Wissenschaft der Manipulation digitaler Bilder. Es steht mit einem Fuß fest in der Mathematik und dem anderen in der Ästhetik und ist ein kritischer Bestandteil grafischer Computersysteme. Wenn Sie jemals eigene Bilder für Webseiten erstellt haben, werden Sie zweifellos die Bedeutung der Bildbearbeitungsfunktionen von Photoshop für die Bereinigung von Scans und die Bereinigung nicht optimaler Bilder zu schätzen wissen.

Wenn Sie Bildverarbeitungsarbeiten in JDK 1.0 oder 1.1 durchgeführt haben, erinnern Sie sich wahrscheinlich daran, dass diese etwas stumpf waren. Das alte Modell von Bilddatenproduzenten und -konsumenten ist für die Bildverarbeitung unhandlich. Vor JDK 1.2 umfasste die Bildverarbeitung MemoryImageSources, PixelGrabbers und andere solche Arcana. Java 2D bietet jedoch ein saubereres und benutzerfreundlicheres Modell.

In diesem Monat werden wir die Algorithmen untersuchen, die hinter mehreren wichtigen Bildverarbeitungsoperationen ( Ops ) stehen, und Ihnen zeigen, wie sie mit Java 2D implementiert werden können. Wir zeigen Ihnen auch, wie diese Operationen verwendet werden, um das Erscheinungsbild des Bildes zu beeinflussen.

Da die Bildverarbeitung eine wirklich nützliche eigenständige Anwendung von Java 2D ist, haben wir das Beispiel ImageDicer dieses Monats erstellt, um es für Ihre eigenen Anwendungen so wiederverwendbar wie möglich zu machen. Dieses einzelne Beispiel zeigt alle Bildverarbeitungstechniken, die wir in der Kolumne dieses Monats behandeln werden.

Beachten Sie, dass Sun kurz vor der Veröffentlichung dieses Artikels das Java 1.2 Beta 4-Entwicklungskit veröffentlicht hat. Beta 4 scheint eine bessere Leistung für unsere Beispiel-Bildverarbeitungsvorgänge zu bieten, fügt jedoch auch einige neue Fehler hinzu, bei denen die Grenzen von ConvolveOps überprüft werden . Diese Probleme wirken sich auf die Beispiele für Kantenerkennung und -schärfung aus, die wir in unserer Diskussion verwenden.

Wir halten diese Beispiele für wertvoll. Anstatt sie ganz wegzulassen, haben wir Kompromisse geschlossen: Um sicherzustellen, dass sie ausgeführt werden, spiegelt der Beispielcode die Beta 4-Änderungen wider. Wir haben jedoch die Zahlen aus der 1.2 Beta 3-Ausführung beibehalten, damit Sie die Vorgänge sehen können richtig funktionieren.

Hoffentlich wird Sun diese Fehler vor der endgültigen Veröffentlichung von Java 1.2 beheben.

Bildverarbeitung ist keine Raketenwissenschaft

Die Bildverarbeitung muss nicht schwierig sein. In der Tat sind die grundlegenden Konzepte wirklich recht einfach. Ein Bild ist schließlich nur ein Rechteck aus farbigen Pixeln. Bei der Verarbeitung eines Bildes muss lediglich für jedes Pixel eine neue Farbe berechnet werden. Die neue Farbe jedes Pixels kann auf der vorhandenen Pixelfarbe, der Farbe der umgebenden Pixel, anderen Parametern oder einer Kombination dieser Elemente basieren.

Die 2D-API führt ein einfaches Bildverarbeitungsmodell ein, mit dem Entwickler diese Bildpixel bearbeiten können. Dieses Modell basiert auf der java.awt.image.BufferedImageKlasse, und Bildverarbeitungsoperationen wie Faltung und Schwellenwertbildung werden durch Implementierungen der java.awt.image.BufferedImageOpSchnittstelle dargestellt.

Die Implementierung dieser Operationen ist relativ einfach. Angenommen, Sie haben das Quellbild bereits als BufferedImageaufgerufen source. Das Ausführen der in der obigen Abbildung dargestellten Operation würde nur wenige Codezeilen erfordern:

001 kurze [] Schwelle = neue kurze [256]; 002 für (int i = 0; i <256; i ++) 003 Schwelle [i] = (i <128)? (kurz) 0: (kurz) 255; 004 BufferedImageOp schwelleOp = 005 neue LookupOp (neue ShortLookupTable (0, Schwelle), null); 006 BufferedImage destination = schwelleOp.filter (source, null);

Das ist wirklich alles. Schauen wir uns nun die Schritte genauer an:

  1. Instanziieren Sie den Bildvorgang Ihrer Wahl (Zeilen 004 und 005). Hier haben wir eine verwendet LookupOp, eine der Bildoperationen, die in der Java 2D-Implementierung enthalten sind. Wie bei jeder anderen Bildoperation wird die BufferedImageOpSchnittstelle implementiert . Wir werden später mehr über diese Operation sprechen.

  2. Rufen Sie die filter()Methode der Operation mit dem Quellbild auf (Zeile 006). Die Quelle wird verarbeitet und das Zielbild zurückgegeben.

Wenn Sie bereits ein BufferedImageBild erstellt haben , das das Zielbild enthält, können Sie es als zweiten Parameter an übergeben filter(). Wenn Sie nullwie im obigen Beispiel bestehen, wird ein neues Ziel BufferedImageerstellt.

Die 2D-API enthält eine Handvoll dieser integrierten Bildoperationen. In dieser Spalte werden drei Themen behandelt: Faltung, Nachschlagetabellen und Schwellenwert. Informationen zu den verbleibenden in der 2D-API (Ressourcen) verfügbaren Vorgängen finden Sie in der Java 2D-Dokumentation.

Faltung

Mit einer Faltungsoperation können Sie die Farben eines Quellpixels und seiner Nachbarn kombinieren, um die Farbe eines Zielpixels zu bestimmen. Diese Kombination wird mithilfe eines Kernels angegeben, eines linearen Operators, der den Anteil jeder Quellpixelfarbe bestimmt, die zur Berechnung der Zielpixelfarbe verwendet wird.

Stellen Sie sich den Kernel als eine Vorlage vor, die dem Bild überlagert wird, um jeweils eine Pixelfaltung durchzuführen. Wenn jedes Pixel gefaltet wird, wird die Vorlage zum nächsten Pixel im Quellbild verschoben und der Faltungsprozess wird wiederholt. Eine Quellkopie des Bildes wird für Eingabewerte für die Faltung verwendet, und alle Ausgabewerte werden in einer Zielkopie des Bildes gespeichert. Sobald der Faltungsvorgang abgeschlossen ist, wird das Zielbild zurückgegeben.

Man kann sich vorstellen, dass das Zentrum des Kernels das gewundene Quellpixel überlagert. Beispielsweise hat eine Faltungsoperation, die den folgenden Kernel verwendet, keine Auswirkung auf ein Bild: Jedes Zielpixel hat dieselbe Farbe wie das entsprechende Quellpixel.

 0,0 0,0 0,0 0,0 1,0 0,0 0,0 0,0 0,0 

Die Grundregel für das Erstellen von Kerneln lautet, dass sich alle Elemente zu 1 addieren sollten, wenn Sie die Helligkeit des Bildes beibehalten möchten.

In der 2D-API wird eine Faltung durch a dargestellt java.awt.image.ConvolveOp. Sie können einen ConvolveOpmithilfe eines Kernels erstellen, der durch eine Instanz von dargestellt wird java.awt.image.Kernel. Der folgende Code erstellt a unter ConvolveOpVerwendung des oben dargestellten Kernels.

001 float [] identityKernel = {002 0.0f, 0.0f, 0.0f, 003 0.0f, 1.0f, 0.0f, 004 0.0f, 0.0f, 0.0f 005}; 006 BufferedImageOp identity = 007 new ConvolveOp (neuer Kernel (3, 3, identityKernel));

Die Faltungsoperation ist nützlich, um mehrere allgemeine Operationen an Bildern durchzuführen, auf die wir gleich näher eingehen werden. Unterschiedliche Kernel führen zu radikal unterschiedlichen Ergebnissen.

Jetzt sind wir bereit, einige Bildverarbeitungskerne und ihre Auswirkungen zu veranschaulichen. Unser unverändertes Bild ist Lady Agnew von Lochnaw, gemalt von John Singer Sargent in den Jahren 1892 und 1893.

Der folgende Code erstellt einen ConvolveOp, der gleiche Mengen jedes Quellpixels und seiner Nachbarn kombiniert. Diese Technik führt zu einem Unschärfeeffekt.

001 float neunter = 1.0f / 9.0f; 002 float [] blurKernel = {003 neunte, neunte, neunte, 004 neunte, neunte, neunte, 005 neunte, neunte, neunte 006}; 007 BufferedImageOp blur = new ConvolveOp (neuer Kernel (3, 3, blurKernel));

Ein weiterer gängiger Faltungskern betont die Kanten im Bild. Diese Operation wird üblicherweise als Kantenerkennung bezeichnet. Im Gegensatz zu den anderen hier vorgestellten Kerneln addieren sich die Koeffizienten dieses Kernels nicht zu 1.

001 float [] edgeKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 4.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp edge = new ConvolveOp (neuer Kernel (3, 3, edgeKernel));

Sie können sehen, was dieser Kernel tut, indem Sie sich die Koeffizienten im Kernel ansehen (Zeilen 002-004). Denken Sie einen Moment darüber nach, wie der Kantenerkennungskern verwendet wird, um in einem Bereich zu arbeiten, der nur eine Farbe hat. Jedes Pixel hat keine Farbe (schwarz), da die Farbe der umgebenden Pixel die Farbe des Quellpixels aufhebt. Helle Pixel, die von dunklen Pixeln umgeben sind, bleiben hell.

Beachten Sie, wie viel dunkler das verarbeitete Bild im Vergleich zum Original ist. Dies liegt daran, dass die Elemente des Kantenerkennungskerns nicht 1 ergeben.

A simple variation on edge detection is the sharpening kernel. In this case, the source image is added into an edge detection kernel as follows:

 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 -1.0 4.0 -1.0 + 0.0 1.0 0.0 = -1.0 5.0 -1.0 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 

The sharpening kernel is actually only one possible kernel that sharpens images.

The choice of a 3 x 3 kernel is somewhat arbitrary. You can define kernels of any size, and presumably they don't even have to be square. In JDK 1.2 Beta 3 and 4, however, a non-square kernel produced an application crash, and a 5 x 5 kernel chewed up the image data in a most peculiar way. Unless you have a compelling reason to stray from 3 x 3 kernels, we don't recommend it.

You may also be wondering what happens at the edge of the image. As you know, the convolution operation takes a source pixel's neighbors into account, but source pixels at the edges of the image don't have neighbors on one side. The ConvolveOp class includes constants that specify what the behavior should be at the edges. The EDGE_ZERO_FILL constant specifies that the edges of the destination image are set to 0. The EDGE_NO_OP constant specifies that source pixels along the edge of the image are copied to the destination without being modified. If you don't specify an edge behavior when constructing a ConvolveOp, EDGE_ZERO_FILL is used.

The following example shows how you could create a sharpening operator that uses the EDGE_NO_OP rule (NO_OP is passed as a ConvolveOp parameter in line 008):

001 float[] sharpKernel = { 002 0.0f, -1.0f, 0.0f, 003 -1.0f, 5.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005 }; 006 BufferedImageOp sharpen = new ConvolveOp( 007 new Kernel(3, 3, sharpKernel), 008 ConvolveOp.EDGE_NO_OP, null); 

Lookup tables

Another versatile image operation involves using a lookup table. For this operation, source pixel colors are translated into destination pixels colors through the use of a table. A color, remember, is composed of red, green, and blue components. Each component has a value from 0 to 255. Three tables with 256 entries are sufficient to translate any source color to a destination color.

The java.awt.image.LookupOp and java.awt.image.LookupTable classes encapsulate this operation. You can define separate tables for each color component, or use one table for all three. Let's look at a simple example that inverts the colors of every component. All we need to do is create an array that represents the table (lines 001-003). Then we create a LookupTable from the array and a LookupOp from the LookupTable (lines 004-005).

001 short[] invert = new short[256]; 002 for (int i = 0; i < 256; i++) 003 invert[i] = (short)(255 - i); 004 BufferedImageOp invertOp = new LookupOp( 005 new ShortLookupTable(0, invert), null); 

LookupTable has two subclasses, ByteLookupTable and ShortLookupTable, that encapsulate byte and short arrays. If you create a LookupTable that doesn't have an entry for any input value, an exception will be thrown.

This operation creates an effect that looks like a color negative in conventional film. Also note that applying this operation twice will restore the original image; you're basically taking a negative of the negative.

What if you only wanted to affect one of the color components? Easy. You construct a LookupTable with separate tables for each of the red, green, and blue components. The following example shows how to create a LookupOp that only inverts the blue component of the color. As with the previous inversion operator, applying this operator twice restores the original image.

001 short[] invert = new short[256]; 002 short[] straight = new short[256]; 003 for (int i = 0; i < 256; i++) { 004 invert[i] = (short)(255 - i); 005 straight[i] = (short)i; 006 } 007 short[][] blueInvert = new short[][] { straight, straight, invert }; 008 BufferedImageOp blueInvertOp = 009 new LookupOp(new ShortLookupTable(0, blueInvert), null); 

Posterizing is another nice effect you can apply using a LookupOp. Posterizing involves reducing the number of colors used to display an image.

A LookupOp can achieve this effect by using a table that maps input values to a small set of output values. The following example shows how input values can be mapped to eight specific values.

001 short [] posterize = new short [256]; 002 für (int i = 0; i <256; i ++) 003 posterize [i] = (kurz) (i - (i% 32)); 004 BufferedImageOp posterizeOp = 005 new LookupOp (neue ShortLookupTable (0, posterize), null);

Schwellenwert

Die letzte Bildoperation, die wir untersuchen werden, ist die Schwellenwertbildung. Durch Schwellenwerte werden Farbänderungen über eine vom Programmierer festgelegte "Grenze" oder Schwelle deutlicher (ähnlich wie die Konturlinien auf einer Karte Höhengrenzen deutlicher machen). Diese Technik verwendet einen festgelegten Schwellenwert, Minimalwert und Maximalwert, um die Farbkomponentenwerte für jedes Pixel eines Bildes zu steuern. Farbwerte unterhalb des Schwellenwerts erhalten den Mindestwert. Werte über dem Schwellenwert erhalten den Maximalwert.