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;
- 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
accesstype for the pixel storage being returned from the image reading function
pgm_readto allow us to dynamically allocate the storage based on how big the actual image is, rather than having to know at compile-time.
transposefunction 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
transposefunction 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
a function to write an
imagerecord 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; wait; end process test1; end architecture test;
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…