Some years ago, I lived in Norfolk, VA with a friend who loved craft beer as much as I did. Our hoppy passion motivated us to brew beer at home, visit local craft beer stores, and generally enjoy our nights with a brew here or there. Subconsciously knowing it would be a brilliant idea, we used a tupperware box next to our refrigerator to house a growing collection of beer caps, feeding it a single cap as each beer departed the cold, destined for enjoyment.

After a few years, I realized the box was close to being filled to the brim. The mechanical gears in my head were cranking out an idea to do something creative with the bottle caps. I wanted to incorporate some things I'd learned in my classes related to imaging, computer vision, data mining, and spectral signatures. I also had an old square coffee table (36"x36") that could be the canvas to this project. This was the beginning of a fun exploration in image processing with python to create a bottle cap magnum opus.

Getting it Done

There were a few things I needed to do; first, I needed more caps. Given the size of a bottle cap (1") minus some width for a border around the table and a little space between the caps, I found that the table would accept a 30 cap by 30 cap grid, therefore I needed at least 900 caps. A local bar supported this effort by hosting a 5 gallon bucket for the bartender to drop caps into in lieu of the trash can.

Second, I needed a way to extract the data (color; specifically red, green, and blue) from each individual cap. I originally arranged the images on a gridded sheet of paper, took a photo, and extracted them row-by-column after performing some image transforms to account for the skewed nature of the image. As it turns out, there is no good way to get a completely top-down image of the caps; it will always turn out to be smaller at the top and larger at the bottom depending on the angle you held the camera at. The data wasn't fantastic and as more caps came in, I knew I needed a better way; a quick survey of computer vision capabilities turned up a concept called a Hough Transform.

Hough transforms are a method of identifying shapes that can be approximated by equations in images; in this case, a circle with a roughly known radius was the target. This synopsis is a vast simplification, but for each pixel in the output image (of the same size as the input image), the algorithm polls those surrounding pixels that might form a circle and assigns a value to that pixel. Based on all the pixels in the image, one can then surmise that the pixels with values above a certain threshold are the center point for a circle of known radius. To facilitate the discovery of circles of a specific radius (as many caps have concentric circle patterns), I used a Sobel operator to convert the image into an edge-only image.

A subset of the caps, spread out and photographed.

Although I initially wrote my own Sobel Filter and Hough Transform in Python, I think it's smarter to use OpenCV, which I later discovered. OpenCV is a set of computer vision related functions written in C/C++ with wrappers for Python; Hough transforms are only one of the things that OpenCV can simplify from a multi-hour debacle into a few minutes of digging in the documentation for the right tool. Here is a quick snippet of something that took me dozens of lines of code in earlier editions:

def cv2hough(sobel_image, color_image, minrad, maxrad): ''' Identifies and cuts out to a directory potentially circular shapes that match a known radius range. :param sobel_image: sobel-filtered image :param color_image: non-sobel filtered version of colored_image :return: ''' in_cv_image = cv2.imread(sobel_image, 0) avg_rad = (minrad+maxrad)/2 circles = cv2.HoughCircles(in_cv_image, cv.CV_HOUGH_GRADIENT, 1, 65, param1=90, param2=15, minRadius=minrad, maxRadius=maxrad) exportCircles(circles, color_image, 'caps', avg_rad)

(Documentation for cv2.HoughCircles)

This code snippet concludes by sending a list of all the places on the image where it thinks a cap might be to "exportCircles", where those caps are then cut out of the image and sent to a 30px x 30px JPG file.

A cap, post-extraction. It's a rough approximation, but the hough transform was sufficient.

Once we have a directory containing hundreds and hundreds of caps, we can begin some analysis. (Full disclosure: I manually scrolled through the images and deleted those that appeared unusable.) Python was used to calculate statistics for each bottle cap image, and eventually stored the data in a structure. Each pixel is calculated applying a red (R), green (G), and blue (B) value. We can average these and get average (R,G,B) triplets for each bottle cap, that is to say; a cap is "mostly" red, or "mostly" blue, etc. I soon found that these numbers weren't that descriptive and began to work in the Hue, Saturation, and Value color spaces. (HSL & HSV: Wikipedia).

Protip: I used a quick one-liner to convert from RGB to HSV before storing the data:

colorsys.rgb_to_hsv(rgb[0], rgb[1], rgb[2])

To save time cataloging 900 bottle caps and associated pixels, I pickle (serialize) each cap as I generate the data on it so I can quickly reference a data structure containing the caps and rearrange them as I see fit.

The caps by average, as the computer sees them.

The caps as we see them; the final arrangement was sorted by hue then saturation.

Final Steps: Output

I originally wanted to see what it would look like when I rearranged the bottle caps into different patterns. This is where we do that, and based on the data we have and the framework we've built, outputting an image or a HTML document (this is a really hasty solution!) becomes fairly easy, minus one hiccup.

def first_sort ( self ): ''' Reach into the internal config and find out what we want to have as our first order sort on the caps. :return: ''' self . sortedDatList = sorted ( self . imageDatList , key = lambda thislist : thislist [ self . sorts [ self . conf [ 'first_sort' ]]]) del self . imageDatList self . imageDatList = self . sortedDatList def second_sort ( self ): ''' Second sort on the caps: sort rows individually. Example: We want a 30x30 grid of caps, sorted first by hue, then by saturation. We sort by hue in first_sort, then in second_sort, we sort 30 lists of 30 caps each individually and replace them into the list. :return: ''' ranges = [[ self . conf [ 'out_caps_cols' ] * ( p - 1 ), self . conf [ 'out_caps_cols' ] * p ] for p in range ( 1 , self . conf [ 'out_caps_rows' ] + 1 )] for r in ranges : self . imageDatList [ r [ 0 ]: r [ 1 ]] = sorted ( self . imageDatList [ r [ 0 ]: r [ 1 ]], key = lambda thislist : thislist [ self . sorts [ self . conf [ 'second_sort' ]]])

Here we see two functions: firstSort and secondSort. You can guess that we sort the caps once, then sort them again; what may not be apparent from this code is that the second sort (which is performed on a different attribute), is performed based on the out_caps_rows attribute. This is the hiccup; you must sort multiple sub-segments of the array of bottle caps, in essence, sorting the rows after the columns have been generated. Otherwise each row of bottle caps will have a random pattern as the image trends from one extreme (top) to the other (bottom).

Caps, laid out and ready to be transferred to the table.

The Physical Construction

To finish the project, I physically arranged a pattern based on informal votes from friends and acquaintances. I built a 1/2" lip around the table with "Quarter-Round" molding. I glued this down, sealed the seams with wood glue, and painted everything black.

I filled the bottom of this open-top area with playground sand. This minimized the volume that I would have to fill in with expensive epoxy resin. It had the secondary benefit of lightening up the entire display, knowing the epoxy would darken it. I ordered two kits of "AeroMarine 300/21 Epoxy Resin" (for a total of 3 Gallons) from the internet. Ultimately, I arranged the caps on a sheet of butcher's paper and transferred them one-by-one to the sand box.

A few friends, the famous roommate, and I gathered (with beers); we mixed the resin and began the process of pouring the resin onto the table. The one unseen consequence of the sand was that when the epoxy entered the sand, it pushed air upwards, causing the caps to float; we quickly used toothpicks to push the caps down into the sand, then poured more resin over top.

The table, post-drying and finished.

Code for this was hacked together over a number of weeks, I've consolidated it here: https://github.com/jamesfe/capViz. Unfortunately, most of this code was written in a disheveled, disconnected manner and I'm just now beginning to tie it together into one well documented, good-practice PEP-8 compliant piece, but feel free to poke around and let me know what you think!

Thanks to The Birch for all the beer caps!

If this interested you, follow me on Twitter! @jimmysthoughts