Efficiently converting color to transparency in python

余生颓废 提交于 2021-02-16 18:13:25

问题


GIMP has a convenient function that allows you to convert an arbitrary color to an alpha channel.

Essentially all pixels become transparent relative to how far away from the chosen color they are.

I want to replicate this functionality with opencv.

I tried iterating through the image:

    for x in range(rows):
        for y in range(cols):
            mask_img[y, x][3] = cv2.norm(img[y, x] - (255, 255, 255, 255))

But this is prohibitively expensive, it takes about 10 times longer to do that iteration than it takes to simply set the field to 0 (6 minutes vs an hour)

This seems more a python problem than an algorithmic problem. I have done similar things in C++ and it's not as as bad in terms of performance.

Does anyone have suggestions on achieving this?


回答1:


Here is my attempt only using numpy matrix operations.

My input image colortrans.png looks like this:

I want to make the diagonal purple part (128, 0, 128) transparent with some tolerance +/- (25, 0, 25) to the left and right, resulting in some transparency gradient.

Here comes the code:

import cv2
import numpy as np

# Input image
input = cv2.imread('images/colortrans.png', cv2.IMREAD_COLOR)

# Convert to RGB with alpha channel
output = cv2.cvtColor(input, cv2.COLOR_BGR2RGBA)

# Color to make transparent
col = (128, 0, 128)

# Color tolerance
tol = (25, 0, 25)

# Temporary array (subtract color)
temp = np.subtract(input, col)

# Tolerance mask
mask = (np.abs(temp) <= tol)
mask = (mask[:, :, 0] & mask[:, :, 1] & mask[:, :, 2])

# Generate alpha channel
temp[temp < 0] = 0                                            # Remove negative values
alpha = (temp[:, :, 0] + temp[:, :, 1] + temp[:, :, 2]) / 3   # Generate mean gradient over all channels
alpha[mask] = alpha[mask] / np.max(alpha[mask]) * 255         # Gradual transparency within tolerance mask
alpha[~mask] = 255                                            # No transparency outside tolerance mask

# Set alpha channel in output
output[:, :, 3] = alpha

# Output images
cv2.imwrite('images/colortrans_alpha.png', alpha)
cv2.imwrite('images/colortrans_output.png', output)

The resulting alpha channel colortrans_alpha.png looks like this:

And, the final output image colortrans_output.png looks like this:

Is that, what you wanted to achieve?




回答2:


I had a go using pyvips.

This version calculates the pythagorean distance between each RGB pixel in your file and the target colour, then makes an alpha by scaling that distance metric by a tolerance.

import sys 
import pyvips 

image = pyvips.Image.new_from_file(sys.argv[1], access='sequential')

# Color to make transparent
col = [128, 0, 128]

# Tolerance ... ie., how close to target before we become solid
tol = 25

# for each pixel, pythagorean distance from target colour
d = sum(((image - col) ** 2).bandsplit()) ** 0.5

# scale d so that distances > tol become 255
alpha = 255 * d / tol

# attach the alpha and save
image.bandjoin(alpha).write_to_file(sys.argv[2])

On @HansHirse's nice test image:

I can run it like this:

$ ./mktrans.py ~/pics/colortrans.png x.png

To make:

To test speed, I tried on a 1920x1080 pixel jpg:

$ time ./mktrans.py ~/pics/horse1920x1080.jpg x.png
real    0m0.708s
user    0m1.020s
sys 0m0.029s

So 0.7s on this two-core 2015 laptop.




回答3:


I've done a project that converted all pixels that are close to white into transparent pixels using the PIL (python image library) module. I'm not sure how to implement your algorithm for "relative to how far away from chosen color they are", but my code looks like:

from PIL import Image

planeIm = Image.open('InputImage.png')
planeIm = planeIm.convert('RGBA')
datas = planeIm.getdata()

newData = []
for item in datas:
    if item[0] > 240 and item[1] > 240 and item[2] > 240:
        newData.append((255, 255, 255, 0)) # transparent pixel
    else:
        newData.append(item) # unedited pixel
planeIm.putdata(newData)
planeIm.save('output.png', "PNG")

This goes through a 1920 X 1080 image for me in 1.605 seconds, so maybe if you implement your logic into this you will see the speed improvements you want?

It might be even faster if newData is initialized instead of being .append()ed every time too! Something like:

planeIm = Image.open('EGGW spider.png')
planeIm = planeIm.convert('RGBA')
datas = planeIm.getdata()

newData = [(255, 255, 255, 0)] * len(datas)
for i in range(len(datas)):
    if datas[i][0] > 240 and datas[i][1] > 240 and datas[i][2] > 240:
        pass # we already have (255, 255, 255, 0) there
    else:
        newData[i] = datas[i]
planeIm.putdata(newData)
planeIm.save('output.png', "PNG")

Although for me this second approach runs at 2.067 seconds...

multithreading

An example of threading to calculate a different image would look like:

from PIL import Image
from threading import Thread
from queue import Queue
import time

start = time.time()
q = Queue()

planeIm = Image.open('InputImage.png')
planeIm = planeIm.convert('RGBA')
datas = planeIm.getdata()
new_data = [0] * len(datas)

print('putting image into queue')
for count, item in enumerate(datas):
    q.put((count, item))

def worker_function():
    while True:
        # print("Items in queue: {}".format(q.qsize()))
        index, pixel = q.get()
        if pixel[0] > 240 and pixel[1] > 240 and pixel[2] > 240:
            out_pixel = (0, 0, 0, 0)
        else:
            out_pixel = pixel
        new_data[index] = out_pixel
        q.task_done()

print('starting workers')
worker_count = 100
for i in range(worker_count):
    t = Thread(target=worker_function)
    t.daemon = True
    t.start()
print('main thread waiting')
q.join()
print('Queue has been joined')
planeIm.putdata(new_data)
planeIm.save('output.png', "PNG")

end = time.time()

elapsed = end - start
print('{:3.3} seconds elapsed'.format(elapsed))

Which for me now takes 58.1 seconds! A terrible speed difference! I would attribute this to:

  • Having to iterate each pixel twice, once to put it into a queue and once to process it and write it to the new_data list.
  • The overhead needed to create threads. Each new thread will take a few ms to create, so making a large amount (100 in this case) can add up.
  • A simple algorithm was used to modify the pixels, threading would shine when large amounts of computation are required on each input (more like your case)
  • Threading doesn't utilize multiple cores, you need multiprocessing to get that -> my task manager says I was only using 10% of my CPU and it idles at 1-2% already...


来源:https://stackoverflow.com/questions/55582117/efficiently-converting-color-to-transparency-in-python

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!