Kean Walmsley


  • About the Author
    Kean on Google+

August 2014

Sun Mon Tue Wed Thu Fri Sat
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            








« Implementing a simple graphing tool inside AutoCAD using F# | Main | Parallelized pixelization inside AutoCAD using F# »

February 01, 2009

Importing and pixelizing images inside AutoCAD using F#

A friend and esteemed colleague asked - very validly - why I decided to use circles on a grid to display the results of a mathematical function in this last post, rather than using a linear object of some kind. Well I did, in fact, have a plan in mind... :-)

This post extends the concept, introduced in that post, of displaying data in a grid of solid-hatched circles. This post focuses on importing a bitmap image from a file, pixelizing the contents and using the "averaged" pixel colours to modify our grid. The idea actually came to me during an R.E.M. concert I attended at Paléo, a Swiss music festival, last summer. The real-time manipulation performed on the live video feed of the band - and especially of Michael Stipe, the lead singer - which was then projected onto screens adjacent to the stage was really, really cool. They managed to pixelize and manipulate the colours in a way that I found incredible - it was like seeing real-time graphic design at work. I understand that much of the work is done in advance, but even so I found it very impressive. Some of you regular concert-goers may find what I've just described to be pretty run-of-the-mill, but I fully admit that these days - what with one thing and another - I don't get out much. :-)

The pixelization approach I decided to take was to read in square chunks of the bitmap image and then average out the RGB values for all the pixels in each square. These average values are then used to colour the circles representing the "pixels" for their respective squares. You'll notice some inadvertent cropping of the imported image, which happens because we ask for the width in terms of our circular pixels and then sample the bitmap in chunks that have a whole number of pixels on each side: if the picture's width is not exactly divisible by the width entered there will be a little cropping.

Why did I choose F# for this rather than C#? The image processing domain in general is a strong fit for functional programming techniques. And while I haven't yet taken the step in this post, certain parts of the below code are inherently parallelizable, especially the operations related to averaging of pixel colours. The reading of the bitmap itself might prove more difficult, as it uses "unsafe" direct memory access (via the NativePtr class), but it's by no means impossible to parallelize, at least in theory.

Anyway, here's the F# code I put together:

// Use lightweight F# syntax


#light


// Declare a specific namespace and module name


module Pixelizor.Commands


// Import managed assemblies


#nowarn "9" // ... because we're using NativePtr


open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open Autodesk.AutoCAD.Colors

open System.Drawing.Imaging

open Microsoft.FSharp.NativeInterop


// Declare our command


[<CommandMethod("pix")>]

let pixelate() =


  // Let's get the usual helpful AutoCAD objects


  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database


  // "use" has the same effect as "using" in C#


  use tr =

    db.TransactionManager.StartTransaction()


  // Get appropriately-typed BlockTable and BTRs


  let bt =

    tr.GetObject

      (db.BlockTableId,OpenMode.ForRead)

    :?> BlockTable


  let ms =

    tr.GetObject

      (bt.[BlockTableRecord.ModelSpace],

      OpenMode.ForWrite)

    :?> BlockTableRecord


  // Function to create a filled circle (hatch) at a

  // specific location


  // Note the valid use of tr and ms, as they are in scope


  let createCircle pt rad =

    let hat = new Hatch()

    hat.SetDatabaseDefaults()

    hat.SetHatchPattern

      (HatchPatternType.PreDefined,

      "SOLID")


    let id = ms.AppendEntity(hat)

    tr.AddNewlyCreatedDBObject(hat, true)


    // Now we create the loop, which we make db-resident

    // (appending a transient loop caused problems, so

    // we're going to use the circle and then erase it)


    let cir = new Circle()

    cir.Radius <- rad

    cir.Center <- pt


    let lid = ms.AppendEntity(cir)

    tr.AddNewlyCreatedDBObject(cir, true)


    // Have the hatch use the loop we created


    let loops = new ObjectIdCollection()

    loops.Add(lid) |> ignore

    hat.AppendLoop(HatchLoopTypes.Default, loops)

    hat.EvaluateHatch(true)


    // Now we erase the loop


    cir.Erase()

    id


  // Function to create our grid of circles


  let createGrid xsize ysize rad offset =

    let ids = new ObjectIdCollection()

    for i = 0 to xsize - 1 do

      for j = 0 to ysize - 1 do

        let pt =

          new Point3d

            (offset * (Int32.to_float i),

            offset * (Int32.to_float j),

            0.0)

        let id = createCircle pt rad

        ids.Add(id) |> ignore

    ids


  // Function to change the colour of an entity


  let changeColour (col : Color) (id : ObjectId) =

    if id.IsValid then

      let ent =

        tr.GetObject(id, OpenMode.ForWrite) :?> Entity

      ent.Color <- col


  // Add up the RGB values of a list of pixels


  // We use a recursive function with an accumulator argument,

  // (rt, gt, bt), to allow tail call optimization


  let rec sumColors (pix : List<(byte*byte*byte)>) (rt,gt,bt) =

    match pix with

      | [] -> (rt, gt, bt)

      | (r, g, b) :: tl ->

          sumColors tl

            (rt + Byte.to_int r,

            gt + Byte.to_int g,

            bt + Byte.to_int b)


  // Average out the RGB values of a list of pixels


  let getAverageColour (pixels : List<(byte*byte*byte)>) =

    let (rsum, gsum, bsum) =

      sumColors pixels (0, 0, 0)

    let count = pixels.Length

    let ravg = Byte.of_int (rsum / count)

    let gavg = Byte.of_int (gsum / count)

    let bavg = Byte.of_int (bsum / count)


    // For some reason the pixel needs ro be reversed - probably

    // because of the bitmap format (needs investigation)


    Color.FromRgb(bavg, gavg, ravg)


  //  Get a chunk of pixels to average from one row


  // We use a recursive function with an accumulator argument

  // to allow tail call optimization


  let rec getChunkRowPixels p xsamp acc =

    if xsamp = 0 then

      acc

    else

      let pix =

        [(NativePtr.get p 0,

          NativePtr.get p 1,

          NativePtr.get p 2)]

      let p = NativePtr.add p 3

      getChunkRowPixels p (xsamp-1) (pix @ acc)


  // Get a chunk of pixels to average from multiple rows


  // We use a recursive function with an accumulator argument

  // to allow tail call optimization


  let rec getChunkPixels p stride xsamp ysamp acc =

    if ysamp = 0 then

      acc

    else

      let pix = getChunkRowPixels p xsamp []

      let p = NativePtr.add p stride

      getChunkPixels p stride xsamp (ysamp-1) (pix @ acc)


  // Get the various chunks of pixels to average across

  // a complete bitmap image


  let pixelateBitmap (image:System.Drawing.Bitmap) xsize ysize =


    // Create a 2-dimensional array of pixel lists (one list,

    // which then needs averaging, per final pixel)


    let (arr2 : List<(byte*byte*byte)>[,]) =

      Array2.create xsize ysize []


    // Lock the entire memory block related to our image


    let bd =

      image.LockBits

        (System.Drawing.Rectangle

          (0, 0, image.Width ,image.Height),

        ImageLockMode.ReadOnly, image.PixelFormat)


    // Establish the number of pixels to sample per chunk

    // in each of the x and y directions


    let xsamp = image.Width / xsize

    let ysamp = image.Height / ysize


    // We have a mutable pointer to step through the image


    let mutable (p:nativeptr<byte>) =

      NativePtr.of_nativeint (bd.Scan0)


    // Loop through the various chunks


    for i = 0 to ysize - 1 do


      // We take a copy of the current value of p, as we

      // don't want to mutate p while extracting the pixels

      // within a row


      let mutable xp = p

      for j = 0 to xsize - 1 do


        // Get the square chunk of pixels starting at

        // this x,y position


        let chk =

          getChunkPixels xp bd.Stride xsamp ysamp []


        // Add it into our array


        arr2.[j,ysize-1-i] <- chk


        // Mutate the pointer to move along to the right

        // by a value of 3 (our RGB value) times the

        // number of pixels we're sampling in x


        xp <- NativePtr.add xp (xsamp * 3)

      done


      // Mutate the original p pointer to move on one row


      p <- NativePtr.add p (bd.Stride * ysamp)

    done


    // Finally unlock the bitmap data and return the array


    image.UnlockBits(bd)

    arr2


  // Prompt the user for the file and the width of the image


  let pofo =

    new PromptOpenFileOptions

      ("Select an image to import and pixelate")

  pofo.Filter <-

    "Jpeg Image (*.jpg)|*.jpg|All files (*.*)|*.*"

  let pfnr = ed.GetFileNameForOpen(pofo)


  let file =

    match pfnr.Status with

    | PromptStatus.OK ->

        pfnr.StringResult

    | _ ->

        ""


  if System.IO.File.Exists(file) then


    let img = System.Drawing.Image.FromFile(file)


    let pio =

      new PromptIntegerOptions

        ("\nEnter number of horizontal pixels: " )

    pio.AllowNone <- true

    pio.UseDefaultValue <- true

    pio.LowerLimit <- 1

    pio.UpperLimit <- img.Width

    pio.DefaultValue <- 100


    let pir = ed.GetInteger(pio)

    let xsize =

      match pir.Status with

        | PromptStatus.None ->

            img.Width

        | PromptStatus.OK ->

            pir.Value

        | _ -> -1


    if xsize > 0 then


      // Calculate the vertical size from the horizontal


      let ysize = img.Height * xsize / img.Width


      if ysize > 0 then


        // Create our basic grid


        let ids = createGrid xsize ysize 0.5 1.2


        // Some helper functions using values we've just set...


        // From a certain index in the list, get an object ID


        let getId i =

          if i >= 0 then

            ids.[i]

          else

            ObjectId.Null


        // From a certain x and y in the grid, get an object ID


        let getId x y =

          getId ((x * ysize) + y)


        // Cast our image to a bitmap and then

        // get the chunked pixels


        let bmp = img :?> System.Drawing.Bitmap

        let arr = pixelateBitmap bmp xsize ysize


        // Loop through the pixel list and average them out

        // (which could be parallelized), using the results

        // to change the colour of the circles in our grid


        for x = 0 to xsize - 1 do

          for y = 0 to ysize - 1 do

            let lst = arr.[x,y]

            let col = getAverageColour lst

            let id = getId x y

            changeColour col id

          done

        done


  // Commit the transaction


  tr.Commit()

Here are the results of running it and choosing a photo I took yesterday during my visit to Kodaikanal in Tamil Nadu (India's most southern state).

First the original image:

Colourful truck on jack

Here's what happens when we run the PIX command, selecting the above image and choosing 20 pixels in width:

 Truck with 20 pixel width

Now with a width of 50...

Truck with 50 pixel width

And finally with a width of 100...

 Truck with 100 pixel width

Give it a try yourself, pixelizing different images at different resolutions - you can get some very cool results.

This one is too fun to just let rest... I'm going to see if I get some time this week to work on the parallelization of the colour averaging operation, to see if that improves performance (even if it doesn't today, it will do eventually when I either get a 64-core machine or move some of the code to be hosted in the cloud... :-)

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e2010536fe6709970b

Listed below are links to weblogs that reference Importing and pixelizing images inside AutoCAD using F#:

blog comments powered by Disqus

10 Random Posts