Box Counting
Fractal dimension is a mathematical concept that describes the way that the apparent complexity of a curve changes at different scales. Purely coincidentally, I had just watched a 3blue1brown video about fractal dimension when Nature published an article about the fractal dimensionality of Rorschach ink blots. Researchers determined that people “see” the most forms in shapes with a fractal dimensionality of about 1.1. It’s as though there’s a magical level of visual complexity that tickles the imagination, and here’s a method that puts a number on it. This suggests that fractal dimension, or at least the related numerical concept of box counting dimension, could be a valuable tool in evaluating generative designs aimed at producing or avoiding pareidolia.
There’s already some box counting code online, by Francesco Turci, an Italian physicist. I’m following his implementation, but packaging it as a function rather than a script.
import numpy as np
def dimensionality(image):
'''Uses box counting to estimate the fractal dimensionality of the given
image. The input image should be a single channel (grayscale) image; any
non-zero value is foreground while zero values are background.'''
width, height = image.shape
# Turci implemented this with nested loops, but this is faster and clearer
xs, ys = image.nonzero()
# The scales become the number of histogram bins
scales=np.logspace(1, 8, num=20, endpoint=False, base=2)
Ns=[]
for scale in scales:
H, _ = np.histogramdd((xs, ys),
bins=(np.arange(0,width,scale),
np.arange(0,height,scale)))
# Count the number of bins that contain any forground
Ns.append(np.sum(H > 0))
# As Turci notes, the dimensionality is the negative of the highest order
# coefficient
D, _ = -np.polyfit(np.log(scales), np.log(Ns), 1)
return D
To validate this measurement, we observe that a line should be one-dimensional, any filled shape should be two-dimensional, and a Sierpinski triangle should be log(3)/log(2) -dimensional, or about 1.585. I’ll be happy with with the procedure if it’s accurate to about ±5%.
Using the Sierpinski triangle from Wikipedia. Of course, this is approximate.
from skimage.io import imread
from matplotlib import pyplot as plt
%matplotlib inline
test_rgb = imread('sierpinski.png')
test_bitmap = test_rgb[:,:,2] > 219
plt.imshow(test_bitmap, cmap='gray')
print('Dimensionality: ', dimensionality(test_bitmap) )
Dimensionality: 1.61741861247
test_line = np.zeros(shape=(512,512), dtype=float)
test_line[255,:] = 1
plt.imshow(test_line, cmap='gray')
print('Dimensionality: ', dimensionality(test_line))
Dimensionality: 1.0298873676
test_square = np.zeros(shape=(1024,1024), dtype=bool)
size = 255
test_square[ 512-size:512+size, 512-size:512+size ] = True
plt.imshow(test_square, cmap='gray')
print('Dimensionality: ', dimensionality(test_square))
Dimensionality: 1.91078271785
from skimage.draw import circle
test_circle = np.zeros(shape=(1024,1024), dtype=bool)
rows, cols = circle(512, 512, 255)
test_circle[ rows, cols ] = True
plt.imshow(test_circle, cmap='gray')
print('Dimensionality: ', dimensionality(test_circle))
Dimensionality: 1.87356472105
The estimates aren’t too bad: the Sierpinski triangle and the line are within about ±0.05 of the expected value. I’m not sure why the square and the circle aren’t closer to 2. Increasing the overall size of the input image and/or increasing the relative area of the filled figure both seem to increase the accuracy of the estimate. Interestingly, these observations lead to a test showing that when the entire field is filled with foreground, we do reach two dimensionality:
test_silliness = np.ones(shape=(1024, 1024), dtype=bool)
print('All True?', np.all(test_silliness)) # no plot, boring to look at
print('Dimensionality: ', dimensionality(test_silliness))
All True? True
Dimensionality: 2.02287408383
Further tests could be performed using other curves with known fractal dimensionality. For now I’m satisfied with this level of validation.