Search code examples
pythonnumpypython-imaging-library

How to partially change the colors of an image in python?


How can I change the RGB values for given indices in an image? This gives me a blank picture:

import numpy as np
from PIL import Image

target_image = Image.open("img.png")
target_image_array = np.array(target_image)
pixels = target_image_array.reshape(-1, 3)

skip_indices = np.arange(101_600, 254_000)

indices_to_grey_out = np.delete(pixels, skip_indices, 0)

pixels_greyed_out = pixels.copy()
pixels_greyed_out[indices_to_grey_out, [0, 1, 2]] = 0

pixels_greyed_out = pixels_greyed_out.reshape(target_image_array.shape)
modified_img = Image.fromarray(pixels_greyed_out)
modified_img.show()

Solution

  • I don't understand what you are trying to do with flattened and discontiguous indices at all, but going purely off your comments, I understand you want to affect some specific colours in an image so I'll try and demonstrate that.

    Here is a radial gradient image that is rgb(20, 50, 140) at the centre and rgb(40, 70, 160) at the edge, so I happen to know there are 4,884 (non lineaarly adjacent, if that matters) pixels with value rgb(25,55,145)

    enter image description here

    Now, let's open it and make it into a Numpy array:

    from PIL import Image
    import numpy as np
    
    # Open image and make into Numpy array
    im = Image.open('a.png').convert('RGB')
    na = np.array(im)
    
    # Make Boolean (True/False) mask of interesting pixels
    mask = np.all(na==[25,55,145], axis=-1)
    
    # Briefly save the mask so we can see what it selects
    Image.fromarray(mask).save('DEBUG-mask.png')
    
    # Let's also count the number of pixels selected by the mask
    print(np.count_nonzero(mask))       # prints 4,884
    

    enter image description here

    Now we can use the mask to change the selected pixels. Let's make the masked pixels red in the original image:

    na[mask] = [255,0,0]
    Image.fromarray(na).save('DEBUG-1.png')
    

    enter image description here

    Now use the inverse of the mask to affect unselected pixels. Let's make them grey:

    na[~mask] = [128,128,128]
    Image.fromarray(na).save('DEBUG-2.png')
    

    enter image description here


    If you want to mask more than one colour, use can use logical operations to combine the masks together. So, let's say you wanted to additionally mask the colour rgb(28,58,148), you could add the following to the end of the code above:

    # Mask for second colour
    mask2 = np.all(na==[28,58,148], axis=-1)
    
    # Combine the two masks into new, bigger mask
    both = mask | mask2
    
    # Make masked pixels yellow and display
    na[both] = [255,255,0]
    Image.fromarray(na).show()
    

    enter image description here


    If you want to mask a range of colours, I would suggest OpenCV inRange() which lets you set a lower and upper limit for R, G and B and then selects everything in-between.

    import cv2
    
    # Mask range 25>R>35, 55>G>65, 145<B<160
    colRange = cv2.inRange(na,np.array([25,55,145]) ,np.array([35,65,160]))
    
    
    # Make range of colours lime green
    na[colRange>0] = [0,255,0]
    

    enter image description here


    If anyone is curious about how I made the radial gradient to start off with, I used ImageMagick in the Terminal and the following command:

    magick -size 500x500 radial-gradient:"rgb(20, 50, 140)-rgb(40, 70, 160)" -depth 8 a.png
    

    A simpler, clearer version might be:

    magick -size 500x500 radial-gradient:lime-magenta -depth 8 a.png