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
- Holograma
- C# usando
- C Manipulação de Arquivos
- Classe de arquivo Java
- Como inicializar a RAM do arquivo usando TEXTIO
- Java BufferedReader:Como Ler Arquivo em Java com Exemplo
- Python JSON:codificar (despejar), decodificar (carregar) e ler arquivo JSON
- Operações de E/S de arquivo Verilog
- C - Arquivos de cabeçalho
- Câmera Plenoptica