Catching up
If you have not currently got the following activities done from last session, before continuing please complete them:
-
Loading the support library for image processing
-
Opening an image file from disk
-
Iterating through the image using loops
-
Saving the modifed image to disk
You need to have these activities completed before attempting to do today’s work, as it assumes you have the ability to load the image.
Colour Distance
You can calculate the distance between two colours by treating them as two 3D coordinates and calculating the distance between the two points.
Two Dimensional Space
If we assume we have two vectors:
We can calculate distance using Pythagoras' theorem
Three Dimensional Space
If we extend this into three dimensions, we get:
For colours, we can treat each of the components (red, green, blue) as the three axis (x, y, z).
Note
|
this does not take into account human colour perception, which does not treat all three of these colours equally. |
Utility function
We’re going to build a utility function to calculate this distance as part of this lab. This is the rough structure of the code. As we are going to using these for comparison purposes, we can avoid the cost of sqrt and omit it from the calculation. In other words, we are going to use the 'squared distance'. You may see this trick used in some game engine code.
public static int GetSquareDistance(SkiaSharp.SKColor first, SkiaSharp.SKColor second) {
int redDifference = first.Red - second.Red;
// TODO do the same for green and blue channels
return (redDifference * redDifference)
+ (greenDifference * greenDifference)
+ (blueDifference * blueDifference);
}
Tip
|
you can use Mathf or Math for mathematical utility functions. |
Complete the variable definitions for the green and blue channels. There is a TODO statement in the above code block to tell you were to put this in the code.
Predicates
A boolean function is sometimes known as a predicate, we’re going to create a function that makes use of our existing function to return true or false depending on how close the value is. We can then use this in our other functions:
public static bool isClose(SkiaSharp.SKColor first, SkiaSharp.SKColor second) {
// use our existing code to calculate distance
int distance = GetSquareDistance(first, second);
// perform the threshold check
if ( distance < 300 ) {
return true;
} else {
return false;
}
}
(advanced) default arguments
In the above function, I hard-coded the threshold we were using. This presents a problem - if we want different thresholds for different use cases, we need to write different functions. This is quite wasteful and results in a lot of code duplication. We can provide a default parameter value when defining the function. If we don’t provide a value, the default will be used, if we do provide a value then the value we provided will be used.
Lets modify our function to make use of this feature:
public static bool isClose(SkiaSharp.SKColor first, SkiaSharp.SKColor second, float threshold=300) {
// use our existing code to calculate distance
int distance = GetSquareDistance(first, second);
// perform the threshold check
if ( distance < threshold ) {
return true;
} else {
return false;
}
}
Tolerance-based pixel manipulation
We can modify images using threshold values. We are going to create a function which uses our new function as a way of detecting if we should alter the pixel based on its colour. A single argument predicate is often useful for fluent interface (linq).
Lets write a single-argument function we could use for this purpose (actually using linq is outside the scope of this workshop).
public static bool shouldTurnRed(SkiaSharp.SKColor newColour) {
SKColor brown = SKColors.Brown;
// TODO if you don't have the three-argument version, you will need a function
// for this part that 'hard-codes' a distance check for 4000.
if ( isClose(newColour, brown, 4000) ) {
return true;
} else {
return false;
}
}
This function can be simplified, as we are effectivly returning the result of isClose. How would we do this?
Applying our function
We can manipulate the image in a similar way that we did in the previous workshop, iterate through every pixel in the image and update it based on some condition. We will use our function as a way of figuring out what pixels we will alter.
In this case, we will selectively turn some pixels bright red.
SkiaSharp.SKColor red = new SkiaSharp.SKColor(255, 0, 0);
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
SkiaSharp.SKColor color = image.GetPixel(x, y);
if ( shouldTurnRed(colour) ) {
image.SetPixel(x, y, red);
}
}
}
Use what we learnt last session to:
-
load an image from disk using the library
-
modify the image according to our new function
-
save the resulting image to disk
Extention Tasks
On the learning space, you can find a range of other algorithms to try. Attempt to convert some of these to functions in your code.