Java - Pixelarray aus dem Bild abrufen

118

Ich suche nach dem schnellsten Weg, um Pixeldaten (im Formular int[][]) von a zu erhalten BufferedImage. Mein Ziel ist es, Pixel (x, y)aus dem Bild mit zu adressieren int[x][y]. Alle Methoden, die ich gefunden habe, tun dies nicht (die meisten geben int[]s zurück).

Ryyst
quelle
Wenn Sie über die Geschwindigkeit besorgt sind, warum wollen Sie das gesamte Bild auf ein Array kopieren , anstatt nur mit getRGBund setRGBdirekt?
Brad Mace
3
@bemace: Weil diese Methoden meiner Profilerstellung zufolge mehr Arbeit zu leisten scheinen, als man denkt. Der Zugriff auf ein Array scheint viel schneller zu sein.
Ryyst
15
@bemace: Es ist wirklich sehr intensiv: Die Verwendung eines Arrays ist mehr als 800% schneller als die Verwendung getRGBund setRGBdirekt.
Ryyst

Antworten:

179

Ich habe nur mit demselben Thema herumgespielt, was der schnellste Weg ist, auf die Pixel zuzugreifen. Ich kenne derzeit zwei Möglichkeiten, dies zu tun:

  1. Verwenden der getRGB()Methode von BufferedImage, wie in der Antwort von @ tskuzzy beschrieben.
  2. Durch direkten Zugriff auf das Pixel-Array mit:

    byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData();

Wenn Sie mit großen Bildern arbeiten und die Leistung ein Problem darstellt, ist die erste Methode absolut nicht der richtige Weg. Die getRGB()Methode kombiniert die Werte für Alpha, Rot, Grün und Blau zu einem Int und gibt dann das Ergebnis zurück. In den meisten Fällen gehen Sie umgekehrt vor, um diese Werte zurückzugewinnen.

Die zweite Methode gibt die Rot-, Grün- und Blauwerte direkt für jedes Pixel zurück. Wenn ein Alphakanal vorhanden ist, wird der Alphawert addiert. Die Verwendung dieser Methode ist bei der Berechnung von Indizes schwieriger, jedoch viel schneller als der erste Ansatz.

In meiner Anwendung konnte ich die Verarbeitungszeit der Pixel um mehr als 90% reduzieren, indem ich einfach vom ersten zum zweiten Ansatz wechselte!

Hier ist ein Vergleich, den ich eingerichtet habe, um die beiden Ansätze zu vergleichen:

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.IOException;
import javax.imageio.ImageIO;

public class PerformanceTest {

   public static void main(String[] args) throws IOException {

      BufferedImage hugeImage = ImageIO.read(PerformanceTest.class.getResource("12000X12000.jpg"));

      System.out.println("Testing convertTo2DUsingGetRGB:");
      for (int i = 0; i < 10; i++) {
         long startTime = System.nanoTime();
         int[][] result = convertTo2DUsingGetRGB(hugeImage);
         long endTime = System.nanoTime();
         System.out.println(String.format("%-2d: %s", (i + 1), toString(endTime - startTime)));
      }

      System.out.println("");

      System.out.println("Testing convertTo2DWithoutUsingGetRGB:");
      for (int i = 0; i < 10; i++) {
         long startTime = System.nanoTime();
         int[][] result = convertTo2DWithoutUsingGetRGB(hugeImage);
         long endTime = System.nanoTime();
         System.out.println(String.format("%-2d: %s", (i + 1), toString(endTime - startTime)));
      }
   }

   private static int[][] convertTo2DUsingGetRGB(BufferedImage image) {
      int width = image.getWidth();
      int height = image.getHeight();
      int[][] result = new int[height][width];

      for (int row = 0; row < height; row++) {
         for (int col = 0; col < width; col++) {
            result[row][col] = image.getRGB(col, row);
         }
      }

      return result;
   }

   private static int[][] convertTo2DWithoutUsingGetRGB(BufferedImage image) {

      final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
      final int width = image.getWidth();
      final int height = image.getHeight();
      final boolean hasAlphaChannel = image.getAlphaRaster() != null;

      int[][] result = new int[height][width];
      if (hasAlphaChannel) {
         final int pixelLength = 4;
         for (int pixel = 0, row = 0, col = 0; pixel + 3 < pixels.length; pixel += pixelLength) {
            int argb = 0;
            argb += (((int) pixels[pixel] & 0xff) << 24); // alpha
            argb += ((int) pixels[pixel + 1] & 0xff); // blue
            argb += (((int) pixels[pixel + 2] & 0xff) << 8); // green
            argb += (((int) pixels[pixel + 3] & 0xff) << 16); // red
            result[row][col] = argb;
            col++;
            if (col == width) {
               col = 0;
               row++;
            }
         }
      } else {
         final int pixelLength = 3;
         for (int pixel = 0, row = 0, col = 0; pixel + 2 < pixels.length; pixel += pixelLength) {
            int argb = 0;
            argb += -16777216; // 255 alpha
            argb += ((int) pixels[pixel] & 0xff); // blue
            argb += (((int) pixels[pixel + 1] & 0xff) << 8); // green
            argb += (((int) pixels[pixel + 2] & 0xff) << 16); // red
            result[row][col] = argb;
            col++;
            if (col == width) {
               col = 0;
               row++;
            }
         }
      }

      return result;
   }

   private static String toString(long nanoSecs) {
      int minutes    = (int) (nanoSecs / 60000000000.0);
      int seconds    = (int) (nanoSecs / 1000000000.0)  - (minutes * 60);
      int millisecs  = (int) ( ((nanoSecs / 1000000000.0) - (seconds + minutes * 60)) * 1000);


      if (minutes == 0 && seconds == 0)
         return millisecs + "ms";
      else if (minutes == 0 && millisecs == 0)
         return seconds + "s";
      else if (seconds == 0 && millisecs == 0)
         return minutes + "min";
      else if (minutes == 0)
         return seconds + "s " + millisecs + "ms";
      else if (seconds == 0)
         return minutes + "min " + millisecs + "ms";
      else if (millisecs == 0)
         return minutes + "min " + seconds + "s";

      return minutes + "min " + seconds + "s " + millisecs + "ms";
   }
}

Können Sie die Ausgabe erraten? ;)

Testing convertTo2DUsingGetRGB:
1 : 16s 911ms
2 : 16s 730ms
3 : 16s 512ms
4 : 16s 476ms
5 : 16s 503ms
6 : 16s 683ms
7 : 16s 477ms
8 : 16s 373ms
9 : 16s 367ms
10: 16s 446ms

Testing convertTo2DWithoutUsingGetRGB:
1 : 1s 487ms
2 : 1s 940ms
3 : 1s 785ms
4 : 1s 848ms
5 : 1s 624ms
6 : 2s 13ms
7 : 1s 968ms
8 : 1s 864ms
9 : 1s 673ms
10: 2s 86ms

BUILD SUCCESSFUL (total time: 3 minutes 10 seconds)
Motasim
quelle
10
Für diejenigen, die zu faul sind, um den Code zu lesen, gibt es zwei Tests convertTo2DUsingGetRGBund convertTo2DWithoutUsingGetRGB. Der erste Test dauert durchschnittlich 16 Sekunden. Der zweite Test dauert durchschnittlich 1,5 Sekunden. Zuerst dachte ich, dass "s" und "ms" zwei verschiedene Spalten sind. @ Mota, tolle Referenz.
Jason
1
@ Reddy Ich habe es ausprobiert und sehe einen Unterschied in der Dateigröße, was ich nicht sicher bin, warum! Mit diesem Code (mithilfe des Alphakanals) konnte ich jedoch die genauen Pixelwerte reproduzieren: pastebin.com/zukCK2tu Je nach dem Bild, mit dem Sie sich befassen, müssen Sie möglicherweise das dritte Argument des BufferedImage-Konstruktors ändern . Hoffe das hilft ein bisschen!
Motasim
4
@Mota Warum nehmen Sie in convertTo2DUsingGetRGB das Ergebnis [row] [col] = image.getRGB (col, row)? anstelle von Ergebnis [Zeile] [Spalte] = image.getRGB (Zeile, Spalte);
Kailash
6
Personen, die einen Farbunterschied und / oder eine falsche Bytereihenfolge bemerken: @ Motas Code setzt eine BGR- Reihenfolge voraus . Sie sollten die eingehenden Check BufferedImage‚s zum typeBeispiel TYPE_INT_RGBoder in TYPE_3BYTE_BGRgeeigneter Weise und Griff. Dies ist eines der Dinge , die getRGB()für Sie tut, dass es langsamer macht :-(
millhouse
2
Korrigieren Sie mich, wenn ich falsch liege, aber wäre es nicht effizienter , die Werte in Methode 2 zu kombinieren, |=anstatt sie +=zu kombinieren?
Ontonator
24

Etwas wie das?

int[][] pixels = new int[w][h];

for( int i = 0; i < w; i++ )
    for( int j = 0; j < h; j++ )
        pixels[i][j] = img.getRGB( i, j );
tskuzzy
quelle
11
Ist das nicht unglaublich ineffizient? Hätte ich BufferedImagedie Pixel trotzdem mit einem 2D-Int-Array gespeichert?
Ryyst
1
Ich bin mir ziemlich sicher, dass das Bild intern als eindimensionale Datenstruktur gespeichert ist. Die Operation dauert also O (B * H), egal wie Sie es tun. Sie können den Aufwand für Methodenaufrufe vermeiden, indem Sie ihn zuerst in einem eindimensionalen Array speichern und das eindimensionale Array in ein 2D-Array konvertieren.
Tskuzzy
4
@ryyst Wenn Sie alle Pixel in einem Array wollen, ist dies ungefähr so ​​effizient wie es nur geht
Sean Patrick Floyd
1
+1, ich glaube nicht, dass dies auf den RasterDatenpuffer des Datenpuffers zugreift , was definitiv eine gute Sache ist, da dies zu einem Beschleunigungs-Punting führt.
mre
2
@tskuzzy Diese Methode ist langsamer. Überprüfen Sie die Methode von Mota, die schneller ist als diese herkömmliche Methode.
h4ck3d
20

Ich fand, dass Motas Antwort mir eine 10-fache Geschwindigkeitssteigerung bescherte - also danke Mota.

Ich habe den Code in eine praktische Klasse eingepackt, die das BufferedImage im Konstruktor verwendet und eine äquivalente getRBG (x, y) -Methode verfügbar macht, wodurch der Code mithilfe von BufferedImage.getRGB (x, y) ersetzt wird.

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;

public class FastRGB
{

    private int width;
    private int height;
    private boolean hasAlphaChannel;
    private int pixelLength;
    private byte[] pixels;

    FastRGB(BufferedImage image)
    {

        pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
        width = image.getWidth();
        height = image.getHeight();
        hasAlphaChannel = image.getAlphaRaster() != null;
        pixelLength = 3;
        if (hasAlphaChannel)
        {
            pixelLength = 4;
        }

    }

    int getRGB(int x, int y)
    {
        int pos = (y * pixelLength * width) + (x * pixelLength);

        int argb = -16777216; // 255 alpha
        if (hasAlphaChannel)
        {
            argb = (((int) pixels[pos++] & 0xff) << 24); // alpha
        }

        argb += ((int) pixels[pos++] & 0xff); // blue
        argb += (((int) pixels[pos++] & 0xff) << 8); // green
        argb += (((int) pixels[pos++] & 0xff) << 16); // red
        return argb;
    }
}
Robert Sutton
quelle
Ich bin neu in der Verarbeitung von Bilddateien in Java. Können Sie erklären, warum es schneller / besser / optimaler ist, getRGB () auf diese Weise zu erstellen als getRGB () der Farb-API? Schätzen !
mk7
@ mk7 Bitte werfen Sie einen Blick auf diese Antwort stackoverflow.com/a/12062932/363573 . Für weitere Informationen geben Sie Java ein, warum getrgb in Ihrer bevorzugten Suchmaschine langsam ist .
Stephan
10

Motas Antwort ist großartig, es sei denn, Ihr BufferedImage stammt von einer monochromen Bitmap. Eine monochrome Bitmap hat nur 2 mögliche Werte für ihre Pixel (zum Beispiel 0 = Schwarz und 1 = Weiß). Wenn eine monochrome Bitmap verwendet wird, wird die

final byte[] pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();

call gibt die rohen Pixel-Array-Daten so zurück, dass jedes Byte mehr als ein Pixel enthält.

Wenn Sie also ein monochromes Bitmap-Bild zum Erstellen Ihres BufferedImage-Objekts verwenden, ist dies der Algorithmus, den Sie verwenden möchten:

/**
 * This returns a true bitmap where each element in the grid is either a 0
 * or a 1. A 1 means the pixel is white and a 0 means the pixel is black.
 * 
 * If the incoming image doesn't have any pixels in it then this method
 * returns null;
 * 
 * @param image
 * @return
 */
public static int[][] convertToArray(BufferedImage image)
{

    if (image == null || image.getWidth() == 0 || image.getHeight() == 0)
        return null;

    // This returns bytes of data starting from the top left of the bitmap
    // image and goes down.
    // Top to bottom. Left to right.
    final byte[] pixels = ((DataBufferByte) image.getRaster()
            .getDataBuffer()).getData();

    final int width = image.getWidth();
    final int height = image.getHeight();

    int[][] result = new int[height][width];

    boolean done = false;
    boolean alreadyWentToNextByte = false;
    int byteIndex = 0;
    int row = 0;
    int col = 0;
    int numBits = 0;
    byte currentByte = pixels[byteIndex];
    while (!done)
    {
        alreadyWentToNextByte = false;

        result[row][col] = (currentByte & 0x80) >> 7;
        currentByte = (byte) (((int) currentByte) << 1);
        numBits++;

        if ((row == height - 1) && (col == width - 1))
        {
            done = true;
        }
        else
        {
            col++;

            if (numBits == 8)
            {
                currentByte = pixels[++byteIndex];
                numBits = 0;
                alreadyWentToNextByte = true;
            }

            if (col == width)
            {
                row++;
                col = 0;

                if (!alreadyWentToNextByte)
                {
                    currentByte = pixels[++byteIndex];
                    numBits = 0;
                }
            }
        }
    }

    return result;
}
CatGuardian
quelle
4

Wenn dies nützlich ist, versuchen Sie Folgendes:

BufferedImage imgBuffer = ImageIO.read(new File("c:\\image.bmp"));

byte[] pixels = (byte[])imgBuffer.getRaster().getDataElements(0, 0, imgBuffer.getWidth(), imgBuffer.getHeight(), null);
C-Crestani
quelle
14
Eine Erklärung wäre hilfreich
Asheeshr
1

Hier ist eine weitere FastRGB Implementierung gefunden hier :

public class FastRGB {
    public int width;
    public int height;
    private boolean hasAlphaChannel;
    private int pixelLength;
    private byte[] pixels;

    FastRGB(BufferedImage image) {
        pixels = ((DataBufferByte) image.getRaster().getDataBuffer()).getData();
        width = image.getWidth();
        height = image.getHeight();
        hasAlphaChannel = image.getAlphaRaster() != null;
        pixelLength = 3;
        if (hasAlphaChannel)
            pixelLength = 4;
    }

    short[] getRGB(int x, int y) {
        int pos = (y * pixelLength * width) + (x * pixelLength);
        short rgb[] = new short[4];
        if (hasAlphaChannel)
            rgb[3] = (short) (pixels[pos++] & 0xFF); // Alpha
        rgb[2] = (short) (pixels[pos++] & 0xFF); // Blue
        rgb[1] = (short) (pixels[pos++] & 0xFF); // Green
        rgb[0] = (short) (pixels[pos++] & 0xFF); // Red
        return rgb;
    }
}

Was ist das?

Das pixelweise Lesen eines Bildes mit der getRGB-Methode von BufferedImage ist recht langsam. Diese Klasse ist die Lösung dafür.

Die Idee ist, dass Sie das Objekt erstellen, indem Sie ihm eine BufferedImage-Instanz zuführen. Dabei werden alle Daten gleichzeitig gelesen und in einem Array gespeichert. Sobald Sie Pixel erhalten möchten, rufen Sie getRGB auf

Abhängigkeiten

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;

Überlegungen

Obwohl FastRGB das Lesen von Pixeln erheblich beschleunigt, kann dies zu einer hohen Speichernutzung führen, da lediglich eine Kopie des Bildes gespeichert wird. Wenn Sie also ein 4MB BufferedImage im Speicher haben, beträgt die Speichernutzung nach dem Erstellen der FastRGB-Instanz 8 MB. Sie können die BufferedImage-Instanz jedoch nach dem Erstellen des FastRGB wiederverwenden.

Achten Sie darauf, dass Sie nicht in OutOfMemoryException fallen, wenn Sie es auf Geräten wie Android-Telefonen verwenden, bei denen RAM ein Engpass ist

Stephan
quelle
-1

Das hat bei mir funktioniert:

BufferedImage bufImgs = ImageIO.read(new File("c:\\adi.bmp"));    
double[][] data = new double[][];
bufImgs.getData().getPixels(0,0,bufImgs.getWidth(),bufImgs.getHeight(),data[i]);    
Eine Pfanne
quelle
8
Was ist die Variable i?
Nicolas
Es ist der Iterator für Daten
Cjen1