# Replace color in image measured by Euclidean distance

This script replaces the red hair in image to black color (replace one color to another)

### Three major parts in this script

• Not just replace color red to black, I hope the output image looks natural, so I will replace the color close to red to black. This "close" is measured by Euclidean distance.

• And not just replace by black color, user can modify the red color's r, g, b value separately, like add more green.

• Choose the area to be changed, rather than change the whole picture

### Suggestions I am looking for:

• I did some NumPy practice before, so any suggestions about NumPy are welcome.
• I think the code to replace target area is not so elegant, I am not quite sure about this part:

(c1, r1), (c2, r2) = area
for i in range(r1, r2+1):
l, r = i*h+c1, i*h+c2+1
data[l:r,:k][D[l:r]] = modification(data[l:r,:k][D[l:r]])


### Full code:

import numpy as np
from PIL import Image
from scipy.spatial.distance import cdist

def replace_corlor(image, original_corlor, modification, area=None, distance=1000, output="new_test.jpg"):
print("[*] START Replace Color")
img = Image.open(image)
data = np.asarray(img, dtype="int32")
w,h,k = data.shape
data = np.reshape(data, (w*h,k))
distMatrix = cdist(data, np.array([original_corlor]))
D = distMatrix<=distance
D = np.reshape(D,w*h)
if area is None:
data[:,:k][D] = modification(data[:,:k][D])
else:
(c1, r1), (c2, r2) = area
for i in range(r1, r2+1):
l, r = i*h+c1, i*h+c2+1
data[l:r,:k][D[l:r]] = modification(data[l:r,:k][D[l:r]])
data = np.reshape(data, (w,h,k))
img = Image.fromarray(np.asarray(np.clip(data, 0, 255), dtype="uint8"), "RGB")
img.save(output)
print("[*] DONE Replace Color")

if __name__ == "__main__":
def modification(color):
return color * [2,0,0]
# return [255,0,0]
replace_corlor("test.jpg",(36,35,30),modification, ((0,0),(400,100)))

• I have no comments on your code, your Python is better than mine. :) But for color modification you could use the distance to the original_color value. The larger this distance (and the closer the distance is to the distance threshold), the less you modify the color. This way, you will get a smoother transition at the edges of the red region. Nov 14, 2018 at 5:42
• Couple of points, firstly, it appears that the modification function only works if it operates on vectors, rather than a single pixel, as you're passing that function the whole image at once. In some ways, it is doing most of the work. Secondly, There's a typo in the variable name original_corlor, nothing major, just confused me for a second. I find the contents of the else clause very confusing, partly because of very short variable names. Jan 30, 2019 at 18:18
• @gbartonowen sorry I don't get what you mean "it operates on vectors, rather than a single pixel", there is a area in parameters, if you wanna just change one pixel, for example the first pixel, just set area=((0,0),(1,1)), and about the typo, you are totally right Feb 17, 2019 at 18:34
• @Aries_is_there The modification function takes a 3D array of pixels, rather than a single pixel, which means you must formulate the function as numpy compatible operators. In this case it works fine but it looks like a happens to work rather than is meant to work like this Feb 19, 2019 at 10:06

Nice code. Let's look at the function signatures. As gbartonowen noted, two typos on corlor for color. The keyword defaults are lovely, thank you for appropriately dealing with a magic number, and defaulting the filespec. Lots of keywords will lead to a somewhat long signature, though, as reported by \$ flake8:

E501 line too long (106 > 79 characters)


Recommend you always run such a linter before sharing code. And heed the linter's advice. In the same vein we see things like PEP-8 wants spaces in the w, h, k assignment, or (w * h, k) expression. And, this being python rather than java, snake-case identifiers of d and dist_matrix would be much more appropriate. Python convention says the Gentle Reader should expect D to be a class. (Yes, I know this clashes with conventions of the mathematical community, something had to give. Ok, on to more substantive comments.)

An identifier of data is always on the vague side, it's a bit like naming your variable the_thing. Yes, it's accurate, but often it could be a little more informative. Consider a rename along these lines:

    img = np.asarray(Image.open(image), dtype='int32')


(Hmmm, as I look at that, maybe we'd like identifiers of in_file and out_file?)

The cdist() call, which defaults to Euclidean, is pretty interesting, since RGB is not a perceptually uniform color scheme. Consider mapping to Lab* colorspace and using that for comparisons.

It is slightly tricky for a naive caller to correctly pass in area, so it warrants mention in the docstring you're going to write, or at least in a comment. Similarly for the modification signature. Consider raising an error if c1 < c2 or r1 < r2 does not hold.

The l, r identifiers are well chosen, and I eventually puzzled out their meaning, but I wouldn't mind a comment mentioning "left, right", as I kept thinking in terms of "row". I also wouldn't mind seeing comments explaining the need for each reshape().

If speed is a concern, then figuring out how to get modification() to broadcast values to a sub-rectangle would be the thing to focus on.

The saturating aspect of [2, 0, 0] is slightly surprising, please comment on it, and how it deliberately interacts with clip(). Also, the list is not pythonic, this should definitely be the tuple (2, 0, 0). A more descriptive name, perhaps red_modification, would be appropriate.

Protecting def modification so import won't see it kind of make sense, but is a little weird. You don't want that example to be part of your public API, that's fine. But consider using def _modification in the usual way, so the __main__ clause is just a one-liner.

Well, OK, two lines, as the magic tuple (36, 35, 30) needs a name like dark_gray.

Looks good, ship it!