亿迅智能制造网
工业4.0先进制造技术信息网站!
首页 | 制造技术 | 制造设备 | 工业物联网 | 工业材料 | 设备保养维修 | 工业编程 |
home  MfgRobots >> 亿迅智能制造网 >  >> Industrial programming >> VHDL

使用 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_typepixel_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_widthimage_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 指针。为了让它指向一个可写的内存空间,我们使用 newimage_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

  1. 全息图
  2. C# 使用
  3. C 文件处理
  4. Java 文件类
  5. 在 Linux 中使用数字签名进行数据完整性检查
  6. 使用 PSL 在 VHDL 中进行形式化验证
  7. 如何使用 TEXTIO 从文件初始化 RAM
  8. Java BufferedReader:如何通过示例在 Java 中读取文件
  9. Python JSON:编码(转储)、解码(加载)和读取 JSON 文件
  10. Verilog 文件 IO 操作
  11. C - 头文件
  12. 全光相机