Lomography effect
In this section, we are going to create another image effect, which is a photograph effect that is very common in different mobile applications, such as Google Camera or Instagram. We are going to discover how to use a look-up table (LUT). We will go through LUTs later in this same section. We are going to learn how to add an over image, in this case a dark halo, to create our desired effect. The function that implements this effect is the lomoCallback callback and it has the following code:
void lomoCallback(int state, void* userData) { Mat result; const double exponential_e = std::exp(1.0); // Create Look-up table for color curve effect Mat lut(1, 256, CV_8UC1); for (int i=0; i<256; i++) { float x= (float)i/256.0; lut.at<uchar>(i)= cvRound( 256 * (1/(1 + pow(exponential_e, -((x-0.5)/0.1)) )) ); } // Split the image channels and apply curve transform only to red channel vector<Mat> bgr; split(img, bgr); LUT(bgr[2], lut, bgr[2]); // merge result merge(bgr, result); // Create image for halo dark Mat halo(img.rows, img.cols, CV_32FC3, Scalar(0.3,0.3,0.3) ); // Create circle circle(halo, Point(img.cols/2, img.rows/2), img.cols/3, Scalar(1,1,1), -1); blur(halo, halo, Size(img.cols/3, img.cols/3)); // Convert the result to float to allow multiply by 1 factor Mat resultf; result.convertTo(resultf, CV_32FC3); // Multiply our result with halo multiply(resultf, halo, resultf); // convert to 8 bits resultf.convertTo(result, CV_8UC3); // show result imshow("Lomography", result); }
Let's look at how the lomography effect works and how to implement it. The lomography effect is divided into different steps, but in our example, we did a very simple lomography effect with two steps:
- A color manipulation effect by using a look-up table to apply a curve to the red channel
- A vintage effect by applying a dark halo to the image
The first step was to manipulate the red color with a curve transform by applying the following function:
This formula generates a curve that makes the dark values darker and the light values lighter, where x is the possible pixels value (0 to 255) and s is a constant that we set to 0.1 in our example. A lower constant value that generates pixels with values lower than 128 is very dark, and over 128 is very bright. Values near to 1 convert the curve into a line and do not generate our desired effect:
This function is very easy to implement by applying an LUT. An LUT is a vector or table that returns a preprocessed value for a given value to perform computation in the memory. An LUT is a common technique used to spare CPU cycles by avoiding performing costly computations repeatedly. Instead of calling the exponential/divide function for each pixel, we perform it only once for each possible pixel value ( 256 times) and store the result in a table. Thus, we have saved CPU time at the cost of a bit of memory. While this may not make a great difference on a standard PC with small image sizes, this makes a huge one for CPU-limited hardware, such as Raspberry Pi.
For example, in our case, if we want to apply a function for every pixel in our image, then we have to make width x height operations; for example, in 100 x 100 pixels, there will be 10,000 calculations. If we can pre-calculate all possible results for all possible inputs, we can create the LUT table. In an image, there are only 256 possible values as a pixel value. If we want to change the color by applying a function, we can pre-calculate the 256 values and save them in an LUT vector. In our sample code, we define the E variable and create an lut matrix of 1 row and 256 columns. Then, we do a loop over all possible pixel values by applying our formula and saving it into an lut variable:
const double exponential_e = std::exp(1.0); // Create look-up table for color curve effect Mat lut(1, 256, CV_8UC1); Uchar* plut= lut.data; for (int i=0; i<256; i++) { double x= (double)i/256.0; plut[i]= cvRound( 256.0 * (1.0/(1.0 + pow(exponential_e, -((x-0.5)/0.1)) )) ); }
As we mentioned earlier in this section, we don't apply the function to all channels; thus, we need to split our input image by channels using the split function:
// Split the image channels and apply curve transform only to red channel vector<Mat> bgr; split(img, bgr);
We then apply our lut table variable to the red channel. OpenCV gives us the LUT function, which has three parameters:
- Input image
- Matrix of look-up table
- Output image
Then, our call to the LUT function and red channel looks like this:
LUT(bgr[2], lut, bgr[2]);
Now, we only have to merge our computed channels:
// merge result merge(bgr, result);
The first step is done and we only have to create the dark halo to finish our effect. Then, we create a gray image with a white circle inside, with the same input image size:
// Create image for halo dark Mat halo(img.rows, img.cols, CV_32FC3, Scalar(0.3,0.3,0.3)); // Create circle circle(halo, Point(img.cols/2, img.rows/2), img.cols/3, Scalar(1,1,1), -1);
Check out the following screenshot:
If we apply this image to our input image, we will get a strong change from dark to white; thus, we can apply a big blur using the blur filter function to our circle halo image to get a smooth effect:
blur(halo, halo, Size(img.cols/3, img.cols/3));
The image will be altered to give us the following result:
Now, if we have to apply this halo to our image from step 1, an easy way to do this is to multiply both images. However, we will have to convert our input image from an 8-bit image to a 32-bit float, because we need to multiply our blurred image, which has values in the 0 to 1 range, with our input image, which has integer values. The following code will do it for us:
// Convert the result to float to allow multiply by 1 factor Mat resultf; result.convertTo(resultf, CV_32FC3);
After converting our image, we only need to multiply each matrix per element:
// Multiply our result with halo multiply(resultf, halo, resultf);
Finally, we will convert the float image matrix result to an 8-bit image matrix:
// convert to 8 bits resultf.convertTo(result, CV_8UC3); // show result imshow("Lomograpy", result);
This will be the result: