Category Archives: image processing

Reading image files with VHDL part 3

Having written a function to read a PGM image, how do we use it?

Well, the testbench at the bottom of the pgm vhdl file has an example:

variable i : pixel\_array\_ptr;  
-- Without the transpose function, we would have to present the initialisation data in a non-intuitive way.  
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))  
);  

Being by setting up a variable to store the image in. We also create a constant which contains the data we expect to read from the file. As explained previously we have to transpose the array in order for it to “look as expected” in the code (which is more important than an extra function call to my mind)

Then we can test and check:

-- 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);  

That simple!

Code for this series can be found on github

Reading image files with VHDL part 2

Having set up a library for reading images, let’s now go on to read an image in!

pgm_read Recall, we have a function:

impure function pgm_read (filename : string)
return pixel_array_ptr;
 So, some declarations - fairly self-explanatory: 

file pgmfile : text;
variable width, height : coordinate; -- storage for image dimensions
variable l : line; -- buffer for a line of text
variable s : string(1 to 2); -- to check the P2 header
variable ints : integer_vector(1 to 3); -- store the first three integers (width, height and depth)
variable int : integer; -- temporary storage
variable ch : character; -- temporary storage
variable good : boolean; -- to record whether a read is successful or not
variable count : positive; -- keep track of how many numbers we've read
variable empty_image : pixel_array_ptr := null; -- return this on error
variable ret : pixel_array_ptr; -- actual return value
variable x, y : coordinate; -- coordinate tracking
 We begin by opening the file in read mode and reading the first line: 

-- setup some defaults
width := 0;
height := 0;
file_open(pgmfile, filename, read_mode);
readline(pgmfile, l);
 Now, reading the header.. The P2 format PGM header is very simple: 

P2
# optional comments - next two ints are
# xsize and ysize
128 90
# maybe some more comments
# next int is the max value for pixel
255
 All the integers are separated by whitespace (may be space characters or linefeeds or carriage returns, we know not). This suits VHDL, as the 

textio library can read whitespace separated integers from a text file! It’s slightly tricky as we have to manage the cases where the values appear on separate lines. First, check the “P2-ness”:
read(l, s(1));
read(l, s(2), good);
if not good or s /= “P2” then
report “PGM file ‘”&filename&”‘ not P2 type” severity warning;
file_close(pgmfile);
return empty_image;
end if;
VHDL’s

textio is a bit different from in most other languages – we read a line from the file then read values from the line buffer. So, we read the first two characters and check them against “P2”. Now to read the next three integers:
allints : loop — read until we have 3 integers (width, height and colour depth).
line_reading:loop
readline(pgmfile, l);
exit when l.all(1) = ‘#’; — skip comments;
if l’length = 0 then
report “EOF reached in pgmfile before opening integers found”
severity warning;
file_close(pgmfile);
return empty_image;
end if;
number_reading: loop
read(l, ints(count), good);
exit number_reading when not good; — need to read some more from file
count := count + 1;
exit allints when count > ints’high;
end loop;
end loop;
exit when count > ints’high;
end loop;
— Now we have our header sorted. store it
width := ints(1);
height := ints(2);
We have three loops – the outermost

allints loop runs until count increments beyond the end of the ints array. The line_reading loop reads a line, checks for comment lines to skip them, passes non-comment lines onto the next loop: number_reading – this reads numbers from the line buffer until it fails or has filled the ints array. In the former case, the line_reading loop provides more data, otherwise, we drop out and store our data. Once we have the width and height we can allocate an array and read the pixels:
— now read the image pixels
x := 0;
y := 0;
— allocate storage
ret := new pixel_array(0 to width-1, 0 to height-1);
allpixels : loop
readline(pgmfile, l);
exit when l = null; — oh dear, something went wrong!
exit when l’length = 0; — more wrongness!
numbers: loop
read(l, int, good);
exit numbers when not good;
ret(x, y) := int;
exit allpixels when x = width-1 and y = height-1;
x := x + 1;
if x >= width then
x := 0;
y := y + 1;
end if;
end loop numbers;
end loop allpixels;
Again a nested loop structure, one for reading the lines from the file and another for extracting the integers from the linebuffer. The x and y coordinates are updated on each integer read to “scan” across and down the image. Once

x and y reach their terminal values, the loop exits.
assert (x = width-1 and y = height-1)
report “Don’t seem to have read all the pixels I should have”
severity warning;
return ret;
Finally, a little error checking and warning, and return the data.

Onwards, to making use of it… Code for this series can be found on github

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));
    

API

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;

Testing

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;

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…