Residual white pixels in transparent background from PIL - python

I used this following code from another stackoverflow post
from PIL import Image as image
img = image.open('output.png')
img = img.convert("RGBA")
datas = img.getdata()
newData = []
for item in datas:
if item[0] == 255 and item[1] == 255 and item[2] == 255:
newData.append((255, 255, 255, 0))
else:
newData.append(item)
img.putdata(newData)
img.save("img2.png", "PNG")
to transform my png's background to transparent. However, when I tried to add some shapes in powerpoint underneath the transparent image, it still has some residual white pixels left. Anyone know how to solve this?

Those pixels are not exactly "white". The color you are testing against and removing from the image is, with its value of #FFFFFF. But those slanted lines are heavily antialiased, "fading" from the pure white of the background to the pure color of the center of the lines.
This can be seen when zooming in just a teeny bit:
You can lower the threshold of when to make a pixel entirely transparent:
if item[0] > 240 and item[1] > 240 and item[2] > 240:
newData.append((255, 255, 255, 0))
else:
newData.append(item)
but no matter how much you do this, you will always end up with either visibly lighter pixels around the lines, or – when only matching the center "line" color exactly – with disconnected pixels, not resembling the original lines anymore.
But there is no reason to use a Yes/No mask with PNG images! PNG supports full 8-bit transparency, and so you can make the 'solid' center lines fully opaque, the solid white fully transparent, and have the gradually darkening pixels fade between these values.
This works best if you know the exact original color that was used to draw the lines with. Measuring it with Adobe PhotoShop, I get something like #818695. Plugging in these values into your program and adjusting the 'tint' (towards white) to transparency, flattened towards the full possible range, I suggest this code:
from PIL import Image as image
img = image.open('input.png')
img = img.convert("RGBA")
datas = img.getdata()
retain = (0x81,0x86,0x95)
retain_gray = (39*retain[0] + 50*retain[1] + 11*retain[2])
newData = []
for item in datas:
if item[0] > retain[0] and item[1] > retain[1] and item[2] > retain[2]:
# convert to grayscale
val = 39*item[0] + 50*item[1] + 11*item[2]
# invert
val = 25500 - val;
# difference with 'retain'
val = retain_gray - val
# scale down
val = 255*val/retain_gray
# invert to act as transparency
transp = 255-val
# apply transparency to original 'full' color value
newData.append((retain[0], retain[1], retain[2], transp ))
else:
newData.append(item)
img.putdata(newData)
img.save("output.png", "PNG")
print "done"
What it essentially does is converting the input image to grayscale, scaling it (because the scale from darkest to lightest should be in the full transparency range of 0..255), then using this as the 'transparent' byte. The result is way better than your on/off approach:

Related

Using Python Pillow to 'Punch' Out transparency Using Second Image

I am currently trying to use an RGBA image to 'punch' out a hole in another RGBA image but all my current attempts have failed to maintain the original transparency. Once I apply an alpha channel using putalpha it will replace the original alpha channel completely and turn previously transparent pixels back to their original colors.
I am trying to perform a "putalpha" on only the pixels with 100% transparency.
In the photos below I attempt to overlap an 'inverted transparency' alpha channel on top of my Circle to perform the 'punch out'. Instead of only applying the transparent pixels it will replace the entire image's alpha which turns the rest of the circle image's transparency white.
Is there a way for me to do this transparency "Merge" to achieve an alpha layer that is a composite of both images?
#image2 is a square, image1 is a circle
# swapTransparency is a function I made that works in swapping the transparency, it just goes pixel by pixel and switches alpha channel value to # max where empty and to 0 everywhere else.
# probably a better and more effective way to invert the transparency but this works right now and might not even be needed.
def swapTransparency(img):
datas = img.getdata()
newData = []
for item in datas:
if item [3] == 0:
newData.append((0, 0, 0, 255))
else:
newData.append((255, 255, 255, 0))
img.putdata(newData)
return img
##This is putting alpha channel overtop but its replacing the entire alpha instead of merging them, losing original cricle transparency.
image2 = swapTransparency(image2)
alphaChannel = image2.getchannel('A')
image1.putalpha(image2)
Image1
Image2
Desired Results

Accurate color quantization of image to minimize color palette

I'm trying to quantize an image keeping all primary colors in place and removing all minor colors such as "anti-aliasing" borders.
E.g. the image below ultimately should be quantized to 3 colors whereas the number of actual colors in the original image is more than 30. All "anti-aliasing" border colors should be considered minors and eliminated upon quantization as well as "jpeg artifacts", which add more colors to the image because of over-optimization.
Note: a source image could be either png or jpeg.
For the quantization itself, I'm using PIL.quantize(...) with K as the number of colors to leave. And it works fairly well and keeps the palette perfectly matching to the original.
def color_quantize(path, K):
image = cv2.imread(path, cv2.IMREAD_UNCHANGED)
img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
im_pil = Image.fromarray(np.uint8(img))
im_pil = im_pil.quantize(K, None, 0, None)
return cv2.cvtColor(np.array(im_pil.convert("RGB")), cv2.COLOR_RGB2BGR)
Thus, if I knew "K" (the number of primary colors) in advance, then I would use it for im_pil.quantize(...). Basically, I need a way to get that "K" number.
Is there any way to determine the number of primary colors?
BTW, regarding the "jpeg artifacts" removal, I'm using img = cv2.bilateralFilter(img, 9, 75, 75) at the moment, which works quite well.
You may want to try to analyze the histograms of the RGB channels to find out how many peaks they have, hopefully you will have a few big peaks, and some very small ones, then the number of big peaks should be your K.
I've ended up with the following function to determine the number for dominant colors:
def get_dominant_color_number(img, threshold):
# remove significant artifacts
img = cv2.bilateralFilter(img, 9, 75, 75)
# resize image to make the process more efficient on 250x250 (without antialiasing to reduce color space)
thumbnail = cv2.resize(img, (250, 250), None)
# convert to HSV color space
imghsv = cv2.cvtColor(thumbnail, cv2.COLOR_BGR2HSV).astype("float32")
(h, s, v) = cv2.split(imghsv)
# quantize saturation and value to merge close colors
v = (v // 30) * 30
s = (s // 30) * 30
imghsv = cv2.merge([h,s,v])
thumbnail = cv2.cvtColor(imghsv.astype("uint8"), cv2.COLOR_HSV2BGR)
(unique, counts) = np.unique(thumbnail.reshape(-1, thumbnail.shape[2]), return_counts=True, axis = 0)
# calculate frequence of each color and sort them
freq = counts.astype("float")
freq /= freq.sum()
count_sort_ind = np.argsort(-counts)
# get frequent colors above the specified threshold
n = 0
dominant_colors = []
for (c) in count_sort_ind:
n += 1;
dominant_colors.append(unique[c])
if (freq[c] <= threshold):
break
return (dominant_colors, n)
# -----------------------------------------------------------
img = cv2.imread("File.png", cv2.IMREAD_UNCHANGED)
channels = img.shape[2]
if channels == 4:
trans_mask = img[:,:,3] == 0
img[trans_mask] = [254, 253, 254, 255]
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
(dom_colors, dom_color_num) = get_dominant_color_number(img, .0045)
For the threshold ".0045" it gives an acceptable result. Yet, it still looks a bit "artificial".

Python OpenCV image editing: Faster way to edit pixels

Using python (openCV2, tkinter etc) I've created an app (a very amateur one) to change blue pixels to white. The images are high quality jpgs or PNGS.
The process: Search every pixel of an image and if the 'b' value of BGR is higher than x, set pixel to white (255, 255, 255).
The problem: There are about 150 pictures to process at a time, so the above process takes quite long. It's around 9 - 15 seconds per iteration depending on the images size (resizing the image speeds up the process, but not ideal).
Here is the code (with GUI and exception handling elements removed for simplicity):
for filename in listdir(sourcefolder):
# Read image and set variables
frame = imread(sourcefolder+"/"+filename)
rows = frame.shape[0]
cols = frame.shape[1]
# Search pixels. If blue, set to white.
for i in range(0,rows):
for j in range(0,cols):
if frame.item(i,j,0) > 155:
frame.itemset((i,j,0),255)
frame.itemset((i,j,1),255)
frame.itemset((i,j,2),255)
imwrite(sourcecopy+"/"+filename, frame)
#release image from memory
del frame
Any help on increasing efficiency / speed would be greatly appreciated!
Start with this image:
Then use this:
import cv2
im = cv2.imread('a.png')
# Make all pixels where Blue > 150 into white
im[im[...,0]>150] = [255,255,255]
# Save result
cv2.imwrite('result.png', im)
Use cv2.threshold to create a mask using x threshold value.
Set the color like this : img_bgr[mask == 255] = [255, 0, 0]

Remove Image background and create a transparent Image using Python's PIL

I'm working on a project in which I need to remove the background of an Image, the only Information we have is that it's an Image which has some (one or more) objects in it, and I need to remove the background and make it a transparent image.
Here's a sample Image:
And, here's what I have tried using PIL:
img = Image.open(url)
img = img.convert("RGBA")
datas = img.getdata()
print('Old Length is: {}'.format(len(datas)))
# print('Exisitng Data is as: {}'.format(datas))
newData = []
for item in datas:
# print(item)
if item[0] == 255 and item[1] == 255 and item[2] == 255:
newData.append((255, 255, 255, 0))
else:
newData.append(item)
img.putdata(newData)
print('New Length is: {}'.format(len(datas)))
img.show()
img.save("/Users/abdul/PycharmProjects/ImgSeg/img/new.png", "PNG")
print('Done')
It saves the same image as input with the name as new.png, nothing has been removed from the image.
When I printed the datas and newData it prints the same values:
Old Length is: 944812
New Length is: 944812
Thanks in advance!
You are filtering out all white pixels:
item[0] == 255 and item[1] == 255 and item[2] == 255
but that does not mean that:
all white pixels (255, 255, 255) belong to the background and
all background contains only white pixels.
A heuristic method (partially applicable to your sample image) would be to increase the threshold of your background pixel definition:
if 50 <= item[0] <= 80 and 60 <= item[1] <= 100 and 80 <= item[2] < 140:
filters out much more pixels.
Do you really want your background pixels being white is also a question to be answered.
Also, your test for checking the output of your filtering won't work since both images will contain the same number of pixels anyway, regardless of their transparency.

Detect white background on images using python

Is there a way to tell whether an image as a white background using python and what could be a good strategy to get a "percentage of confidence" about this question? Seems like the literature on internet doesn't cover exactly this case and I can't find anything strictly related.
The images I want to analyze are typical e-commerce website product pictures, so they should have a single focused object in the middle and white background only at the borders.
Another information that could be available is the max percentage of image space the object should occupy.
I would go with something like this.
Reduce the contrast of the image by making the brightest, whitest pixel something like 240 instead of 255 so that the whites generally found within the image and within parts of the product are no longer pure white.
Put a 1 pixel wide white border around your image - that will allow the floodfill in the next step to "flow" all the way around the edge (even if the "product" touches the edges of the frame) and "seep" into the image from all borders/edges.
Floofdill your image starting at the top-left corner (which is necessarily pure white after step 2) and allow a tolerance of 10-20% when matching the white in case the background is off-white or slightly shadowed, and the white will flow into your image all around the edges until it reaches the product in the centre.
See how many pure white pixels you have now - these are the background ones. The percentage of pure white pixels will give you an indicator of confidence in the image being a product on a whitish background.
I would use ImageMagick from the command line like this:
convert product.jpg +level 5% -bordercolor white -border 1 \
-fill white -fuzz 25% -draw "color 0,0 floodfill" result.jpg
I will put a red border around the following 2 pictures just so you can see the edges on StackOverflow's white background, and show you the before and after images - look at the amount of white in the resulting images (there is none in the second one because it didn't have a white background) and also at the shadow under the router to see the effect of the -fuzz.
Before
After
If you want that as a percentage, you can make all non-white pixels black and then calculate the percentage of white pixels like this:
convert product.jpg -level 5% \
-bordercolor white -border 1 \
-fill white -fuzz 25% -draw "color 0,0 floodfill" -shave 1 \
-fuzz 0 -fill black +opaque white -format "%[fx:int(mean*100)]" info:
62
Before
After
ImageMagick has Python bindings so you could do the above in Python - or you could use OpenCV and Python to implement the same algorithm.
This question may be years ago but I just had a similar task recently. Sharing my answer here might help others that will encounter the same task too and I might also improve my answer by having the community look at it.
import cv2 as cv
import numpy as np
THRESHOLD_INTENSITY = 230
def has_white_background(img):
# Read image into org_img variable
org_img = cv.imread(img, cv.IMREAD_GRAYSCALE)
# cv.imshow('Original Image', org_img)
# Create a black blank image for the mask
mask = np.zeros_like(org_img)
# Create a thresholded image, I set my threshold to 200 as this is the value
# I found most effective in identifying light colored object
_, thres_img = cv.threshold(org_img, 200, 255, cv.THRESH_BINARY_INV)
# Find the most significant contours
contours, hierarchy = cv.findContours(thres_img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_NONE)
# Get the outermost contours
outer_contours_img = max(contours, key=cv.contourArea)
# Get the bounding rectangle of the contours
x,y,w,h = cv.boundingRect(outer_contours_img)
# Draw a rectangle base on the bounding rectangle of the contours to our mask
cv.rectangle(mask,(x,y),(x+w,y+h),(255,255,255),-1)
# Invert the mask so that we create a hole for the detected object in our mask
mask = cv.bitwise_not(mask)
# Apply mask to the original image to subtract it and retain only the bg
img_bg = cv.bitwise_and(org_img, org_img, mask=mask)
# If the size of the mask is similar to the size of the image then the bg is not white
if h == org_img.shape[0] and w == org_img.shape[1]:
return False
# Create a np array of the
np_array = np.array(img_bg)
# Remove the zeroes from the "remaining bg image" so that we dont consider the black part,
# and find the average intensity of the remaining pixels
ave_intensity = np_array[np.nonzero(np_array)].mean()
if ave_intensity > THRESHOLD_INTENSITY:
return True
else:
return False
These are the images of the steps from the code above:
Here is the Original Image. No copyright infringement intended.
(Cant find the url of the actual imagem from unsplash)
First step is to convert the image to grayscale.
Apply thresholding to the image.
Get the contours of the "thresholded" image and get the contours. Drawing the contours is optional only.
From the contours, get the values of the outer contour and find its bounding rectangle. Optionally draw the rectangle to the image so that you'll see if your assumed thresholding value fits the object in the rectangle.
Create a mask out of the bounding rectangle.
Lastly, subtract the mask to the greyscale image. What will remain is the background image minus the mask.
To Finally identify if the background is white, find the average intensity values of the background image excluding the 0 values of the image array. And base on a certain threshold value, categorize it if its white or not.
Hope this helps. If you think it can still be improve, or if there are flaws with my solution pls comment below.
The most popular image format is .png. PNG image can have a transparent color (alpha). Often match with the white background page. With pillow is easy to find out which pixels are transparent.
A good starting point:
from PIL import Image
img = Image.open('image.png')
img = img.convert("RGBA")
pixdata = img.load()
for y in xrange(img.size[1]):
for x in xrange(img.size[0]):
pixel = pixdata[x, y]
if pixel[3] == 255:
# tranparent....
Or maybe it's enough if you check if top-left pixel it's white:
pixel = pixdata[0, 0]
if item[0] == 255 and item[1] == 255 and item[2] == 255:
# it's white

Categories