Focal Black & White effect
Focal Black & White Effect
A well known Google Picasa effect is the Focal Black & White Effect. This effect preserves the color within a focal region and converts pixels outside this region to grayscale.
The algorithm is surprisingly simple: it consists of calculating a weighting factor (that depends on the focal radius), converting the pixel RGB values at each position to grayscale, and calculating a weighted average of this value with the original RGB value. Let us see how this can be achieved in Quasar:
function [] = __kernel__ focus_bw(x, y, focus_spot, falloff, radius, pos)
% Calculation of the weight
p = (pos - focus_spot) / max(size(x,0..1))
weight = exp(-1.0 / (0.05 + radius * (2 * dotprod(p,p)) ^ falloff))
% Conversion to grayscale & averaging
rgbval = x[pos[0],pos[1],0..2]
grayval = dotprod(rgbval,[0.3,0.59,0.11])*[1,1,1]
y[pos[0],pos[1],0..2] = lerp(grayval, rgbval, weight)
end
Code explanation
First, the kernel function focus_bw
is defined. __kernel__
is a special function qualifier that identifies a kernel function (similar to OpenCL’s __kernel
or kernel
qualifier). Kernel functions are natively compiled to any target architecture that you have in mind. This can be multi-core CPU x86/64 ELF, even ARM v9 with Neon instructions; up to NVidia PTX code. Next, a parameter list follows, the type of the parameters is (in this case) not specified.
Note the special parameter pos
. In Quasar, pos
is a parameter name that is reserved for kernel functions to obtain the current position in the image.
The kernel function contains two major blocks:
- In the weight calculation step, first the relative position compared to the focus spot coordinates is computed. This relative position is normalized by dividing by the maximum size in the first two dimensions (note that Quasar uses base-0), so this is the maximum of the width and the height of the image. Next, the weight is obtained as being inversely proportional to distance to the the focal spot. A special built-in function
dotprod
, that can also be found in high-level shader languages (HLSL/GLSL) and that calculates the dot product between two vectors, is used for this purpose. - For extracting the RGB value at the position
pos
, we use a matrix slice indexer:0..2
constructs a vector of length 3 (actually[0,1,2]
), which vector is used for indexing. In fact:x[pos[0],pos[1],0..2] = [x[pos[0],pos[1],0],x[pos[0],pos[1],1],x[pos[0],pos[1],2]]
which form do you prefer, the left-handed side, or the right-handed side? You can choose. Note that it is not possible to write:
x[pos[0..1],0..2]
, because this expression would construct a matrix of size2 x 3
. - The gray value is calculated by performing the dot product of
[0.3,0.59,0.11]
with the original RGB value. Finally, the gray value is mixed with the original RGB value using thelerp
“linear interpolation” function. In fact,lerp
is nothing more than the function:lerp = (x,y,a) -> (1-a) * x + a * x
The resulting RGB value is written to the output image
y
. That’s it!
Finally, we still need to call the kernel function. For this, we use the parallel_do
construct:
img_in = imread("quasar.jpg")
img_out = zeros(size(img_in))
parallel_do(size(img_out,0..1),img_in,img_out,[256,128],0.5,10,focus_bw)
imshow(img_out)
First, an input image img_in
is loaded using the function imread
“image read”. Then, an output image is allocated with the same size as the input image.
The parallel_do
function is called, with some parameters. The first parameter specifies the dimensions of the “work items” that can run in parallel. Here, each pixel of the image can be processed in parallel, hence the dimensions are the size (i.e., height + width) of the output image. The following parameters are argument values that are passed to the kernel function and that are declared in the kernel function definition. Finally, the kernel function to be called is passed.
Note that in contrast to scripting languages that are dynamically typed, the Quasar language is (mostly) statically typed and the Quasar compiler performs type inference in order to derive the data types of all the parameters. This is done based on the surrounding context. Here, Quasar will find out that img_in
is a cube
data type (a 3D array) and it will derive all other missing data types based on that. Consequently, efficient parallel code can be generated in a manner that is independent of the underlying platform.
Now: the complete code again:
function [] = __kernel__ focus_bw(x, y, focus_spot, falloff, radius, pos)
p = (pos - focus_spot) / max(size(x,0..1))
weight = exp(-1.0 / (0.05 + radius * (2 * dotprod(p,p)) ^ falloff))
rgbval = x[pos[0],pos[1],0..2]
grayval = dotprod(rgbval,[0.3,0.59,0.11])*[1,1,1]
y[pos[0],pos[1],0..2] = lerp(grayval, rgbval, weight)
end
img_in = imread("flowers.jpg")
img_out = zeros(size(img_in))
parallel_do(size(img_out,0..1),img_in,img_out,[256,128],0.5,10,focus_bw)
imshow(img_out)
Example
With eleven lines of code, you have a beautifully shining Focal Black & White effect: