Найти - Пользователи
Полная версия: [PIL] bmp/jpg/png c цветовым ключом -> png c прозрачным фоном
Начало » Python для новичков » [PIL] bmp/jpg/png c цветовым ключом -> png c прозрачным фоном
1 2
Eliont
Попробовал тут сделать конвертер bmp/jpg/png c цветовым ключом -> png c прозрачным фоном пригодный для использования в RenPy.
Сначала заменяет все ключевые пиксели прозрачными затем обрезает все прозрачные строки и столбцы.

С приведением от http://dl.dropbox.com/u/11931230/Images/converter/data01_00165_3.png
такого к http://dl.dropbox.com/u/11931230/Images/converter/data01_00165_3.jpg
такому работает нормально, с http://dl.dropbox.com/u/11931230/Images/converter/OVE1a.jpg
более сложными начинаются http://dl.dropbox.com/u/11931230/Images/converter/OVE1a.png
косяки.

К тому же работает медленно так как маска строится попиксельно, да и вычисление прямоугольника обрезки тоже.

Подскажите пожалуйста как можно конвертировать более аккуратно, и желательно, быстро.

import os, sys, Image

path = os.path.realpath(os.path.dirname(sys.argv[0]))

for fname in os.listdir(path):
if fname.lower().endswith(('.bmp','.jpg',".png")):
outfile = fname[:-4] + ".png"
im = Image.open(fname)
print "Old: %s - %dx%d"%(fname,im.size[0],im.size[1])
#convert to png
im = im.convert('RGBA')

source = im.split()
colorkey = im.getpixel((im.size[0]-1,im.size[1]-1))

mask = Image.new("RGBA", (im.size[0], im.size[1]))
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
if im.getpixel((x,y)) == colorkey:
mask.putpixel((x,y),(0,0,0,255))
else:
mask.putpixel((x,y),(255,255,255,0))

#mask.save("_file.png")
# break
# process the alpha band fully transparent
out = source[3].point(lambda i: i * 0.0)
# paste the processed band back, but only to selected pixels
source[3].paste(out, None, mask)
# build a new multiband image
im = Image.merge(im.mode, source)

#crop rectangle
box = [0,0,im.size[0],im.size[1]]
border = []

#search non-transparent pixels
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
p = im.getpixel((x,y))
if p[3] is not 0:
border.append(y)
if box[0] is 0:
box[0] = x
else:
box[2] = x
break



box[1] = min(border)
im = im.crop(box)
border = []
im = im.transpose(Image.FLIP_TOP_BOTTOM)

#lower boundary
for x in xrange(im.size[0]):
for y in xrange(im.size[1]):
p = im.getpixel((x,y))
if p[3] is not 0:
border.append(y)

box = [0,0,im.size[0],im.size[1]]
box[1] = min(border)

im = im.crop(box)
im = im.transpose(Image.FLIP_TOP_BOTTOM)

path = os.path.split(os.path.realpath(outfile))
outfile = os.path.join(path[0],"converted",path[1])

if os.path.exists(os.path.join(path[0],"converted")):
im.save(outfile)
else:
#get dir name
path = os.path.realpath(outfile)
dir = os.path.split(path)[0]
if not os.path.exists(dir):
os.makedirs(dir)
im.save(outfile)
print "New: %s - %dx%d"%(fname,im.size[0],im.size[1])

print "Done"
http://dl.dropbox.com/u/11931230/Images/converter/script.zip
Сам скрипт в нескольких вариантах.
asv13
Можно попробовать Image.eval(function, image) чтобы убрать два цикла x-y.

Или же преобразовать картинку в массив numpy, и работать c ним уже.
im1arr = numpy.asarray(im1)
# ... do something
resultImage = Image.fromarray(im1arr)
igor.kaist
Eliont
более сложными начинаются http://dl.dropbox.com/u/11931230/Images … /OVE1a.png
косяки.
тут возможна проблема с артефактами jpeg, попробуйте открыть изображение в графическом редакторе и выкрутить контраст на полную, поймете
Андрей Светлов
Дело не в артефактах.
Удаление фона следует вести от контрольной точки, используя мягкую границу и вычитание цвета этой самой точки.
Eliont
asv13
Можно попробовать Image.eval(function, image) чтобы убрать два цикла x-y.

Или же преобразовать картинку в массив numpy, и работать c ним уже.
Хм. Надо попробовать.

Андрей Светлов
Удаление фона следует вести от контрольной точки, используя мягкую границу и вычитание цвета этой самой точки.
Можно поподробней?
ZAN
Если подразумевается auto-crop, то я видел такой вариант http://bytes.com/topic/python/answers/24415-auto-crop-pil :
def autocrop(im, bgcolor):
if im.mode != "RGB":
im = im.convert("RGB")
bg = Image.new("RGB", im.size, bgcolor)
diff = ImageChops.difference(im, bg)
bbox = diff.getbbox()
if bbox:
return im.crop(bbox)
return None # no contents
Если использование PIL не обязательно, то в ImageMagick есть специальный ключ -trim (для диапазона цветов -fuzz)
$> mogrify -trim +repage myimage.png
Eliont
Спасибо.
Вообще не обязательно, но если на питоне можно такое сделать то хотелось бы сделать.
Андрей Светлов
Eliont
Можно поподробней?
Возьмем вашу картинку с девочкой.

Удаление фона можно рассматривать как закраску прозрачной кистью.

1. Закрашивание “от точки”.
Цвет внутри фигурки может совпадать с цветом фона.
Поэтому нужно красить от выбраного места (правый нижний угол), распространяя закраску до границы цвета.
Поищите “алгоритм заливки области” - есть много статей на эту тему. Например, http://en.wikipedia.org/wiki/Flood_fill

2. Мягкая граница.
У девочки нечеткие края: пряди волос, например, на кончиках плавно переходят в цвет фона. В случае фонаря с гало (http://ru.wikipedia.org/wiki/%D0%93%D0%B0%D0%BB%D0%BE - примеры) эффект более очевиден.
Если использовать простейшее определение: “границей считается любая точка с цветом, отличающимся от контрольного”, получим резкую границу между
удаляемым фоном и прядью волос.
При помещении этой картинки на белый, например, фон - увидим темные края прядей. Это нехорошо.

Решение может быть следующим:
- определим расстояние между цветами как sqrt((R1-R2)**2 + (G1-G2)**2 + (B1-B2)**2), где R, G, B - компоненты цвета.
- зададим определение границы как “цветовое расстояние должно быть больше константы”, например 10.
Это означает: цвета, отличающиеся меньше чем на 10 попадают в область закраски
- зададим прозрачность кисти как обратную расстоянию.
Т.е. если цвет полностью совпадает и цветоразность нулевая - прозрачность кисти 255.
Для разности 4 прозрачность равна (10-4)*255/10 = 153

3. Компенсация цвета.
В результате первых двух шагов картинка получилась почти правильная: мягкие переходы в прозрачность, при этом внутренние части фигурки не затронуты.
Осталась последняя неприятность: края волос хоть и переходят в прозрачность, но у них остается темный фон. При наложение на белое этот фон будет виден (хоть и не так явно как при четкой границе).
Этот фон (цвет нашей контрольной точки) нужно убрать - заменить его на белый

Делаем так:
- берем прозрачность из пункта 2.
- меняем цвет на белый для каждого канала R,G,B в соответствии с этой прозрачностью
C` = C*A + 255*(255-A)
Если в канале было 0 при альфе 255 - станет 255
И так далее - плавный переход к цвету картинки при уменьшении прозрачности.
Eliont
Гм. Спасибо.
Сегодня попробую разобраться.

UPD 1:
Спасибо за автокроп. Я его чуть изменил и всё заработало. Сейчас с очисткой оставшегося места разбираться буду.
Eliont
Вроде получилось.
Исходник: http://dl.dropbox.com/u/11931230/Images/converter/cg00001.QNT.png
Результат: http://dl.dropbox.com/u/11931230/Images/converter/cg00001.QNT_.png
Маска: http://dl.dropbox.com/u/11931230/Images/converter/mask.png

Эльфийка из первого сообщения - http://dl.dropbox.com/u/11931230/Images/converter/OVE1a___.png

Правда при попытке вычислений в массиве NumPy интерпретатор вываливал кучу варнингов:
http://dl.dropbox.com/u/11931230/Images/converter/Screenshot-400.jpg

При использовании getpixel/putpixel - всё нормально.

Правда всё равно осталась каёмка фонового цвета.
Насколько я понял - её удалением занимается третий пункт, но не врубаюсь как его делать.
Может пройтись по строкам и первому непрозрачному пикселю поставит альфу 0, второму 100, третьем у 200, дальше не трогать. И так со свеми интервалами - типа полосового метода заливки.

import os, sys, math, Image, ImageChops
import numpy as np

path = os.path.realpath(os.path.dirname(sys.argv[0]))
rng = 30

def autocrop(im, bgcolor):
bg = Image.new(im.mode, im.size, bgcolor)
diff = ImageChops.difference(im, bg)
bbox = diff.getbbox()
if bbox: return im.crop(bbox) # cropped
else: return im # no contents

def color_range(color1,color2):
R1,G1,B1,A1 = color1
R2,G2,B2,A2 = color2
return math.sqrt((R1-R2)**2 + (G1-G2)**2 + (B1-B2)**2)


for fname in os.listdir(path):
if fname.lower().endswith(('.bmp','.jpg','.png')):
outfile = fname[:-4] + "_.png"
im = Image.open(fname)
print "Old: %s - %dx%d"%(fname,im.size[0],im.size[1])
# im = autocrop(im,im.getpixel((0,0)))
#convert to png
im = im.convert('RGBA')

source = im.split()
colorkey = im.getpixel((0,0))
size = im.size

mask = Image.new("RGBA", (im.size[0], im.size[1]), (0,0,0,0))

vp = np.zeros((1000, 1000)) #visited pixels
# im = np.asarray(im) #image to array
# mask = np.asarray(mask) #mask image array
# mask.flags.writeable = True #check off read-only flag
# colorkey = im[0][0]

# print mask[0][0]
# print mask[0][0][3]

# mask[0][0][3] = 5
# print mask[0][0][3]
# break

stack = [(0,0,0),(size[0]-1,0,0),(size[0]/2,0,0)] # x - y - color range
while len(stack) > 0:
point = stack.pop(0)
for x in [-1,0,1]:
for y in [-1,0,1]:
if x == 0 and y==0: continue
_x_ , _y_ = x+point[0],y+point[1]
if abs(_x_)>=size[0] or abs(_y_)>=size[1]: continue
if _x_ < 0 or _y_ < 0 : continue
if vp[_x_][_y_] == 0:
try:
r = color_range(colorkey,im.getpixel((_x_,_y_)))
except IndexError:
print point[0],point[1],_x_,_y_
exit(1)
if r < rng:
stack.append((_x_,_y_,r))
vp[_x_][_y_] = 1 #pixel visited
else:
vp[_x_][_y_] = 1 #this pixel - probably image
# print len(stack)
# clr = im[x,y]
clr = im.getpixel((point[0],point[1]))
diff = int((rng-(rng-point[2]))*(255/rng))
R = clr[0]*clr[3] + 255*(255-clr[3])
G = clr[1]*clr[3] + 255*(255-clr[3])
B = clr[2]*clr[3] + 255*(255-clr[3])
mask.putpixel((point[0],point[1]),(R,G,B,255-diff))
# for index,value in enumerate([R,G,B,255-diff]): mask[point[0],point[1],index] = value


# for x in xrange(size[1]):
# for y in xrange(size[0]):
# r = color_range(im[x][y],im[0][0])
# if r < rng:
# for index,value in enumerate([0,0,0,254]): mask[x,y,index] = value
# else:
# for index,value in enumerate([254,254,254,0]): mask[x,y,index] = value

# for x in xrange(im.size[0]):
# for y in xrange(im.size[1]):
# r = color_range(im.getpixel((x,y)),colorkey)
# if r <= rng:
# clr = im.getpixel((x,y))
# diff = int((rng-(rng-r))*(255/rng))
# R = clr[0]*clr[3] + 255*(255-clr[3])
# G = clr[1]*clr[3] + 255*(255-clr[3])
# B = clr[2]*clr[3] + 255*(255-clr[3])
# mask.putpixel((x,y),(R,G,B,255-diff))
# else:
# mask.putpixel((x,y),(255,255,255,0))

# mask = Image.fromarray(mask)
# im = Image.fromarray(im)
mask.save("mask.png")

# process the alpha band fully transparent
out = source[3].point(lambda i: i * 0.0)
# paste the processed band back, but only to selected pixels
source[3].paste(out, None, mask)
# build a new multiband image
im = Image.merge(im.mode, source)

# path = os.path.split(os.path.realpath(outfile))
# outfile = os.path.join(path[0],"converted",path[1])

# if os.path.exists(os.path.join(path[0],"converted")):
# im.save(outfile)
# else:
# #get dir name
# path = os.path.realpath(outfile)
# dir = os.path.split(path)[0]
# if not os.path.exists(dir):
# os.makedirs(dir)
# im.save(outfile)
# print "New: %s - %dx%d"%(fname,im.size[0],im.size[1])
im.save(outfile)
print "Done"
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Powered by DjangoBB