Image Editor using Unity
I was recently accepted into the Florida Interactive Entertainment Academy for the Technical Art track. I was checking out some medium blogs to to learn some image manipulation to brush up some of my technical art skils. Them medium gods were actually pretty darn helpful and I somehow ended up developing a janky photo editor using Unity. It transforms any regular image into a “low-quality-meme”. I call it the “Low Res Meme Editor”. I’ll be discussing my approach towards developing the photo editor in this blog.
1.0 Preparing Images for Editing
The first step was transforming the images into a suitable format for easy manipulation. Texture2D
felt appopriate for this purpose, as it gives me more control over the pixel data. I could directly manipulate pixel values using the GetPixel()
and SetPixel()
methods of any Texture2D object.
- So I created method that took an image an returned a Texture2D corresponding to it.
- Then I created an empty texture with the width and height of the image.
- Finally the pixels on the original image are mirrored on the pixels of the empty texture using the
SetPixels()
method.
2.0 Image Editing Algorithms: Approach
I chose to work on a series of image editing techniques. My plan was to apply these algorithms with different settings, essentially creating pre-defined configurations for editing images. Most of these algorithms shall serve the sole purpose of giving the image a low-quality-meme look”.
2.1 Saturation Control
The first control I wished to establish was for Saturation i.e the intensity of the color of each pixel of the image.
- A saturation value of 0 makes the image grayscale.
- A value of 1 keeps the original colors unchanged.
- Anything above 1 makes the image more vibrant.
So the basic algorithm I used to achieve saturation control was–
- Getting the texture from the image and creating an empty texture to assign the edits texture to.
- Looping through the pixels to access the HSV values from their color. The
saturation
value is of importance here as we are going to alter it, so it is passed as a reference type.
- Applying the saturation to each pixel. The
saturation
value is multiplied by asaturationFactor
to alter it. And finally a new array of Color of length the same as the original image is assigned the color with the newsaturation
value.
- A new texture with the saturated pixels is created and assigned to a sprite.
2.2 Adding Gaussian Blur
Next in the list are algorithms that helps blur an image using a Gaussian Function. This helps achieve the bokeh effect. It has the effect of reducing an image’s high frequency components and thus acting like a low-pass filter.
- Creating the Gaussian Kernel
- First we create a gaussian kernel of a given size and radius. The gaussian kernel is a float 2D matrix (
float[,]
). Each element in the matrix is a weight that determines the contribution of each pixel to the blurring process. These weights are caluclated based on the Gaussian distribution formula.
- Input Parameters: The
CreateGaussianKernel()
method takes theradius
andsize
of the kernel as the parameter and returns a 2D matrix (float[,]
). Thesize
represents the dimensions of the kernel, and theradius
controls the spread of the blur effect.
- Initialization: An empty 2D array called
kernel
of size x size is initialized. This array will store the weights that determine the strength of blurring for each pixel. The variablesigma
is calculated asradius / 3f
.sigma
is a parameter that affects the spread of the Gaussian distribution. A larger sigma value results in a wider spread of the blur. The constant for the gaussian distribution is calculated as twice thesigma
squared.
- Gaussian and Total Weight Calculation: The nested loops iterate over each element of the
kernel
array. For each element at position [y, x], it calculates the squared distance from the center of the kernel. The formula x * x + y * y calculates the squared Euclidean distance. Using the squared distance, it calculates the weight using the formula Mathf.Exp(-distance / twoSigmaSquare). This weight is a measure of how much influence a pixel at that distance should have on the blurring process. The greater the distance, the smaller the weight. The calculated weight is assigned to the corresponding position in the kernel array. ThetotalWeight
variable keeps track of the sum of all the weights in the kernel. This sum is used to normalize the kernel later.
- Kernel Normalization: After calculating all the weights, the kernel is normalized to ensure that the sum of all the weights equals 1. This step is essential to maintain the overall brightness of the image during blurring. The nested loops iterate over each element of the
kernel
array again. Each weight is divided by thetotalWeight
, effectively scaling down the weights to ensure their sum equals 1. And then thekernel
is returned.
- Applying the Gaussian Kernel
The gaussian kernel created above is applied to the pixels of the image thereby convulating the kernel with the particular pixel and its neighbouring pixels, thus calculating the weighted sum of their colors. This sum becomes the new color value for the central pixel, resulting in the blurring effect.
- Input Parameters: The
ApplyKernel()
method takes a Texture2D as input along with akernel
,x
,y
(position within the image) andsize
of the kernel as the parameter. It then calculates and returns the weighted average of colors in the neighborhood around the (x, y) position using the provided Gaussian kernel.
- Initialization: The 3 float variables for the RGB values are initalized to accumulate the weighted sum of red, green, and blue color channels. The half-size of the kernel is calculated as well and this is used to determine the bounds of the neighborhood around the (x, y) position.
- Looping through the Kernel: Nested for loops iterate over each cell in kernel. The outer
j
loop iterates over the rows of the kernel whereas the inneri
loop iterates over its columns.
- Calculating pixel coordinates of Input Image:
offsetX
andoffsetY
is calculated inside the loop to represent the corresponding pixel coordinates in the input image based on the currentx
andy
position within the image and thei
andj
indices. Both these values are clamped to the bounds of the image.
- Retrieving and Updating the color values of pixels: The color values of pixels at calculated coordinates
offsetX
andoffsetY
are retrieved from the input texture. The RGB values intialized earlier are updated by adding the color values retrieved multiplied by the weight value from the Gaussian Kernel matrix from thei
andj
position. The final color obtained is returned.
- Applying the Gaussian Blur Now we apply the gaussian blur to the image by calculating the kernel, processing the pixel and updating the image.
- Input Parameters: The
ApplyGaussianBlur()
method takes a Texture2D as input along with theradius
of the Gaussian Blur needed (basically used as the size of the kernel) and returns the blurred texture.
- Calculating Kernel size and Kernel: The Kernel size is obtained by rounding the
radius
to the nearest integer, doubling it and then adding 1 to it, ensuring coverage of sufficient area around each pixel.
- Creating Blurred Texture: A new
blurredTexture
with same dimensions assourceTexture
is initialized.
- Iterating through pixels: A nested loop is used to access the pixels of the
sourceTexture
. For each pixel at (x
,y
), theApplyKernel()
function is called to calculate the new color of that pixel. The calculated blurrer color is stored in the corresponding position of theblurredPixels
array.
- Applying Blurred pixels to the Texture: After processing all the pixels, the
blurredPixels
array is assigned to theblurredTexture
and it is returned.
- Blurred Sprite: A sprite is created from the blurred texture and is applied to the image.