Reading image files with VHDL part 1 (again)

Data storage

One of the things VHDL can do (contrary to popular belief :)) is 2-D arrays. So reading images into a 2-D array is a very natural way to store the data. We’ll create a package called pgm to keep all our image reading and writing code together:

package pgm is
  subtype coordinate : natural;
  subtype pixel is integer range 0 to 255;
  type pixel_array is array (coordinate range <>, coordinate range <>) of pixel;
  type pixel_array_ptr is access pixel_array;
  -- Function: transpose
  -- Useful for initialising arrays such that the first coordinate is the x-coord, but the initialisation can "look" like the
  -- image in question within the code
  function transpose (i : pixel_array) return pixel_array;

Some points:

  • We’ve got a subtype for the raw pixel type – this enables us to change a single place if we decide to extend to (say) 16-bit pixels.
  • Images are stored in a 2d array, with the x-coordinate as the first dimension, as is traditional (unless you use Matlab!)
  • We’ll use an access type for the pixel storage being returned from the image reading function pgm_read to allow us to dynamically allocate the storage based on how big the actual image is, rather than having to know at compile-time.
  • The transpose function is the best way I can come up with so far that allows us to create constants which represent images where the code looks like the image it represents. Without this an initialiser for a 4×2 image would look like this:

    constant c : pixel_array(0 to 3, 0 to 1) := ( (11, 12), (21, 22), (31, 32), (41, 42));

    A bit counter-intuitive, no? With the transpose function we can do:

    constant c : pixel_array(0 to 3, 0 to 1) := transpose(pixel_array'(
    (11, 21, 31, 41),(12, 22, 32, 42));


The public API for playing with the images will be simple:

  • a function to read an image from a file into an image record
  • a function to write an image record out to a file

    impure function pgm_read (filename : string) return pixel_array_ptr;
    procedure pgm_write (filename : in string; i : in pixel_array);
    end package pgm;


For the input side, a simple testbench is all that is required, along with a carefully crafted PGM file for which we “know the right answers”. We can also create an image from scratch and write the new image out. Checking that final image will have to be done by hand. We could “round-trip” the image back in through our image reading function, but this may mask any errors we’ve made in the reading and writing functions which happen to cancel each other out!

entity tb_pgm is  
end entity tb_pgm;

use work.libv.all;  
use work.pgm.all;  
architecture test of tb_pgm is  
begin -- architecture test  
  test1 : process is  
    variable i : pixel_array_ptr;  
    constant testdata : pixel_array(0 to 7, 0 to 3) := transpose(pixel_array'(  
(000, 027, 062, 095, 130, 163, 198, 232),  
(000, 000, 000, 000, 000, 000, 000, 000),  
(255, 255, 255, 255, 255, 255, 255, 255),  
(100, 100, 100, 100, 100, 255, 255, 255))  
    variable blacksquare : pixel_array(0 to 7, 0 to 7) := (others => (others => 0));  
  begin -- process test1  
    -- test on a proper image  
    i := pgm_read("testimage_ascii.pgm");  
    assert i /= null report "pixels are null" severity error;  
    assert_equal("ASCII PGM Width", i.all'length(1), 8);  
    assert_equal("ASCII PGM Height", i.all'length(2), 4);  
    assert_equal("ASCII PGM data", i.all, testdata);

    -- make sure we return a non-image for the binary-style PGM file
    i := pgm_read("testimage.pgm");
    assert i = null report "Binary pixels should be null" severity error;

    -- Now create an image from scratch - a letter M
    blacksquare(1,1) := 255; blacksquare(5,1) := 255;
    blacksquare(1,2) := 255; blacksquare(2,2) := 255; blacksquare(4,2) := 255; blacksquare(5,2) := 255;
    blacksquare(1,3) := 255; blacksquare(3,3) := 255; blacksquare(5,3) := 255;
    blacksquare(1,4) := 255; blacksquare(5,4) := 255;
    blacksquare(1,5) := 255; blacksquare(5,5) := 255;
    blacksquare(1,6) := 255; blacksquare(5,6) := 255;
    pgm_write("test_write.pgm", blacksquare);
    report "End of tests" severity note;
  end process test1;
end architecture test;

Aside (libv)

You can read about the non-standard looking assert_equal functions here.

Back to the tests

In the spirit of Test-Driven Development, all the API elements are left empty, except for the pgm_read function which has to return something. So it returns NULL – an empty image! With the results:

pgm.vhd:204:9:@0ms:(assertion error): pixels are null  

Hurrah! It fails in the way we’d hope!

Onwards to actually reading and writing…

Leave a Reply

Your email address will not be published. Required fields are marked *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    Markdown is turned off in code blocks:
     [This is not a link](

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see