1. Getting started with Funcs, Vars, and Exprs#
// Halide tutorial lesson 1: Getting started with Funcs, Vars, and Exprs
// This lesson demonstrates basic usage of Halide as a JIT compiler for imaging.
// On linux, you can compile and run it like so:
// g++ lesson_01*.cpp -g -I <path/to/include> -L <path/to/lib> -lHalide -lpthread -ldl -o lesson_01 -std=c++17
// LD_LIBRARY_PATH=<path/to/lib> ./lesson_01
// On macOS:
// g++ lesson_01*.cpp -g -I <path/to/include> -L <path/to/lib> -lHalide -o lesson_01 -std=c++17
// DYLD_LIBRARY_PATH=<path/to/lib> ./lesson_01
// The only Halide header file you need is Halide.h. It includes all of Halide.
#include "Halide.h"
// We'll also include stdio for printf.
#include <cstdio>
int main() {
// This program defines a single-stage imaging pipeline that
// outputs a grayscale diagonal gradient.
// A 'Func' object represents a pipeline stage. It's a pure
// function that defines what value each pixel should have. You
// can think of it as a computed image.
Halide::Func gradient;
// Var objects are names to use as variables in the definition of
// a Func. They have no meaning by themselves.
Halide::Var x, y;
// We typically use Vars named 'x' and 'y' to correspond to the x
// and y axes of an image, and we write them in that order. If
// you're used to thinking of images as having rows and columns,
// then x is the column index, and y is the row index.
// Funcs are defined at any integer coordinate of its variables as
// an Expr in terms of those variables and other functions.
// Here, we'll define an Expr which has the value x + y. Vars have
// appropriate operator overloading so that expressions like
// 'x + y' become 'Expr' objects.
Halide::Expr e = x + y;
// Now we'll add a definition for the Func object. At pixel x, y,
// the image will have the value of the Expr e. On the left hand
// side we have the Func we're defining and some Vars. On the right
// hand side we have some Expr object that uses those same Vars.
gradient(x, y) = e;
// This is the same as writing:
//
// gradient(x, y) = x + y;
//
// which is the more common form, but we are showing the
// intermediate Expr here for completeness.
// That line of code defined the Func, but it didn't actually
// compute the output image yet. At this stage it's just Funcs,
// Exprs, and Vars in memory, representing the structure of our
// imaging pipeline. We're meta-programming. This C++ program is
// constructing a Halide program in memory. Actually computing
// pixel data comes next.
// Now we 'realize' the Func, which JIT compiles some code that
// implements the pipeline we've defined, and then runs it. We
// also need to tell Halide the domain over which to evaluate the
// Func, which determines the range of x and y above, and the
// resolution of the output image. Halide.h also provides a basic
// templatized image type we can use. We'll make an 800 x 600
// image.
Halide::Buffer<int32_t> output = gradient.realize({800, 600});
// Halide does type inference for you. Var objects represent
// 32-bit integers, so the Expr object 'x + y' also represents a
// 32-bit integer, and so 'gradient' defines a 32-bit image, and
// so we got a 32-bit signed integer image out when we call
// 'realize'. Halide types and type-casting rules are equivalent
// to C.
// Let's check everything worked, and we got the output we were
// expecting:
for (int j = 0; j < output.height(); j++) {
for (int i = 0; i < output.width(); i++) {
// We can access a pixel of an Buffer object using similar
// syntax to defining and using functions.
if (output(i, j) != i + j) {
printf("Something went wrong!\n"
"Pixel %d, %d was supposed to be %d, but instead it's %d\n",
i, j, i + j, output(i, j));
return -1;
}
}
}
// Everything worked! We defined a Func, then called 'realize' on
// it to generate and run machine code that produced an Buffer.
printf("Success!\n");
return 0;
}
#!/usr/bin/python3
# Halide tutorial lesson 1.
# This lesson demonstrates basic usage of Halide as a JIT compiler for imaging.
import halide as hl
def main():
# This program defines a single-stage imaging pipeline that
# outputs a grayscale diagonal gradient.
# A 'hl.Func' object represents a pipeline stage. It's a pure
# function that defines what value each pixel should have. You
# can think of it as a computed image.
gradient = hl.Func("gradient")
# hl.Var objects are names to use as variables in the definition of
# a hl.Func. They have no meaning by themselves.
x, y = hl.Var("x"), hl.Var("y")
# We typically use Vars named 'x' and 'y' to correspond to the x
# and y axes of an image, and we write them in that order. If
# you're used to thinking of images as having rows and columns,
# then x is the column index, and y is the row index.
# Funcs are defined at any integer coordinate of its variables as
# an hl.Expr in terms of those variables and other functions.
# Here, we'll define an hl.Expr which has the value x + y. Vars have
# appropriate operator overloading so that expressions like
# 'x + y' become 'hl.Expr' objects.
e = x + y
assert isinstance(e, hl.Expr)
# Now we'll add a definition for the hl.Func object. At pixel x, y,
# the image will have the value of the hl.Expr e. On the left hand
# side we have the hl.Func we're defining and some Vars. On the right
# hand side we have some hl.Expr object that uses those same Vars.
gradient[x, y] = e
# This is the same as writing:
#
# gradient[x, y] = x + y
#
# which is the more common form, but we are showing the
# intermediate hl.Expr here for completeness.
# That line of code defined the hl.Func, but it didn't actually
# compute the output image yet. At this stage it's just Funcs,
# Exprs, and Vars in memory, representing the structure of our
# imaging pipeline. We're meta-programming. This Python program is
# constructing a Halide program in memory. Actually computing
# pixel data comes next.
# Now we 'realize' the hl.Func, which JIT compiles some code that
# implements the pipeline we've defined, and then runs it. We
# also need to tell Halide the domain over which to evaluate the
# hl.Func, which determines the range of x and y above, and the
# resolution of the output image. The halide module also provides
# a basic hl.Buffer type we can use to hold the result. We'll make
# an 800 x 600 image.
output = gradient.realize([800, 600])
assert output.type() == hl.Int(32)
# Halide does type inference for you. hl.Var objects represent
# 32-bit integers, so the hl.Expr object 'x + y' also represents a
# 32-bit integer, and so 'gradient' defines a 32-bit image, and
# so we got a 32-bit signed integer image out when we call
# 'realize'. Halide's types and type-casting rules use fixed-width
# integer semantics, unlike Python's own arbitrary-precision ints.
# Let's check everything worked, and we got the output we were
# expecting:
for j in range(output.height()):
for i in range(output.width()):
# We can access a pixel of an hl.Buffer object using similar
# syntax to defining and using functions.
assert output[i, j] == i + j, (
"Something went wrong!\n"
f"Pixel {i}, {j} was supposed to be {i + j}, but instead it's {output[i, j]}\n"
)
# Everything worked! We defined a hl.Func, then called 'realize' on
# it to generate and run machine code that produced a hl.Buffer.
print("Success!")
return 0
if __name__ == "__main__":
main()