Manufaturação industrial
Internet das coisas industrial | Materiais industriais | Manutenção e reparo de equipamentos | Programação industrial |
home  MfgRobots >> Manufaturação industrial >  >> Industrial programming >> VHDL

Imagem de bitmap de arquivo BMP lida usando TEXTIO


Converter o arquivo de imagem em um formato de bitmap torna a maneira mais fácil de ler uma imagem usando VHDL. O suporte para o formato de arquivo de imagem de gráficos raster BMP está embutido no sistema operacional Microsoft Windows. Isso torna o BMP um formato de imagem adequado para armazenar fotos para uso em testbenches VHDL.

Neste artigo, você aprenderá a ler um arquivo de imagem binário como o BMP e armazenar os dados na memória dinâmica do simulador. Usaremos um exemplo de módulo de processamento de imagem para converter a imagem em tons de cinza, este será nosso dispositivo em teste (DUT). Por fim, escrevemos a saída do DUT em uma nova imagem que podemos comparar visualmente com a original.

Esta postagem de blog é parte de uma série sobre o uso da biblioteca TEXTIO em VHDL. Leia os outros artigos aqui:

Como inicializar a RAM do arquivo usando TEXTIO

Arquivo de estímulo lido no testbench usando TEXTIO

Por que bitmap é o melhor formato para VHDL


Os formatos de arquivo de imagem mais comuns na internet são JPEG e PNG. Ambos usam compactação, JPEG é com perdas enquanto PNG é sem perdas. A maioria dos formatos oferece alguma forma de compactação porque isso pode reduzir drasticamente o tamanho de armazenamento da imagem. Embora isso seja bom para uso normal, não é ideal para leitura em um testbench VHDL.

Para poder processar uma imagem em software ou hardware, você precisa ter acesso aos dados brutos de pixel em seu aplicativo. Você deseja ter os dados de cor e luminância armazenados em uma matriz de bytes, isso é chamado de bitmap ou gráficos raster.

A maioria dos editores de imagem conhecidos, como Photoshop ou GIMP, são baseados em raster. Eles podem abrir uma ampla variedade de formatos de imagem, mas todos são convertidos em gráficos raster internamente no editor.

Você também pode fazer isso em VHDL, mas isso exigiria um esforço de codificação considerável porque não há soluções prontas para decodificar imagens compactadas. Uma solução melhor é converter as imagens de entrada de teste em um formato de bitmap como BMP manualmente ou incorporá-lo no script que inicia seu testbench.

O formato de arquivo de imagem BMP


O formato de arquivo BMP está bem documentado na Wikipedia. Esse formato tem muitas variantes diferentes, mas vamos concordar com algumas configurações específicas que facilitarão muito para nós. Para criar nossas imagens de entrada, nós as abrimos no Microsoft Paint que vem pré-instalado com o Windows. Em seguida, clicamos em Arquivo→Salvar como , selecione Salvar como tipo:Bitmap de 24 bits (*bmp; *.dib) . Dê um nome ao arquivo que termine com o sufixo .bmp e clique em salvar.

Ao garantir que o arquivo seja criado assim, podemos supor que o cabeçalho sempre é a variante BITMAPINFOHEADER de 54 bytes com formato de pixel RGB24 mencionado na página da Wikipedia. Além disso, vamos nos preocupar apenas com alguns campos selecionados dentro do cabeçalho. A tabela abaixo mostra os campos de cabeçalho que vamos ler.
Deslocamento (dezembro) Tamanho (B) Esperado (Hex) Descrição
0 2 “BM” (42 4D) Campo de ID
10 4 54 (36 00 00 00) Deslocamento da matriz de pixels
14 4 40 (28 00 00 00) Tamanho do cabeçalho
18 4 Ler valor Largura da imagem em pixels
22 4 Ler valor Altura da imagem em pixels
26 1 1 (01) Número de planos de cores
28 1 24 (18) Número de bits por pixel

Os valores marcados em verde são os únicos que realmente precisamos observar porque sabemos quais valores esperar nos outros campos de cabeçalho. Se você concordou em usar apenas imagens de dimensões fixas predefinidas todas as vezes, pode pular o cabeçalho inteiro e começar a ler no deslocamento de byte número 54 dentro do arquivo BMP, é onde os dados de pixel serão encontrados.

No entanto, verificaremos se os demais valores listados estão de acordo com o esperado. Não é difícil de fazer, pois já estamos lendo o cabeçalho. Ele também fornece uma proteção contra erros do usuário, caso você ou um de seus colegas forneça uma imagem da codificação errada ao testbench a qualquer momento no futuro.

O caso de teste


Esta postagem de blog é sobre como ler uma imagem de um arquivo em um testbench VHDL, mas para completar, incluí um exemplo de DUT. Transmitiremos os dados de pixel através do DUT enquanto lemos a imagem. Finalmente, gravamos os resultados em outro arquivo BMP de saída que pode ser examinado em seu visualizador de imagens favorito.
entity grayscale is
  port (
    -- RGB input
    r_in : in std_logic_vector(7 downto 0);
    g_in : in std_logic_vector(7 downto 0);
    b_in : in std_logic_vector(7 downto 0);

    -- RGB output
    r_out : out std_logic_vector(7 downto 0);
    g_out : out std_logic_vector(7 downto 0);
    b_out : out std_logic_vector(7 downto 0)
  );
end grayscale; 

O código acima mostra a entidade do nosso DUT. O módulo em tons de cinza pega os dados RGB de 24 bits para um pixel como entrada e os converte em uma representação em tons de cinza que é apresentada na saída. Observe que o pixel de saída representa um tom de cinza ainda dentro do espaço de cores RGB, não estamos convertendo o BMP em um BMP em escala de cinza, que é um formato diferente.

O módulo é puramente combinacional, não há entrada de clock ou reset. O resultado aparece imediatamente na saída quando algo é atribuído à entrada. Para simplificar, a conversão para escala de cinza usa uma aproximação de ponto fixo do valor de luma (brilho) de acordo com o sistema de codificação ITU-R BT.2100 RGB para luma.

Você pode baixar o código para o módulo de tons de cinza e todo o projeto usando o formulário abaixo.





A imagem do Boeing 747 que você está vendo abaixo será nossa imagem de entrada de exemplo. Ou seja, não é a imagem BMP real que está incorporada nesta postagem do blog, isso não seria possível. É uma representação JPEG da imagem BMP que vamos ler em nosso testbench. Você pode solicitar a imagem BMP original deixando seu endereço de e-mail no formulário acima e você a receberá imediatamente em sua caixa de entrada.

A imagem de teste tem 1000 x 1000 pixels. No entanto, o código apresentado neste artigo deve funcionar com qualquer dimensão de imagem, desde que esteja no formato BMP BITMAPINFOHEADER de 24 bits. No entanto, a leitura de uma imagem grande levará muito tempo de simulação porque o acesso ao arquivo na maioria dos simuladores VHDL é lento. Esta imagem tem 2930 kB e leva alguns segundos para carregar no ModelSim.

Importar a biblioteca TEXTIO


Para ler ou gravar em arquivos em VHDL, você precisa importar a biblioteca TEXTIO. Certifique-se de incluir as linhas da lista abaixo na parte superior do arquivo VHDL. Também precisamos importar o finish palavra-chave do pacote padrão para interromper a simulação quando todos os testes forem concluídos.
use std.textio.all;
use std.env.finish;

As declarações acima requerem que VHDL-2008 ou mais recente seja usado.

Declarações de tipo personalizado


Declararemos alguns tipos personalizados no início da região declarativa do nosso testbench. O formato das estruturas de dados para armazenar dados de pixel depende do tipo de entrada que o DUT espera. O módulo de tons de cinza espera três bytes, cada um representando um dos componentes de cor vermelho, verde e azul. Como ele opera em um pixel de cada vez, somos livres para armazenar o conjunto de pixels como quisermos.

Como podemos ver no código abaixo, primeiro declaramos um header_type array que usaremos para armazenar todos os dados do cabeçalho. Examinaremos alguns campos dentro do cabeçalho, mas também precisamos armazená-lo porque vamos gravar os dados da imagem processada em um novo arquivo no final do testbench. Em seguida, precisamos incluir o cabeçalho original na imagem de saída.
type header_type  is array (0 to 53) of character;

type pixel_type is record
  red : std_logic_vector(7 downto 0);
  green : std_logic_vector(7 downto 0);
  blue : std_logic_vector(7 downto 0);
end record;

type row_type is array (integer range <>) of pixel_type;
type row_pointer is access row_type;
type image_type is array (integer range <>) of row_pointer;
type image_pointer is access image_type;

A segunda declaração declara um registro chamado pixel_type . Esse tipo personalizado atuará como um contêiner para os dados RGB de um pixel.

Finalmente, as estruturas de dados dinâmicas para armazenar todos os pixels são declaradas. Enquanto row_type é uma matriz irrestrita do pixel_type , o row_pointer é um tipo de acesso a ele, um ponteiro VHDL. Da mesma forma, construímos um image_type irrestrito array para armazenar todas as linhas de pixels.

Assim, o image_pointer type funcionará como um identificador para a imagem completa na memória alocada dinamicamente.

Instanciando o DUT


Ao final da região declarativa, declaramos os sinais de interface para o DUT, conforme mostrado abaixo. Os sinais de entrada são pós-fixados com _in e os sinais de saída com _out . Isso nos permite identificá-los facilmente no código, bem como na forma de onda. O DUT é instanciado no início da arquitetura com os sinais atribuídos através do mapa de portas.
signal r_in : std_logic_vector(7 downto 0);
signal g_in : std_logic_vector(7 downto 0);
signal b_in : std_logic_vector(7 downto 0);
signal r_out : std_logic_vector(7 downto 0);
signal g_out : std_logic_vector(7 downto 0);
signal b_out : std_logic_vector(7 downto 0);

begin

DUT :entity work.grayscale(rtl)
port map (
  r_in => r_in,
  g_in => g_in,
  b_in => b_in,
  r_out => r_out,
  g_out => g_out,
  b_out => b_out
);

Variáveis ​​de processo e identificadores de arquivo


Vamos criar um único processo testbench para conter toda a leitura e escrita do arquivo. A região declarativa do processo é mostrada abaixo. Começamos declarando um novo char_file type para definir o tipo de dados que desejamos ler do arquivo de imagem de entrada. O arquivo BMP é codificado em binário; portanto, queremos operar em bytes, o character digite em VHDL. Nas próximas duas linhas, usamos o tipo para abrir um arquivo de entrada e um de saída.
process
  type char_file is file of character;
  file bmp_file : char_file open read_mode is "boeing.bmp";
  file out_file : char_file open write_mode is "out.bmp";
  variable header : header_type;
  variable image_width : integer;
  variable image_height : integer;
  variable row : row_pointer;
  variable image : image_pointer;
  variable padding : integer;
  variable char : character;
begin

Em seguida, declaramos uma variável para conter os dados do cabeçalho, bem como duas variáveis ​​inteiras para conter a largura e a altura da imagem. Depois disso, declaramos um row ponteiro e um image ponteiro. Este último será nosso identificador para a imagem completa uma vez que tenha sido lida do arquivo.

Finalmente, declaramos duas variáveis ​​de conveniência; padding do tipo integer e char do tipo character . Vamos usá-los para armazenar valores que lemos do arquivo temporariamente.

Ler o cabeçalho BMP


No início do corpo do processo, lemos todo o cabeçalho do arquivo BMP no header variável, conforme mostrado no código abaixo. O cabeçalho tem 54 bytes de comprimento, mas em vez de usar o valor codificado, obtemos o intervalo para iterar referenciando o header_type'range atributo. Você deve sempre usar atributos quando puder para manter os valores constantes definidos no menor número de lugares possível.
  for i in header_type'range loop
    read(bmp_file, header(i));
  end loop;

Em seguida, seguem algumas declarações assert onde verificamos se alguns dos campos de cabeçalho estão conforme o esperado. Esta é uma proteção contra erros do usuário, pois não usamos os valores lidos para nada, apenas verificamos se eles estão conforme o esperado. Os valores esperados são os listados nesta tabela, mostrados anteriormente no artigo.

O código abaixo mostra as declarações assert, cada uma com um report declaração que descreve o erro e um severity failure instrução para parar a simulação se a expressão declarada for false . Precisamos usar um nível de gravidade elevado porque pelo menos com as configurações padrão no ModelSim, ele apenas imprimirá uma mensagem de erro e continuará a simulação.
  -- Check ID field
  assert header(0) = 'B' and header(1) = 'M'
    report "First two bytes are not ""BM"". This is not a BMP file"
    severity failure;

  -- Check that the pixel array offset is as expected
  assert character'pos(header(10)) = 54 and
    character'pos(header(11)) = 0 and
    character'pos(header(12)) = 0 and
    character'pos(header(13)) = 0
    report "Pixel array offset in header is not 54 bytes"
    severity failure;

  -- Check that DIB header size is 40 bytes,
  -- meaning that the BMP is of type BITMAPINFOHEADER
  assert character'pos(header(14)) = 40 and
    character'pos(header(15)) = 0 and
    character'pos(header(16)) = 0 and
    character'pos(header(17)) = 0
    report "DIB headers size is not 40 bytes, is this a Windows BMP?"
    severity failure;

  -- Check that the number of color planes is 1
  assert character'pos(header(26)) = 1 and
    character'pos(header(27)) = 0
    report "Color planes is not 1" severity failure;

  -- Check that the number of bits per pixel is 24
  assert character'pos(header(28)) = 24 and
    character'pos(header(29)) = 0
    report "Bits per pixel is not 24" severity failure;

Em seguida, lemos os campos de largura e altura da imagem do cabeçalho. Esses são os únicos dois valores que vamos realmente usar. Portanto, nós os atribuímos ao image_width e image_height variáveis. Como podemos ver no código abaixo, temos que multiplicar os bytes subsequentes pela potência ponderada de dois valores para converter os campos de cabeçalho de quatro bytes em valores inteiros adequados.
  -- Read image width
  image_width := character'pos(header(18)) +
    character'pos(header(19)) * 2**8 +
    character'pos(header(20)) * 2**16 +
    character'pos(header(21)) * 2**24;

  -- Read image height
  image_height := character'pos(header(22)) +
    character'pos(header(23)) * 2**8 +
    character'pos(header(24)) * 2**16 +
    character'pos(header(25)) * 2**24;

  report "image_width: " & integer'image(image_width) &
    ", image_height: " & integer'image(image_height);

Por fim, imprimimos a altura e a largura de leitura no console do simulador usando o report declaração.

Ler os dados de pixel


Precisamos descobrir quantos bytes de preenchimento haverá em cada linha antes de podermos começar a ler os dados de pixel. O formato BMP requer que cada linha de pixels seja preenchida com um múltiplo de quatro bytes. No código abaixo, cuidamos disso com uma fórmula de uma linha usando o operador módulo na largura da imagem.
  -- Number of bytes needed to pad each row to 32 bits
  padding := (4 - image_width*3 mod 4) mod 4;

Também temos que reservar espaço para todas as linhas de dados de pixel que vamos ler. O image variável é um tipo de acesso, um ponteiro VHDL. Para fazê-lo apontar para um espaço de memória gravável, usamos o new palavra-chave para reservar espaço para image_height número de linhas na memória dinâmica, conforme mostrado abaixo.
  -- Create a new image type in dynamic memory
  image := new image_type(0 to image_height - 1);

Agora é hora de ler os dados da imagem. A listagem abaixo mostra o loop for que lê a matriz de pixels, linha por linha. Para cada linha, reservamos espaço para um novo row_type objeto, apontado pelo row variável. Em seguida, lemos o número esperado de pixels, primeiro o azul, depois o verde e finalmente o vermelho. Esta é a ordenação de acordo com o padrão BMP de 24 bits.
  for row_i in 0 to image_height - 1 loop

    -- Create a new row type in dynamic memory
    row := new row_type(0 to image_width - 1);

    for col_i in 0 to image_width - 1 loop

      -- Read blue pixel
      read(bmp_file, char);
      row(col_i).blue :=
        std_logic_vector(to_unsigned(character'pos(char), 8));

      -- Read green pixel
      read(bmp_file, char);
      row(col_i).green :=
        std_logic_vector(to_unsigned(character'pos(char), 8));

      -- Read red pixel
      read(bmp_file, char);
      row(col_i).red :=
        std_logic_vector(to_unsigned(character'pos(char), 8));

    end loop;

    -- Read and discard padding
    for i in 1 to padding loop
      read(bmp_file, char);
    end loop;

    -- Assign the row pointer to the image vector of rows
    image(row_i) := row;

  end loop;

Depois de ler a carga útil para cada linha, lemos e descartamos os bytes de preenchimento extras (se houver). Por fim, no final do loop, atribuímos a nova linha dinâmica de pixels ao slot correto do image variedade. Quando o loop for termina o image A variável deve conter dados de pixel para toda a imagem BMP.

Testando o DUT


O módulo em tons de cinza usa apenas lógica combinacional, portanto, não precisamos nos preocupar com nenhum sinal de clock ou reset. O código abaixo passa por cada pixel em cada linha enquanto grava os valores RGB nas entradas do DUT. Depois de atribuir os valores de entrada, esperamos 10 nanossegundos para permitir que todos os atrasos do ciclo delta dentro do DUT sejam desenrolados. Qualquer valor de tempo maior que 0 funcionará, ou até wait for 0 ns; repetido o suficiente.
  for row_i in 0 to image_height - 1 loop
    row := image(row_i);

    for col_i in 0 to image_width - 1 loop

      r_in <= row(col_i).red;
      g_in <= row(col_i).green;
      b_in <= row(col_i).blue;
      wait for 10 ns;

      row(col_i).red := r_out;
      row(col_i).green := g_out;
      row(col_i).blue := b_out;

    end loop;
  end loop;

Quando o programa sai da instrução de espera, as saídas do DUT devem conter os valores RGB para a cor em tons de cinza desse pixel. No final do loop, deixamos a saída do DUT substituir os valores de pixel que lemos do arquivo BMP de entrada.

Escrevendo o arquivo BMP de saída


Neste ponto, todos os pixels no image variável deveria ter sido manipulada pelo DUT. É hora de gravar os dados da imagem no out_file objeto, que aponta para um arquivo local chamado “out.bmp”. No código abaixo, percorremos cada pixel nos bytes de cabeçalho que armazenamos do arquivo BMP de entrada e os gravamos no arquivo de saída.
  for i in header_type'range loop
    write(out_file, header(i));
  end loop;

Após o cabeçalho, precisamos escrever os pixels na ordem em que os lemos do arquivo de entrada. Os dois loops for aninhados na lista abaixo cuidam disso. Observe que após cada linha usamos o deallocate palavra-chave para liberar a memória alocada dinamicamente para cada linha. A coleta de lixo está incluída apenas no VHDL-2019, nas versões anteriores do VHDL, você pode esperar vazamentos de memória se omitir esta linha. No final do loop for, escrevemos bytes de preenchimento, se necessário, para trazer o comprimento da linha para um múltiplo de 4 bytes.
  for row_i in 0 to image_height - 1 loop
    row := image(row_i);

    for col_i in 0 to image_width - 1 loop

      -- Write blue pixel
      write(out_file,
        character'val(to_integer(unsigned(row(col_i).blue))));

      -- Write green pixel
      write(out_file,
        character'val(to_integer(unsigned(row(col_i).green))));

      -- Write red pixel
      write(out_file,
        character'val(to_integer(unsigned(row(col_i).red))));

    end loop;

    deallocate(row);

    -- Write padding
    for i in 1 to padding loop
      write(out_file, character'val(0));
    end loop;

  end loop;

Após o término do loop, desalocamos o espaço de memória para o image variável, como mostrado abaixo. Em seguida, fechamos os arquivos chamando file_close nas alças de arquivo. Isso não é estritamente necessário na maioria dos simuladores porque o arquivo é fechado implicitamente quando o subprograma ou processo termina. No entanto, nunca é errado fechar arquivos quando terminar com eles.
  deallocate(image);

  file_close(bmp_file);
  file_close(out_file);

  report "Simulation done. Check ""out.bmp"" image.";
  finish;
end process;

No final do processo do testbench, imprimimos uma mensagem no console do ModelSim informando que a simulação terminou, com uma dica de onde a imagem de saída pode ser encontrada. O finish palavra-chave requer VHDL-2008, é uma maneira elegante de parar o simulador após a conclusão de todos os testes.




A imagem BMP de saída


A imagem abaixo mostra a aparência do arquivo “out.bmp” após a conclusão do testbench. O arquivo real mostrado nesta postagem do blog é um JPEG porque os BMPs não são adequados para incorporação em páginas da web, mas você pode deixar seu endereço de e-mail no formulário acima para obter um zip com o projeto completo, incluindo o arquivo “boeing.bmp”.

Últimas observações


Para processamento de imagem em FPGA, o esquema de codificação de cores YUV é frequentemente usado em vez de RGB. Em YUV, o componente luma (luminância), Y, é mantido separado das informações de cor. O formato YUV é mapeado mais de perto para a percepção visual humana. Felizmente, é fácil converter entre RGB e YUV.

Converter RGB para CMYK é um pouco mais complicado porque não existe uma fórmula de pixel um para um.

Outra alternativa ao usar esses esquemas de codificação exóticos é inventar seu próprio formato de arquivo de imagem. Simplesmente armazene as matrizes de pixels em um formato de arquivo personalizado com o sufixo “.yuv” ou “.cmyk”. Não há necessidade de um cabeçalho quando você sabe que tipo de formato de imagem os pixels terão, apenas vá em frente e leia em seu testbench.

Você sempre pode incorporar uma conversão de software em seu fluxo de design. Por exemplo, converta automaticamente uma imagem PNG para o formato BMP usando o software de conversão de imagem de linha de comando padrão antes do início da simulação. Em seguida, leia-o em seu testbench usando VHDL como você aprendeu neste artigo.

VHDL

  1. Holograma
  2. C# usando
  3. C Manipulação de Arquivos
  4. Classe de arquivo Java
  5. Como inicializar a RAM do arquivo usando TEXTIO
  6. Java BufferedReader:Como Ler Arquivo em Java com Exemplo
  7. Python JSON:codificar (despejar), decodificar (carregar) e ler arquivo JSON
  8. Operações de E/S de arquivo Verilog
  9. C - Arquivos de cabeçalho
  10. Câmera Plenoptica