In this recipe, we'll learn how one can apply a custom photo effect to images from Gallery. We will implement a "retro" filter with OpenCV, so that the photographs look old, as shown in the following screenshot:
The source code for this recipe can be found in the Recipe07_ApplyingRetroEffect
folder in the code bundle that accompanies this book. You can use the iOS Simulator to work on this recipe.
This recipe heavily relies on the previous one, as we're going to implement the same workflow: loading images from Gallery, processing them with OpenCV, and displaying them on the screen.
The following are the steps required to apply our filter to an image from Gallery:
First of all, we need to implement our custom filter. We'll create the
RetroFilter
class in C++ for that purpose.Then we have to modify the
ViewController
class properly, by adding appropriate fields and its initialization in theviewDidLoad
method.Finally, we'll implement the
applyFilter
method that wraps the call to theRetroFilter
class.
Let's implement the described steps:
The following is a declaration from the
RetroFilter.hpp
file for a class that is going to be used for photo stylization:class RetroFilter { public: struct Parameters { cv::Size frameSize; cv::Mat fuzzyBorder; cv::Mat scratches; }; RetroFilter(const Parameters& params); virtual ~RetroFilter() {}; void applyToPhoto(const cv::Mat& frame, cv::Mat& retroFrame); void applyToVideo(const cv::Mat& frame, cv::Mat& retroFrame); protected: Parameters params_; cv::RNG rng_; float multiplier_; cv::Mat borderColor_; cv::Mat scratchColor_; std::vector<cv::Mat> sepiaPlanes_; cv::Mat sepiaH_; cv::Mat sepiaS_; };
We'll consider implementations for two main methods from the
RetroFilter.cpp
file. The following is a constructor for the class:RetroFilter::RetroFilter(const Parameters& params) : rng_(time(0)) { params_ = params; multiplier_ = 1.0; borderColor_.create(params_.frameSize, CV_8UC1); scratchColor_.create(params_.frameSize, CV_8UC1); sepiaH_.create(params_.frameSize, CV_8UC1); sepiaH_.setTo(Scalar(19)); sepiaS_.create(params_.frameSize, CV_8UC1); sepiaS_.setTo(Scalar(78)); sepiaPlanes_.resize(3); sepiaPlanes_[0] = sepiaH_; sepiaPlanes_[1] = sepiaS_; resize(params_.fuzzyBorder, params_.fuzzyBorder, params_.frameSize); if (params_.scratches.rows < params_.frameSize.height || params_.scratches.cols < params_.frameSize.width) { resize(params_.scratches, params_.scratches, params_.frameSize); } }
And the following is the implementation of the main processing method:
void RetroFilter::applyToPhoto(const Mat& frame, Mat& retroFrame) { Mat luminance; cvtColor(frame, luminance, CV_BGR2GRAY); // Add scratches Scalar meanColor = mean(luminance.row(luminance.rows / 2)); scratchColor_.setTo(meanColor * 2.0); int x = rng_.uniform(0, params_.scratches.cols - luminance.cols); int y = rng_.uniform(0, params_.scratches.rows - luminance.rows); cv::Rect roi(cv::Point(x, y), luminance.size()); scratchColor_.copyTo(luminance, params_.scratches(roi)); // Add fuzzy border borderColor_.setTo(meanColor * 1.5); alphaBlendC1(borderColor_, luminance, params_.fuzzyBorder); // Apply sepia-effect sepiaPlanes_[2] = luminance + 20; Mat hsvFrame; merge(sepiaPlanes_, hsvFrame); cvtColor(hsvFrame, retroFrame, CV_HSV2RGB); }
On the Objective-C side, we need to add the
RetroFilter::Parameters
member to theViewController
class and theapplyFilter
method with the following implementation:- (UIImage*)applyFilter:(UIImage*)inputImage; { cv::Mat frame; UIImageToMat(inputImage, frame); params.frameSize = frame.size(); RetroFilter retroFilter(params); cv::Mat finalFrame; retroFilter.applyToPhoto(frame, finalFrame); return MatToUIImage(finalFrame); }
The remaining Objective-C code is based on the previous recipe, so it is not shown here.
The only new information in this recipe is the implementation of the RetroFilter
class. It uses popular OpenCV functions, and we will explain only its most interesting part—the applyToPhoto
method.
This method applies a sequence of processing steps that help us to achieve a "retro" effect. The key idea is to convert an image to a monochrome color space, do all the processing in it, and eventually convert it back to RGB with the sepia effect.
Both scratches and borders are rendered with a color that depends on a mean color of the image. To avoid costly analysis of the whole image, we only look into middle row of the image:
Scalar meanColor = mean(luminance.row(luminance.rows / 2));
You can also see that we are using the cv::RNG
class (initialized with rng_(time(0))
) to choose a region on the image with scratches randomly. This allows us to get different patterns of scratches for different images.
Finally, we assemble back the channels of our image. We add a value of 20
to the luminance plane, so the contrast is artificially decreased. After that, we use the OpenCV merge
function to pack color planes into the single image, then convert it to the RGB color space with the help of the cvtColor
function.