使用 TEXTIO 读取 BMP 文件位图图像
将图像文件转换为位图格式是使用 VHDL 读取图片的最简单方法。 Microsoft Windows 操作系统中内置了对 BMP 光栅图形图像文件格式的支持。这使得 BMP 成为一种合适的图像格式,用于存储用于 VHDL 测试平台的照片。
在本文中,您将学习如何读取像 BMP 这样的二进制图像文件并将数据存储在模拟器的动态内存中。我们将使用一个示例图像处理模块将图像转换为灰度,这将是我们的被测设备 (DUT)。最后,我们将 DUT 的输出写入一个新图像,我们可以在视觉上与原始图像进行比较。
这篇博文是关于在 VHDL 中使用 TEXTIO 库的系列文章的一部分。在此处阅读其他文章:
如何使用 TEXTIO 从文件初始化 RAM
使用 TEXTIO 在测试台中读取刺激文件
为什么位图是 VHDL 的最佳格式
互联网上最常见的图像文件格式是 JPEG 和 PNG。它们都使用压缩,JPEG是有损的,而PNG是无损的。大多数格式都提供某种形式的压缩,因为这可以显着减少图像的存储大小。虽然这对于正常使用来说很好,但对于在 VHDL 测试平台中读取来说并不理想。
为了能够在软件或硬件中处理图像,您需要访问应用程序中的原始像素数据。您希望将颜色和亮度数据存储在字节矩阵中,这称为位图或光栅图形。
大多数著名的图像编辑器(例如 Photoshop 或 GIMP)都是基于光栅的。它们可以打开多种图像格式,但它们都在编辑器内部转换为光栅图形。
您也可以在 VHDL 中执行此操作,但这需要大量的编码工作,因为没有任何现成的解决方案可用于解码压缩图像。更好的解决方案是手动将测试输入图像转换为像 BMP 这样的位图格式,或者将其合并到启动测试平台的脚本中。
BMP 图像文件格式
BMP 文件格式在 Wikipedia 上有详细记录。这种格式有许多不同的变体,但我们将就一些特定设置达成一致,这将使我们更容易。为了创建我们的输入图像,我们在 Windows 预装的 Microsoft Paint 中打开它们。然后,我们点击文件→另存为 , 选择另存为类型:24-bit Bitmap (*bmp; *.dib) .将文件命名为以 .bmp 后缀结尾的名称,然后单击保存。
通过确保像这样创建文件,我们可以假设标题始终是 54 字节长的 BITMAPINFOHEADER 变体,像素格式为 RGB24,在维基百科页面上提到。此外,我们只关心标题中的几个选定字段。下表显示了我们将要读取的标题字段。
偏移量(十二月) | 尺寸(B) | 预期(十六进制) | 说明 |
---|---|---|---|
0 | 2 | “BM”(42 4D) | ID 字段 |
10 | 4 | 54 (36 00 00 00) | 像素数组偏移量 |
14 | 4 | 40 (28 00 00 00) | 标题大小 |
18 | 4 | 读取值 | 图像宽度(以像素为单位) |
22 | 4 | 读取值 | 图像高度(以像素为单位) |
26 | 1 | 1 (01) | 彩色平面数 |
28 | 1 | 24 (18) | 每个像素的位数 |
标记为绿色的值是我们真正需要查看的唯一值,因为我们知道在其他标头字段中期望哪些值。如果您同意每次只使用预定义的固定尺寸的图像,您可以跳过整个标题并从 BMP 文件中的字节偏移数 54 开始读取,这就是像素数据所在的位置。
尽管如此,我们将检查其他列出的值是否符合预期。这并不难,因为我们已经在阅读标题了。如果您或您的同事将来随时向测试平台提供错误编码的图像,它还可以防止用户错误。
测试用例
这篇博客文章是关于如何在 VHDL 测试平台中从文件中读取图像,但为了完整起见,我提供了一个示例 DUT。在读取图像时,我们将通过 DUT 流式传输像素数据。最后,我们将结果写入另一个输出 BMP 文件,该文件可以在您喜欢的图片查看器中查看。
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;
上面的代码显示了我们 DUT 的实体。灰度模块将一个像素的 24 位 RGB 数据作为输入,并将其转换为在输出上呈现的灰度表示。请注意,输出像素表示仍在 RGB 颜色空间内的灰色阴影,我们不会将 BMP 转换为不同格式的灰度 BMP。
该模块是纯组合的,没有时钟或复位输入。当某些东西被分配给输入时,结果会立即出现在输出上。为简单起见,根据 ITU-R BT.2100 RGB 到亮度编码系统,向灰度的转换使用亮度(亮度)值的定点近似值。
您可以使用下面的表格下载灰度模块和整个项目的代码。
您在下面看到的波音 747 的图片将是我们的示例输入图像。也就是说,这不是嵌入在这篇博文中的实际 BMP 图像,这是不可能的。这是我们将在测试平台中读取的 BMP 图像的 JPEG 表示。您可以通过在上面的表格中留下您的电子邮件地址来索取原始 BMP 图片,您将立即在收件箱中收到它。

测试图像大小为 1000 x 1000 像素。不过,本文中提供的代码应该适用于任何图像尺寸,只要它是 BITMAPINFOHEADER 24 位 BMP 格式。但是,读取大图像将花费大量仿真时间,因为大多数 VHDL 仿真器中的文件访问速度很慢。此图片大小为 2930 kB,在 ModelSim 中加载需要几秒钟。
导入 TEXTIO 库
要在 VHDL 中读取或写入文件,您需要导入 TEXTIO 库。确保在 VHDL 文件的顶部包含以下列表中的行。我们还需要导入 finish
标准包中的关键字,用于在所有测试完成后停止模拟。
use std.textio.all; use std.env.finish;
以上语句需要使用 VHDL-2008 或更新版本。
自定义类型声明
我们将在测试平台的声明区域的开头声明一些自定义类型。用于存储像素数据的数据结构的格式取决于 DUT 期望的输入类型。灰度模块需要三个字节,每个字节代表红色、绿色和蓝色中的一种颜色分量。因为它一次只对一个像素进行操作,所以我们可以随意存储像素集。
从下面的代码可以看出,我们首先声明了一个header_type
我们将使用该数组来存储所有标题数据。我们将检查标题中的一些字段,但我们还需要存储它,因为我们将在测试平台结束时将处理后的图像数据写入一个新文件。然后,我们需要在输出图像中包含原始标题。
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;
第二条语句声明了一条名为 pixel_type
的记录 .此自定义类型将充当一个像素的 RGB 数据的容器。
最后,声明了用于存储所有像素的动态数据结构。而 row_type
是 pixel_type
的无约束数组 , row_pointer
是一个访问类型,一个 VHDL 指针。同样,我们构造一个无约束的 image_type
用于存储所有像素行的数组。
因此,image_pointer
type 将用作动态分配内存中完整图像的句柄。
实例化 DUT
在声明区域的末尾,我们声明了 DUT 的接口信号,如下所示。输入信号以 _in
为后缀 以及带有 _out
的输出信号 .这使我们能够在代码和波形中轻松识别它们。 DUT 在架构开始时通过端口映射分配的信号进行实例化。
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 );
进程变量和文件句柄
我们将创建一个单独的测试平台进程来包含所有文件的读取和写入。该过程的声明区域如下所示。我们首先声明一个新的 char_file
type 定义我们希望从输入图像文件中读取的数据类型。 BMP文件是二进制编码的;因此我们要对字节进行操作,即 character
输入VHDL。在接下来的两行中,我们使用类型来打开一个输入和一个输出文件。
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
接下来,我们声明一个变量来包含标题数据,以及两个用于保存图像宽度和高度的整数变量。之后,我们声明一个 row
指针和一个 image
指针。后一个将是我们从文件中读取完整图像后的句柄。
最后,我们声明了两个便利变量; padding
integer
类型 和 char
character
类型 .我们将使用这些临时存储从文件中读取的值。
读取 BMP 标头
在流程体开始时,我们将 BMP 文件中的整个 header 读入 header
变量,如下面的代码所示。标头长度为 54 字节,但我们没有使用硬编码值,而是通过引用 header_type'range
来获取要迭代的范围 属性。你应该尽可能地使用属性来尽可能少地定义常量值。
for i in header_type'range loop read(bmp_file, header(i)); end loop;
然后是一些断言语句,我们在其中检查某些标头字段是否符合预期。这是针对用户错误的安全措施,因为我们不会将读取的值用于任何事情,我们只是检查它们是否符合预期。预期值是该表中列出的值,如本文前面所示。
下面的代码显示了断言语句,每个语句都有一个 report
描述错误的语句和 severity failure
如果断言的表达式是 false
则停止模拟的语句 .我们需要使用提高的严重性级别,因为至少在 ModelSim 中使用默认设置时,它只会打印一条错误消息并继续仿真。
-- 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;
然后我们从标题中读取图像宽度和高度字段。这是我们实际要使用的仅有的两个值。因此,我们将它们分配给 image_width
和 image_height
变量。从下面的代码中我们可以看出,我们必须将后续字节乘以两个值的加权幂,才能将四字节的头部字段转换为适当的整数值。
-- 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);
最后,我们使用 report
将读取的高度和宽度打印到模拟器控制台 声明。
读取像素数据
在开始读取像素数据之前,我们需要找出每行有多少字节的填充。 BMP 格式要求将每行像素填充为四个字节的倍数。在下面的代码中,我们使用图像宽度上的模运算符使用单线公式来处理这个问题。
-- Number of bytes needed to pad each row to 32 bits padding := (4 - image_width*3 mod 4) mod 4;
我们还必须为要读取的所有像素数据行保留空间。 image
variable 是一个访问类型,一个 VHDL 指针。为了让它指向一个可写的内存空间,我们使用 new
为 image_height
保留空间的关键字 动态内存中的行数,如下图。
-- Create a new image type in dynamic memory image := new image_type(0 to image_height - 1);
现在是时候读取图像数据了。下面的清单显示了逐行读取像素数组的 for 循环。对于每一行,我们为新的 row_type
保留空间 row
指向的对象 多变的。然后,我们读取预期的像素数,首先是蓝色,然后是绿色,最后是红色。这是按照 24 位 BMP 标准的排序。
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;
在读取每一行的有效负载后,我们读取并丢弃额外的填充字节(如果有的话)。最后,在循环结束时,我们将新的动态像素行分配给 image
的正确槽 大批。当 for 循环终止 image
变量应该包含整个 BMP 图像的像素数据。
测试 DUT
灰度模块仅使用组合逻辑,因此我们无需担心任何时钟或复位信号。下面的代码在将 RGB 值写入 DUT 输入时遍历每一行中的每个像素。分配输入值后,我们等待 10 纳秒,让 DUT 内的所有增量周期延迟解除。任何大于 0 的时间值都可以使用,甚至 wait for 0 ns;
重复了足够的次数。
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;
当程序退出等待语句时,DUT 输出应包含该像素灰度颜色的 RGB 值。在循环结束时,我们让 DUT 输出替换我们从输入 BMP 文件中读取的像素值。
编写输出 BMP 文件
此时,image
中的所有像素 变量应该由 DUT 操纵。是时候将图像数据写入out_file
了 对象,它指向一个名为“out.bmp”的本地文件。在下面的代码中,我们遍历了从输入 BMP 文件中存储的头字节中的每个像素,并将它们写入输出文件。
for i in header_type'range loop write(out_file, header(i)); end loop;
在标头之后,我们需要按照从输入文件中读取像素的顺序写入像素。下面清单中的两个嵌套 for 循环负责处理这个问题。请注意,在每一行之后,我们使用 deallocate
关键字为每一行释放动态分配的内存。垃圾收集仅包含在 VHDL-2019 中,在以前的 VHDL 版本中,如果省略此行,您可能会遇到内存泄漏。在 for 循环结束时,如果需要,我们会写入填充字节以使行长度为 4 字节的倍数。
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;
循环终止后,我们为 image
释放内存空间 变量,如下图。然后我们通过调用 file_close
关闭文件 在文件句柄上。这在大多数模拟器中并不是绝对必要的,因为当子程序或进程终止时文件会隐式关闭。不过,完成文件后关闭文件永远不会出错。
deallocate(image); file_close(bmp_file); file_close(out_file); report "Simulation done. Check ""out.bmp"" image."; finish; end process;
在测试台过程结束时,我们向 ModelSim 控制台打印一条消息,表明模拟结束,并提示可以在哪里找到输出图像。 finish
关键字需要 VHDL-2008,这是一种在所有测试完成后停止模拟器的优雅方式。
输出的 BMP 图片
下图显示了测试平台完成后“out.bmp”文件的样子。这篇博文中显示的实际文件是 JPEG,因为 BMP 不适合嵌入网页,但您可以在上面的表格中留下您的电子邮件地址,以获取包含“boeing.bmp”文件的完整项目的 zip。

最后的话
对于 FPGA 中的图像处理,通常使用 YUV 颜色编码方案来代替 RGB。在 YUV 中,亮度(亮度)分量 Y 与颜色信息分开。 YUV 格式更接近于人类视觉感知。幸运的是,RGB 和 YUV 之间的转换很容易。
将 RGB 转换为 CMYK 有点复杂,因为没有一对一的像素公式。
使用这种奇特编码方案的另一种选择是发明自己的图像文件格式。只需将像素数组存储为以“.yuv”或“.cmyk”为后缀的自定义文件格式。当您知道像素将具有哪种图像格式时,无需标题,只需在您的测试平台中阅读即可。
您始终可以将软件转换合并到您的设计流程中。例如,在模拟开始之前,使用标准的命令行图像转换软件将 PNG 图像自动转换为 BMP 格式。然后,按照您从本文中学到的方法,使用 VHDL 在您的测试平台中阅读它。
VHDL