This year, I decided to see what I could do to automate most of the pattern creation steps using Julia's image processing libraries. Inspired by the grumpy mug of my former foster cat Bruno, I got to work.
This tutorial was made with Literate.jl; you can find the source code here.
We'll only be making use of a few image processing packages.
using Images, ImageFiltering, ImageContrastAdjustment
Here's an outline of the steps I used to create a pattern (we'll look at each in detail shortly):
Manual Background Removal: you can get a fairly good template without removing the background, but I found the contrast was improved by doing this on my iPad first. I leave the implementation of an automatic segmentation algorithm for future work.
Grayscale: since I can only control how much light gets through the pumpkin, there's no need for colour.
Equalization: this step spreads out the dynamic range of the pixels to create greater contrast for a clearer image.
Smoothing: real photographs contain too much fine detail for a novice carver like me to recreate, so some form of smoothing filter is needed.
Thresholding: once again, I'm not a skilled artist, so I will only be creating a template with three shades (white, black, and gray). It's possible to have a more shades, or even continuous gradients, but that's well beyond my carving abilities.
Manual Touch-up: unfortunately, the process isn't perfect, and some manual changes on my iPad were needed to simplify the template.
Let's load up the image and make it grayscale.
rgb_image = load("_assets/img/blog/pumpkinizer/bruno_pumpkin_black_background.jpg")
gray_image = Gray.(rgb_image);
Next, we'll apply histogram equalization. Notice how much this simple step has improved the contrast, which will help us to eventually separate Bruno's feature's into three clear shades.
hist_equal = adjust_histogram(gray_image, Equalization(nbins = 256));
We use a bilateral filter for its edge-preserving properties.
function bilateral_filter(img, n::Int=5, σr=0.5, σd=0.5)
img_filt = zeros(size(img))
for i = 1:size(img, 1)
for j = 1:size(img, 2)
w_sum = 0.0
img_sum = 0.0
for u = max(1, i-n):min(size(img, 1), i+n)
for v = max(1, j-n):min(size(img, 2), j+n)
w_ijuv = exp(-((i-u)^2 + (j-v)^2)/(2*σd^2) - (img[i, j] - img[u, v])^2/(2*σr^2))
w_sum += w_ijuv
img_sum += w_ijuv*img[u, v]
end
end
img_filt[i, j] = img_sum/w_sum
end
end
return img_filt
end;
The kernel size and smoothing parameters \(\sigma_r\) and \(\sigma_d\) allow for a great deal of control. I eventually settled on the values below.
σr = 3 # Smoothing parameter based on pixel proximity (reduce this for more detail)
σd = 7 # Smoothing parameter based on pixel intensity similarity (reduce this for more detail as well)
kernel_size = 14 # Size of the window to use (make this larger for less detail)
filtered_image = bilateral_filter(hist_equal, kernel_size, σr, σd);
Bruno's face is now blurrier and therefore more easily segmented into simple blobs, but notice how the lines are still fairly crisp (and therefore easy to carve):
The final step was simply a thresholding procedure so that the pattern only featured three shades. Carving with a greater number of shades is beyond my abilities, but more skilled pumpkin pulp-sculptors can easily modify this procedure to support more detailed shading.
function threshold_image(img, black_threshold, gray_threshold)
thresholded_image = zeros(size(img))
for ind in eachindex(img)
if img[ind] < black_threshold
thresholded_image[ind] = 0.0
elseif img[ind] > gray_threshold
thresholded_image[ind] = 1.0
else
thresholded_image[ind] = 0.5
end
end
return thresholded_image
end;
After some experimentation, the following parameters were used:
black_threshold = 0.5 # Increase for more black vs. gray
gray_threshold = 0.75 # Increase this for more gray vs. white
thresholded_image = threshold_image(filtered_image, black_threshold, gray_threshold);
The resulting image is now pretty much ready to be saved (with save("template.png", thresholded_image)
) and used as a carving pattern:
The final step before printing and carving involved some manual simplification on my iPad, the re-addition of some whiskers that were lost in translation, and adding some whitespace to save ink.
I set up my workspace and taped the template on the nicest face of a newly-gutted pumpkin:
After a few hours of detailed carving, the un-illuminated Brun-o'-lantern looked pretty terrible:
But I was pretty pleased with the final product when lit up, even though I made some mistakes:
Here's an image of the full pipeline that helps to visualize the changes each step introduces:
Tips to myself (and others) for improving next year's pumpkin:
I carved the wall WAY too thin! Brun-o'-lantern's mouth fell off and I needed to support it with a pin.
The mouth was also just way too small, I need to make sure shapes have better structural integrity.
I need to make smaller holes in the dotting step - is my hole-poking tool too big, or did I just poke too hard?
Projecting a flat image onto a curved surface is never perfect, but the hole in the middle of his head is mostly my fault.
I need to be choosier with my pumpkin: this one was too small and curved for someone with my skill level.
I hope you found this informative! Feel free to use and modify the source code to make your own pumpkin carving templates next Halloween!