Procedurally Generate Bokeh Kernels
Part I: Aperture Blades
(Well presume there will be other parts in the future…)
People commonly mistake the out-of-focus areas in an image as merely "blurred". While it may look blurred visually, from a technical aspect, it is not blurring at all (at least not Gaussian blur).
When the object is outside of the depth of field, light reflected from the points on the object and reaches the image plane as a circle rather than a point, when the size of the circle exceeds the circle of confusion (CoC) of the given image plane, it will appear as "blurred".
"A point projected as a circle"? This sounds quite similar to a 2D convolution. And this will be the way with which I approximate them here.
The easiest way to replicate a bokeh is, apparently, a circle:
img.ellipse((left, top, bottom, right))
A lens wide open will surely produce a bokeh like this, but this kernel will be next to useless due to how blank and unoriginal it is.
The next step would be to add the polygon effect caused by aperture blades, which, at a glance, will be an easy task to do with a simple polygon draw function:
img.polygon(xy)
This is... for sure, a polygon. But it does not look like a bokeh?
The reason is that most aperture blades are curved, not straight, so I need to add curvature onto the edges.
One way to do that is to literally draw curves, such as a bezier. But such a method can be very demanding in terms of execution time, finding and filling in the edges will take even more resources, not to mention editing the control points and their weights. Bezier is thus not a good choice.
I could also subdivide the edges of a polygon and slightly protrude them outwards. But how should I approach the interpolate function? Also worrying about the time complicity, I decided to give up on this method as well.
Eventually, I settled on a peculiar design:
coords = self._PolyCoord()
temp = np.array(Image.new('RGB', ImgSize, ImgColor))
for point in coords:
temp = self.BooleanAnd(temp, self._generateCircle(point))
that is, using the coordinates of the points of the polygon as the base, generate several circles with their center being the polygon point, and then boolean them together:
I also separated coordinate generation and boolean into 2 separate methods, which gave me an unexpected feature to rotate the bokeh kernel.
Just as I thought I have done a great job at coding this, I realized that for certain amount of aperture blades, it will exceeds the image boundary:
It took me some time to figure out what happened.
So, the original polygon is a convex shape, which will not protrude outside of the image as long as every coordinate is within the image coordinate. But these points do NOT represent the sharp corners of the bokeh, they each guide the edge in their opposite direction. As a result, the polygon points are often inside the bokeh's white area.
After knowing what happened, it is quite easy to fix: calculate the coordinates again and normalize them, move them to the center of the render area (or add a hard clamp to ensure the circle radius is never big enough to exceed image size):
... not perfect, but it works.
I then realize another problem: the curvature of the blades is all the same if I generate them this way.
Since I have normalized the polygon points' coordinates, I could just use them as a directional vector and put the circles in that direction, using the distance or magnitude to (indirectly) control the curvature.
Basically, move the circle along the axis whose direction is determined by the point coordinate of the polygon.
And tada:
The entire process can be generalized as:
Draw a blank canvas
Generate polygon coordinates
Move the coordinates to the center of the render target
Convert into vectors with render target center as the origin
Normalize the vectors
For each vector:
Multiply the inverse of curvature
Use the result as the origin of a circle
Boolean the circle with the canvas
With this done, it is also possible to add other effects, such as the bubble effect, anamorphic squeeze, chromatic aberration, etc. I will publish the notebook code once the entirety of kernel generation is finished.