# Write simple image filters in Python

Recently I became interested in how simple image filters work: grayscale, brightness, contrast, sepia, etc. Armoured with Google and Python, I decided to spend an evening doing something useful and figuring out the subject a little.

This article isn't a comprehensive manual. Some algorithms I got from Wikipedia or other sites and some I rewrote to Python from another languages. All of them are available on the Internet and you won’t find anything especially new here.

You can find the source code and images from the article here.

This is an image we will mock on in the article: When working with images we use Python library PIL. We need only one class from it:

``from PIL import Image``

## Brightness changing

Let's start from the simplest - brightness changing. If we want to increase brightness, then we should increase the value of each colour channel by needed coefficient. If we want to decrease brightness - decrease values. For all channels we should use the same coefficients. If the resulting value of the channel is greater than 255 or less than 0, then we must normalize it.

``````def bright(source_name, result_name, brightness):
source = Image.open(source_name)
result = Image.new('RGB', source.size)
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))

red = int(r * brightness)
red = min(255, max(0, red))

green = int(g * brightness)
green = min(255, max(0, green))

blue = int(b * brightness)
blue = min(255, max(0, blue))

result.putpixel((x, y), (red, green, blue))
result.save(result_name, "JPEG")``````

If `brightness` if greater than `1`, the image will become brighter. If `brightness` is less than `1`, the image will become darker. Here is how the source image will look with `brightness = 0.5` and `brightness = 1.5` respectively:

## Negative

One more elementary filter is negative. For it we should invert the value of each channel. Just subtract the channel value from `255`. The resulting value can't go beyond limits here, so we need no validation.

``````def negative(source_name, result_name):
source = Image.open(source_name)
result = Image.new('RGB', source.size)
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
result.putpixel((x, y), (255 - r, 255 - g, 255 - b))
result.save(result_name, "JPEG")``````

This is the result of processing the source image by this filter: ## Black and white image

An image is drawn by only two colours: black and white. So we should split all pixels into two groups: light and dark. For this we summarize values of all channels and compare the result with the threshold value. If the result if greater than this value, pixels will be drawn in black colour, otherwise - in white colour. There are several ways to calculate the threshold value. I like this one: `separator = 255 / brightness / 2 * 3`. Where `brightness` is a value, specified by a user. The higher the number, the more pixels will be painted white.

``````def white_black(source_name, result_name, brightness):
source = Image.open(source_name)
result = Image.new('RGB', source.size)
separator = 255 / brightness / 2 * 3
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
total = r + g + b
if total > separator:
result.putpixel((x, y), (255, 255, 255))
else:
result.putpixel((x, y), (0, 0, 0))
result.save(result_name, "JPEG")``````

The source image with `brightness = 0.8` and `brightness = 1.2` respectively:

## Grayscale

Speaking roughly, for this filter we should paint each pixel with an average value of all its colour channels `gray = (red + green + blue) /3`. This formula will work but the result won’t be very good. The fact is that the human eye perceives the brightness of different colours differently. Because of this, for example, red colour will have more impact on the result than other colours. We should remember this if we want to get a pretty resulting image.

For this issue we will multiply values of all colour channels by a specific coefficient. The coefficient will be different for all channels. There are some different variants of these coefficients. In my opinion this formula is the best: `gray = int(r * 0.2126 + g * 0.7152 + b * 0.0722)`.

``````def gray_scale(source_name, result_name):
source = Image.open(source_name)
result = Image.new('RGB', source.size)
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
gray = int(r * 0.2126 + g * 0.7152 + b * 0.0722)
result.putpixel((x, y), (gray, gray, gray))
result.save(result_name, "JPEG")``````

The filter result: ## Sepia

To represent the source image in sepia we should average the value of all colour channels and displace the resulting value to the sepia colour (`#704214`). Same as in grayscale there are some different variants of displaced coefficients. I didn't notice any difference between them.

``````def sepia(source_name, result_name):
source = Image.open(source_name)
result = Image.new('RGB', source.size)
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
red = int(r * 0.393 + g * 0.769 + b * 0.189)
green = int(r * 0.349 + g * 0.686 + b * 0.168)
blue = int(r * 0.272 + g * 0.534 + b * 0.131)
result.putpixel((x, y), (red, green, blue))
result.save(result_name, "JPEG")``````

The source image in sepia: ## Contrast

The last filter reviewed - changing of an image contrast. This algorithm is more difficult and contains several steps.

At first we calculate an average brightness value for the full image.

``````avg = 0
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
avg += r * 0.299 + g * 0.587 + b * 0.114
avg /= source.size * source.size``````

After this for each channel of each pixel we calculate the deviation from the value that we calculated in the previous step and increase/decrease it by coefficient specified by a user. This step can be optimized. Since the channels values are between `0` and `255`, we can precalculate new values for all of them. And then just take the precalculated value for each channel.

``````palette = []
for i in range(256):
temp = int(avg + coefficient * (i - avg))
if temp < 0:
temp = 0
elif temp > 255:
temp = 255
palette.append(temp)``````

Process the image:

``````for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
result.putpixel((x, y), (palette[r], palette[g], palette[b]))``````

In the end our filter looks like this:

``````def contrast(source_name, result_name, coefficient):
source = Image.open(source_name)
result = Image.new('RGB', source.size)

avg = 0
for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
avg += r * 0.299 + g * 0.587 + b * 0.114
avg /= source.size * source.size

palette = []
for i in range(256):
temp = int(avg + coefficient * (i - avg))
if temp < 0:
temp = 0
elif temp > 255:
temp = 255
palette.append(temp)

for x in range(source.size):
for y in range(source.size):
r, g, b = source.getpixel((x, y))
result.putpixel((x, y), (palette[r], palette[g], palette[b]))

result.save(result_name, "JPEG")``````

And contrast image with `coefficient = 2` looks like this: ## Conclusion

In this article we reviewed only the simplest image filters. I didn't set a goal to show something cool. I just wanted to know how they work and try to implement them by myself. Maybe it will be interesting for someone else.