Pixelating An Image With ColdFusion

Posted January 7, 2010 at 10:02 AM by Ben Nadel

Tags: ColdFusion

Last night, I was building some internal company tools for prototyping when I needed to access the color of an area of a given image (in order to programmatically set a background color). While ColdFusion does not make this information available directly, it does provide a way to access the underlying buffered image of the ColdFusion image object. The buffered image is the collection of pixel data that represents the current image. Once we have access to the actual pixel data, we can really start to monkey around with the image.

For fun, I thought I would try to create a "pixelation" effect in which you can take a ColdFusion image and turn it into a series of larger pixels (abstractly speaking - in practice, pixels are still only 1x1 squares). The strategy behind this effect is one of averaging: to create a "single" pixel out of a 20x20 area of the image, I have to get all of the pixel values in that 20x20 area and then average their colors together.

The one caveat here is that the buffered image returns pixel color values as a single integer. When we average the colors together, we can't just average this value since it's not a "true" number; rather, it's a collection of bits that represent the Red, Green, Blue (and sometimes Alpha) channels of the given pixel color. As such, we need to extract and average each channel individually, and then merge them back together to create another single-integer representation.

I have taken this algorithm and wrapped it up in the following function, imagePixelate():

  • <cffunction
  • name="imagePixelate"
  • access="public"
  • returntype="void"
  • output="false"
  • hint="I take a ColdFusion image object and pixelate it with the given pixel size. NOTE: To keep inline with the exisitng image functions, this will work directly on the existing image (rather than creating a new image).">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="image"
  • type="any"
  • required="true"
  • hint="I am the image being pixelated."
  • />
  •  
  • <cfargument
  • name="pixelSize"
  • type="numeric"
  • required="false"
  • default="20"
  • hint="I am size of the pixels to use in the effect."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var local = {} />
  •  
  • <!---
  • Get the underlying buffered image. This is going to give
  • us access to the individual pixels that make up the image.
  • --->
  • <cfset local.bufferedImage = imageGetBufferedImage(
  • arguments.image
  • ) />
  •  
  • <!---
  • Store the image dimensions - these will be referenced a
  • lot, so let's create a short hand.
  • --->
  • <cfset local.imageWidth = imageGetWidth( arguments.image ) />
  • <cfset local.imageHeight = imageGetHeight( arguments.image ) />
  •  
  • <!---
  • Loop over the image, examining a pixel chunk at a time.
  • Each chunk will be the NxN pixel area size.
  • --->
  • <cfloop
  • index="local.x"
  • from="0"
  • to="#(local.imageWidth - 1)#"
  • step="#arguments.pixelSize#">
  •  
  • <cfloop
  • index="local.y"
  • from="0"
  • to="#(local.imageHeight - 1)#"
  • step="#arguments.pixelSize#">
  •  
  •  
  • <!---
  • When grabbing the given area of pixels, we have
  • to be sure not to go out of bounds on the image.
  • As such, we cannot rely on the pixelSize to be
  • available (this becomes likely only the last
  • row and / or column).
  • --->
  • <cfset local.pixelWidth = min(
  • arguments.pixelSize,
  • (local.imageWidth - local.x)
  • ) />
  •  
  • <cfset local.pixelHeight = min(
  • arguments.pixelSize,
  • (local.imageHeight - local.y)
  • ) />
  •  
  • <!---
  • Grab the collection of pixels from the
  • underlying buffered image. This will return a
  • single-dimension array of pixel values.
  • --->
  • <cfset local.pixelBuffer = local.bufferedImage.getRGB(
  • javaCast( "int", local.x ),
  • javaCast( "int", local.y ),
  • javaCast( "int", local.pixelWidth ),
  • javaCast( "int", local.pixelHeight ),
  • javaCast( "null", "" ),
  • javaCast( "int", 0 ),
  • javaCast( "int", local.pixelWidth )
  • ) />
  •  
  • <!---
  • Now that we have the colors, we want to average
  • them together. However, we can't simply use the
  • integer to average; rather, we have to pick out
  • the individual R,G,B channels and average them
  • seperately.
  • --->
  • <cfset local.red = 0 />
  • <cfset local.green = 0 />
  • <cfset local.blue = 0 />
  •  
  • <!---
  • Loop over each pixel color value in our current
  • pixel buffer.
  • --->
  • <cfloop
  • index="local.pixelColor"
  • array="#local.pixelBuffer#">
  •  
  • <!---
  • Using Bit-wise SHIFTing and ANDing, get
  • each of the color channels and add it to
  • the running sum.
  •  
  • NOTE: I am not taking into account the ALPHA
  • channel which would require another 24 bit
  • right-shift.
  • --->
  •  
  • <!--- Red. --->
  • <cfset local.red += bitAnd(
  • bitSHRN( local.pixelColor, 16 ),
  • 255
  • ) />
  •  
  • <!--- Green. --->
  • <cfset local.green += bitAnd(
  • bitSHRN( local.pixelColor, 8 ),
  • 255
  • ) />
  •  
  • <!--- Blue. --->
  • <cfset local.blue += bitAnd(
  • local.pixelColor,
  • 255
  • ) />
  •  
  • </cfloop>
  •  
  • <!---
  • Now that we have summed the R,G,B channels, we can
  • average them together.
  • --->
  •  
  • <!--- Red. --->
  • <cfset local.redAverage = fix(
  • local.red / arrayLen( local.pixelBuffer )
  • ) />
  •  
  • <!--- Green. --->
  • <cfset local.greenAverage = fix(
  • local.green / arrayLen( local.pixelBuffer )
  • ) />
  •  
  • <!--- Blue. --->
  • <cfset local.blueAverage = fix(
  • local.blue / arrayLen( local.pixelBuffer )
  • ) />
  •  
  • <!---
  • Now that we have averaged the individual colors,
  • we need to merge them together to create a full,
  • RGB integer value.
  •  
  • NOTE: Again, I am not taking the ALPHA channel
  • into account.
  • --->
  • <cfset local.pixelAverage = bitOr(
  • bitOr(
  • bitSHLN( local.redAverage, 16 ),
  • bitSHLN( local.greenAverage, 8 )
  • ),
  • local.blueAverage
  • ) />
  •  
  • <!---
  • Now that we have the average, we want to
  • overwrite every array value in the pixel buffer
  • with the given, average color. However, since
  • ColdFusion doesn't play nicely with true Java
  • arrays, we'll create our own arrray.
  • --->
  • <cfset local.averagePixelBuffer = [] />
  •  
  • <!---
  • Set NxN indicies of the ColdFusion array with the
  • pixel color average (we will convert it to a Java
  • array in the succeeding step).
  • --->
  • <cfset arraySet(
  • local.averagePixelBuffer,
  • 1,
  • (local.pixelWidth * local.pixelHeight),
  • local.pixelAverage
  • ) />
  •  
  • <!---
  • Now that we have our new average RGB color array,
  • let's write it back to the buffered image.
  •  
  • NOTE: We are casting our ColdFusion array to a
  • Java int array.
  • --->
  • <cfset local.bufferedImage.setRGB(
  • javaCast( "int", local.x ),
  • javaCast( "int", local.y ),
  • javaCast( "int", local.pixelWidth ),
  • javaCast( "int", local.pixelHeight ),
  • javaCast( "int[]", local.averagePixelBuffer ),
  • javaCast( "int", 0 ),
  • javaCast( "int", local.pixelWidth )
  • ) />
  •  
  • <!---
  • NOTE: Editing the buffered image has a direct
  • affect on the image from which we retrieved the
  • buffered image. It is passed by REFERENCE!
  •  
  • As such, we don't need to explicitly move the
  • updated buffered image data "back" into the
  • image for the changes to take place.
  • --->
  •  
  • </cfloop>
  •  
  • </cfloop>
  •  
  • <!---
  • Return out. No need to return the original image since
  • it is be altered directly.... I don't really care for this
  • strategy, but it keeps in line with the exisitng image
  • functions provided by ColdFusion.
  • --->
  • <cfreturn />
  • </cffunction>

As you can see in the code above, I am getting a reference to the ColdFusion image's underlying BufferedImage object. This buffered image (pixel data) is passed by reference. This means that we are not working with a copy of it; as such, any changes that we make to the buffered image data will immediately affect the ColdFusion image from which we got it. That is why, in the above function, I never have to set the altered buffered image data back into the ColdFusion image object.

Once I had the imagePixelate() function in place, I wrote some test code to see how this would affect images at different "pixel" sizes:

  • <!--- Read in the image. --->
  • <cfimage
  • name="girl"
  • action="read"
  • source="./girl2.jpg"
  • />
  •  
  • <cfoutput>
  •  
  • <div align="center">
  •  
  • <h3>
  • Original Image
  • </h3>
  •  
  • <p>
  • <cfimage
  • action="writetobrowser"
  • source="#girl#"
  • />
  • </p>
  •  
  •  
  • <!---
  • Now, let's loop over a few different pixel sizes and
  • apply each to the *original* image.
  • --->
  • <cfloop
  • index="pixelSize"
  • from="5"
  • to="30"
  • step="5">
  •  
  • <!---
  • Copy the original image since the pixelation is
  • applied directly to the image passed-in.
  • --->
  • <cfset testGirl = imageCopy(
  • girl,
  • 0,
  • 0,
  • imageGetWidth( girl ),
  • imageGetHeight( girl )
  • ) />
  •  
  • <!---
  • Pixelate the image using the given pixel size of
  • this iteration.
  • --->
  • <cfset imagePixelate( testGirl, pixelSize ) />
  •  
  • <!--- Output the image. --->
  • <h3>
  • Pixel Size: #pixelSize#
  • </h3>
  •  
  • <p>
  • <cfimage
  • action="writetobrowser"
  • source="#testGirl#"
  • />
  • </p>
  •  
  • </cfloop>
  •  
  • </div>
  •  
  • </cfoutput>

As you can see in this code, we are iterating over an image, applying the effect with incrementing pixel dimensions. Notice that before I apply the pixelation effect, I have to create a copy of the image. This is due to the fact that effect works directly on the original image (via its buffered image data); I have to copy the image to make sure the subsequent effects are not all applied to the same variable.

When we run the above code, we get the following page output:

 
 
 
 
 
 
Pixelate A ColdFusion Image Using The Underlying Buffered Image Data. 
 
 
 

Anyway, I just think that's a cool little effect.




Reader Comments

Jan 7, 2010 at 10:18 AM // reply »
304 Comments

Don't forget to add this to imageUtils. :)


Jan 7, 2010 at 10:27 AM // reply »
37 Comments

Will this work with ColdFusion 7 or 8 or is this a CF9 only feature? (The post doesn't state.)


Jan 7, 2010 at 11:05 AM // reply »
10,640 Comments

@Raymond,

Ah, good point!

@James,

This was built in CF8. I can't remember off-hand when images were introduced. I think they were in ColdFusion starting in CF8.


Jan 7, 2010 at 11:06 AM // reply »
304 Comments

Image support was definitely added in CF8.


Jan 7, 2010 at 11:59 AM // reply »
66 Comments

Doesn't resizing it down and then back up again produce the same effect?


Jan 7, 2010 at 12:01 PM // reply »
10,640 Comments

@Rick,

It might create something similar, but I don't think you would have any control over the size of the resultant pixels... or at least it might take a different set of calculations (ie. smaller size to resize to before re-enlarging).


Jan 7, 2010 at 1:45 PM // reply »
7 Comments

I have done the same via actionscript and used to scale the 20*20 area to 1*1 and it merged the colors automatically, so you would just have to get the color of the scaled pixel.
Also, not sure it would be faster ...


Jan 7, 2010 at 1:48 PM // reply »
10,640 Comments

@Philippe,

Oh that's an interesting idea; and I think it kind of goes nicely with what @Rick was talking about - just at the smaller scale.


Jan 7, 2010 at 3:08 PM // reply »
2 Comments

Resizing it down and back up will just make a blurry image, not a pixeled image. The image would just be resampled.


Jan 7, 2010 at 6:16 PM // reply »
26 Comments

Sweet!


Jan 8, 2010 at 3:42 AM // reply »
2 Comments

To me this is Spanish and Chinese together at the same time. Right now Im trying to learn PHP which is enough for me so far :)

But thanks anyway, maybe in a few months I will understand that.


Jan 8, 2010 at 7:55 AM // reply »
10,640 Comments

@Hans,

Good luck with the PHP!


Jan 26, 2010 at 6:58 PM // reply »
66 Comments

Tested and verified:

<cfset var origWidth = image.width>
<cfset var origHeight = image.height>
<cfset imageResize(arguments.image, arguments.image.width / arguments.pixelSize, "")>
<cfset imageResize(arguments.image, origWidth, origHeight, "nearest")>

By specifying the interpolation as "nearest", you disable any of the smarter interpolation that would lead to a blurry image.

(Not that your post isn't useful in its own right -- I'm just nit-picking.)


Jan 27, 2010 at 8:23 AM // reply »
10,640 Comments

@Rick,

Very cool! I just tested this to see for myself and it worked perfectly. I guess this goes to show you how important it can be to understand the differences between all the various interpolation methods (is that the word I'm looking for)?



Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
InVision App - Prototyping Made Beautiful With Prototyping Tools Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Feb 12, 2012 at 3:37 AM
Learning ColdFusion 8: CFImage Part III - Watermarks And Transparency
Hi Ben, Just to ask currently it is placed bottom right corner, if i need to replace the same rendered image on the bottom left side or in the bottom center, how that can be calculated. bottom ce ... read »
Feb 11, 2012 at 9:29 PM
Use jQuery's SlideDown() With Fixed-Width Elements To Prevent Jumping
I can't say how glad I am that I found your post. Thank you very much. ... read »
Feb 10, 2012 at 7:21 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
Update! Instead of $(eval(options.insertAfter)).after(data['insertData']); I now use: var ajaxNode = document.createElement('span'); var parent = $(eval(options.insertAfter))[0].parentNode; ... read »
Feb 10, 2012 at 6:18 PM
jQuery AJAX Strips Script Tags And Inserts Them After Parent-Most Elements
encountered this same, what I consider, jQuery bug last week. I'm building a site in which I load some content via AJAX. This content contains Linkedin share button placeholders which Linkedin API ne ... read »
Feb 10, 2012 at 11:30 AM
Cross-Origin Resource Sharing (CORS) AJAX Requests Between jQuery And Node.js
After you understand the concepts here, this is an awesome cheatsheet for enabling CORS in just about anything http://enable-cors.org/ ... read »
JM
Feb 10, 2012 at 9:10 AM
My Safari Browser SQLite Database Hello World Example
@Amy, Here is a very good tutorial on how to use JOIN: http://www.sqltutorial.org/sqljoin-innerjoin.aspx ... read »
Feb 10, 2012 at 4:42 AM
Building A Twitter-Inspired RESTful API Architecture In ColdFusion
This is great, very useful Ben. I spotted a small typo in the api.cgm listing: <cfthrow type="Unauthroized" /> Cheers Stefan ... read »
Feb 9, 2012 at 10:35 PM
CFDirectory Filtering Uses Pipe Character For Multiple Filters (Thanks Steve Withington)
I was wondering if there would be a filter you could apply so that you got everything but what you included in the filter. As in show me all docs that are not a .pdf. ... read »