Title: Use a color matrix to set an image's color tone in Python
This will be my last post about image coloring for a while. My post Use a color matrix to remove red, green, and blue color components from images in Python showed how to use a color matrix to modify the colors in an image in specific ways. This post shows how you can do the same thing more generally. It shows how you can:
- Make an image use colors more in line with a desired color tone
- Use average and grayscale techniques to make an image use only specific color tones
- Combine color matrices to perform complex operations with a single matrix
Enhanced Colors
The Color menu's Custom Tone command displays the dialog shown at the right. Use the dialog to select a color and brightness level. The tkinter code that builds the dialog is interesting but fairly long so I won't cover it here. Download the example to see how it works.
If you click the OK button, the dialog returns the red, green, and blue color components that you selected, each on a 0.0 to 1.0 scale. It returns the brightness as a scale factor between 0.0 and 2.0. Change the allowed range on the Brightness Scale widget if you want to allow larger values.
If you click the dialog's close button (the little X in its upper right corner), the dialog returns null.
When you select the Custom Tone command, the following code executes.
def mnu_custom_tone(self):
dlg = ToneDialog(self.window)
if dlg.result is not None:
# Get the dialog's results.
r, g, b, brightness = dlg.result
# Scale the color components for brightness.
r *= brightness
g *= brightness
b *= brightness
# Build the color tone matrix.
matrix = [
[r, 0, 0, 0],
[0, g, 0, 0],
[0, 0, b, 0],
[0, 0, 0, 1]
]
# Apply the matrix.
self.pil_image = PointOps.apply_matrix(self.pil_image, matrix)
self.show_result()
This code creates a new ToneDialog and, when it closes, gets its result. If the result is not null, the code gets the returned values, uses the brightness value to scale the returned color components, and then uses the components to make a color matrix. This matrix scales the image's color components in proportion to the components that you selected. For example, if the tone you selected was yellow, then the image's colors will be scaled toward yellow as shown in the picture on the right. Notice that this doesn't remove other colors, it just pushes them all toward yellow.
Tone by Average
The previous result moves the image toward a color tone but doesn't make the image use only that tone. When you invoke the Color menu's Tone by Average command, the program first averages the image's color components to create a monochrome image. It then uses the pixels' values to scale the selected color tone. For example, if you select yellow in the Color Tone dialog, you get a result that uses only yellow tones as in the picture on the right.
Here's the code that performs this coloring.
def mnu_tone_by_average(self):
dlg = ToneDialog(self.window)
if dlg.result is not None:
r, g, b, brightness = dlg.result
# Scale the color components for brightness.
r *= brightness
g *= brightness
b *= brightness
# Build the color tone matrix.
m1 = [
[r, 0, 0, 0],
[0, g, 0, 0],
[0, 0, b, 0],
[0, 0, 0, 1],
]
# Build an averaging matrix.
m2 = [
[0.33, 0.33, 0.33, 0],
[0.33, 0.33, 0.33, 0],
[0.33, 0.33, 0.33, 0],
[0.00, 0.00, 0.00, 1],
]
# Combine the matrices.
m3 = [
[sum([m1[r][i] * m2[i][c] for i in range(4)])
for c in range(4)]
for r in range(4)]
# Apply the combined matrix.
self.pil_image = PointOps.apply_matrix(self.pil_image, m3)
self.show_result()
This code displays the Color Tone dialog as before. It uses the returned brightness to scale the tone's color components and builds a color tone matrix as before.
Next, the code creates a matrix that averages the values of a pixel's red, green, and blue color components. By itself, this matrix would convert an image to monochrome by averaging.
The method then multiplies the two matrices to form a matrix that combines the two. The code then calls PointOps.apply_matrix to apply the combined matrix to the image.
The list comprehension that multiplies the matrices calculates m1 * m2. The PointOps.apply_matrix method then multiplies the image's pixels on the right so the result is basically m1 * m2 * p. Essentially matrix m2 is applied to the pixel first and then m1 is applied to the result. That means the pixel's components are first averaged and then the result is multiplied by the selected color tone. The end result is a color that is a shade of the selected tone.
This is one of the cool things about transformation matrices: you can multiply them together to combine all of their effects into one matrix. Then you can apply the single matrix instead of applying multiple matrices one at a time. In this example that reduces the number of operations by half.
If you combine more than two matrices, you can save even more time. For example, the step that scales the red, green, and blue color components with the brightness factor is the same as multiplying the following two matrices, so really we're working with three matrices here all combined into one.
|r 0 0 0| |brightness 0 0 0|
|0 g 0 0| × | 0 brightness 0 0|
|0 0 b 0| | 0 0 brightness 0|
|0 0 0 1| | 0 0 0 1|
(This technique of combining multiple operations into one matrix is more obviously useful when you're using homogeneous coordinates to transform the points in space. That's a topic for another day.)
The whole process tends to darken the result so you may want to experiment with increasing the brightness factor. The image at the right uses the same yellow color tone as the previous image but with a brightness factor of 1.5.
Tone by Grayscale
One of my earlier posts explained how to convert images to monochrome by either averaging or using a weighted average of each pixel's color components. in this example, the Color menu's Tone by Grayscale command is similar to the Tone by Average command except it uses the weighted average instead of a plain average. The only difference is in the following code, which creates the averaging matrix.
# Build a grayscale matrix.
m2 = [
[0.3, 0.3, 0.3, 0],
[0.5, 0.5, 0.5, 0],
[0.2, 0.2, 0.2, 0],
[0.0, 0.0, 0.0, 1],
]
The rest of the code is the same so I won't show it here. The result is also very similar to the result with averaging, so I won't show it here, either. Download the example to see for yourself.
Conclusion
Combining color matrices lets you perform multiple operations with a single matrix. In this case, the code scales a pixel's color components, averages the components, and then scales them unequally to produce a shade of a color tone. For example, that lets you make an image use only shades of yellow, magenta, or orange.
Download the example to experiment with it and to see additional details.
|