Niemand wird Gimp mit Java neu erfinden wollen. Trotzdem: Es gibt eine ganze Reihe von Java-Anwendungen, in denen die Darstellung und Bearbeitung von Bildern eine wichtige Rolle spielen. APIs wie Java 2D, Image IO und JAI setzen solche Applikationen ins Bild.
Die Weiterentwicklung von Java, insbesondere der Standardbibliotheken, ist Fluch und Segen zugleich. Wie beim Druck- oder Font-API (Coffeeshops [1] und [2]) gibt es auch für die Bildbearbeitung mehrere API-Generationen. Dabei ersetzen die neuen Bibliotheken ihre Vorgänger nicht, sondern ergänzen und erweitern sie.
Die Anfänge
Den Höhepunkt seiner Popularität erreichte Java durch Applets auf Webseiten. Bilder spielten hier in Form von Animationen und kleinen Gifs auf Buttons und anderen Schaltflächen eine besondere Rolle. Sie lagerten nicht auf der Festplatte, waren also nicht immer sofort verfügbar.
In solchen Anwendungsfällen kommt das Push Model zum Einsatz. Die Klasse »java.awt.Image« zeigt Bilder an, ändert sie aber nicht. »Image«-Objekte entstehen aus einer Pipeline von »ImageProducer«- zu »ImageConsumer«-Objekten, am Ende geschieht die Ausgabe über den »Graphics«-Kontext eines AWT-Objekts. Das Push-Modell trägt seinen Namen, weil allein der Producer den Fortschritt bestimmt. Somit eignet es sich auch für langsame Netzwerke.
Die Dokumentation der Klasse »java.awt.MediaTracker«, die Bilder asynchron herunterlädt, enthält ein Beispiel. Die Methode »getImage()« der »Applet«-Klasse liefert zwar sofort ein »Image«-Objekt, aber normalerweise startet erst die »drawImage()«-Methode des »Graphics«-Objekts den Download. »MediaTracker« lädt dagegen auch mehrere Bilder im Voraus asynchron herunter.
Ein simpler Bildbetrachter
Die Beispiele aus den Listings 1 und 2 zeigen die Implementation eines einfachen Bildbetrachters auf Basis von »awt.Image« (siehe Abbildung 1). Der Benutzer übergibt die Dateinamen als Kommandozeilenparameter und blättert dann über das Menü in der Liste vor und zurück. Die Methode »setImages()« (Zeilen 123 bis 129, Listing 1) erzeugt ein Array aus »Image«-Objekten. Das kostet keine Performance, denn diese Objekte enthalten wie oben beschrieben keine Bilddaten.
|
Listing 1: Bildbetrachter auf |
|---|
022: import java.io.*;
023: import java.awt.*;
024: import java.awt.event.*;
025: import javax.swing.*;
026:
034: public class ImageViewer extends JFrame {
035:
066: public ImageViewer(String title, ImagePanel panel) {
067: super(title);
068: setJMenuBar(getMenu());
069: iImagePanel = panel;
070: iScrollPane = new JScrollPane((JPanel) iImagePanel);
071: getContentPane().add(iScrollPane);
072: setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
073: setLocation(100, 100);
074: setVisible(false);
075: }
076:
123: protected void setImages(String[] filenames) throws IOException {
124: Image[] imgArray = new Image[filenames.length];
125: for (int i=0;i<filenames.length;++i) {
126: imgArray[i] = Toolkit.getDefaultToolkit().getImage(filenames[i]);
127: }
128: setFiles(imgArray);
129: }
130:
137: protected void setImage(Image img) {
138: iImagePanel.setImage(img);
139: iScrollPane.getViewport().setViewPosition(new Point(0,0));
140: }
203: }
|
|
Listing 2: Ausgeben eines |
|---|
032: public class ImagePanel1 extends JPanel implements ImagePanel {
033:
100: public void paint(Graphics g) {
101: if (iImage != null) {
102: g.drawImage(iImage,0,0,this);
103: }
104: }
106: }
|

Abbildung 1: Die Bilder durchlaufen eine dreistufige Pipeline von der Bildquelle bis zur Anzeige auf dem Bildschirm (siehe Listings 1 und 2).
Der Bildbetrachter setzt ein »ImagePanel« ein. Dabei handelt es sich um nichts anderes als ein »JPanel« mit eigener »paint()«-Methode (Zeilen 100 bis 104, Listing 2). Erst die Ausgabe in Zeile 102 liest das Bild ein. Auf langsamen Rechnern macht sich dieser Effekt dadurch bemerkbar, dass bei der ersten Anzeige deutlich mehr Zeit verstreicht als bei späteren. Der Bildbetrachter ist damit bereits komplett, die »Image«-Objekte ermöglichen nicht wesentlich mehr Funktionalität. Mehr geht erst mit der nächsten API-Generation.
Neues Modell mit Java 2D
Das Java-2D-API brachte einen Leistungsschub beim Verarbeiten von Grafiken, Texten und Bildern. Es verwendet Objekte, die die Bilddaten direkt enthalten (»BufferedImage«, Raster). Dieses Vorgehen heißt auch Immediate Mode Image Buffer Model.
Mit Java 2D lässt sich ein »BufferedImage« unabhängig von der Anzeige direkt bearbeiten, sogar auf Pixel-Ebene. Letzteres ist aber bei Standardoperationen gar nicht nötig, denn Objekte vom Typ »BufferedImageOp« beziehungsweise »RasterOp« kapseln die Funktionalität. Folgende Operationen stehen dem Entwickler zur Verfügung:
- »AffineTransformOp«: Skalieren und Verschieben
- »ColorConvertOp«: Ändert Farbräume
- »ConvolveOp«: Ändern von Pixeln mit
Matrix-Operationen, zum Beispiel schärfen - »LookupOp«: Tabellen-gesteuerte Operationen
- »RescaleOp«: Farbwerte spreizen und stauchen
»BufferedImageOp« hat eine Methode »filter()«, die ein Quell-Image zum Ziel-Image umwandelt.
Lesen und schreiben mit Java Image IO
Java 2D wird ergänzt durch das Image-IO-API, das »BufferedImage«-Objekte erzeugt. Es arbeitet mit »ImageReader«- und »ImageWriter«-Objekten und verwendet die gleiche Plugin-Architektur wie viele andere Java-APIs. Über ein definiertes SPI (Service Provider Interface) fügt man neue Funktionen hinzu. Bestehende Programme nutzen die neuen Bibliotheken ohne Änderungen am Code, wenn sie diese als Extension oder im Classpath vorfinden.
Image IO unterstützt nur eine begrenzte Anzahl Standardformate: Jpeg, Gif, PNG und ab Java 5 auch BMP und WBMP. Ein unten beschriebenes Zusatzpaket unterstützt zudem Tiff-Dateien. Der Bildbetrachter im Beispiel ändert sich mit Java 2D und Image IO an mehreren Stellen. Listing 3 zeigt den neuen Code zum Lesen der Bilddaten. Zuerst verwendet er eine Factory-Methode der Image-IO-Klasse, um einen passenden »ImageInput«-Stream zu erhalten (Zeile 133). Daraus leitet sich ein »ImageReader« ab (Zeilen 134 bis 136).
|
Listing 3: Java-2D- und |
|---|
039: public class ImageViewer2 extends ImageViewer {
082:
083: private BufferedImage scaleImage(BufferedImage srcImg,double scale) {
084: AffineTransform atf = AffineTransform.getScaleInstance(scale,scale);
085: AffineTransformOp ato =
086: new AffineTransformOp(atf,AffineTransformOp.TYPE_BILINEAR);
088: return ato.filter(srcImg,null);
089: }
096:
097: public BufferedImage sharpenImage(BufferedImage srcImg) {
098: srcImg = scaleImage(srcImg,1.0);
099: final float[] kernelMatrix = {
100: 0.f, -1.f, 0.f,
101: -1.f, 5.0f, -1.f,
102: 0.f, -1.f, 0.f};
103: Kernel kernel = new Kernel(3,3,kernelMatrix);
104: ConvolveOp co =
105: new ConvolveOp(kernel,ConvolveOp.EDGE_NO_OP,null);
106: return co.filter(srcImg,null);
107: }
114:
115: protected void setImages(String[] filenames) throws IOException {
116: BufferedImage[] imgArray = new BufferedImage[filenames.length];
117: for (int i=0;i<filenames.length;++i) {
118: ImageReader reader = getReader(filenames[i]);
119: ImageReadParam params = reader.getDefaultReadParam();
120: imgArray[i] = reader.read(0,params);
121: }
122: setFiles(imgArray);
123: }
124:
131: protected ImageReader getReader(String filename) throws IOException {
132: File f = new File(filename);
133: ImageInputStream iis = ImageIO.createImageInputStream(f);
134: Iterator readers = ImageIO.getImageReaders(iis);
135: ImageReader reader = (ImageReader) readers.next();
136: reader.setInput(iis,true);
137: return reader;
138: }
161: }
|
Je nach Anzahl der verfügbaren Plugins kommen manchmal mehrere Reader in Frage, deshalb der Umweg über einen Iterator. Das Beispiel verwendet in Zeile 135 den ersten und meist einzigen verfügbaren Reader. Er nimmt optional Parameter entgegen, die das Leseverhalten steuern, etwa um eine bereits skalierte Version des Bildes zu verwenden.
Der Reader gibt dann Zugriff auf verschiedene Inhalte der Bilddatei. Dabei handelt es sich beispielsweise um Metadaten (siehe Kasten “Zugriff auf Metadaten”) und Thumbnails oder im Falle der Formate Gif und Tiff um mehrere enthaltene einzelne Bilder. Zeile 120 liest das erste Bild der Datei. Im Gegensatz zum AWT-basierten Bildbetrachter kostet das Zeit und Speicher, deshalb dient das Beispielprogramm nicht als Vorbild für effizientes Programmieren.
|
Zugriff auf |
|---|
|
Neben den eigentlichen Bilddaten enthalten Bilder noch Zusatzinformationen, entweder explizit oder als Teil der Inhalte, zum Beispiel die Anzahl der Farben. Diese Angaben sind nützlich für den Betrachter oder zum effizienten Verarbeiten der Daten durch die Software. Ein »ImageReader« greift über folgende Zeilen auf die Metadaten zu: ImageReader reader = getReader(f);
IIOMetadata metaData = reader.getImageMetadata(0);
if (metaData == null) {
return;
}
String[] formats = metaData.getMetadataFormatNames();
for (int i=0; i<formats.length;++i) {
Node rootNode = metaData.getAsTree(formats[i]);
printNode(rootNode,0);
}
Die Ausgabe erfolgt in einem XML-DOM-Baum, den das Beispiel einfach strukturiert ausgibt. Der Zugriff auf die Daten ist durch das zusätzliche XML-API zwar standardisiert, aber für die geringe Menge der Informationen auch unangemessen komplex. Exif-Daten liest »ImageReader« allerdings noch nicht aus. Hier hilft eine freie Bibliothek [3], die jedoch nur das Jpeg-Format unterstützt: Metadata metadata = JpegMetadataReader.readMetadata(f);
Iterator directories = metadata.getDirectoryIterator();
while (directories.hasNext()) {
Directory directory = (Directory)directories.next();
Iterator tags = directory.getTagIterator();
while (tags.hasNext()) {
Tag tag = (Tag) tags.next();
System.out.println(tag);
}
}
Wenn eine Anwendung die Exif-Daten anderer Bildformate benötigt, bleibt derzeit leider nur der aufwändigere und weniger portable Weg über ein externes Programm. |
Bildmanipulationen
Die zweite Version des Bildbetrachters unterstützt exemplarisch zwei Operationen: Skalieren (fest eingestellt auf 75 Prozent) und Schärfen, das zur Vereinfachung ebenfalls fest kodiert ist. Beim Skalieren erzeugt das Programm eine passende affine Transformation (Zeile 84) und initialisiert damit eine »AffineTransformOp«. Zum Schärfen braucht es eine »ConvolveOp«, die als Argument eine Matrix mit den relativen Gewichten der Pixel benötigt.
Die Zeilen 88 und 106 rufen die »filter()«-Methode auf, die die Umwandlung durchführt. Hat das zweite Argument den Wert 0, erzeugt sie ein passendes Ziel-Image. Bei Zeile 98 handelt es sich um einen Workaround, denn »ConvolveOp« kann nicht mit jedem Color Model umgehen; die eingebaute Skalierung mit dem Faktor 1 wandelt das Farbmodell passend um. Die Änderungen an den Bildern sind nur temporär, Vor- und Zurückblättern führt zum Original aus dem Array zurück.
Highend-Bildverarbeitung mit JAI
Java Advanced Imaging Framework (JAI) heißt das dritte API für die Bildverarbeitung. Es integriert sich gut in die bestehenden APIs und ergänzt sie. Ein wichtiges Merkmal ist das Pull Model: Es steuert zum Beispiel explizit, welche Teile eines Bildes das Programm lesen und verarbeiten soll. Bei großen Bildern, die sowieso nur ausschnittsweise auf dem Bildschirm erscheinen können, führt das mitunter zu deutlichen Performancegewinnen.
Weitere wichtige Vorteile liegen in der Netzwerkfähigkeit, die die erste Generation nur sehr eingeschränkt anbot, und in der Möglichkeit, mehrere Quellen zu verarbeiten, um zum Beispiel die Differenz zweier Bilder zu berechnen. Darüber hinaus unterstützt JAI dreidimensionale Bilddaten. Native-Code-Bibliotheken optimieren die Performance. Falls sie zur Verfügung stehen, nutzt JAI sie transparent, ansonsten greift es auf eine Java-Lösung zurück.
Das Mehr an Funktionalität erfordert einen größeren Programmieraufwand, allerdings hält er sich bei einfachen Aufgaben im Rahmen, wie der nach JAI migrierte Bildbetrachter zeigt. Vor der Programmierung steht aber der Download der Pakete und ihre Installation.

Abbildung 2: Die zweite Version des Bildbetrachters verwendet das Java-2D-API und unterstützt Transformationen.

Abbildung 3: Die dritte Version des Bildbetrachters setzt auf das Java Advanced Imaging Framework und schärft Bilder.
Download und Installation von JAI
Die verschiedenen JAI-Pakete stehen unter [4] zur Verfügung. Die in diesem Artikel vorgestellten Beispiele basieren auf »JAI-1.1.3-beta« und »JAI-ImageIO-1.1-alpha«. Es gibt mehrere Installationsformate für Linux (x86 und x86_64), die auch native Bibliotheken enthalten. Sinnvollerweise installiert man die Pakete als Extensions unter »/lib/ext«, dann kann der Classpath unverändert bleiben.
JAI-Image-IO ist zwar nicht notwendig, erweitert das Paket aber um Plugins für die Formate Tiff, BMP, PNM und Jpeg 2000, inklusive des optimierten und nativen Code. Für die Bibliotheken existiert außerdem eine vollständige Dokumentation. Daneben gibt es einen User\’s Guide zur JAI-Version 1.0.1, der trotz einiger veralteter Beispiele hilfreich ist. Der Zugriff auf den Quellcode und die Beispielprogramme per CVS setzt eine Registrierung auf Java.net [5] voraus. Alternativ bietet das Webinterface die Dateien einzeln zum Download an.
Version 3
Einen Eindruck von JAI vermittelt der Bildbetrachter in einer dritten Version. Listing 4 zeigt das Grundprinzip anhand des Lesens, Skalierens und Schärfens. In den Zeilen 178 und 180, 141 und 156 erzeugt die Factory-Methode eine »RenderedOp«, den Teil einer Kette von Operationen. Bei »RenderedOp« handelt es sich um eine Subklasse von »PlanarImage«, die das AWT-Interface »RenderedImage« implementiert.
|
Listing 4: Bildverarbeitung mit |
|---|
039: public class ImageViewer3 extends JFrame {
040:
133: private RenderedImage scaleImage(RenderedImage srcImg,float scale) {
134: ParameterBlock pblock = new ParameterBlock();
135: pblock.addSource(srcImg);
136: pblock.add(scale); // x-scale
137: pblock.add(scale); // y-scale
138: pblock.add(0.0f); // x-translation
139: pblock.add(0.0f); // y-translation
140: pblock.add(new InterpolationNearest());
141: return JAI.create("scale",pblock,null);
142: }
143:
150: public RenderedImage sharpenImage(RenderedImage srcImg) {
151: final float[] kernelMatrix = {
152: 0.f, -1.f, 0.f,
153: -1.f, 5.0f, -1.f,
154: 0.f, -1.f, 0.f};
155: KernelJAI kernel = new KernelJAI(3,3,1,1,kernelMatrix);
156: return JAI.create("convolve",srcImg,kernel);
157: }
158:
175: protected void setImages(String[] filenames) throws IOException {
176: RenderedImage[] imgArray = new RenderedImage[filenames.length];
177: for (int i=0;i<filenames.length;++i) {
178: // imgArray[i] = (RenderedImage) JAI.create("fileload",filenames[i]);
179: // ImageRead uses jai-imageio
180: imgArray[i] = (RenderedImage) JAI.create("ImageRead",filenames[i]);
181: }
182: setFiles(imgArray);
183: }
257: }
|
Der Zugriff auf die Operationen erfolgt über den Namen. Alle Operationen verwaltet eine »OperationRegistry«, die das Programm abfragen und verändern kann. Die »RenderedOp« von JAI beruht auf dem so genannten Immediate-Modell: Sie setzt Anweisungen direkt um. Wenn dies unerwünscht ist, steht als Alternative zu »create()« zusätzlich die Methode »createRenderable()« zur Verfügung. Von dieser Factory-Methode erzeugte Objekte enthalten lediglich die Operationsbeschreibung, das Rendering erfolgt später über »RenderableOp.createRendering()«.
Wer diesen Bildbetrachter mit vielen oder sehr großen Bildern aufruft, stößt schnell an Speicher- und Performancegrenzen. An dieser Stelle würde sich ein Umstieg auf »RenderableOp« lohnen. Weitere Möglichkeiten wie die Arbeit mit Image-Collections oder Farbräumen beschreibt der User\’s Guide näher. Die Leistungsfähigkeit des API und seine Praxistauglichkeit zeigen sich in einer langen Liste von Applikationen, die JAI nutzen, von der Raumfahrt bis zur Medizin. Die JAI-Homepage verweist auf diese Anwendungen.
finally{}
Es wäre durchaus möglich, Gimp mit Java neu zu implementieren. Sinnvoller sind natürlich Anwendungen, die Java voraussetzen und einzelne Bildbearbeitungsfunktionen benötigen.
Zum Abschluss ein kleiner Blick über den Tellerrand: Eclipse hat zwar mit seinem Standard Widget Toolkit (SWT) eigene Abstraktionen geschaffen, unter anderem auch die Klasse »org.eclipse.swt.graphics.Image«. Eine Unterstützung für Bildmanipulationen bietet sie allerdings de facto nicht.
Imagemagick
Als bessere Alternative bieten sich die Java-Bindings zum Imagemagick-Toolkit an. Wer die Arbeit mit nativem Code nicht scheut, erschließt sich die große Funktionsvielfalt, die dieses Toolkit bietet. Den Umgang mit den Imagemagick-Bindings behandelte bereits ein früherer Coffeeshop [6]. (csc)
|
Infos |
|---|
|
[1] Bernhard Bablok, “Sauberer Abdruck”: Linux-Magazin 12/04, S. 108 [2] Bernhard Bablok, “Gesetzte Typen”: Linux-Magazin 02/04, S. 111 [https://www.linux-magazin.de/Artikel/ausgabe/2004/02/111_coffeeshop/coffeeshop.html] [3] Zugriff auf Exif-Daten: [http://www.drewnoakes.com/code/exif/] [4] JAI: [http://java.sun.com/products/java-media/jai/index.jsp] [5] Sammlung von Java-Projekten: [http://dev.java.net] [6] Bernhard Bablok, “Bilder-Zauberer”: Linux-Magazin 12/03, S. 99, [https://www.linux-magazin.de/Artikel/ausgabe/2003/12/099_coffeeshop/coffeeshop.html] |
|
Der Autor |
|---|
|
Bernhard Bablok arbeitet bei der AGIS mbH als Anwendungsentwickler. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Objektorientierung. Er ist unter [coffee-shop@bablokb.de] zu erreichen. |





