A few days ago I wanted to do some batch image processing. It could be done in Photoshop, I know, but I wanted to have some fun as well and learn some algorithms. I started studying Go in January and it sounded like an opportunity to practice a little, so I began to write my own program to process images: blzimg.
blzimg will have some image operations. The first of them (and the only one until now) is called “lightest”. It merges the lightest pixels of a list of images into a single image.
Comparing the luminance of pixels
The first image operation I wanted to do was to get some images and, for every pixel (x,y) of them, their RGB values would be compared. The lightest pixels at the same position (x,y) compose the final image.
Images talk better than text. Let’s use these 3 images:
img1.jpg | img2.jpg | img3.jpg |
The lightest operation will merge these three images into a final image that will be this:
The grey pixels where fully replaced by the white pixels, since the latter are lighter. The formula to obtain the lightest pixel of an image was borrowed from this question at StackOverflow. The L is for luminance, and r, g, b are the color components of a pixel:
The more the luminance, the lighter a pixel is. Our main idea is that if the luminance value L for a pixel is greater than the L value for another pixel, that pixel will be the lightest one and, therefore, be chosen.
I don’t know if it’s the scientifically proved best choice, but it has worked for my purposes. This is my implementation in Go:
Comparing images
At first, I created a function to receive a slice of image.Image
‘s and travel through their pixels, comparing them:
In this old version, notice that the first image was read twice in the for
loop and an empty slice would ruin everything. 😀 But our idea is this: store the lightest pixel and compare it to the pixel in the current image. If the newer pixel is lightest, we replace the current lightest pixel.
I created this function using TDD and it worked well with image.Image
‘s, but how can we parse images from File
‘s and keep the code testable?
Using containers for testing
The first version of Result()
function received a slice of image.Image
‘s, based on my test where I created some image.Image
‘s to verify. But it has some limitations in the real world. How could I handle real files?
- If I used a slice of
image.Image
‘s as arguments, I would have to get a list of files, decode them and create a very heavy slice ofimage.Image
‘s to pass toResult()
. - If I used a slice of
File
‘s as parameter, it would become harder to do unit testing.
I created an interface to solve both cases: ImageContainer
.
Its implementations must have a GetImage()
function that will return a image.Image
only when needed, so it’s a more lightweight approach. For example, a FileImageContainer
would keep the file path within it and return the image.Image
when GetImage()
is called. An ImageItselfContainer
, used in the unit tests, can keep the image data itself and returns this data when GetImage()
is called. This is the current implementation:
The final version of Result()
, now using an ImageContainer
‘s instead of image.Image
‘s, is shown below. The image operation won’t know (and it doesn’t need to know!) what kind of container it’s dealing with, and now the same code can handle image.Image
‘s and File
‘s!
Parsing command line arguments with cli.go
To parse command line arguments I used cli.go. It’s a library that parses command line parameters and creates a nice help output:
$ blzimg NAME: blzimg - Execute some operations on images USAGE: blzimg [global options] command [command options] [arguments...] VERSION: 0.1 AUTHOR(S): Esdras BelezaCOMMANDS: lightest, l Merge the lightest pixels of some images in a single one help, h Shows a list of commands or help for one command GLOBAL OPTIONS: --output "final.jpg" Output file --help, -h show help --version, -v print the version
Finally: running blzimg
A few days ago I took some crappy pictures just to test my new Rokinon 12mm lens with my Fuji X-E1 camera. I used a intervalometer to take these pictures with a interval of 20 seconds between them, in a total of 8 minutes that were compressed in this timelapse of a few seconds:
What if we run blzimg to merge all the images from this timelapse into a single image using the lightest operation?
blzimg --output final.jpg lightest 2015-04-07_21-*
Since the lightest points in these pictures are the clouds and the stars, the output is a giant cloud and the amazing beginning of a star trail as if it were below the clouds:
Show me the code!
The full code can be downloaded at my GitHub. 😀