Image Transformations

Image Transformations

This was a post from my old blog. Some links/formatting/images may be out of date.

A while back I wrote an article about a simple scripting language I created called ImageQuery. The project was a small proof-of-concept, and while it worked, it had numerous issues. The first issue was that it made some weird assumptions about how a scripting language should work - I won't go into too much detail about this. The second issue was that it was written in C# - now, I love C# and the .NET framework, but it isn't exactly the most ideal language to implement another scripting language in (and creating a language on top of .NET is a bit overkill for what I'm doing).

A rewrite in C++ was in order, and imquery was born.

imquery is a dynamically-typed interpreted language focused on image manipulation. Here's a quick sample that implements the recursive Fibonacci algorithm:

func fib(n) {
  if n < 3 {
    return 1;
  }

  return fib(n - 1) + fib(n - 2);
}

# Calculate the 6th Fibonacci number and print it.
print(fib(6));

The syntax is similar to C albeit without types. This changes a bit when we start throwing images, inputs, and outputs into the equation:

in myInput = image();
out myOutput = image(myInput.w, myInput.h);
myOutput: {(color.r + color.g + color.b) / 3.} from myInput;

This bit of code can be run through iqc, the imquery command-line frontend like so (assuming the script is called greyscale.imq):

iqc -f "greyscale.imq" -i "input=image_load('flowers.png')" -o "output=result.png"

Assuming flowers.png is this image:

flowers-1-

Then after a bit, iqc will give us this image in result.png:

test-1-

This is a very simple greyscale filter. Let's go through this line-by-line:

in myInput = image();

imquery scripts support the concept of inputs and outputs. These are (mostly) normal variables that can be used by whatever program is calling the script. In the case of iqc, we can specify inputs via the -i flag and specify destinations for images (outputs) with the -o flag.

Inputs must be set to something on declaration, and their value cannot be changed once set by the script itself. Additionally, the type of the input matters - it'll be used to check whether or not we've set a valid input from outside the script. If we don't specify the value of an input outside of the script, the input will

Here, we're telling imquery that myInput is an image.

out myOutput = image(myInput.w, myInput.h);

Outputs work similarly to inputs, except that they can be set as many times as you want (though you always have to include the out flag) and don't need to be set when they are declared (they default to nil). iqc any outputs we specify and saves them to image files (other types of outputs are not currently supported).

For myOutput, we are storing a new image of the same dimensions as myInput.

myOutput: {(color.r + color.g + color.b) / 3.} from myInput;

This is called a selection. It has a few different forms, but the most basic has an expression, a source, and a destination. For each element in the source, an expression is applied and then sent to the destination - in the case of images, this runs an expression over each pixel from the source and writes the result to the destination. This is a simple way of making filters.

You might recognize selections as a limited form of a for-each loop (which imquery has too!) - this is true. That said, selections are set apart from for-each loops by my future plans for them - eventually, I'd like to let selection calculations run in parallel, and one day selections may even be able to run on the GPU (via OpenCL or similar). Additionally, they have a fairly simple syntax and should be easier to optimize than a for-each loop, as certain assumptions can be made about how selections work. I may write a followup post about this in the future.

After the code has finished running, iqc will look through the list of outputs passed on the command line and write those to the specified files.

Conclusion

Alright, that's it for now. I'll leave a quick bit of neat code you can run on your own, along with another link to the project (if you can't build it yourself, try downloading a prebuilt release).

in input = image();
out output = image(input.w, input.h);
output: ((color.each((v) => sin(v * 2 * pi)) + 1) / 2 + {0.,1.}).clamp() from input;
Show Comments