Java - obter matriz de pixels da imagem

118

Estou procurando a maneira mais rápida de obter dados de pixel (int o formulário int[][]) de a BufferedImage. Meu objetivo é ser capaz de endereçar o pixel (x, y)da imagem usando int[x][y]. Todos os métodos que encontrei não fazem isso (a maioria deles retorna int[]s).

ryyst
fonte
Se você está preocupado com a velocidade, por que deseja copiar a imagem inteira para um array em vez de apenas usar getRGBe setRGBdiretamente?
Brad Mace
3
@bemace: Porque esses métodos parecem fazer mais trabalho do que se possa imaginar, de acordo com meu perfil. Acessar um array parece muito mais rápido.
ryyst
15
@bemace: É realmente intenso: usar um array é mais de 800% mais rápido do que usar getRGBe setRGBdiretamente.
ryyst

Respostas:

179

Eu estava brincando com esse mesmo assunto, que é a maneira mais rápida de acessar os pixels. Atualmente, conheço duas maneiras de fazer isso:

  1. Usando o getRGB()método de BufferedImage conforme descrito na resposta de @tskuzzy.
  2. Acessando a matriz de pixels diretamente usando:

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

Se você estiver trabalhando com imagens grandes e o desempenho for um problema, o primeiro método não é absolutamente o melhor. O getRGB()método combina os valores alfa, vermelho, verde e azul em um int e retorna o resultado, que na maioria dos casos você fará o inverso para obter esses valores de volta.

O segundo método retornará os valores de vermelho, verde e azul diretamente para cada pixel e, se houver um canal alfa, ele adicionará o valor alfa. Usar este método é mais difícil em termos de cálculo de índices, mas é muito mais rápido do que a primeira abordagem.

No meu aplicativo, consegui reduzir o tempo de processamento dos pixels em mais de 90% apenas mudando da primeira abordagem para a segunda!

Aqui está uma comparação que configurei para comparar as duas abordagens:

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";
   }
}

Você consegue adivinhar a saída? ;)

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
fonte
10
Para aqueles com preguiça de ler o código, existem dois testes convertTo2DUsingGetRGBe convertTo2DWithoutUsingGetRGB. O primeiro teste leva em média 16 segundos. O segundo teste leva em média 1,5 segundos. No início, pensei que "s" e "ms" eram duas colunas diferentes. @Mota, ótima referência.
Jason
1
@Reddy, experimentei e vejo uma diferença no tamanho do arquivo, mas não tenho certeza do motivo! No entanto, consegui reproduzir os valores exatos de pixel usando este código (usando o canal alfa): pastebin.com/zukCK2tu Você pode precisar modificar o terceiro argumento do construtor BufferedImage, dependendo da imagem com a qual está lidando . Espero que isso ajude um pouco!
Motasim 01 de
4
@Mota Em convertTo2DUsingGetRGB, por que você pega o resultado [linha] [col] = imagem.getRGB (col, linha); em vez de resultado [linha] [col] = imagem.getRGB (linha, col);
Kailash
6
Pessoas notando uma diferença de cor e / ou ordem de byte incorreta: o código do @Mota assume uma ordem BGR . Deve verificar a entrada BufferedImagede type, por exemplo TYPE_INT_RGB, ou TYPE_3BYTE_BGRe punho de forma adequada. Esta é uma das coisas que getRGB()faz por você, que o torna mais lento :-(
millhouse
2
Corrija-me se eu estiver errado, mas não seria mais eficiente usar em |=vez de +=combinar os valores no método 2?
Ontonator
24

Algo assim?

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
fonte
11
Isso não é incrivelmente ineficiente? Eu teria, porém, BufferedImageiria armazenar os pixels usando uma matriz 2D int, de qualquer maneira?
ryyst
1
Tenho certeza de que a imagem é armazenada internamente como uma estrutura de dados unidimensional. Portanto, a operação levará O (W * H), não importa como você a faça. Você poderia evitar a sobrecarga de chamada de método armazenando-o primeiro em uma matriz unidimensional e convertendo a matriz unidimensional em uma matriz 2D.
tskuzzy
4
@ryyst se você quiser todos os pixels em uma matriz, é o mais eficiente possível
Sean Patrick Floyd
1
+1, não acho que isso acesse o Rasterbuffer de dados do, o que é definitivamente uma coisa boa, já que resulta em punção de aceleração.
mês de
2
@tskuzzy Este método é mais lento. Verifique o método da Mota, que é mais rápido que o método convencional.
h4ck3d
20

Achei que a resposta de Mota me deu um aumento de velocidade de 10 vezes - então, obrigado Mota.

Eu embrulhei o código em uma classe conveniente que pega o BufferedImage no construtor e expõe um método getRBG (x, y) equivalente que o torna uma substituição para o código que usa BufferedImage.getRGB (x, y)

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
fonte
Sou novo no processamento de arquivos de imagem em java. Você pode explicar por que fazer getRGB () dessa forma é mais rápido / melhor / mais otimizado do que getRGB () da API de cores? Agradeço!
mk7
@ mk7 Por favor, dê uma olhada nesta resposta stackoverflow.com/a/12062932/363573 . Para obter mais detalhes, digite java por que getrgb está lento em seu mecanismo de pesquisa favorito.
Stephan
10

A resposta de Mota é ótima, a menos que seu BufferedImage veio de um Bitmap monocromático. Um Bitmap monocromático tem apenas 2 valores possíveis para seus pixels (por exemplo 0 = preto e 1 = branco). Quando um bitmap monocromático é usado, o

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

call retorna os dados brutos do Pixel Array de forma que cada byte contenha mais de um pixel.

Portanto, quando você usa uma imagem bitmap monocromática para criar seu objeto BufferedImage, este é o algoritmo que deseja usar:

/**
 * 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
fonte
4

Se for útil, tente isto:

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

byte[] pixels = (byte[])imgBuffer.getRaster().getDataElements(0, 0, imgBuffer.getWidth(), imgBuffer.getHeight(), null);
C-Crestani
fonte
14
Uma explicação seria útil
asheeshr
1

Aqui está outra implementação FastRGB encontrada aqui :

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;
    }
}

O que é isso?

Ler uma imagem pixel a pixel através do método getRGB de BufferedImage é bastante lento, esta classe é a solução para isso.

A ideia é que você construa o objeto alimentando-o com uma instância BufferedImage, e ele lê todos os dados de uma vez e os armazena em um array. Quando você deseja obter pixels, chame getRGB

Dependências

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

Considerações

Embora FastRGB torne a leitura de pixels muito mais rápida, pode levar a um alto uso de memória, pois simplesmente armazena uma cópia da imagem. Portanto, se você tiver um BufferedImage de 4 MB na memória, depois de criar a instância FastRGB, o uso da memória será de 8 MB. No entanto, você pode reciclar a instância BufferedImage depois de criar o FastRGB.

Tenha cuidado para não cair em OutOfMemoryException ao usá-lo em dispositivos como telefones Android, onde RAM é um gargalo

Stephan
fonte
-1

Isso funcionou para mim:

BufferedImage bufImgs = ImageIO.read(new File("c:\\adi.bmp"));    
double[][] data = new double[][];
bufImgs.getData().getPixels(0,0,bufImgs.getWidth(),bufImgs.getHeight(),data[i]);    
Uma panela
fonte
8
Qual é a variável i?
Nicolas
é o iterador de dados
Cjen1