Weitere aktuelle Java-Titel finden Sie bei dpunkt.
 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index


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: 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 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[1.1] 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: 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
Abbildung 12.7


 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index

Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.