Saturday, April 3, 2010

A PGM Image Viewer in F#, with PGM writer and reader

This post is to serve the face recognition project.

In that project, we need a little tool to visualize/display Eigen faces. It would be very helpful during debugging/testing to display the Eigen faces and mean faces visually after they are calculated. We would also like to display an image to see if our IO part is correct or not.

The ORL face data set I use is of PGM format, so I need to load these images and display them.

First let’s get familiar with the PGM image format. PGM stands for Netpbm grayscale image. Its format is every easy:

P5 or P2
# 0 or more lines for comments
nrows ncolumns
max-value
image content

The format of image content part could be either text(P2) or binary(P5). More detail is here and here.


Before reading the file, let’s define the data structure for a PGM image. Because it is grey scale, thus it is actually an F# matrix:


type
pgm = matrix


We would use two readers: a BinaryReader for binary image content and a StreamReader for text image content. (I think we could also use StreamReader for both, But I started using BinaryReader for binary image content and added the text image content later, so two readers are used.)

exception
PgmFormatError of string
let readPgm path =
   let rec skipCommentLine br =
let l = getline br
if l.StartsWith("#") then
skipCommentLine br
else
l
let br = new BinaryReader(File.Open(path, FileMode.Open))
let pgmType = getline br
let sizeStr = skipCommentLine br
let s = sizeStr.Split ' '
let ncol = int (s.[0])
let nrow = int (s.[1])
printfn "nrow = %d ncol = %d" nrow ncol
let maxStr = getline br

if (pgmType.StartsWith("P5")) then
// binary format
let mutable b = Array.create (nrow * ncol) 0uy
let nread = br.Read(b, 0, (nrow*ncol))
let bb = b
Matrix.init nrow ncol (fun i j -> float (bb.[i*ncol+j]))
elif (pgmType.StartsWith("P2")) then
// text format
br.Close() // close the file and re-open
use sr = new StreamReader(path)
sr.ReadLine() |> ignore
let mutable line = sr.ReadLine()
while line.StartsWith("#") do
line <- sr.ReadLine()
sr.ReadLine() |> ignore

let str =
seq {
while not sr.EndOfStream do
yield
sr.ReadLine()
}
|> Seq.fold (fun a b -> a + b) ""

let pic = Matrix.create nrow ncol 0.0
let vals = str.Split([|' ';'\t';'\r';'\n'|], StringSplitOptions.RemoveEmptyEntries)
printfn "%A" vals
Matrix.init nrow ncol (fun i j -> float (vals.[i*ncol+j]))
else
// unknown format
raise (PgmFormatError(path))

The getline function for BinaryReader:

let
getline (br:BinaryReader) =
   let chSeq = seq {
let ch = ref (br.ReadChar())
while !ch <> '\n' do
yield
!ch
ch := br.ReadChar()
}

let chArr = chSeq |> Seq.toArray
let line = new String(chArr)

line
Next we write the writer for a pgm image, I only implemented the writer for the text content case as this is mainly used for outputting some intermediate results, thus need to be human-readable.

let writePgm (path:string) (p:pgm) =
use fs = new FileStream(path, FileMode.Create)
   use sw = new StreamWriter(fs, Encoding.ASCII)
sw.Write("P2\n{0} {1}\n255\n", p.NumCols, p.NumRows)

for i=0 to (p.NumRows - 1) do
for
j=0 to (p.NumCols - 1) do
sw.Write(" " + string (p.[i,j]))
sw.WriteLine()

Finally, the viewer. F# actually is quite good at doing small GUI programs. First inherits from a form:

type
PGMViewer(pic:matrix, name:string) as this =
   inherit Form(Width = 300, Height = 300)

then create a main menu, and menu items in it, a PictureBox to display bitmap images and add a SaveFileDialog:

let
mainmenu = new MainMenu()
let mFile = new MenuItem()
let miFileSave = new MenuItem()
let miExit = new MenuItem()
let pb = new PictureBox()
let filesave = new SaveFileDialog()
Set necessary properties of these objects (put these code in a do block)

do
// set the property of picture box
let nrow = pic.NumRows
let ncol = pic.NumCols
let image = new Bitmap(ncol, nrow)
printfn "%A %A" nrow ncol
for i=0 to nrow-1 do
for
j=0 to ncol-1 do
image.SetPixel(j, i, Color.FromArgb(int pic.[i,j], int pic.[i,j], int pic.[i,j]))
pb.Image <- image
pb.Height <- nrow
pb.Width <- ncol

// add picture box
this.Controls.Add(pb)

// set memu property
miFileSave.Index <- 0
miFileSave.Text <- "&Save"
miExit.Index <- 1
miExit.Text <- "&Exit"
mFile.Index <- 0
mFile.Text <- "&File"
mFile.MenuItems.Add(miFileSave) |> ignore
mFile.MenuItems.Add(miExit) |> ignore
mainmenu.MenuItems.Add(mFile) |> ignore
// add menu
this.Menu <- mainmenu
And add event handler for click event of the save menu item:
// savefile dialog
filesave.DefaultExt <- "pgm"
filesave.Filter <- "PGM Files|*.pgm"
let filesaveClick e =
if pb.Image = null then
()
else
if
filesave.ShowDialog() = DialogResult.OK then
let
bm = pb.Image :?> Bitmap
let pic = Matrix.init (bm.Height) (bm.Width) (
fun i j ->
let
c = bm.GetPixel(j,i)
float c.R * 0.3 + float c.G * 0.59 + float c.B * 0.11
)

writePgm filesave.FileName pic
miFileSave.Click.Add(filesaveClick)
Put them together:

open System.Windows.Forms
open System.Drawing

type PGMViewer(pic:matrix, name:string) as this =
inherit Form(Width = 300, Height = 300)

let mainmenu = new MainMenu()
let mFile = new MenuItem()
let miFileSave = new MenuItem()
let miExit = new MenuItem()
let pb = new PictureBox()
let filesave = new SaveFileDialog()

do
// set the property of picture box
let nrow = pic.NumRows
let ncol = pic.NumCols
let image = new Bitmap(ncol, nrow)
printfn "%A %A" nrow ncol
for i=0 to nrow-1 do
for
j=0 to ncol-1 do
image.SetPixel(j, i, Color.FromArgb(int pic.[i,j], int pic.[i,j], int pic.[i,j]))
pb.Image <- image
pb.Height <- nrow
pb.Width <- ncol

// add picture box
this.Controls.Add(pb)

// set memu property
miFileSave.Index <- 0
miFileSave.Text <- "&Save"
miExit.Index <- 1
miExit.Text <- "&Exit"
mFile.Index <- 0
mFile.Text <- "&File"
mFile.MenuItems.Add(miFileSave) |> ignore
mFile.MenuItems.Add(miExit) |> ignore
mainmenu.MenuItems.Add(mFile) |> ignore
// add menu
this.Menu <- mainmenu

// savefile dialog
filesave.DefaultExt <- "pgm"
filesave.Filter <- "PGM Files|*.pgm"
let filesaveClick e =
if pb.Image = null then
()
else
if
filesave.ShowDialog() = DialogResult.OK then
let
bm = pb.Image :?> Bitmap
let pic = Matrix.init (bm.Height) (bm.Width) ( fun i j ->
let
c = bm.GetPixel(j,i)
float c.R * 0.3 + float c.G * 0.59 + float c.B * 0.11
)

writePgm filesave.FileName pic
miFileSave.Click.Add(filesaveClick)


// set title of the dialog
this.Text <- "PGMViewer" + " - " + name

And let’s test the viewer now:

(new PGMViewer(readPgm @"C:\temp\temp4.pgm")).Show()

pgmviewer

(Copyright: this face is from the ORL face database.)

Attachment

wait.

No comments:

Post a Comment