Image gradients are a fundamental transformation used in image processing, search indexing, and computer vision. Like a weather map showing the direction and strength of the wind, an image gradient depicts the strength and direction of changes in intensity over the surface of the image.

Through a fluke in my interpretation / understanding of the technique the gradients shown here on of images of objects from the Guttormsgaard archive, follow not the direction of the change but rather its perpendicular. As a result, the arrows chase around the edges of objects rather than piercing straight into them. Indeed, the image gradients shares a close affiliation between with the operation of edge-detection (where the movements are further segmented into discrete segments).

Following the description of Image derivatives in the book Programming Computer Vision with Python (p. 18), the following python code uses gaussian filters to approximate the derivative (rate of change) in first the x and the the y directions, and then uses a square root function to find the magnitude of change, and an arc tangent function to find the direction (or its perpendicular) at each point. The results are used to construct a second “gradient image” where the pixels color (hue in degrees on the HSL color wheel) represent the direction, and the value (or lightness) the magnitude of change.

On this page, clicking on the image switches between 3 views: (1) drawing the gradient at the mouse location, (2) drawing a grid of equally spaced gradients, and (3) showing the underlying (and pre-calated) “hsl” gradient image, which is the source of data used to perform the first two views.

```#!/usr/bin/python

from PIL import Image
from numpy import *
from scipy.ndimage import filters
import sys, json, os, math
from hsl import hsl_to_rgb
import argparse

# Based on Programming Computer Vision, chapter 1, Image Derivatives

parser = argparse.ArgumentParser(description='Calculate an image gradient.')
parser.add_argument('input', help='an image path as input')
parser.add_argument('--format', default="hslimage", help='save output format, default hslimage (direction is hue, magnitude is lightness). Other options: magimage (magnitude image), json.')
args = parser.parse_args()
p = args.input
path, base = os.path.split(p)
base, ext = os.path.splitext(base)
out = args.output or os.path.join(path, base + ".gradient.png")
im = array(Image.open(p).convert('L'))

sigma = 5 # standard deviation
imx = zeros(im.shape)
filters.gaussian_filter(im, (sigma,sigma), (0,1), imx)
imy = zeros(im.shape)
filters.gaussian_filter(im, (sigma,sigma), (1,0), imy)
magnitude = sqrt(imx**2+imy**2)
dirs = arctan2(imy, imx)

if args.format == "hslimage":
# Map direction to Hue, Magnitude to value
from math import pi
maxmag = amax(magnitude)
height, width = magnitude.shape
im = Image.new("RGBA", (width, height))
for y in range(height):
p = int(math.ceil(float(y)/height*100))
sys.stderr.flush()
for x in range(width):
d = dirs[y][x]
hue = ((d+pi) / (2 * pi)) * 360
value = (magnitude[y][x]/maxmag)
r, g, b = hsl_to_rgb(hue, 1.0, value)
im.putpixel((x, y), (r, g, b, 255))
sys.stderr.write("\n")
im.save(out)
elif args.format == "magimage":
pil_im = Image.fromarray(magnitude)
pil_im.convert("LA").save(out)
elif args.format == "json":
with open(out, "w") as f:
json.dump({
'dirs': dirs.tolist(),
'magnitudes': magnitude.tolist()
}, f)```

The code is published here.