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 »
321 Comments

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


Jan 7, 2010 at 10:27 AM // reply »
46 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 »
11,314 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 »
321 Comments

Image support was definitely added in CF8.


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

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


Jan 7, 2010 at 12:01 PM // reply »
11,314 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 »
11,314 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 »
11,314 Comments

@Hans,

Good luck with the PHP!


Jan 26, 2010 at 6:58 PM // reply »
68 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 »
11,314 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
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Jun 19, 2013 at 11:31 PM
Directive Link, $observe, And $watch Functions Execute Inside An AngularJS Context
@Ben, bunch to learn indeed, but thats fun part : ) ... read »
Jun 19, 2013 at 10:41 PM
Referencing ColdFusion Query Columns In A Loop Using Both Array And Dot Notation
Burdock-roots Are you going fat day by day? You need to be good for your family and make some money too. So we bring for you a best product that helps you to be more energetic every day. You will b ... read »
Jun 19, 2013 at 9:52 PM
Working With Inherited Collections In AngularJS
I recognize the applicability of your solution, and how easy it makes to share data across multiple views or even "submodules" of rather simple application. But it seems to me that it creat ... read »
Jun 19, 2013 at 9:38 PM
Directive Link, $observe, And $watch Functions Execute Inside An AngularJS Context
@Alesei, Glad you like it. Even after working with AngularJS for months, I still get a bunch of unexpected, "$digest is already in progress". So hard to debug sometimes! ... read »
Jun 19, 2013 at 9:36 PM
Working With Inherited Collections In AngularJS
@Mike, The relationship of $scope values is definitely an interesting thing! But it's not simple - it really forces you to understand prototypal inheritance, which is not at all a simple topic! Gla ... read »
Jun 19, 2013 at 9:35 PM
Experimenting With The Amazon Simple Storage Service (S3) API Using ColdFusion
@Joe, Oh, super interesting! I had only thought to url-encode the signature; but I think that's because the S3 docs actually have a special NOTE telling you to do so. It would have never occurred t ... read »
Jun 19, 2013 at 9:32 PM
Experimenting With The Amazon Simple Storage Service (S3) API Using ColdFusion
@Richard, Glad you like! Hopefully I'll have some more interesting stuff coming. This morning, I blogged a bit more about generating the pre-signed, query string authenticated URLs; but, then deeme ... read »
Jun 19, 2013 at 9:31 PM
Filter vs. ngHide With ngRepeat In AngularJS
@Mike, Honestly, in the majority of cases, I would say there isn't going to be a difference. Both approaches have trade-offs. If you use the filter, then you have fewer DOM elements and fewer $scop ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools