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:
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:
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 of
image.Image‘s to pass to
- 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:
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
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
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 Beleza
COMMANDS: 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. 😀