skip to Main Content

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

2

Answers


  1. Chosen as BEST ANSWER

    I answer my own question by adding the numpy/scipy version of the code, if it can be of any interest for anybody in the future. If you want to upvote an answer, you should upvote the answer of Mark Ransom !

    import numpy as np
    import scipy.misc
    import matplotlib.pyplot as plt
    
    %matplotlib inline
    
    def black_and_white_adjustment(img, weights):
        rw, yw, gw, cw, bw, mw = weights / 100
    
        h, w = img.shape[:2]
        min_c = np.min(img, axis=-1).astype(np.float)
        # max_c = np.max(img, axis=-1).astype(np.float)
    
        # Can try different definitions as explained in the Ligtness section from
        # https://en.wikipedia.org/wiki/HSL_and_HSV
        # like: luminance = (min_c + max_c) / 2 ...
        luminance = min_c 
        diff = img - min_c[:, :, None]
    
        red_mask = (diff[:, :, 0] == 0)
        green_mask = np.logical_and((diff[:, :, 1] == 0), ~red_mask)
        blue_mask = ~np.logical_or(red_mask, green_mask)
    
        c = np.min(diff[:, :, 1:], axis=-1)
        m = np.min(diff[:, :, [0, 2]], axis=-1)
        yel = np.min(diff[:, :, :2], axis=-1)
    
        luminance = luminance + red_mask * (c * cw + (diff[:, :, 1] - c) * gw + (diff[:, :, 2] - c) * bw) 
                    + green_mask * (m * mw + (diff[:, :, 0] - m) * rw + (diff[:, :, 2] - m) * bw)  
                    + blue_mask * (yel * yw + (diff[:, :, 0] - yel) * rw + (diff[:, :, 1] - yel) * gw)
    
        return np.clip(luminance, 0, 255).astype(np.uint8)
    
    input_img = scipy.misc.imread("palette.jpg")
    
    weights = np.array([106, 65, 17, 17, 104, 19])
    bw_image = black_and_white_adjustment(input_img, weights)
    
    plt.figure(figsize=(15, 20))
    plt.imshow(bw_image, cmap="gray")
    

    This code is faster as it uses vect operations.


  2. 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:temptemp.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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search