PIL Best Way To Replace Color? - python

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

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:
to
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)

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.

#!/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)

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

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.

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")

Related

How Can I Add an Outline/Stroke/Border to a PNG Image with Pillow Library in Python?

I am trying to use the Pillow (python-imaging-library) Python library in order to create an outline/stroke/border (with any color and width chosen) around my .png image. You can see here the original image and my wanted result (create by a phone app):
https://i.stack.imgur.com/4x4qh.png
You can download the png file of the original image here: https://pixabay.com/illustrations/brain-character-organ-smart-eyes-1773885/
I have done it in the medium size(1280x1138) but maybe it is better to do it with the smallest size (640x569).
I tried to solve the problem with two methods.
METHOD ONE
The first method is to create a fully blacked image of the brain.png image, enlarge it, and paste the original colored brain image on top of it. Here is my code:
brain_black = Image.open("brain.png") #load brain image
width = brain_black.width #in order not to type a lot
height = brain_black.height #in order not to type a lot
rectangle = Image.new("RGBA", (width, height), "black") #creating a black rectangle in the size of the brain image
brain_black.paste(rectangle, mask=brain_black) #pasting on the brain image the black rectangle, and masking it with the brain picture
#now brain_black is the brain.png image, but all its pixels are black. Let's continue:
brain_black = brain_black.resize((width+180, height+180)) #resizing the brain_black by some factor
brain_regular = Image.open("brain.png") #load the brain image in order to paste later on
brain_black.paste(brain_regular,(90,90), mask=brain_regular) #paste the regular (colored) brain on top of the enlarged black brain (in x=90, y=90, the middle of the black brain)
brain_black.save("brain_method_resize.png") #saving the image
This method doesn't work, as you can see in the image link above. It might have worked for simple geometric shapes, but not for a complicated shape like this.
METHOD TWO
The second method is to load the brain image pixels data into a 2-dimensional array, and loop over all of the pixels. Check the color of every pixel, and in every pixel which is not transparent (means A(or Alpha) is not 0 in the rgbA form) to draw a black pixel in the pixel above, below, right, left, main diagonal down, main diagonal up, secondary diagonal (/) down and secondary diagonal (/) up. Then to draw a pixel in the second pixel above, the second pixel below and etc. this was done with a "for loop" where the number of repetitions is the wanted stroke width (in this example is 30). Here is my code:
brain=Image.open("brain.png") #load brain image
background=Image.new("RGBA", (brain.size[0]+400, brain.size[1]+400), (0, 0, 0, 0)) #crate a background transparent image to create the stroke in it
background.paste(brain, (200,200), brain) #paste the brain image in the middle of the background
pixelsBrain = brain.load() #load the pixels array of brain
pixelsBack=background.load() #load the pixels array of background
for i in range(brain.size[0]):
for j in range(brain.size[1]):
r, c = i+200, j+200 #height and width offset
if(pixelsBrain[i,j][3]!=0): #checking if the opacity is not 0, if the alpha is not 0.
for k in range(30): #the loop
pixelsBack[r, c + k] = (0, 0, 0, 255)
pixelsBack[r, c - k] = (0, 0, 0, 255)
pixelsBack[r + k, c] = (0, 0, 0, 255)
pixelsBack[r - k, c] = (0, 0, 0, 255)
pixelsBack[r + k, c + k] = (0, 0, 0, 255)
pixelsBack[r - k, c - k] = (0, 0, 0, 255)
pixelsBack[r + k, c - k] =(0, 0, 0, 255)
pixelsBack[r - k, c + k] = (0, 0, 0, 255)
background.paste(brain, (200,200), brain) #pasting the colored brain onto the background, because the loop "destroyed" the picture.
background.save("brain_method_loop.png")
This method did work, but it is very time-consuming (takes about 30 seconds just for one picture and 30 pixels stroke). I want to do it for many pictures so this method is not good for me.
Is there an easier and better way to reach my wanted result using Python Pillow library. How can I do it?
And also, how can I fasten my loop code (I understood something about Numpy and OpenCV, which is better for this purpose?)
I know that if a phone app could do it in a matter of milliseconds, also python can, but I didn't find any way to do it.
Thank you.
I tried some solution similar with photoshop stroke effect using OpenCV (It is not perfect and I still finding better solution)
This algorithm is based on euclidean distance transform. I also tried dilation algorithm with ellipse kernel structure, it is bit different with photoshop, and there are some information that distance transform is the way that photoshop using.
def stroke(origin_image, threshold, stroke_size, colors):
img = np.array(origin_image)
h, w, _ = img.shape
padding = stroke_size + 50
alpha = img[:,:,3]
rgb_img = img[:,:,0:3]
bigger_img = cv2.copyMakeBorder(rgb_img, padding, padding, padding, padding,
cv2.BORDER_CONSTANT, value=(0, 0, 0, 0))
alpha = cv2.copyMakeBorder(alpha, padding, padding, padding, padding, cv2.BORDER_CONSTANT, value=0)
bigger_img = cv2.merge((bigger_img, alpha))
h, w, _ = bigger_img.shape
_, alpha_without_shadow = cv2.threshold(alpha, threshold, 255, cv2.THRESH_BINARY) # threshold=0 in photoshop
alpha_without_shadow = 255 - alpha_without_shadow
dist = cv2.distanceTransform(alpha_without_shadow, cv2.DIST_L2, cv2.DIST_MASK_3) # dist l1 : L1 , dist l2 : l2
stroked = change_matrix(dist, stroke_size)
stroke_alpha = (stroked * 255).astype(np.uint8)
stroke_b = np.full((h, w), colors[0][2], np.uint8)
stroke_g = np.full((h, w), colors[0][1], np.uint8)
stroke_r = np.full((h, w), colors[0][0], np.uint8)
stroke = cv2.merge((stroke_b, stroke_g, stroke_r, stroke_alpha))
stroke = cv2pil(stroke)
bigger_img = cv2pil(bigger_img)
result = Image.alpha_composite(stroke, bigger_img)
return result
def change_matrix(input_mat, stroke_size):
stroke_size = stroke_size - 1
mat = np.ones(input_mat.shape)
check_size = stroke_size + 1.0
mat[input_mat > check_size] = 0
border = (input_mat > stroke_size) & (input_mat <= check_size)
mat[border] = 1.0 - (input_mat[border] - stroke_size)
return mat
def cv2pil(cv_img):
cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGRA2RGBA)
pil_img = Image.fromarray(cv_img.astype("uint8"))
return pil_img
output = stroke(test_image, threshold=0, stroke_size=10, colors=((0,0,0),))
I can't do a fully tested Python solution for you at the moment as I have other commitments, but I can certainly show you how to do it in a few milliseconds and give you some pointers.
I just used ImageMagick at the command line. It runs on Linux and macOS (use brew install imagemagick) and Windows. So, I extract the alpha/transparency channel and discard all the colour info. Then use a morphological "edge out" operation to generate a fat line around the edges of the shape in the alpha channel. I then invert the white edges so they become black and make all the white pixels transparent. Then overlay on top of the original image.
Here's the full command:
magick baby.png \( +clone -alpha extract -morphology edgeout octagon:9 -threshold 10% -negate -transparent white \) -flatten result.png
So that basically opens the image, messes about with a cloned copy of the alpha layer inside the parentheses and then flattens the black outline that results back onto the original image and saves it. Let's do the steps one at a time:
Extract the alpha layer as alpha.png:
magick baby.png -alpha extract alpha.png
Now fatten the edges, invert and make everything not black become transparent and save as overlay.png:
magick alpha.png -morphology edgeout octagon:9 -threshold 10% -negate -transparent white overlay.png
Here's the final result, change the octagon:9 to octagon:19 for fatter lines:
So, with PIL... you need to open the image and convert to RGBA, then split the channels. You don't need to touch the RGB channels just the A channel.
im = Image.open('baby.png').convert('RGBA')
R, G, B, A = im.split()
Some morphology needed here - see here.
Merge the original RGB channels with the new A channel and save:
result = Image.merge((R,G,B,modifiedA))
result.save('result.png')
Note that there are Python bindings to ImageMagick called wand and you may find it easier to translate my command-line stuff using that... wand. Also, scikit-image has an easy-to-use morphology suite too.
I've written this function which is based on morphological dilation and lets you set the stroke size and color. But it's EXTREMELY slow and it seems to not work great with small elements.
If anyone can help me speed it up it would be extremely helpful.
def addStroke(image,strokeSize=1,color=(0,0,0)):
#Create a disc kernel
kernel=[]
kernelSize=math.ceil(strokeSize)*2+1 #Should always be odd
kernelRadius=strokeSize+0.5
kernelCenter=kernelSize/2-1
pixelRadius=1/math.sqrt(math.pi)
for x in range(kernelSize):
kernel.append([])
for y in range(kernelSize):
distanceToCenter=math.sqrt((kernelCenter-x+0.5)**2+(kernelCenter-y+0.5)**2)
if(distanceToCenter<=kernelRadius-pixelRadius):
value=1 #This pixel is fully inside the circle
elif(distanceToCenter<=kernelRadius):
value=min(1,(kernelRadius-distanceToCenter+pixelRadius)/(pixelRadius*2)) #Mostly inside
elif(distanceToCenter<=kernelRadius+pixelRadius):
value=min(1,(pixelRadius-(distanceToCenter-kernelRadius))/(pixelRadius*2)) #Mostly outside
else:
value=0 #This pixel is fully outside the circle
kernel[x].append(value)
kernelExtent=int(len(kernel)/2)
imageWidth,imageHeight=image.size
outline=image.copy()
outline.paste((0,0,0,0),[0,0,imageWidth,imageHeight])
imagePixels=image.load()
outlinePixels=outline.load()
#Morphological grayscale dilation
for x in range(imageWidth):
for y in range(imageHeight):
highestValue=0
for kx in range(-kernelExtent,kernelExtent+1):
for ky in range(-kernelExtent,kernelExtent+1):
kernelValue=kernel[kx+kernelExtent][ky+kernelExtent]
if(x+kx>=0 and y+ky>=0 and x+kx<imageWidth and y+ky<imageHeight and kernelValue>0):
highestValue=max(highestValue,min(255,int(round(imagePixels[x+kx,y+ky][3]*kernelValue))))
outlinePixels[x,y]=(color[0],color[1],color[2],highestValue)
outline.paste(image,(0,0),image)
return outline
Very simple and primitive solution: use PIL.ImageFilter.FIND_EDGES to find edge of drawing, it is about 1px thick, and draw a circle in every point of the edge. It is quite fast and require few libs, but has a disadvantage of no smoothing.
from PIL import Image, ImageFilter, ImageDraw
from pathlib import Path
def mystroke(filename: Path, size: int, color: str = 'black'):
outf = filename.parent/'mystroke'
if not outf.exists():
outf.mkdir()
img = Image.open(filename)
X, Y = img.size
edge = img.filter(ImageFilter.FIND_EDGES).load()
stroke = Image.new(img.mode, img.size, (0,0,0,0))
draw = ImageDraw.Draw(stroke)
for x in range(X):
for y in range(Y):
if edge[x,y][3] > 0:
draw.ellipse((x-size,y-size,x+size,y+size),fill=color)
stroke.paste(img, (0, 0), img )
# stroke.show()
stroke.save(outf/filename.name)
if __name__ == '__main__':
folder = Path.cwd()/'images'
for img in folder.iterdir():
if img.is_file(): mystroke(img, 10)
Solution using PIL
I was facing the same need: outlining a PNG image.
Here is the input image:
Input image
I see that some solution have been found, but in case some of you want another alternative, here is mine:
Basically, my solution workflow is as follow:
Read and fill the non-alpha chanel of the PNG image with the border
color
Resize the unicolor image to make it bigger
Merge the original image to the bigger unicolor image
Here you go! You have an outlined PNG image with the width and color of your choice.
Here is the code implementing the workflow:
from PIL import Image
# Set the border and color
borderSize = 20
color = (255, 0, 0)
imgPath = "<YOUR_IMAGE_PATH>"
# Open original image and extract the alpha channel
im = Image.open(imgPath)
alpha = im.getchannel('A')
# Create red image the same size and copy alpha channel across
background = Image.new('RGBA', im.size, color=color)
background.putalpha(alpha)
# Make the background bigger
background=background.resize((background.size[0]+borderSize, background.size[1]+borderSize))
# Merge the targeted image (foreground) with the background
foreground = Image.open(imgPath)
background.paste(foreground, (int(borderSize/2), int(borderSize/2)), foreground.convert("RGBA"))
imageWithBorder = background
imageWithBorder.show()
And here is the outputimage:
Output image
Hope it helps!
I found a way to do this using the ImageFilter module, it is much faster than any custom implementation that I've seen here and doesn't rely on resizing which doesn't work for convex hulls
from PIL import Image, ImageFilter
stroke_radius = 5
img = Image.open("img.png") # RGBA image
stroke_image = Image.new("RGBA", img.size, (255, 255, 255, 255))
img_alpha = img.getchannel(3).point(lambda x: 255 if x>0 else 0)
stroke_alpha = img_alpha.filter(ImageFilter.MaxFilter(stroke_radius))
# optionally, smooth the result
stroke_alpha = stroke_alpha.filter(ImageFilter.SMOOTH)
stroke_image.putalpha(stroke_alpha)
output = Image.alpha_composite(stroke_image, img)
output.save("output.png")

How to get a floodfilled area and its borders in an image with Python OpenCV?

I have an image such as this one, which is only black and white:
I would like to obtain only the flooded area of the image with the border using cv2.floodfill, like so (pardon my Paint skills):
Here's my current code:
# Copy the image.
im_floodfill = cv2.resize(actual_map_image, (500, 500)).copy()
# Floodfill from point (X, Y)
cv2.floodFill(im_floodfill, None, (X, Y), (255, 255, 255))
# Display images.
cv2.imshow("Floodfilled Image", im_floodfill)
cv2.waitKey(0)
The output I get is equal to the original image. How can I get only the flooded area with borders?
EDIT: I want to floodfill from any white point inside the "arena", like the red dot (X,Y) in the image. I wish to have only the outer border of the small circles inside the arena and the inner border of the outside walls.
EDIT2: I'm halfway there with this:
# Resize for test purposes
actual_map_image = cv2.resize(actual_map_image, (1000, 1000))
actual_map_image = cv2.cvtColor(actual_map_image, cv2.COLOR_BGR2GRAY)
h, w = actual_map_image.shape[:2]
flood_mask = np.zeros((h+2, w+2), dtype=np.uint8)
connectivity = 8
flood_fill_flags = (connectivity | cv2.FLOODFILL_FIXED_RANGE | cv2.FLOODFILL_MASK_ONLY | 255 << 8)
# Copy the image.
im_floodfill = actual_map_image.copy()
# Floodfill from point inside arena, not inside a black dot
cv2.floodFill(im_floodfill, flood_mask, (h/2 + 20, w/2 + 20), 255, None, None, flood_fill_flags)
borders = []
for i in range(len(actual_map_image)):
borders.append([B-A for A,B in zip(actual_map_image[i], flood_mask[i])])
borders = np.asarray(borders)
borders = cv2.bitwise_not(borders)
# Display images.
cv2.imshow("Original Image", cv2.resize(actual_map_image, (500, 500)))
cv2.imshow("Floodfilled Image", cv2.resize(flood_mask, (500, 500)))
cv2.imshow("Borders", cv2.resize(borders, (500, 500)))
cv2.waitKey(0)
I get this:
However, I feel like this is the wrong way of getting the borders, and they are incomplete.
I think the easiest, and fastest, way to do this is to flood-fill the arena with mid-grey. Then extract just the grey pixels and find their edges. That looks like this, but bear in mind more than half the lines are comments and debug statements :-)
#!/usr/bin/env python3
import cv2
# Load image as greyscale to use 1/3 of the memory and processing time
im = cv2.imread('arena.png', cv2.IMREAD_GRAYSCALE)
# Floodfill arena area with value 128, i.e. mid-grey
floodval = 128
cv2.floodFill(im, None, (150,370), floodval)
# DEBUG cv2.imwrite('result-1.png', im)
# Extract filled area alone
arena = ((im==floodval) * 255).astype(np.uint8)
# DEBUG cv2.imwrite('result-2.png', arena)
# Find edges and save
edges = cv2.Canny(arena,100,200)
# DEBUG cv2.imwrite('result-3.png',edges)
Here are the 3 steps of debug output showing you the sequence of processing:
result-1.png looks like this:
result-2.png looks like this:
result-3.png looks like this:
By the way, you don't have to write any Python code to do this, as you can just do it in the Terminal with ImageMagick which is included in most Linux distros and is available for macOS and Windows. The method used here corresponds exactly to the method I used in Python above:
magick arena.png -colorspace gray \
-fill gray -draw "color 370,150 floodfill" \
-fill white +opaque gray -canny 0x1+10%+30% result.png
How about dilating and xor
kernel = np.ones((3,3), np.uint8)
dilated = cv2.dilate(actual_map_image, kernel, iterations = 1)
borders = cv2.bitwise_xor(dilated, actual_map_image)
That will give you only the borders, I'm not clear if you want the circle borders only or also the interior borders, you should be able to remove borders you don't want based on size.
You can remove the exterior border with a size threshold, define a function like this:
def size_threshold(bw, minimum, maximum):
retval, labels, stats, centroids = cv.connectedComponentsWithStats(bw)
for val in np.where((stats[:, 4] < minimum) + (stats[:, 4] > maximum))[0]:
labels[labels==val] = 0
return (labels > 0).astype(np.uint8) * 255
result = size_threshold(borders, 0, 500)
Replace 500 with the a number larger than borders you want to keep and smaller than the border you want to lose.
I had to create my own Flood Fill implementation to get what I wanted. I based myself on this one.
def fill(data, start_coords, fill_value, border_value, connectivity=8):
"""
Flood fill algorithm
Parameters
----------
data : (M, N) ndarray of uint8 type
Image with flood to be filled. Modified inplace.
start_coords : tuple
Length-2 tuple of ints defining (row, col) start coordinates.
fill_value : int
Value the flooded area will take after the fill.
border_value: int
Value of the color to paint the borders of the filled area with.
connectivity: 4 or 8
Connectivity which we use for the flood fill algorithm (4-way or 8-way).
Returns
-------
filled_data: ndarray
The data with the filled area.
borders: ndarray
The borders of the filled area painted with border_value color.
"""
assert connectivity in [4,8]
filled_data = data.copy()
xsize, ysize = filled_data.shape
orig_value = filled_data[start_coords[0], start_coords[1]]
stack = set(((start_coords[0], start_coords[1]),))
if fill_value == orig_value:
raise ValueError("Filling region with same value already present is unsupported. Did you already fill this region?")
border_points = []
while stack:
x, y = stack.pop()
if filled_data[x, y] == orig_value:
filled_data[x, y] = fill_value
if x > 0:
stack.add((x - 1, y))
if x < (xsize - 1):
stack.add((x + 1, y))
if y > 0:
stack.add((x, y - 1))
if y < (ysize - 1):
stack.add((x, y + 1))
if connectivity == 8:
if x > 0 and y > 0:
stack.add((x - 1, y - 1))
if x > 0 and y < (ysize - 1):
stack.add((x - 1, y + 1))
if x < (xsize - 1) and y > 0:
stack.add((x + 1, y - 1))
if x < (xsize - 1) and y < (ysize - 1):
stack.add((x + 1, y + 1))
else:
if filled_data[x, y] != fill_value:
border_points.append([x,y])
# Fill all image with white
borders = filled_data.copy()
borders.fill(255)
# Paint borders
for x,y in border_points:
borders[x, y] = border_value
return filled_data, borders
The only thing I did was adding the else condition. If the point does not have a value equal to orig_value or fill_value, then it is a border, so I append it to a list that contains the points of all borders. Then I only paint the borders.
I was able to get the following images with this code:
# Resize for test purposes
actual_map_image = cv2.resize(actual_map_image, (500, 500))
actual_map_image = cv2.cvtColor(actual_map_image, cv2.COLOR_BGR2GRAY)
h, w = actual_map_image.shape[:2]
filled_data, borders = fill(actual_map_image, [h/2 + 20, w/2 + 20], 127, 0, connectivity=8)
cv2.imshow("Original Image", actual_map_image)
cv2.imshow("Filled Image", filled_data)
cv2.imshow("Borders", borders)
The one on the right was what I was aiming for. Thank you all!

How to remove unwanted parts in an image and then create a new small image using Python?

I want to remove the dark(black strips) and also the white curves in the image, and then align the remained parts connected in a new small-sized image, making the colored parts looks continuously. I hope can get some suggestions for solutions.
I have tried to use PIL to read the image.
I don't know how to set the right threshold and resize the image
I'm not an expert at all in image processing, but let me know if this is enough for you.
Looking at the brightness (sum of the RGB values) distribution, maybe one option is to just filter pixel based on their value:
It looks like the dark parts have a brightness below 100 (or something like that). I filtered it this way:
from PIL import Image
import numpy as np
def filter_image(img,threshold=100):
data = np.array(img.convert('RGB'))
brightness = np.sum(data,axis=2)
filtered_img = data.copy()*0
for i in range(data.shape[0]):
k = 0 # k index the columns that are bright enough
for j in range(data.shape[1]):
if brightness[i,j] > threshold:
filtered_img[i,k,:] = data[i,j,:]
k += 1 # we increment only if it's bright enough
# End of column iterator. The right side of the image is black
return Image.fromarray(filtered_img)
img = Image.open("test.png")
filtered = filter_image(img)
filtered.show()
I get the following result. I'm sure experts can do much better, but it's a start:
The following is only looking for black pixels, as can be seen by the first image, many of the pixels you want out are not black. You will need to find a way to scale up what you will take out.
Also, research will need to be done on collapsing an image, as can be seen by my collapse. Although, this image collapse may work if you are able to get rid of everything but the reddish colors. Or you can reduce by width, which is what the third picture shows.
from PIL import Image, ImageDraw
def main():
picture = Image.open('/Volumes/Flashdrive/Stack_OverFlow/imageprocessing.png', 'r')
# pix_val = list(im.getdata())
# print(pix_val)
# https://code-maven.com/create-images-with-python-pil-pillowimg = Image.new('RGB', (100, 30), color = (73, 109, 137))
blackcount = 0
pix = picture.convert('RGB') # https://stackoverflow.com/questions/11064786/get-pixels-rgb-using-pil
width, height = picture.size
img = Image.new('RGB', (width, height), color=(73, 109, 137))
newpic = []
for i in range(width):
newpictemp = []
for j in range(height):
# https://stackoverflow.com/questions/13167269/changing-pixel-color-python
r, g, b = pix.getpixel((i, j))
if r == 0 and g == 0 and b == 0:
blackcount += 1
else:
img.putpixel((i, j), (r, g, b))
newpictemp.append((r, g, b))
newpic.append(newpictemp)
img.save('pil_text.png')
newheight = int(((width * height) - blackcount) / width)
print(newheight)
img2 = Image.new('RGB', (width, newheight), color=(73, 109, 137))
for i in range(width):
for j in range(newheight):
try:
z = newpic[i][j]
img2.putpixel((i, j), newpic[i][j])
except:
continue
img2.save('pil_text2.png')
if __name__ == "__main__":
main()
No black pixels on left, removed black pixels on right, remove and resize by width (height resize shown in code)

Changing pixel color Python

I am suppose to get an image from my fluke robot and determine the color of each pixel in my image. Then if the pixel is mostly red, change it to completely green. If the pixel is mostly green, change it to completely blue. If the pixel is mostly blue, change it to completely red. This is what I am able to do, but I can't get it to work to get the image I have to change. There is no syntax error, it is just semantic I am having trouble with. I am using python.
My attempted code:
import getpixel
getpixel.enable(im)
r, g, b = im.getpixel(0,0)
print 'Red: %s, Green:%s, Blue:%s' % (r,g,b)
Also I have the picture saved like the following:
pic1 = makePicture("pic1.jpg"):
for pixel in getpixel("pic1.jpg"):
if pixel Red: %s:
return Green:%s
if pixel Green:%s:
return Blue:%s
I assume you're trying to use the Image module. Here's an example:
from PIL import Image
picture = Image.open("/path/to/my/picture.jpg")
r,g,b = picture.getpixel( (0,0) )
print("Red: {0}, Green: {1}, Blue: {2}".format(r,g,b))
Running this on this image I get the output:
>>> from PIL import Image
>>> picture = Image.open("/home/gizmo/Downloads/image_launch_a5.jpg")
>>> r,g,b = picture.getpixel( (0,0) )
>>> print("Red: {0}, Green: {1}, Blue: {2}".format(r,g,b))
Red: 138, Green: 161, Blue: 175
EDIT:
To do what you want I would try something like this
from PIL import Image
picture = Image.open("/path/to/my/picture.jpg")
# Get the size of the image
width, height = picture.size()
# Process every pixel
for x in width:
for y in height:
current_color = picture.getpixel( (x,y) )
####################################################################
# Do your logic here and create a new (R,G,B) tuple called new_color
####################################################################
picture.putpixel( (x,y), new_color)
You have mistakes:
# Get the size of the image
width, height = picture.size()
for x in range(0, width - 1):
for y in range(0, height - 1):
Brackets are mistake!! omit them.
int is not iterable.
I also recommend you to use load(), because it's much faster :
pix = im.load()
print pix[x, y]
pix[x, y] = value
I'm using python 3.6.4 and Pillow 5.0.0. Gizmo's code snippet didn't work for me. After little work I created a fixed snippet:
import Image
picture = Image.open("/path/to/my/picture.jpg")
# Get the size of the image
width, height = picture.size
# Process every pixel
for x in range(width):
for y in range(height):
current_color = picture.getpixel( (x,y) )
####################################################################
# Do your logic here and create a new (R,G,B) tuple called new_color
####################################################################
picture.putpixel( (x,y), new_color)
import cv2
import numpy as np
m = cv2.imread("C:/../Sample Pictures/yourImage.jpg")
h,w,bpp = np.shape(m)
for py in range(0,h):
for px in range(0,w):
#can change the below logic of rgb according to requirements. In this
#white background is changed to #e8e8e8 corresponding to 232,232,232
#intensity, red color of the image is retained.
if(m[py][px][0] >200):
m[py][px][0]=232
m[py][px][1]=232
m[py][px][2]=232
cv2.imshow('matrix', m)
cv2.waitKey(0)
cv2.imwrite('yourNewImage.jpg',m)
Adding some remarks on Gizmo's answer. This:
px = im.load()
current_color = (px[i, j])
is probably faster than this:
picture.getpixel( (x,y) )
Also, make sure to use this:
picture.putdata(colors)
instead of this inside the loop:
picture.putpixel( (x,y), new_color)
Change color of pixel on picture Меняем цвет пикселя в картинке(меняем фон)
https://colorscheme.ru/color-converter.html¶
Find one color on picture and change На всей картинке находит цвет и заменяет его другим.
import numpy as np
from PIL
import Image
import os,sys
im = Image.open('1.png')
data = np.array(im)
#Original value(цвет который будем менять)
r1, g1, b1 = 90, 227, 129
#Value that we want to replace it with(цвет выхода)
r2, g2, b2 = 140, 255, 251
red, green, blue = data[:,:,0], data[:,:,1], data[:,:,2]
mask = (red == r1) & (green == g1) & (blue == b1)
data[:,:,:3][mask] = [r2, g2, b2]
im = Image.fromarray(data)
im.save('1_mod.png')

PIL Check pixel that it's on with eval function

Is there any way using the eval function in PIL to run through all pixels, while checking to see what each value is? The program runs through an image to see if each pixel is a certain rgb, and if it is, then it will turn that pixel into transparency. the eval function in PIL seems it would do the job, but can my function that converts the pixels check the value of the pixel it's on? Thanks in advance.
Updated: Ahh, I see what you want to do. Here is an example using only PIL. It converts all white pixels to red with 50% alpha:
import Image
img = Image.open('stack.png').convert('RGBA')
width, _ = img.size
for i, px in enumerate(img.getdata()):
if px[:3] == (255, 255, 255):
y = i / width
x = i % width
img.putpixel((x, y), (255, 0, 0, 127))
img.save('stack-red.png')
Orig answer: Yes, the Image.eval() function lets you pass in a function which evaluates each pixel, and lets you determine a new pixel value:
import Image
img1 = Image.open('foo.png')
# replace dark pixels with black
img2 = Image.eval(img1, lambda px: 0 if px <= 64 else px)
No, eval will not pass an RGB tuple to a function. It maps a function over each band. You could, however process each band using eval and then use an ImageChops operation to logically combine the bands and get a mask that will be pixel-tuple specific.
By the way, this could be done much more cleanly and efficiently in NumPy if you are so inclined..
import numpy as np
import Image
import ImageChops
im_and = ImageChops.lighter
im = Image.open('test.png')
a = np.array(im)
R,G,B,A = im.split()
color_matches = []
for level,band in zip((255,255,255), (R,G,B)):
b = Image.eval(band, lambda px: 255-(255*(px==level)))
color_matches.append(b)
r,g,b = color_matches
mask = im_and(r, im_and(g, b))
im.putalpha(mask)
im.save('test2.png')

Categories

Resources