21

I am trying to remove a certain color from my image however it's not working as well as I'd hoped. I tried to do the same thing as seen here Using PIL to make all white pixels transparent? however the image quality is a bit lossy so it leaves a little ghost of odd colored pixels around where what was removed. I tried doing something like change pixel if all three values are below 100 but because the image was poor quality the surrounding pixels weren't even black.

Does anyone know of a better way with PIL in Python to replace a color and anything surrounding it? This is probably the only sure fire way I can think of to remove the objects completely however I can't think of a way to do this.

The picture has a white background and text that is black. Let's just say I want to remove the text entirely from the image without leaving any artifacts behind.

Would really appreciate someone's help! Thanks

Community
  • 1
  • 1
Cookies
  • 447
  • 2
  • 5
  • 9

7 Answers7

28

The best way to do it is to use the "color to alpha" algorithm used in Gimp to replace a color. It will work perfectly in your case. I reimplemented this algorithm using PIL for an open source python photo processor phatch. You can find the full implementation here. This a pure PIL implementation and it doesn't have other dependences. You can copy the function code and use it. Here is a sample using Gimp:

alt text to alt text

You can apply the color_to_alpha function on the image using black as the color. Then paste the image on a different background color to do the replacement.

By the way, this implementation uses the ImageMath module in PIL. It is much more efficient than accessing pixels using getdata.

EDIT: Here is the full code:

from PIL import Image, ImageMath

def difference1(source, color):
    """When source is bigger than color"""
    return (source - color) / (255.0 - color)

def difference2(source, color):
    """When color is bigger than source"""
    return (color - source) / color


def color_to_alpha(image, color=None):
    image = image.convert('RGBA')
    width, height = image.size

    color = map(float, color)
    img_bands = [band.convert("F") for band in image.split()]

    # Find the maximum difference rate between source and color. I had to use two
    # difference functions because ImageMath.eval only evaluates the expression
    # once.
    alpha = ImageMath.eval(
        """float(
            max(
                max(
                    max(
                        difference1(red_band, cred_band),
                        difference1(green_band, cgreen_band)
                    ),
                    difference1(blue_band, cblue_band)
                ),
                max(
                    max(
                        difference2(red_band, cred_band),
                        difference2(green_band, cgreen_band)
                    ),
                    difference2(blue_band, cblue_band)
                )
            )
        )""",
        difference1=difference1,
        difference2=difference2,
        red_band = img_bands[0],
        green_band = img_bands[1],
        blue_band = img_bands[2],
        cred_band = color[0],
        cgreen_band = color[1],
        cblue_band = color[2]
    )

    # Calculate the new image colors after the removal of the selected color
    new_bands = [
        ImageMath.eval(
            "convert((image - color) / alpha + color, 'L')",
            image = img_bands[i],
            color = color[i],
            alpha = alpha
        )
        for i in xrange(3)
    ]

    # Add the new alpha band
    new_bands.append(ImageMath.eval(
        "convert(alpha_band * alpha, 'L')",
        alpha = alpha,
        alpha_band = img_bands[3]
    ))

    return Image.merge('RGBA', new_bands)

image = color_to_alpha(image, (0, 0, 0, 255))
background = Image.new('RGB', image.size, (255, 255, 255))
background.paste(image.convert('RGB'), mask=image)
Community
  • 1
  • 1
Nadia Alramli
  • 111,714
  • 37
  • 173
  • 152
  • 1
    I tried to get this to work but it said no module named core and things like that, it was just a mess. I'm probably an idiot but I just couldn't get it to work. Thanks anyway I'm sure your answer will help someone else though. – Cookies Oct 24 '09 at 16:41
  • You should not try to run the whole file. Just copy the color_to_alpha function itself. Anyway, I'm glad you found a solution that works for you. If you need a more efficient solution, you know where to look ;) – Nadia Alramli Oct 24 '09 at 18:10
  • I did, and it first said global name 'OPTIONS' is not defined, so I copied that part out and then it said _t is not defined, but it was a module I didn't have. That's what I meant by mess, I tried to get it to work but couldn't, the method suggested below that worked for me is okay but if your function could really take out all the background pixels in the image that'd be great. There are still some left that confuse tesseract. – Cookies Oct 24 '09 at 22:10
  • I updated the solution with the full answer. You can change the colors in the last 3 lines to match the ones you want. – Nadia Alramli Oct 24 '09 at 22:26
  • Thanks, it works except it still leaves behind a ghost of pixels unlike the solution below. Basically I have a solid dark colored text, it is black/blue like a gradient because of the poor quality. I want to keep the solid objects and remove all the pixels around it that are sort of like a shadow. Like this: http://img36.imageshack.us/img36/6963/expuq.jpg I am going to play around with it a bit and see if I can get something to work.. Thanks again! – Cookies Oct 24 '09 at 23:56
  • I wish I was able to help. Go with the solution that works best for your case :) – Nadia Alramli Oct 25 '09 at 00:27
  • Can you explain the 2 background lines at the bottom? If I want to save the final image, would I be saving this background object?e – BigBoy1337 Jan 16 '15 at 21:01
  • I would put this as a comment but I don't have enough reputation, this is supposed to be a comment for @Nadia Alramli 's answer. So here is the comment: > In your answer the Phatch you mentioned you said you used the method in the Phatch photo processing program. I decided to check it out but couldn't download the windows zip. The website told me the file was missing. I am assuming that you control the website since you developed the program. Please fix the download please. – PythonPro Jan 03 '19 at 02:24
22

Using numpy and PIL:

This loads the image into a numpy array of shape (W,H,3), where W is the width and H is the height. The third axis of the array represents the 3 color channels, R,G,B.

import Image
import numpy as np

orig_color = (255,255,255)
replacement_color = (0,0,0)
img = Image.open(filename).convert('RGB')
data = np.array(img)
data[(data == orig_color).all(axis = -1)] = replacement_color
img2 = Image.fromarray(data, mode='RGB')
img2.show()

Since orig_color is a tuple of length 3, and data has shape (W,H,3), NumPy broadcasts orig_color to an array of shape (W,H,3) to perform the comparison data == orig_color. The result in a boolean array of shape (W,H,3).

(data == orig_color).all(axis = -1) is a boolean array of shape (W,H) which is True wherever the RGB color in data is original_color.

unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • By far the fastest solution. As note, some images might have alpha, then tuple should have 4 values, with 255 as the alpha value to not be transparent. Also .convert('RGBA') – Christo Labuschagne Oct 03 '22 at 09:21
9
#!/usr/bin/python
from PIL import Image
import sys

img = Image.open(sys.argv[1])
img = img.convert("RGBA")

pixdata = img.load()

# Clean the background noise, if color != white, then set to black.
# change with your color
for y in xrange(img.size[1]):
    for x in xrange(img.size[0]):
        if pixdata[x, y] == (255, 255, 255, 255):
            pixdata[x, y] = (0, 0, 0, 255)
Yuda Prawira
  • 12,075
  • 10
  • 46
  • 54
6

You'll need to represent the image as a 2-dimensional array. This means either making a list of lists of pixels, or viewing the 1-dimensional array as a 2d one with some clever math. Then, for each pixel that is targeted, you'll need to find all surrounding pixels. You could do this with a python generator thus:

def targets(x,y):
    yield (x,y) # Center
    yield (x+1,y) # Left
    yield (x-1,y) # Right
    yield (x,y+1) # Above
    yield (x,y-1) # Below
    yield (x+1,y+1) # Above and to the right
    yield (x+1,y-1) # Below and to the right
    yield (x-1,y+1) # Above and to the left
    yield (x-1,y-1) # Below and to the left

So, you would use it like this:

for x in range(width):
    for y in range(height):
        px = pixels[x][y]
        if px[0] == 255 and px[1] == 255 and px[2] == 255:
            for i,j in targets(x,y):
                newpixels[i][j] = replacementColor
Benson
  • 22,457
  • 2
  • 40
  • 49
5

If the pixels are not easily identifiable e.g you say (r < 100 and g < 100 and b < 100) also doesn't match correctly the black region, it means you have lots of noise.

Best way would be to identify a region and fill it with color you want, you can identify the region manually or may be by edge detection e.g. http://bitecode.co.uk/2008/07/edge-detection-in-python/

or more sophisticated approach would be to use library like opencv (http://opencv.willowgarage.com/wiki/) to identify objects.

Anurag Uniyal
  • 85,954
  • 40
  • 175
  • 219
2

This is part of my code, the result would like: source

target

import os
import struct
from PIL import Image
def changePNGColor(sourceFile, fromRgb, toRgb, deltaRank = 10):
    fromRgb = fromRgb.replace('#', '')
    toRgb = toRgb.replace('#', '')

    fromColor = struct.unpack('BBB', bytes.fromhex(fromRgb))
    toColor = struct.unpack('BBB', bytes.fromhex(toRgb))

    img = Image.open(sourceFile)
    img = img.convert("RGBA")
    pixdata = img.load()

    for x in range(0, img.size[0]):
        for y in range(0, img.size[1]):
            rdelta = pixdata[x, y][0] - fromColor[0]
            gdelta = pixdata[x, y][0] - fromColor[0]
            bdelta = pixdata[x, y][0] - fromColor[0]
            if abs(rdelta) <= deltaRank and abs(gdelta) <= deltaRank and abs(bdelta) <= deltaRank:
                pixdata[x, y] = (toColor[0] + rdelta, toColor[1] + gdelta, toColor[2] + bdelta, pixdata[x, y][3])

    img.save(os.path.dirname(sourceFile) + os.sep + "changeColor" + os.path.splitext(sourceFile)[1])

if __name__ == '__main__':
    changePNGColor("./ok_1.png", "#000000", "#ff0000")
Joseph
  • 21
  • 2
  • There's no need to manually convert from hexadecimal color codes — you can use Pillow's [ImageColor.getrgb()](https://pillow.readthedocs.io/en/stable/reference/ImageColor.html#PIL.ImageColor.getrgb), which supports a lot more color input formats. – Micah Yeager Jan 10 '23 at 22:29
0

Solution as class

from PIL import Image
import numpy as np
import os

class ImageEditor:

  def __init__(self, from_path: str) -> None:
    # Lets you use tilde ~ in your file path names
    new_from_path = os.path.expanduser(from_path)
    self.img = Image.open(new_from_path)

  # 
  # og_color = (75,174,79)
  # new_color = (18,133,72)
  # 
  def replace(self, og_color: tuple, new_color: tuple) -> Image.Image:
    img = self.img.convert('RGBA')
    data = np.array(img)

    og_rgba = og_color + (255,)
    new_rgba = new_color + (255,)
    original_indices = (data == og_rgba).all(axis = -1)
    data[original_indices] = new_rgba

    img2 = Image.fromarray(data)
    
    return img2

Usage Example:

from image_editor import *

from_path = "~/Downloads/approved.png"
editor = ImageEditor(from_path=from_path)

og_color = (75,174,79)
new_color = (18,133,72)

updated_img = editor.replace(og_color, new_color)

updated_img.show()

Before / After

Before After
enter image description here enter image description here

Source / Info

Shout out to @unutbu. This code is based on their answer.
(unutbu's answer here https://stackoverflow.com/a/3169874/838517)

Written using python 3.11.2

Riveascore
  • 1,724
  • 4
  • 26
  • 46