Pipeline is a tool for applying image transformations to sequences of images, whether they be video files or directories. In particular, I wrote Pipeline to aid in processing animations, however it’s convenience is not limited to this scope.
The driving ideas behind Pipeline are:
In other words, Pipeline parses and applies a meta-language contained within so-called plan files across video files, directories of images, etc.:
Pipeline takes a variable number of plan files which describe a set of image transformations. Next, Pipeline applies these transformations over either:
stdin
,Finally, the resulting processed images may be dumped to a new directory or saved or in-place, or—in the case of stdin
—the singular image which gets processed outputs to stdout
.
The syntax for Pipeline.rb’s plan-files is inspired in part by the patching systems implemented in both Max/MSP and Pure Data. Moreover, the language is intended to be equally intuitive.
Plans allow for two types of commands:
Both variable assignments and function calls take arguments of the following types:
$1
, $alpha
, etc.)%(masks/big_willie_style.png)
)For example, a plan to swap the green and blue channels of an image, while inverting the red channel, takes the following form:
splitRGB $1
invert $1
joinRGB $1 $3 $4
It is important to note that the numeric variables—$1
, $2
, etc.—are routinely overwritten by function calls within a plan. For example, had we wanted to apply ImageMagick’s edge filter with a strength of 8 to an image, we would have written:
splitRGB
$red = $1
edge $3 8
joinRGB $red $2 $1
In the above example, splitRGB
saves the red channel of our input to $1
. We must store this before we call edge
on the blue channel ($2
), since the output of edge
will be stored in $1
. Finally we rejoin the channels. Note that the green channel ($2
) is unmodified.
Relying on the numbered variable system in the above examples grows cumbersome as the complexity of a plan file increases. Named variables circumvent this problem. Prior to this language feature, the following plan would have taken nearly 7 lines:
$r, $g, $b = split_rgb $1
$mask = center_fit %(images/masks/big_willie_style.png) $r
$r = multiply $mask $r
join_rgb $r $g $b
Significantly, in the above example, only the final line modifies a numbered variable; join_rgb
overwrites $1
.
I am considering adopting a Ruby-esque syntax, wherein any function call with an exclamation mark modifies its first argument in-place; i.e.,
multiply! $red $mask
would be the same as
$red = multiply $red $mask
All plan file commands are stored in a Hash
, with keys corresponding to function names and values corresponding to Lambda functions of type:
State -> Args -> State
Where State
is a Hash
of String/Object
pairs corresponding to variable names and values.
Args
is an array of Lambda functions of type:
State -> Int, Float, String, [String], Magick::Image, ...
Variable arguments lookup their variable name against the State
passed to them and return the corresponding value. Int
, Float
, and String
arguments ignore State
and return themselves.
The aforementioned architecture makes Pipeline’s image processing as simple as:
Parse plan file(s) line-by-line,
Hash
andList
of transformsinject
an initial State
populated with the source image through transforms
State
passed to it accordinglyReturn or save the final image contained in the State
This algorithm is evident in the following class definitions:
class Transforms
def initialize
@transforms = []
end
def add(&transform)
@transforms << transform
end
def to_proc
# Here’s the work-horse: inject() is like Haskell’s `mapAccumL`
lambda { |state| @transforms.inject(state) { |state, fn| fn.call(state) } }
end
end
class Transform
def initialize(name, args)
@transform = $transforms[name] # instance looks itself up by name
@args = args # `args` is list of Lambdas of type State -> Arg
end
def to_proc
# The following returns an `update`-ed hash
lambda { |state| state.update(@transform.call(state, @args)) }
end
end