Einführung in die Java Advanced Imaging Klassen verfasst von Beat Trachsler EMPA St. Gallen Gruppe Medientechnik 6. April 2002 1 Einleitung Dieser Text gibt eine kurze Einführung in die Java Advanced Imaging Klassen (JAI). Diese Klassen bieten Methoden zur Bildverarbeitung an, so z.B. geometrische Transformationen (Skalierung, Drehung u.ä.) und Filter (Weichzeichner, Scharfzeichner, etc.). Daneben werden auch komplexere Bildtransformationen wie die Diskrete Fourier Transformation (DFT) und die Diskrete Cosinus Transformation (DCT) zur Verfügung gestellt. Zudem bietet JAI Methoden zur Bildanalyse, wie z.B. den Mittelwert, die Extremalwerte und das Histogramm. Viele der oben erwähnten Transformationen sind in den Java 2D Klassen bereits enthalten. Der grosse Vorteil von JAI ist der einfache Umgang mit den gängigen Dateiformaten für Bilder. Java 2 unterstützt in diesem Bereich nämlich nach wie vor nur das JPEG Format. Dieses genügt jedoch nicht den Anforderungen der professionellen Bildverarbeitung, was die Farbechtheit betrifft. Mit JAI hingegen können ohne grossen Aufwand auch TIFF Dateien gelesen und geschrieben werden. Im Abschnitt 2 werden einige Datenstrukturen, welche man zur Darstellung von Bildern mit Java und JAI benötigt, kurz vorgestellt. Wir konzentrieren uns dabei auf das Sample Modell, welches für den Zugriff auf die Pixels entscheidend ist. Im Abschnitt 3 wird das Operatormodell von JAI vorgestellt. Wir gehen dort auf die create-Methoden ein, welche als Interface für sämtliche JAI-Operatoren dienen. Zudem geben wir drei Beispiele für die Anwendung von JAI-Operatoren. Im Abschnitt 4 schliesslich wird ein kurzes Beispielprogramm vorgestellt. 2 2.1 Datenstrukturen Samples und Pixels Das Graphik Modell von Java ist in den AWT Klassen definiert. Wir halten uns im folgenden an die Methoden und Objekte der Java 2D Klassen, welche seit Java 2 zum Kern von Java gehören. Die kleinste Einheit im Graphik Modell von Java ist das Sample. Jedes Pixel eines Bildes wird durch eine bestimmte Anzahl Samples beschrieben. Bei einem RGB-Bild braucht es z.B. drei Samples pro Pixel, ein Rot-, ein Grün und ein Blau-Sample. Alle Samples der gleichen Art bilden zusammen einen Farbkanal. Demnach braucht man für ein Graustufenbild nur ein Sample pro Pixel und für ein CMYK-Bild vier Samples pro Pixel. Die Samples werden in Java in einem Objekt namens Raster verwaltet. Dieses Objekt bietet Methoden an, mit denen man die Samples als eindimensionales Array exportieren kann, z.B. samplemap = pixelraster.getPixels(0,0,width,height,samplemap); Dabei ist pixelraster eine Instanz des Objekts Raster. Es enthält die Samples eines Bildes gemäss der jeweiligen Anzahl Farbkanäle und gemäss den Dimensionen width und height des Bildes. samplemap ist ein eindimensionales Array der Länge width · height · 1 numbands, wobei numbands für die Anzahl der Farbkanäle des Bildes steht. Der Datentyp des Arrays ist entweder int, float oder double. Die abgeleitete Klasse WritableRaster erlaubt es zudem, die Samples wieder ins Raster zurück zu schreiben. Das geschieht mit der Methode setPixels: pixelraster.setPixels(0,0,width,height,samplemap); Die Methode setPixels funktioniert aber erst, nachdem das Raster initialisiert wurde. Dies geschieht am einfachsten über ein Objekt namens ColorModel. Dieses Objekt enthält Daten über den Farbraum des Bildes. Zudem wird der Datentyp des Rasters verwaltet (in der Regel byte). Mit der folgenden Methode kann ein zum ColorModel kompatibles Raster erzeugt werden: WritableRaster pixelraster = colormodel.createCompatibleWritableRaster(width,height); Dabei ist colormodel eine Instanz des Objekts ColorModel. Wenn Sie ausgehend von einem Raster ein neues Bild definieren wollen, müssen Sie also zunächst ein ColorModel definieren. Für den vordefinierten sRGB-Farbraum geschieht das wie folgt: // Definition des ColorModel (sRGB) int[] bits = {8, 8, 8}; // Farbtiefe 8 bit pro Farbkanal ColorModel colormodel = new ComponentColorModel( ColorSpace.getInstance(ColorSpace.CS_sRGB), bits, false, false, Transparency.OPAQUE, // keine Transparenz DataBuffer.TYPE_BYTE); // Datentyp byte (default) Dabei enthält das Array bits die Farbtiefe für jeden der drei Farbkanäle. 2.2 Das Planar Image Die JAI Klassen bieten eine ganze Reihe von Objekten an, mit denen ein zweidimensionales Bild repräsentiert werden kann. Die Klasse, welche diesen Varianten zugrunde liegt, heisst PlanarImage. Es handelt sich dabei um die JAI Implementation des Interface RenderedImage von Java 2D. Ein PlanarImage enthält neben dem Raster des Bildes auch dessen ColorModel. Für den Zugriff auf das Raster bietet die PlanarImage-Klasse die Methode copyData an: WritableRaster pixelraster = image.copyData(); Dabei ist image eine Instanz des Objekts PlanarImage. Will man umgekehrt ein PlanarImage aus einem Raster konstruieren, so beginnt die Konstruktion wie im Abschnitt 2.1 beschrieben mit dem ColorModel. Damit lässt sich ein SampleModel definieren, das mit dem ColorModel kompatibel ist: SampleModel samplemodel = colormodel.createCompatibleSampleModel(width,height); 2 Das SampleModel ist diejenige Komponente des Raster-Objekts, welche beschreibt, wie die Samples im Raster verwaltet werden. Mit dem ColorModel und dem SampleModel kann jetzt ein JAI Bild definiert werden. Da die Klasse PlanarImage eine abstrakte Klasse ist, müssen wir dazu auf eine Subklasse namens TiledImage zurückgreifen: image = new TiledImage(0,0,width,height,0,0,samplemodel,colormodel); Nun kann man mit der Methode setData ein Raster in das PlanarImage einfügen: image.setData(pixelraster); Dabei ist zu beachten, dass das Raster zum ColorModel kompatibel sein muss. 3 3.1 Das Operatormodell Die create-Methoden Die Operatoren werden in JAI nicht direkt auf das Bild angewendet. Ein Operator wird indirekt mit Hilfe der create-Methode aufgerufen. Dabei gibt es eine ganze Menge von syntaktischen Varianten für die create-Methode, die den Umgang mit dem Konstrukt in der Praxis erleichtern sollen. Im folgenden zunächst diejenige Version, welche den anderen, abgeleiteten Varianten zugrunde liegt: JAI.create(opname, parameterblock, hints); Dabei bezeichnet opname einen String, welcher den Namen des Operators enthält, also z.B. ’scale’ für den Skalierungsoperator. Die Variable parameterblock ist eine Instanz der AWT Klasse ParameterBlock. Dieses Objekt enthält Parameterwerte für den JAI Operator und die Datenquelle(n), auf die der Operator angewendet werden soll, also in der Regel das Ausgangsbild. Die Parameterwerte werden dabei sequentiell mit der Methode add eingelesen. Die Datenquelle wird mit der Methode addSource spezifiziert. Die folgenden Abschnitte enthalten Beispiele für die Anwendung dieser Klasse. Die hints schliesslich sind vom Typ RenderingHints. Sie dienen zur Einstellung spezieller Rendering Optionen, wie z.B. Antialiasing, etc. In diesem Text werden wir nicht näher auf diese Optionen eingehen. Wir verweisen dazu auf den JAI Guide von Sun. Neben der oben beschriebenen create-Methode existieren eine Menge von Abkürzungen. So ist es z.B. möglich, die hints wegzulassen. Zudem kann der parameterblock ersetzt werden durch die direkte Angabe der Datenquelle gefolgt von den Parameterwerten: JAI.create(opname, image, param1, param2, ...); Für eine vollständige Beschreibung aller Varianten verweisen wir auf den JAI Guide von Sun. 3 3.2 Der fileload- und der filestore-Operator Eine sehr einfache und grundlegende Anwendung von JAI Operatoren ergibt sich beim Lesen und Schreiben von Bilddateien. Die folgende Methode liest ein Bild mit bekanntem Format1 ein. Beachten Sie, dass das Format des Bildes nicht explizit angegeben werden muss. PlanarImage image = JAI.create("fileload", path); Dabei bezeichnet die String-Variable path den Pfad des entsprechenden Files. Will man umgekehrt ein JAI Bild in ein File hinaus schreiben, so bietet sich der Operator filestore an. Hier muss natürlich das gewünschte Ausgabeformat als String spezifiziert werden. Für die Ausgabe eines Bildes in ein TIFF File ergibt sich damit: JAI.create("filestore", image, path, "TIFF"); 3.3 Der addconst-Operator Der Operator addconst ist ein sogenannter Punktoperator. Dabei wird pixelweise zu jedem Samplewert eine bestimmte Konstante addiert. Wird dabei der Wertebereich des Samples verlassen, setzt JAI einfach die obere oder untere Schranke des Wertebereichs, also z.B. 0 oder 255. Die nachfolgende Codesequenz zeigt, wie man bei einem Bild mit drei Farbkanälen, pixelweise zu jedem Sample den Wert 10 addieren kann. Beachten Sie dabei, dass die Konstante für jeden Farbkanal einzeln angegeben werden muss und dass das entsprechende Parameterarray vom Datentyp double sein muss. double[] constants = {10., 10., 10.}; ParameterBlock paramblock = new ParameterBlock(); paramblock.addSource(image); paramblock.add(constants); image = JAI.create("addconst", paramblock); 3.4 Der mean-Operator Der mean-Operator ist ein sogenannter statistischer Operator. Er dient zur Berechnung des arithmetischen Mittelwerts für jeden Farbkanal. Das spezielle an diesem Operator ist, dass der Ausgabewert eigentlich kein Bild sondern ein Zahlenarray sein müsste, das für jeden Farbkanal den mittleren Farbwert angibt. JAI-Operatoren liefern aber immer ein Bild als Resultat. Die berechneten Mittelwerte werden als Property im PlanarImage gespeichert und müssen hinterher mit der Methode getProperty exportiert werden. // compute the channelwise mean of the image ParameterBlock pb = new ParameterBlock(); pb.addSource(image); // The source image 1 Zum Zeitpunkt, als dieser Text verfasst wurde, wurden die folgenden Dateiformate von JAI unterstützt: TIFF, JPEG, GIF, BMP, FPX, PNG und PNM. 4 pb.add(null); // The region to scan, ’null’ = whole image pb.add(1); // The horizontal sampling rate pb.add(1); // The vertical sampling rate image = JAI.create("mean", pb); double[] mean = (double[])image.getProperty("mean"); 4 Beispielprogramm In diesem Abschnitt wird das Beispielprogramm ’TestGraphics’ vorgestellt. Es zeigt, wie man mit JAI auf sehr einfache Art und Weise eine Java 2D Graphik in einem TIFF Dokument abspeichern kann. /* * @(#)TestGraphics.java 1.0 01/11/20 */ import import import import java.awt.*; java.awt.color.*; java.awt.image.*; javax.media.jai.*; class TestGraphics { // Dimensionen des Bildes: private static final int WIDTH = 500; private static final int HEIGHT = 350; public static void main(String args[]) { System.out.println("Starting TestGraphics..."); // Definition des ColorModel (sRGB) int[] bits = {8, 8, 8}; // Farbtiefe 8 bit ColorModel colormodel = new ComponentColorModel( ColorSpace.getInstance(ColorSpace.CS_sRGB), // Farbraum sRGB bits, false, false, Transparency.OPAQUE, // keine Transparenz DataBuffer.TYPE_BYTE); // Datentyp Byte (default) // Erzeugen des SampleModel SampleModel samplemodel = colormodel.createCompatibleSampleModel(WIDTH,HEIGHT); // Erzeugen des Bildes 5 TiledImage image = new TiledImage(0,0,WIDTH,HEIGHT,0,0,samplemodel,colormodel); // Initialisierung von 2D Graphics Graphics2D graphics = image.createGraphics(); graphics.setBackground(Color.white); graphics.clearRect(0,0,WIDTH,HEIGHT); // Erzeugen der Graphik graphics.setColor(Color.red); graphics.fillRect(50,50,100,100); graphics.setColor(Color.green); graphics.fillRect(200,50,100,100); graphics.setColor(Color.blue); graphics.fillRect(350,50,100,100); graphics.setColor(Color.cyan); graphics.fillRect(50,200,100,100); graphics.setColor(Color.magenta); graphics.fillRect(200,200,100,100); graphics.setColor(Color.yellow); graphics.fillRect(350,200,100,100); // Bild in TIFF-File schreiben JAI.create("filestore",image,"./graphik.tif","TIFF"); } } 6