12.8 | PixelGrabber und MemoryImageSource
|
PixelGrabber ist eine Klasse, mit deren Hilfe man
die Daten eines Bildes in ein Array kopieren kann.
Ein Exemplar der Klasse PixelGrabber wird meist
zusammen mit dem ImageProducer MemoryImageSource
verwendet. Mit MemoryImageSource kann man aus einem Array mit
Pixel-Daten ein Bild erzeugen.
Dies wird anhand eines Beispiels erläutert:
Ein Bild soll in ein anderes überblendet werden.
Bei der verwendeten Methode wird das erste Bild
gedanklich in horizontale Scheiben geschnitten.
Nacheinander wird scheibenweise jeweils der entsprechende Teil des
neuen Bildes eingeblendet, wodurch das zweite Bild das erste wie eine
sich schließende Jalousie überdeckt.
Um den
Übergang fließend zu gestalten, muss eine bestimmte Anzahl
an Zwischenbildern erstellt werden.
In diesem Beispiel übernimmt die Klasse Transformer diese Aufgabe:
abstract class Transformer {
protected Image start, end, actual;
protected int width, height;
protected int pos;
protected int[] pixbuf, pixstart, pixend;
protected MemoryImageSource memImg;
protected int number;
public Transformer(Image start, Image end,
int width, int height, int number) {
this.number = number;
this.width = width;
this.height = height;
pixstart = new int[width*height];
pixend = new int[width*height];
pixbuf = new int[width*height];
grab(start, pixstart);
grab(end, pixend);
System.arraycopy(pixstart, 0, pixbuf, 0, width*height);
memImg = new MemoryImageSource(width, height, pixbuf, 0, width);
memImg.setAnimated(true);
actual = Toolkit.getDefaultToolkit().createImage(memImg);
pos = 0;
}
abstract Image nextImage();
public void reset() {
System.arraycopy(pixstart, 0, pixbuf, 0, width*height);
pos = 0;
}
protected void grab(Image img, int pix[]) {
// Kopiert die Bilddaten des übergebenen
// Bildes in das übergebene Array
PixelGrabber grabber =
new PixelGrabber(img, 0, 0,
width, height, pix, 0, width);
try {
grabber.grabPixels();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Dem Konstruktor dieser Klasse werden beim Erzeugen eines neuen Exemplars die
beiden Bilder, zwischen denen ein Übergang realisiert werden soll, die
Breite und Höhe der Region und die Anzahl an Übergangsbildern übergeben.
Die Klasse Transformer stellt eine abstrakte Klasse dar, die
einen Übergang zwischen zwei Bildern repräsentiert. Um einen konkreten
Übergang zu implementieren, muss man von Transformer eine
neue Klasse ableiten und die Methode nextImage()
implementieren. Die Aufgabe von nextImage() ist es, das nächste
Bild in der Übergangsfolge zu berechnen und als Ergebnis zurückzugeben.
Wurden bereits alle Bilder des Übergangs von nextImage()
zurückgeliefert, so sollte das Ergebnis eines
nextImage()-Aufrufs null lauten.
Die Klasse Transformer bietet außerdem die Möglichkeit, die Berechnung eines
Übergangs auf den Anfangszustand zurückzusetzen. Hierfür steht die Methode reset()
zur Verfügung.
Wenn nextImage() nach einem Aufruf von reset() aufgerufen wird, sollte der Übergang wieder von neuem erzeugt werden.
In diesem Beispiel wurde eine konkrete Implementierung LineTransformer der Klasse
Transformer erstellt. Auf diese Implementierung wird später eingegangen.
Nun zunächst zu den allgemeinen
Eigenschaften der Klasse Transformer.
Noch innerhalb des Konstruktors werden die
Daten der Originalbilder in Arrays geschrieben. Dies übernimmt
die Methode grab():
protected void grab(Image img, int pix[]) {
// Kopiert die Bilddaten des übergebenen
// Bildes in das übergebene Array
PixelGrabber grabber =
new PixelGrabber(img, 0, 0,
width, height, pix, 0, width);
try {
grabber.grabPixels();
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
grab() verlangt als Parameter ein Image-Objekt und ein
Array vom Typ int. Dieses Array wird
durch den PixelGrabber mit Bilddaten gefüllt.
Hierzu muss zuerst ein Exemplar von
PixelGrabber erzeugt werden:
PixelGrabber grabber =
new PixelGrabber(img, 0, 0,
width, height, pix, 0, width);
Dem
Konstruktor werden folgende Werte übergeben:
- Das Bild, aus dem Daten bereitgestellt werden sollen (img).
- Der Bildausschnitt, dessen Daten gebraucht werden.
Dieser wird durch die Koordinaten der linken oberen Ecke
innerhalb des Bildes, der Breite sowie der Höhe angegeben (0, 0, width, height).
- Das Array, in das die Daten geschrieben werden sollen.
Dieses Array muss schon vor dem Aufruf erzeugt worden sein (pix).
- Der Index innerhalb
des Arrays, ab dem das Array mit Daten gefüllt wird (0)
- Der Abstand von einer Pixel-Reihe zur nächsten innerhalb
des Arrays. Ist dieser Wert gleich der Breite des Ausschnitts,
wird das Array lückenlos gefüllt (width).
Die Werte in Klammern sind jeweils die korrespondierenden
Werte in diesem Beispiel.
Der PixelGrabber beginnt nach dem Aufruf von
grabPixels(),
die einzelnen Pixel in das Array zu schreiben.
Die Methode liefert als Ergebnis true, wenn alle Pixel
geschrieben werden konnten.
Es wird eine InterruptedException
ausgelöst, wenn dieser Vorgang durch einen
anderen Thread unterbrochen wurde.
Das Array, das dem PixelGrabber übergeben wird,
muss genügend Platz bieten, um die Daten aufzunehmen. Also ist
die minimale Anzahl der Elemente gleich Breite mal Höhe
des Bildbereichs.
Bietet das Array nicht genügend Platz, wird eine ArrayIndexOutOfBoundsException
ausgelöst.
Nun
kann man auf die Daten eines Bildes zugreifen.
Jedes Element des Arrays stellt eine RGB-Farbe dar, die in
einem 4-Byte-int-Wert kodiert ist.
Durch »Mischen« der Daten dieser beiden Arrays kann man
die Übergangsbilder erzeugen.
Neben den Arrays, die die Bilddaten von Start- und Endbild aufnehmen, definiert die Klasse
Transformer das Array pixbuf, das die Bilddaten des letzten Übergangsbildes
enthält. Das erste Bild des Übergangsbildes entspricht dem Startbild. Deshalb werden zunächst die
Bilddaten des Startbildes in das Array pixbuf kopiert:
System.arraycopy(pixstart, 0, pixbuf, 0, width*height);
Hierzu wird
die Methode arraycopy() der Klasse System verwendet.
arraycopy()
kopiert einen Bereich eines Arrays in ein anderes Array gleichen
Typs. Diese Methode reserviert jedoch keinen Speicherplatz. Das an arraycopy()
übergebene Array muss also schon initialisiert sein.
Wenn die Grenzen des Arrays
beim Kopieren überschritten werden, wird eine ArrayIndexOutOfBoundsException
ausgelöst. Stimmen die Typen der Arrays nicht überein, wird
eine ArrayStoreException ausgelöst.
Der Methode werden Quell-Array mit Offset,
Ziel-Array mit Offset und die Anzahl der
zu kopierenden Elemente übergeben.
Der Offset ist der Index innerhalb des Arrays, an dem die Kopie beginnen soll.
Anschließend befindet sich in dem Array pixbuf
eine Kopie der Bilddaten des Anfangsbildes.
Aus den Bilddaten des Arrays pixbuf soll nun ein neues Bild erzeugt werden.
Zur Erzeugung eines Bildes wird die Methode createImage()
verwendet.
In diesem Fall
wird jedoch kein Offscreen-Image erzeugt, in das man zeichnen kann,
sondern ein Bild, das aufgrund der einzelnen im Array gespeicherten
Pixel erstellt wurde.
Der Unterschied besteht darin, dass in diesem Fall nicht
das Ausmaß eines Bildes, sondern ein ImageProducer
angegeben wird (in diesem Fall MemoryImageSource).
MemoryImageSource ist die Klasse, welche die eigentliche
Transformation der Array-Daten in Bilddaten vornimmt.
Dem Konstruktor
von MemoryImageSource übergibt man:
- Die Ausmaße des Bildes
- Das Array, in dem die Daten gespeichert sind
- Den Offset in das Daten-Array
- Den Abstand zwischen einer Pixel-Reihe im Array zur nächsten
Die hier vorhandenen Parameter treten auch bei
der Klasse PixelGrabber auf.
memImg = new MemoryImageSource(width, height, pixbuf, 0, width);
memImg.setAnimated(true);
actual = Toolkit.getDefaultToolkit().createImage(memImg);
Nachdem ein Exemplar MemoryImageSource erzeugt ist,
wird in diesem Fall die
Methode setAnimated() mit dem Wert true
als Parameter aufgerufen. Über dieses Flag kann eingestellt werden, ob eine
Änderung in den Bilddaten, die im Array gespeichert sind, auch eine Änderung
am zugehörigen Bild bewirkt. Das Flag, das mit der Methode setAnimated(boolean)
eingestellt werden kann, hat per Voreinstellung den Wert false.
Ein Aufruf von setAnimated(boolean), der dieses Flag auf true setzt, sollte
sofort erfolgen, bevor ein ImageConsumer mit der Methode createImage()
hinzugefügt wird. Erfolgt der Aufruf von setAnimated(true) nach createImage(), so
enthält das erzeugte Bild nur eine Momentaufnahme der Daten des Pixelarrays. Das Bild kann nicht
verändert werden, wenn man die Daten des Pixelarrays modifiziert.
In dem hier vorliegenden Beispiel soll eine Änderung der Daten des Pixelarrays allerdings eine Änderung
des Bildes bewirken, da dies die Grundlage für die Berechnung des Übergangs bildet.
Deshalb erfolgt der Aufruf in diesem Fall direkt nach der Erzeugung der MemoryImageSource.
Das Beispiel verwendet die createImage()-Methode der Klasse Toolkit.
Es könnte aber genauso die createImage()-Methode der Klasse Component verwendet werden.
Man sollte beachten, dass die Klasse Toolkit nur eine createImage()-Methode zur Verfügung stellt,
die direkt aus einem ImageProducer ein Bild erzeugt. Toolkit definiert aber keine
createImage()-Methode, um ein Offscreen-Image anzulegen.
Die Berechnung der Übergangsbilder wird von
der Methode nextImage() in der konkreten Implementierung
LineTransformer übernommen:
class LineTransformer extends Transformer {
protected float step;
protected float pixel;
public LineTransformer(Image start, Image end,
int width, int height,
int number, int space) {
super(start, end, width, height, number);
step = (float)height / (float)space;
System.out.println(height);
System.out.println(space);
System.out.println(step);
}
public Image nextImage() {
if (pos <= number) {
pixel = 0;
while(pixel < width*height) {
// Kopiere eine Bildscheibe in den Pixelpuffer
System.arraycopy(pixend, (int)pixel, pixbuf,
(int)pixel, (int)(pos*(width*step/number)));
// Nächste Bildscheibe bearbeiten
pixel+=step*width;
}
memImg.newPixels();
pos++;
return actual;
}
return null;
}
}
Dem Konstruktor der Klasse LineTransformer wird zusätzlich
die Anzahl der Scheiben, in die
die Bilder eingeteilt werden sollen, übergeben.
step beschreibt die Breite einer einzelnen Scheibe,
also Höhe des Bildes geteilt durch die Anzahl der Scheiben.
number ist ein internes Feld der Klasse Transformer, das
die Anzahl der zu erzeugenden Übergangsbilder speichert. pos
ist ein Zähler, der die bisher von nextImage() zurückgelieferte Anzahl
von Bildern enthält. pos wird deshalb bei jedem Aufruf von
nextImage() inkrementiert, falls das letzte Bild noch nicht zurückgegeben wurde.
Die Berechnung des nächsten Übergangsbildes läuft nun folgendermaßen ab:
Zunächst wird überprüft, ob bereits alle Bilder des Übergangs zurückgeliefert wurden.
Falls dies der Fall ist, wird null von nextImage() zurückgeliefert.
Wenn der Wert von pos kleiner als der Wert von number ist, muss
das nächste Übergangsbild berechnet werden. Dabei werden einfach Daten aus dem
Pixelarray des Endbildes in das Pixelarray pixbuf
der MemoryImageSource kopiert.
Je größer der Wert von pos, desto mehr Daten müssen kopiert werden, da der
Übergang fließend erscheinen soll.
Das
Mischen der Bilddaten übernimmt folgender Codeausschnitt:
pixel = 0;
while(pixel < width*height) {
// Kopiere eine Bildscheibe in den Pixelpuffer
System.arraycopy(pixend, (int)pixel, pixbuf,
(int)pixel, (int)(pos*(width*step/number)));
// Nächste Bildscheibe bearbeiten
pixel+=step*width;
}
memImg.newPixels();
pos++;
return actual;
Pro Bildscheibe wird die Schleife einmal durchlaufen.
Nachdem die neuen Bilddaten in das Pixelarray der MemoryImageSource kopiert wurden,
wird die Methode newPixels() aufgerufen.
Durch Aufruf von newPixels() übermittelt die MemoryImageSource an alle
ImageConsumer Bilddaten. newPixels() ist in der Klasse
MemoryImageSource in verschiedenen Varianten definiert, jeweils mit anderen Parametern.
Ein Unterschied dieser einzelnen Methoden besteht in der Menge der Pixel, die an die
ImageConsumer weitergegeben werden:
- public void newPixels()
Liefert alle Pixel an die ImageCosumer.
- public void newPixels(int x, int y, int w, int h)
Liefert den durch die Parameter angegebenen Bereich an Pixeln an die ImageConsumer.
Weitere setPixels()-Methoden können der Referenz entnommen werden.
Die einzelnen setPixels()-Methoden haben aber nur einen Effekt, wenn zuvor
setAnimated(true) der entsprechenden MemoryImageSource aufgerufen wurde.
Letztendlich gibt nextImage() das Image-Objekt des Bildes zurück.
Es ist zu beachten, dass in diesem Beispiel nextImage() immer einen Verweis
auf dasselbe Image-Objekt zurückliefert. Wenn der Aufrufer einen Verweis auf ein
zurückgeliefertes Image-Objekt speichert und anschließend nextImage() aufruft,
so ist das neue Image-Objekt identisch mit dem alten.
Im Hauptprogramm wird ein Exemplar von LineTransformer erzeugt und anschließend
das erste Übergangsbild durch nextImage() erfragt:
trans = new LineTransformer(start, end, width, height,
NUMBER, SPACE);
actual = trans.nextImage();
Das zurückgelieferte Bild kann nun in der paint()-Methode durch eine
drawImage()-Methode gezeichnet werden.
In einem Thread kann man nun z. B. nextImage() so lange in einer
Schleife aufrufen, bis null als Ergebnis zurückgeliefert wird. Nach dem Aufruf
von nextImage() muss jeweils auch ein Aufruf von repaint() erfolgen, damit
das neue Bild auch wieder in paint() gezeichnet wird. In nextImage() finden
nur die Berechnungen des neuen Übergangbildes statt, das Bild muss aber erneut durch eine
drawImage()-Methode auf den Bildschirm gezeichnet werden. Die Aktualisierung erfolgt
nicht automatisch:
public void run() {
Thread me = Thread.currentThread();
while(t == me) {
while(trans.nextImage() != null) {
repaint();
try {
t.sleep(20);
}
catch(Exception e) {
e.printStackTrace();
}
}
trans.reset();
}
}
Wurde im vorherigen Beispiel der komplette Übergang am Bildschirm angezeigt, so wird in diesem Fall
die Methode reset() aufgerufen, die den Übergang zurücksetzt. Dadurch
wird der Übergang von neuem am Bildschirm präsentiert.
Abbildung 12.7: Ein Übergang zwischen zwei Bildern
 |
Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.