Search code examples
pythonimage-processingcolorsreverse-engineeringphotoshop

What Is the Algorithm Behind Photoshop's “Black and White” Adjustment Layer?


I did lot's of research but I didn't find anything (but I also don't know what kind of keywords to search for exactly). I want to be able to convert an input RGB image to grayscale but I want to be able to add more or less Reds/Yellows/Greens/Cyans/Blues/Magentas like in Photoshop. Do you know what are the equation or where I can found these equations so that I can implemented my own optimized RGB to Grayscale conversion?

Edit: In Photoshop it is called Black/White adjustment layer. I have found something but actually it doesn't seem to work. Here is my implementation (in comments are the resources needed to understand the algorithm):

import numpy as np
import scipy.misc
import matplotlib.pyplot as plt


%matplotlib inline

# Adapted from the answers of Ivan Kuckir and Royi here:
# https://dsp.stackexchange.com/questions/688/what-is-the-algorithm-behind-photoshops-black-and-white-adjustment-layer?newreg=77420cc185fd44099d8be961e736eb0c

def rgb2hls(img):
    """Adapted to use numpy from
       https://github.com/python/cpython/blob/2.7/Lib/colorsys.py"""
    r, g, b = img[:, :, 0], img[:, :, 1], img[:, :, 2]

    maxc = np.max(img, axis=-1)
    minc = np.min(img, axis=-1)
    l = (minc + maxc) / 2

    mask = np.ones_like(r)
    mask[np.where(minc == maxc)] = 0
    mask = mask.astype(np.bool)

    smask = np.greater(l, 0.5).astype(np.float32)

    s = (1.0 - smask) * ((maxc - minc) / (maxc + minc)) + smask * ((maxc - minc) / (2.0 - maxc - minc))
    s[~mask] = 0
    rc = np.where(mask, (maxc - r) / (maxc - minc), 0)
    gc = np.where(mask, (maxc - g) / (maxc - minc), 0)
    bc = np.where(mask, (maxc - b) / (maxc - minc), 0)

    rmask = np.equal(r, maxc).astype(np.float32)
    gmask = np.equal(g, maxc).astype(np.float32)
    rgmask = np.logical_or(rmask, gmask).astype(np.float32)

    h = rmask * (bc - gc) + gmask * (2.0 + rc - bc) + (1.0 - rgmask) * (4.0 + gc - rc)
    h = np.remainder(h / 6.0, 1.0)
    h[~mask] = 0
    return np.stack([h, l, s], axis=-1)


def black_and_white_adjustment(image, weights):  
    # normalize input image to (0, 1) if uint8
    if 'uint8' in (image).dtype.name:
        image = image / 255

    # linearly remap input coeff [-200, 300] to [-2.5, 2.5]
    weights = (weights - 50) / 100
    n_weights = len(weights)
    h, w = image.shape[:2]

    # convert rgb to hls
    hls_img = rgb2hls(image)

    output = np.zeros((h, w), dtype=np.float32)

    # see figure 9 of https://en.wikipedia.org/wiki/HSL_and_HSV
    # to understand the algorithm
    for y in range(h):
        for x in range(w):
            hue_val = 6 * hls_img[y, x, 0]

            # Use distance on a hexagone (maybe circular distance is better?)
            diff_val = min(abs(0 - hue_val), abs(1 - (0 - hue_val)))
            luminance_coeff = weights[0] * max(0, 1 - diff_val)

            for k in range(1, n_weights):
                luminance_coeff += weights[k] * max(0, 1 - abs(k - hue_val))

            # output[y, x] = min(max(hls_img[y, x, 1] * (1 + luminance_coeff), 0), 1)
            output[y, x] = hls_img[y, x, 1] * (1 + luminance_coeff)


    return output


image = scipy.misc.imread("your_image_here.png")
w = np.array([40, 85, 204, 60, 20, 80])
out = black_and_white_adjustment(image, w)
plt.figure(figsize=(15, 20))
plt.imshow(out, cmap='gray')

Thank you


Solution

  • Here's an attempt using PIL rather than numpy. It should be easy to convert. Without a copy of Photoshop to compare with, I can't guarantee it matches the output exactly but it does produce the exact values for the sample shown in your link. The values r_w, y_w, g_w, c_w, b_w, m_w are the weights to be applied to each color, with 1.0 equating to 100% in the corresponding Photoshop slider. Naturally they can also be negative.

    from PIL import Image
    im = Image.open(r'c:\temp\temp.png')
    def ps_black_and_white(im, weights):
        r_w, y_w, g_w, c_w, b_w, m_w = [w/100 for w in weights]
        im = im.convert('RGB')
        pix = im.load()
        for y in range(im.size[1]):
            for x in range(im.size[0]):
                r, g, b = pix[x, y]
                gray = min([r, g, b])
                r -= gray
                g -= gray
                b -= gray
                if r == 0:
                    cyan = min(g, b)
                    g -= cyan
                    b -= cyan
                    gray += cyan * c_w + g * g_w + b * b_w
                elif g == 0:
                    magenta = min(r, b)
                    r -= magenta
                    b -= magenta
                    gray += magenta * m_w + r * r_w + b * b_w
                else:
                    yellow = min(r, g)
                    r -= yellow
                    g -= yellow
                    gray += yellow * y_w + r * r_w + g * g_w
                gray = max(0, min(255, int(round(gray))))
                pix[x, y] = (gray, gray, gray)
        return im
    

    Using this provided test image, here are some example results.

    color test image

    ps_black_and_white(im, [-17, 300, -100, 300, -200, 300])
    

    -17, 300, -100, 300, -200, 300

    ps_black_and_white(im, [40, 60, 40, 60, 20, 80])
    

    40, 60, 40, 60, 20, 80

    ps_black_and_white(im, [106, 65, 17, 17, 104, 19])
    

    106, 65, 17, 17, 104, 19