Title: Use point operations to make grayscale images in Python
Some of my earlier posts showed how to apply filters to modify images. This example shows how to use point processes to convert an image to grayscale or sepia tone.
Point Processes
In image processing, a point process is one that modifies a pixel's color based only on its current color. In contrast, a filter uses the colors of pixels surrounding a target pixel to set the target's new color. Because a point process uses only the target pixel's color, this operation is simpler.
For this example, I created a PointOps mostly to keep all of the point operations in one place. Here's how the class begins.
class PointOps:
@staticmethod
def apply(image, func):
'''Apply the function to each point.'''
# Make a copy and get its pixels.
result_image = image.copy()
result_pixels = result_image.load()
# process the pixels.
for y in range(image.height):
for x in range(image.width):
# Apply the function to pixel[x, y].
result_pixels[x, y] = func(*result_pixels[x, y])
# Return the result.
return result_image
This code defines a static apply method that applies a function to each of the pixels in an image. The method makes a copy of the image and calls load so it can quickly access the image's pixel values.
The code then loops through the image's pixels. For each pixel, it gets the pixel's red, green, and blue color components, passes those values into the func function, and saves the result back in the result image's pixel.
After it processes all of the image's pixels, the function returns the result image.
The apply method is fairly straightforward. The thing that makes it useful is the func function that you pass into it.
Averaging
The PointOps class provides a few methods to apply point operations to images, and they all follow the same basic pattern. Each defines a nested function and then passes the image and the nested function to the apply method.
The following code shows the to_average method.
@staticmethod
def to_average(image):
def rgb_average(r, g, b):
a = round((r + g + b) / 3)
return a, a, a
return PointOps.apply(image, rgb_average)
This code defines the nested rgb_average function. That function simply averages the red, green, and blue color components and then returns three copies of the result, one for the new red, green, and blue color values.
After it defines the nested function, the to_average method calls apply, passing it the image and the rgb_average function.
That's all there is to it! The result is an image where each pixel's color components are set to the average of their original values. The other point processes in this example are just as easy.
Grayscale
The to_average method averages each pixel's color components, but it turns out that the human eye is more sensitive to green color than it is to red or blue color. That means the green color should count more towards an image's brightness when we convert the image to grayscale. You can make a grayscale image seem more natural if you use a weighted average of each pixel's color components instead of taking a simple mean.
The following code shows how the PointOps class implements its to_grayscale method.
@staticmethod
def to_grayscale(image):
def rgb_grayscale(r, g, b):
a = round(0.3 * r + 0.5 * g + 0.2 * b)
return a, a, a
return PointOps.apply(image, rgb_grayscale)
This code first defines a nested rgb_grayscale function that takes a weighted average of the pixel's color components. It gives the most weight to the green component, less weight to the red component, and the least weight to the blue component.
The to_grayscale method calls apply, passing it the image and the rgb_grayscale function and Bob's your uncle!
Sepia Tone
Back in the old days, when people used stinky chemicals to develop film instead of capturing images on a phone and then printing them, photographers and archivists could use a chemical process to convert a photo's silver into more stable silver sulfide. That made the picture last longer without fading, but it also gave them a warm reddish-brown color that gives the images a nostalgic, antique quality.
Converting an image to sepia tone is a point process, although it's slightly more complicated than averaging an image or converting it to grayscale. In sepia tone, a pixel's red, green, and blue components each depend on the values of the other components. Basically you multiply the vector of color components by a transformation matrix to get the new component values.
Here's the PointOps class's to_sepia_tone method.
@staticmethod
def to_sepia_tone(image):
def rgb_sepia_tone(r, g, b):
new_r = round(r * 0.393 + g * 0.769 + b * 0.189)
new_g = round(r * 0.349 + g * 0.686 + b * 0.168)
new_b = round(r * 0.272 + g * 0.534 + b * 0.131)
return new_r, new_g, new_b
return PointOps.apply(image, rgb_sepia_tone)
This code defines the nested rgb_sepia_tone to adjust a pixel's color components. The numbers that this function uses are magically chosen to convert a color value into a sepia tone value. (You'll see something similar in my next post, which converts images to more general color tones.)
The to_sepia_tone calls apply, passing it the image and the rgb_sepia_tone function.
Conclusion
The picture on the right shows a picture of a bird and three recolored versions. The image picture on the upper right has been averaged, the image on the lower left has been converted to grayscale, and the image on the lower right has been converted to sepia tone. You'll have to look closely to see the difference between the averaged and grayscaled images.
Applying point operations to an image is pretty easy. First define a function that maps RGB values to new RGB values. Then pass the image and your function to the ImageOps class's apply method.
The to_sepia_tone method showed how you can give an image a particular tone. In my next post, I'll show how you can make an image use a more general color tone such as a yellow, orange, or purple tone.
Meanwhile, download the example to experiment with it and to see additional details.
|