Pixel processing and LUT

What is pixel processing?

The program performs an operation on each pixel, ignoring the values of any of the neighbouring pixels. It is suitable for simple modifications and runs quickly, but is not suitable for more complex operations such as noise filtering, blurring or sharpening, which require knowledge of the surrounding pixels.

Brightness, Contrast

Increasing the value of each pixel in an image by a constant value increases the brightness, thus subtracting the constant value decreases the brightness. Contrast is the difference between the darkest and the brightest points in an image, and can be adjusted by multiplication. The formula involves subtraction, to compensate for the change in brightness by the contrast operation.

Grayscale conversion

There are several methods to convert colour images to greyscale, such as averaging the three colour channels of pixels. The values of each channel are usually multiplied by numbers given by a standard. Each new pixel is calculated using the dot product of the original pixel (as a vector) and the vector from the standard.

According to Rec.709:

$ 0.2126 ⋅ R + 0.7152 ⋅ G + 0.0722 ⋅ B $

Lookup Table (LUT)

A very quick way to make pixel-level changes is to use a Lookup Table, or LUT for short. A list of all the possible values at the given bit depth is created along with their new values according to the formula. The computer does not have to do any calculations, just look up the new value for the current pixel. This technique is widely used for movie editing, adding filters in social media applications, storing sine values, etc.

Help

Boglárka image: Download

Source code: Brightness, Contrast

        
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

# Open the image using PIL
image = Image.open("path-to-resources/boglarka.jpg")

# Convert image to a NumPy array
data = np.array(image, dtype=np.uint8)
print(data.shape)  # (height, width, channels)

# Set brightness and contrast
brightness = 25  # [-127;127] where 0 means unchanged
contrast = 75    # [-127;127] where 0 means unchanged

# Extend data type to potential negative values (will be converted back later)
data = np.int16(data) # 16 bit integer
# Perform the modification
data = data * (contrast / 127 + 1) - contrast + brightness
# Clip values between 0 and 255 (<= 0 to 0 and >= 255 to 255)
data = np.clip(data, 0, 255)
# Convert back to 8 bit unsigned integer
final_image = np.uint8(data)

# Display the image
plt.imshow(final_image)
plt.axis('off')
plt.show()
          
        

Source code: Grayscale

        
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

# Open the image using PIL and ensure the correct (RGB) color mode
image = Image.open("path-to-resources/boglarka.jpg").convert("RGB")

# Convert image to a NumPy array
data = np.array(image, dtype=np.uint8)
print(data.shape)  # (height, width, channels)

# RGB array for the conversion (Rec.709 standard)
conv_arr = [0.2126, 0.7152, 0.0722] # R, G, B

# Perform the conversion
final_image = np.dot(data, conv_arr).astype(np.uint8)

# Display the image
plt.imshow(final_image, cmap="gray")
plt.axis('off')
plt.show()
          
        

Source code: LUT

        
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
from timeit import default_timer as timer

# Open the image using PIL and convert to grayscale
image = Image.open("path-to-resources/boglarka.jpg").convert("L")

# Convert image to a NumPy array
data = np.array(image, dtype=np.uint8)
print(data.shape)  # (height, width, channels)

# Set brightness and contrast
brightness = 25  # [-127;127] where 0 means unchanged
contrast = 75    # [-127;127] where 0 means unchanged

# Build a grayscale LUT
myLUT = np.zeros(256, dtype = np.uint8)
for li in range(255):
    myLUT[li]= np.clip(li * (contrast / 127 + 1) - contrast + brightness, 0, 255)

# Measure execution time of applying the LUT
start1 = timer()
# Apply the LUT
final_image1 = myLUT[data]
# End timer
end1 = timer()

# Measure execution time of basic processing
start2 = timer()
# Change brigtness and contrast with the usual method
final_image2 = np.clip(data * (contrast / 127 + 1) - contrast + brightness, 0, 255)
final_image2 = np.uint8(final_image2)
# End timer
end2 = timer()

print(f"Basic method: {round((end2 - start2) * 1000, 3)}ms")
print(f"Using a LUT: {round((end1 - start1) * 1000, 3)}ms")

# Display images
fig, ax = plt.subplots(1, 2, figsize=(12, 6))
ax[0].imshow(final_image1, cmap="gray")
ax[0].set_title("LUT-applied Image")
ax[0].axis("off")
ax[1].imshow(final_image2, cmap="gray")
ax[1].set_title("Basic Processing Image")
ax[1].axis("off")
plt.show()